Linux文件系统(上)
-
目录
前言
1.文件接口——用户与文件的“桥梁”
2.C语言中FILE结构与Linux系统调用中fd的关系
3.fd字段如何在文件读写操作中发挥作用
4.fd的分配规则与文件重定向
5.文件缓冲区
6.如何理解Linux中一切皆文件的管理方案
7.涉及代码一览
总结
前言
在Linux中存在“两列”文件,一种是加载到内存中的文件,另一种是保存在磁盘等设备中的文件。本文要讲的就是加载到内存中的文件是以何种形态存在,是如何进行表征的。
1.文件接口——用户与文件的“桥梁”
在C语言中我们接触到过一些与文件有关的接口:诸如fopen、fwrite、fread、fclose等,这些接口都一定与一个名为FILE*的类型有关,这个所谓的FILE*就是C语言中封装的一个描述文件属性的结构体的指针。

在C语言中我们通过接口最直观的能感受到C语言接口给我们返回了一个描述文件的结构体指针,我们对这个指针进行相应的读写调用就可以实现对文件的读写。在Linux平台上,Linux也给我们提供了一些访问文件的接口:

我们写一个代码示例观察一下两者的行为差异:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// int main()
// {
// //向一个名为test.txt的文件中写入hello world,
// //并将写入的文件内容在读取出来
// FILE * f1=fopen("./test.txt","r+"); //r+表示以读写方式打开文件
// const char * message="hello world";
// char output[64];// //向f1指向的文件中写入信息
// fwrite(message,strlen(message),1,f1);// //r+状态表示可读可写,但是在写状态完成后,
// //此时的文件指针不一定为0,所以要手动置零,从文件开头开始读文件
// rewind(f1);// //读取文件
// fread(output,strlen(message),1,f1);// //关闭文件流
// fclose(f1); // //打印结果
// printf("%s\n",output);
// return 0;
// }#include <fcntl.h>
#include <unistd.h>
int main()
{//向一个名为test.txt的文件中写入hello world,//并将写入的文件内容在读取出来int fd=open("./test.txt",O_RDWR|O_TRUNC|O_CREAT); //O_RDWR表示以读写方式打开文件,O_TRUNC表示每次写入前清空文件内容,O_CREAT表示写入文件不存在就创建const char * message="hello world";char output[64];//向fd指向的文件中写入信息write(fd,message,strlen(message));//O_RDWR状态表示可读可写,但是在写状态完成后,//此时的文件指针不一定为0,所以要手动置零,从文件开头开始读文件lseek(fd,0,SEEK_SET);//读取文件read(fd,output,strlen(message));//关闭文件流close(fd); //打印结果 printf("%s\n",output);return 0;
}

我们从上述代码中不难看到,无论是C语言的文件接口还是Linux平台的系统调用接口,都可以实现对文件的读写操作,只不过,C语言中的文件读写接口与一个名为FILE的结构有关,Linux平台的读写文件系统调用与一个名为fd的整形有关。
2.C语言中FILE结构与Linux系统调用中fd的关系
我们在第一点中的讨论与代码实践中,隐隐约约感觉到,C语言的文件接口与Linux系统调用接口一定存在着某种必然联系。
先说结论:
FILE结构中一定包含fd这一字段,因为C语言的接口都是对各平台语言的系统调用的封装。
理由如下:
在博主的【浅谈冯诺依曼体系与操作系统】一文中讨论了,操作系统在整个计算机体系中的定位如下。

我们使用的Linux内核其实就是一种操作系统,这一点是毋庸置疑的,操作系统的内核不允许用户直接访问,如果用户想要访问内核就必须使用Linux规定的系统调用接口。在第一部分中我们讨论的诸如open、close、read、write接口就属于系统调用接口,所以如果有任何的一种语言想在Linux平台下,访问Linux下的文件管理资源,那么这种语言就必须使用系统调用接口或者再系统调用接口上进行进一步的封装。C语言使用FILE结构进行文件的操作,但是对文件读写操作就一定绕不开系统调用接口,也就一定绕不开fd这一字段。所以FILE这一结构中一定包含fd这一字段。
3.fd字段如何在文件读写操作中发挥作用
在讲解这一部分之前,我想请读者想这样一个问题:
读写文件是否要以进程的形式进行?
答案是需要以进程的形式进行。
如果我们要对一个文件进行读写操作就一定需要将这个文件加载到内存中,而一旦文件被加载到内存中就需要对“文件”这一数据进行管理,要对数据做管理就必须要对数据作描述的结构,有了对数据描述的结构,下一步就需要交给操作系统集中调度处理,而操作系统只会对进程进行调度和管理,那么就意味着对数据的描述就必须要与进程相关联。那么我们可以查看一下:Linux内核中有关描述文件的结构体的字段、Linux内核中管理文件结构的字段。



