文件的管理
在一个进程中可以打开多个文件并且可以对这些文件做出不同的操作,那这里就会存在一个问题,当我们在程序里面打开多个文件时,操作系统肯定是得对这些文件进行管理的,而管理的本质就是对数据进行管理,管理的方法就是先描述再组织,所以操作系统为了管理被打开的文件就会创建内核数据结构来描述这些文件,在操作系统中这个结构体就叫做struct file,在这个结构体里面包含了文件的大部分属性进程可以通过这些属性来找到文件并访问文件的内容,每打开一个文件操作系统就会创建一个file结构体,然后采用链式结构的方式将这些文件的结构体连接起来,这样操作系统只要找到一个文件结构体的起始地址就能找到所有被该进程打开的文件的file结构体,对文件的管理就变为对file结构体的增删查改,比如说下面的图片
那这里就存在一个问题,我们之前说文件操作的本质是:进程和被打开文件的关系,可是根据上面的描述这里的进程和被打开的文件好像没有任何联系啊,所以要想知道直接的联系我们得来回顾一下open函数的返回值。
fd是什么?
我们通常把fd称为文件描述符,对于fd大家最熟悉的一点就是:通常使用fd来记录open函数的返回值,比如说下面代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #define FILE_NAME(number) "mytest"#number
8 int main()
9 {
10 umask(1);
11 int fd1=open("FILE_NAME(1)",O_WRONLY|O_CREAT|O_APPEND,0666);
12 int fd2=open("FILE_NAME(2)",O_WRONLY|O_CREAT|O_APPEND,0666);
13 int fd3=open("FILE_NAME(3)",O_WRONLY|O_CREAT|O_APPEND,0666);
14 int fd4=open("FILE_NAME(4)",O_WRONLY|O_CREAT|O_APPEND,0666);
15 int fd5=open("FILE_NAME(5)",O_WRONLY|O_CREAT|O_APPEND,0666);
16 printf("fd1:%d\n",fd1);
17 printf("fd2:%d\n",fd2);
18 printf("fd3:%d\n",fd3);
19 printf("fd4:%d\n",fd4);
20 printf("fd5:%d\n",fd5);
21 close(fd1);
22 close(fd2);
23 close(fd3);
24 close(fd4);
25 close(fd5);
26 }
这段代码的运行结果如下:
通过代码的运行结果我们可以看到open函数的返回值都是整数,而且随着打开文件的数目增加open函数的返回值也在有规律的增加从3开始依次往后加1,可是这里就存在一个问题为什么这里的文件描述符是从3开始的呢?而不是从0开始的呢?原因很简单当我们运行一个程序的时候操作系统会帮自动帮我们打开三个输入输出流:stdin—键盘输入流,stdout—显示器输出流,stderr—显示器输出流,这三个流的类型都是FILE*
通过之前的学习我们知道c语言的文件操作接口是对操作系统提供的接口进行封装实现的:
c语言的文件函数中通过FILE*指针来访问具体的文件,操作系统的文件函数中是通过fd文件描述符来访问具体的文件,
而c语言的文件函数是基于操作系统的文件函数实现的,FILE是一个结构体fd是一个整型变量,所以这里我们就可以推测出结构体FILE中一定存在着一个字段记录着fd的数值,这三个流分别占用着文件描述符的0 1 2所以我们在程序中打开文件的描述符是从3开始,这里可以通过下面的代码来验证上述的内容:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
8 int main()
9 {
10 printf("stdin-fd:%d\n",stdin->_fileno);//输出stdin的文件描述符
11 printf("stdout-fd:%d\n",stdout->_fileno);//输出stdout的文件描述符
12 printf("stderr-fd:%d\n",stderr->_fileno);//输出stderr的文件描述符
13 }
这段代码的运行结果如下:
这里便可以验证我们上面的猜想。好!知道了文件描述符为什么从0 1 2开始,但是这里还存在一个问题为什么文件描述符是一个串连续的整数呢?要想知道这个问题我们得换个角度来分析,首先将磁盘中的一个程序A加载进内存操作系统会创建一个内核数据结构PCB来描述进程A然后再创建三个file结构体对象来描述自动打开的stdin ,stdout,stderr
在程序A的代码里面要将打开文件B,所以这时操作系统又会给文件B创建一个struct file对象
可是在操作系统里面不止这一个进程要打开文件,还有很多的进程也要打开各种各样的文件,所以操作系统中存在着很多被打开的文件:
进程是没有办法从这么多被打开的文件中找到属于本进程的文件,所以在task_struct里面就会存在一个名为files的指针,这个指针指向的对象是一个名为files_struct的结构体:
在files_struct结构体里面存在一个数组这个数组叫做fd_array,数组的元素类型为struct file也就是说这个数组的每个元素都是一个指针,指针指向的对象是描述文件属性的file结构体,当我们打开文件时操作系统就会在fd_arry数组里面从上往下查找没有被用到的元素,找到之后就会就会将file结构体的地址填入该元素里面,这时数组的第四个元素就指向了程序B的file结构体
当操作系统将地址填入数组之后就会将该文件的file在数组中对应的下标返回给用户,所以当我们使用完open函数时就可以得到一个返回值,我们把这个返回值称为文件描述符也可以叫fd,fd的本质就是数组下标,当我们通过fd对文件执行操作时,实际上就是进程的PCB通过指针struct files_struct找到结构体files_struct,files_struct中有个数组fd_array,再把fd的值作为数组的下标找到记录文件属性的file结构体的地址,然后再根据file结构体找到具体的文件最后执行对应的操作,那么这就是fd的作用他的本质就是一个数组的下标所以他是一个连续的整数,希望大家能够理解。
fd的分配规则
我们在上面提到fd的分配规则是从数组fd_array中从上往下依次寻找没有被用到的元素,因为每运行一个进程操作系统会自动打开三个文件,所以我们再打开文件时得到的fd就是从3开始依次往后增加,那这里就有个问题:既然操作系统会自动给我们打开三个文件而且这三个文件的fd分别是0 1 2,那我们是不是能够通过这三个fd加close函数将这三个文件关闭呢?答案是可以的,并且将这几个文件关闭之后这些文件对应在fd_array上的数据是会被清空的,所以当我们关闭这些文件再打开新的文件时,新文件会按顺序占用已经关闭文件的下标,比如说将stdin文件关闭再随机打开一个文件,我们就可以发现打开文件的fd为0,比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(0);
10 int fd = open("mytest",O_WRONLY|O_CREAT|O_APPEND,0666);
11 printf("fd:%d\n",fd);
12 close(fd);
13 return 0;
14 }
这段代码的运行结果如下:
可以看到打印的结果确实为0,我们还可以将stdin和stderr也关闭然后再打开两个文件,就可以看到先打开的文件的fd为0后打开文件的fd为2,比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(2);
10 close(0);
11 int fd1 = open("mytest1",O_WRONLY|O_CREAT|O_APPEND,0666);
12 int fd2 = open("mytest2",O_WRONLY|O_CREAT|O_APPEND,0666);
13 printf("fd1:%d\n",fd1);
14 printf("fd2:%d\n",fd2);
15 close(fd1);
16 close(fd2);
17 return 0;
18 }
这段代码的运行结果如下:
既然stdin和stderr都可以关闭的话,那么同样的道理stdout也可以关闭比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(1);
10 int fd1 = open("mytest1",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 printf("fd1:%d\n",fd1);
12 close(fd1);
13 return 0;
14 }
这段代码的运行结果如下:
这里好像就出问题屏幕上没有显示打印结果这是为什么呢?要想明白这个问题我们就得聊聊重定向是什么?
重定向
FILE是一个结构体在这个结构体里面有一个字段记录着文件描述符的值,所以在stdout的结构体里面就会存在一个字段来专门记录stdout的文件描述符(fd = 1),当我们在程序里面使用printf函数向屏幕上打印内容时,实际上就是向stdout文件里面打印内容,就好比下面的代码:
printf("fd1:%d\n",fd1);//两者一样
fprintf(stdout,"fd1:%d",fd1);
printf函数是默认向stdout里面打印内容,fprintf函数可以向指定的文件里面打印内容,当fprintf函数里面的参数填入stdout时这两个函数的功能是一样的,向stdout里面输出内容实际上就是向stdout内部的那个文件描述符所指向的文件里面打印内容,stdout内部的文件描述符永远都是1,所以每次使用printf函数向屏幕上打印数据时,操作系统都会在数组中寻找下标为1的元素得到元素里面的地址,然后往该地址指向的文件里面输出对应的数据,最后这些数据就会显示在屏幕上面
那知道了这一点我们在回过头来看看上面的那段代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(1);
10 int fd1 = open("mytest",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 printf("fd1:%d\n",fd1);
12 close(fd1);
13 return 0;
14 }
一开始将文件描述符为1的文件关闭了,这一步实际上就是将数组下标为1的元素的内容清空,此时他就不再指向stdout文件
然后我们又使用open函数打开了一个新文件,当打开新文件时操作系统会在数组中从上往下寻找没有被用到的元素,找到之后将file结构体的地址填入该元素并将该元素在数组中对应的下标作为返回值返回用户,因为在程序开始的时候我们关闭了下标为1的文件,所以数组中最先为空的元素就是1,所以新打开文件的file结构体地址就会填入下标为1的数组元素里面,这时该元素就会指向新打开的文件A而不是stdout文件:
之后我们就使用printf函数打印一些内容,printf函数默认向stdout文件里面输出数据,而stdout所记载的文件描述符一直是1,所以printf函数默认向数组中下标为1的元素所指向的文件输出数据,可是此时下标为1的元素不再指向stdout文件,也就是说下标为1的元素不再指向屏幕了而是新打开的文件A,所以printf输出的数据就不会显示在屏幕上而是文件A里面,我们将下面的代码执行以下并查看mytest文件里面的内容就可以验证我们上面讲的对不对,代码如下:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(1);
10 int fd1 = open("mytest",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 printf("fd1:%d\n",fd1);
12 fprintf(stdout,"fd1:%d\n",fd1);
13 close(fd1);
14 return 0;
15 }
运行的结果如下:
mytest文件的内容如下:
不对啊这个文件里面也没有printf函数的内容啊!是不是你讲错了呢?其实不是的因为往屏幕上输出内容和往文本文件里面输出内容所采用的缓冲区刷新策略是不一样的,所以这里得再添加一个fflush函数来刷星缓冲区就能看到printf函数所输出到文件里面的内容,再执行一下就可以看到文件mytest中出现我们想要的结果:
那么我们把本应该输出到屏幕的内容通过修改文件描述符指向而输出到其他文件的行为称之为重定向。
dup2函数
上面的代码是通过人为的使用close函数关闭指定文件描述符来实现重定向,将本应该输出到屏幕上的数据输出到了文件里面,但是这种方法使用起来还是很麻烦的所以操作系统提供了一个函数接口来专门实现重定向,这个函数叫做dup2该函数的参数如下;
该函数执行完之后会将数组fd_array中下标为oldfd的内容复制到下标为newfd的元素里面去,比如说oldfd的值为3,newfd的值为1,执行dup2之前数组的数据是这样的:
执行完dup2函数之后就会将下标为oldfd的数据拷贝到下标为newfd的元素里面去,也就是将fd_array[3]赋值给
fd_array[1],所以此时的数组就变成了下面的样子:
那么这也是一个重定向的过程,有了这个函数就可以将上面的代码进行修改:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 int fd1 = open("mytest",O_WRONLY|O_CREAT|O_TRUNC,0666);
10 dup2(fd1,1);
11 printf("fd1:%d\n",fd1);
12 fprintf(stdout,"fd1:%d\n",fd1);
13 fflush(stdout);
14 close(fd1);
15 return 0;
16 }
这样就可以不用手动的关闭文件,并且当想实现多个文件重定向的时候也不用频繁的关闭文件,比如说一开始将一些数据输出到mytest1文件里面,然后再将一些数据输出到mytest2文件,最后再将一些数据输出到mytest3文件里面,如果没有dup2函数又想通过重定向来实现上述功能的话就会不停的打开文件关闭文件,一开始关闭fd为1的文件,然后使用open函数打开mytest1文件输出一些数据,然后关闭文件mytest1文件再打开mytest2文件输出一些数据等等,比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 close(1);
10 int fd1 = open("mytest1",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 printf("我是mytest1文件\n");
12 fprintf(stdout,"我是mytest1文件\n");
13 fflush(stdout);
14 close(fd1);
15 fd1 = open("mytest2",O_WRONLY|O_CREAT|O_TRUNC,0666);
16 printf("我是mytest2文件\n");
17 fprintf(stdout,"我是mytest2文件\n");
18 fflush(stdout);
19 close(fd1);
20 fd1 = open("mytest3",O_WRONLY|O_CREAT|O_TRUNC,0666);
21 printf("我是mytest3文件\n");
22 fprintf(stdout,"我是mytest3文件\n");
23 fflush(stdout);
24 close(fd1);
25 return 0;
26 }
这段代码的运行结果如下:
对吧再往另外一个文件输出内容时必须得先关闭当前占用文件描述符为1的文件,那万一这个文件马上就要用怎么办是不是就很麻烦啊,所以有了dup2函数上述的代码就可以变得很简单,也不用频繁的关闭和打开文件改进后的代码如下:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 int fd1 = open("mytest1",O_WRONLY|O_CREAT|O_TRUNC,0666);
10 int fd2 = open("mytest2",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 int fd3 = open("mytest3",O_WRONLY|O_CREAT|O_TRUNC,0666);
12 dup2(fd1,1);
13 printf("我是mytest1文件\n");
14 fprintf(stdout,"我是mytest1文件\n");
15 fflush(stdout);
16 dup2(fd2,1);
17 printf("我是mytest2文件\n");
18 fprintf(stdout,"我是mytest2文件\n");
19 fflush(stdout);
20 dup2(fd3,1);
21 printf("我是mytest3文件\n");
22 fprintf(stdout,"我是mytest3文件\n");
23 fflush(stdout);
24 close(fd1);
25 close(fd2);
26 close(fd3);
27 return 0;
28 }
这段代码运行结果如下
那么这就是dup2函数的用法以及带来的好处。
输出重定向
输出重定向就跟上面讲的一样,将原本输出到屏幕上的内容输出到其他文件里面,这里就不多赘述。
追加重定向
追加重定向相比于输出重定向就只有一个区别就是打开文件的方式不同,输出重定向打开文件时会将原文件的内容清空,而追加重定向就不会清空原文件而是往文件里面继续添加新内容所以得将open函数的标记位O_TRUNC修改成O_APPEND,这样就能实现追加重定向,比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 int fd1=open("mytest1",O_WRONLY|O_CREAT|O_APPEND,0666);
10 if(fd1<0)
11 {
12 perror("open");
13 }
14 dup2(fd1,1);
15 printf("这是追加的内容\n");
16 fprintf(stdout,"这也是追加的内容\n");
17 return 0;
18 }
mytest1文件的内容如下:
执行完代码之后mytest1文件的内容如下:
那么这就是追加重定向,希望大家能够理解。
输入重定向
平时执行程序的时候一般都是通过键盘输入一些数据给程序,比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 char line[64];
10 while(1)
11 {
12 printf(">");
13 if(fgets(line,sizeof(line),stdin)==NULL)
14 {
15 break;
16 }
17 printf("%s",line);
18 }
19 return 0;
20 }
这段代码的运行结果如下:
但是有了dup2函数之后,我们就可以实现输入重定向将另外一个文件里面的内容代替键盘输出到程序里面,因为键盘对应的是stdin,而stdin里面的文件描述符fd等于0,所以将新打开的文件描述符覆盖到下标为0的元素上就可以实现输入重定向,所以比如说下面的代码:
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 int main()
8 {
9 int fd1=open("mytest1",O_RDONLY,0666);
10 if(fd1<0)
11 {
12 perror("open");
13 }
14 dup2(fd1,0);
15 char line[64];
16 while(1)
17 {
18 printf(">");
19 if(fgets(line,sizeof(line),stdin)==NULL)
20 {
21 break;
22 }
23 printf("%s",line);
24 }
25 return 0;
26 }
mytest1文件里面的内容如下:
那么上面代码的运行结果如下:
那么这就是输入重定向,他可以代替键盘将数据输入到程序里面。
如何理解linux下一切接文件
首先电脑存在很多的硬件比如说键盘,显示器,磁盘,网卡等等,
操作系统为了管理这些外设会给这些外设创建对应的结构体,在这些结构体里面就记录着外设的各种属性除此之外,外设在使用的时候肯定得进行各种输入输出,所以在外设里面就一定存在着各种输入输出函数,但是每个外设的功能是不一样,所以每个外设所对应的输入输出函数也不一样,我们每个外设所对应的输入输出函数和结构体组合在一起称之为驱动:
每个外设的输入输出函数不一样对应的各种属性也不一样,所以操作系统为了更好的管理这些文件的结构体就会再创建一个结构体file来管理驱动层,在file结构体里面就记载着文件的各种属性,比如说int type这个type表示的就是文件所对应的类型,比如说磁盘文件对应的就是1 ,网卡之类的文件就是2等等,还有int status表示的就是每个文件所对应的状态等等:并且在结构体里面还存在着很多与外设读函数写函数有关的函数指针,这些指针指向的就是不同的外设的写函数和读函数,
这样操作系统要是想访问外设的话就不用去找每个外设所对应的结构体,而是直接通过file结构体来访问外设,当操作系统要用到外设的读写方法时就会直接通过file结构体里面的函数指针来找到对应的读写函数,那么这就是一个先描述再组织的方法,所以站在struct file的上层看来所有的外设和文件都是struct file,所以操作系统管理打开的文件或者外设的本质就是管理操作系统中的每个struct file结构体,所以linux下一切皆文件。
子进程和重定向的问题
父进程没有创建子进程时对应关系如下:
当父进程要创建子进程时操作系统不仅会为子进程创建task_struct(PCB),因为进程的独立性还会为子进程创建files_struct结构体,因为子进程的files_struct结构体是以父进程的为模板进行创建的,所以子进程的files_struct也会指向与父进程同样的stdin stdout srderr 的file结构体
那么这里就有个问题?在创建子进程的过程中需要给右边的方括号的内容也拷贝一份吗?答案是不用的,左边方括号的内容属于进程体系,而右边方括号的内容是属于文件系统,创建子进程的过程中要保护进程的独立性跟文件系统没有关系,所以这里不需要再为子进程创建对应的标准输入,标准输出,标准错误结构体,并且子进程发生重定向时也不会影响到父进程,因为父子进程都有个属于自己的file_struct结构体,所以重定向时不会相互影响也间接保护了进程的独立性。那么这就是本篇文章的全部内容希望大家能够理解。