我们来整理一下三者的逻辑:
首先,在Linux操作系统管理存在着描述进程的结构体,在这个结构体中有一个指针,这个指针指向管理文件的结构体,管理文件的结构体中又有一个指向文件结构体的指针数组。而fd就是这个数组的下标。如果我们想对相应文件进行读写操作只需要先为文件开辟一个空间,,然后令指针数组中一个为空的指针指向该文件结构即可,当我们想对该文件进行访问是,只需要对应的数组下标即可。
4.fd的分配规则与文件重定向
当一个进程启动时,会有三个文件为我们自动打开。它们分别是:标准输入文件、标准输出文件、标准错误文件。它们是对应的fd按照顺序分别是:0、1、2。在C语言中我们使用的输出接口本质上就是向标准输出文件中写入数据,在C语言中我们使用的输入接口本质上就是从标准输入进行数据的读取,这些接口与fd相绑定,这些接口默认从fd为0的文件中进行数据的读取,向fd为1的文件进行写入。我们可以使用相应接口测试一下:

那么如果我们呢关闭对应的读写文件后在创建新的文件后,对新创建的文件进行读写发生什么呢?


我们不难看到。之前对标准输出输出的内容,现在输出到了log.txt文件中,之前需要从标准输入中阻塞读取的数据,现在可以从文件中非阻塞读取数据。
这其实是因为fd的分配规则所导致的:
当文件读写被关闭时,除了要对描述文件的结构体作释放外,还需要将对应fd的文件指针置为空,当再有新的文件被打开时,操作系统会优先令文件指针数组中下标最小的、未被使用文件指针指向新创建的文件,这就导致了C语言中向屏幕输出,从键盘读取数据的接口,从新创建的文件中读取数据,向新创建的文件写入数据。这种将输入输出定向的其它文件的操作就叫做输入输出重定向。
这是我们无意间发现的文件重定向,我们来看一下Linux操作系统中的文件重定向接口:
int dup2(int sourcefd , int copyfd);
说明:
dup2的行为是将文件指针数组中sourcefd下标对应的内容拷贝到copyfd下标对应的内容,本质上就是令两个文件指针指向同一个文件。

不同于我们关闭一个读写流后,创建一个新的读写流顶替旧的读写流的行为,这种方式实现文件的重定向只能保持一个文件描述符指向一个文件,但是使用dup2令文件重定向,可以令多个文件描述符指向同一个文件,这也就是我们图11结果的成因。
5.文件缓冲区
我们在讲解文件缓冲区之前看看看这段代码,请你推测一下这段代码执行后的结果是什么:
int main()
{printf("hello world!");while(1);
}

从图12中不难发现,该程序进入了死循环,但是在进入死循环之前,应该打印一条“hello world!”的信息,但是我们从终端中并没有发现这条消息。这个现象的成因实际上就与缓冲区有关系,C语言有这样的考量:
如果对一个文件进行多次少量的写入,会明显增加接口调用的时间,但是若将需要读写的内容存储到一块大小合适的空间中,当缓冲区中的内容达到一定大小时,进行一次大量的读写,就可以相对减少调用的时间。
以上就是C语言缓冲区存在意义的理解,那么缓冲区刷新的策略有哪些呢:
①无缓冲:这通常是对于一些需要即时输出的文件,如stderr(标准错误流),不对其进行缓冲是为了尽快获取错误信息。
②行缓冲:当C语言识别到“\n”换行符是会进行缓存区的刷新。
③全缓冲:当C语言层缓冲区被填满时,才会进行刷新操作。
④进程退出时,自动刷新缓冲区。

我们介绍的C语言缓冲区实际上是:C语言与内核交互数据的缓冲区。
以C语言的FILE结构为例,其底层一定封装了Linux内核相关的系统调用,而我们的缓冲区实际上就包含在FILE结构中,C语言在进行读写操作的时候,将数据拷贝到相应的缓冲区位置中,而后可利用缓冲区中数据进行其它调用或返回。
那么,Linux内核与硬件之间有没有缓冲呢?如何证明该缓冲区与C语言中的缓冲区不是一个?

当我们使用C语言的调用接口(fwrite)时,向屏幕写一次但是不令其刷新缓冲区,当创建一个新的进程的时候,子进程继承父进程的所有数据包括缓冲区,因为没有对数据进行修改所以父子进程的缓冲区在页表映射中指向同一块区域,但当父子进程退出时,要对缓冲区作清空,对缓冲区作清空是一种修改操作,此时发生写时拷贝,子进程拷贝父进程的缓冲区中的内容,父子进程将缓冲区中的内容向Linux内核传递,调用系统调用接口,写入到硬件输出缓冲区,而后输出到显示器,所以打印两次。但对于直接调用系统调用接口(write)时,由于其不将内容写入到进程缓冲区,而是直接写入到硬件输出缓冲区,所以只打印一次。
6.如何理解Linux中一切皆文件的管理方案
在Linux操作系统中,Linux将所有硬件设备都视作文件,让我们带入操作系统的视角,所以现在无论是键盘、鼠标还是网卡、显示。它们都是文件,即然是文件那么一定就有读写方法,对于磁盘这类可读可写的存储介质还好说,我们如何对键盘、鼠标这种只读设备进行写入呢?我们需要对只读设备进行写入吗?当然不需要,对于只读文件,我们可以将它的写方法设置为空,同理,对于只写设备,我们可以将它们的读方法设置为空。将所有硬件设备视作文件的一个重要因素就是函数指针,当读写方法存在是,将对应的函数指针指向对应的读写方法即可,对于不存在的方法,函数指针设置成空。对文件的操作在Linux内核中struct file结构中的struct file_operations字段中存储。我们来看看struct file_operations的字段。

7.涉及代码一览
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// int main()
// {
// //向一个名为test.txt的文件中写入hello world,
// //并将写入的文件内容在读取出来
// FILE * f1=fopen("./test.txt","r+"); //r+表示以读写方式打开文件
// const char * message="hello world";
// char output[64];// //向f1指向的文件中写入信息
// fwrite(message,strlen(message),1,f1);// //r+状态表示可读可写,但是在写状态完成后,
// //此时的文件指针不一定为0,所以要手动置零,从文件开头开始读文件
// rewind(f1);// //读取文件
// fread(output,strlen(message),1,f1);// //关闭文件流
// fclose(f1); // //打印结果
// printf("%s\n",output);
// return 0;
// }#include <fcntl.h>
#include <unistd.h>
// int main()
// {
// //向一个名为test.txt的文件中写入hello world,
// //并将写入的文件内容在读取出来
// int fd=open("./test.txt",O_RDWR|O_TRUNC|O_CREAT); //O_RDWR表示以读写方式打开文件,O_TRUNC表示每次写入前清空文件内容,O_CREAT表示写入文件不存在就创建
// const char * message="hello world";
// char output[64];// //向fd指向的文件中写入信息
// write(fd,message,strlen(message));// //O_RDWR状态表示可读可写,但是在写状态完成后,
// //此时的文件指针不一定为0,所以要手动置零,从文件开头开始读文件
// lseek(fd,0,SEEK_SET);// //读取文件
// read(fd,output,strlen(message));// //关闭文件流
// close(fd); // //打印结果
// printf("%s\n",output);
// return 0;
// }// int main()
// {
// close(1);
// int fd1=open("./log.txt",O_WRONLY|O_CREAT|O_TRUNC);
// const char * message="hello linunx\n";
// write(fd1,message,strlen(message));// close(0);
// int fd2=open("./test.txt",O_RDONLY);
// char buffer[64];// int end=read(fd2,buffer,63);
// buffer[end]='\0';
// printf("buffer contens:%s",buffer);
// return 0;
// }// int main()
// {
// //开启一个写文件描述符
// FILE* f1=fopen("./log.txt0","w");
// dup2(f1->_fileno,1);// char message[64]="hello linux!!\n";
// fwrite(message,strlen(message),1,f1);
// //向屏幕进行打印
// printf("hello world!!\n");// return 0;
// }// int main()
// {// printf("hello world!\n");
// while(1);// }int main()
{write(1,"hello ",6);fwrite(" hello world!! ",16,1,stdout);int id=fork();if(id==0){}return 0;
}
总结
本文通过C语言中的读写接口作为引入介绍了Linux中的文件读写接口和Linux读写接口的参数,以及如何在Linux内核中fd如何实现定位要读写的文件,并由fd的分配规则引出了文件重定向和文件缓冲区,最后介绍了Linux内核对文件的管理视角是怎么样的。在下一篇文章中将解释文件如何在磁盘上的存储。