文章目录
- 1. open的返回值
- 2. 文件描述符
- 2.1 看看现象
- 2.2 谈谈原理+看源码(什么是文件描述符)
- 2.3 补充了解(缓冲区)
- 2.4 文件描述符的分配规则
- 3. 代码
上一篇文章中,我们还遗留了一些问题,后面的文章会一一解决。
这篇文章,先来解决:文件描述符。
1. open的返回值
上一篇文章我们已经学过open系统调用的用法了,但是open的返回值我们并没有详细介绍
open的返回值:
成功:返回最小的未使用的文件描述符(非负整数)。
失败:返回 -1,并设置 errno 以指示错误原因。
那这篇文章我们就要来解开这个疑惑了
到底什么是文件描述符呢?
又为什么是一个非负整数呢?
2. 文件描述符
2.1 看看现象
再来看我们上篇文章的那段代码:
open的返回值不就是给打开的文件分配的文件描述符吗?(后续的操作如write、close都要使用这个文件描述符)
那我们把这个返回值打印出来看看他是几?
运行
我们看到是3,确实是一个非负整数,但是为什么是3呢?
我们可以再多打开几个文件看看:
我们来重新写一个代码
依次打开4个文件,并打印它们的文件描述符
看看结果
是3,4,5,6
目前来看,我们打开的文件,文件描述符好像是从3开始,依次递增的整数。
那为什么是这样呢?这些数字又到底是什么呢?
2.2 谈谈原理+看源码(什么是文件描述符)
一个进程可以打开多个文件,那在操作系统内,就可能同时存在多个进程,它们一共打开了很多的文件。并且文件和进程之间还有一定的从属关系。
那这么多的进程,要不要被操作系统管理起来呢?
当然!
如何管理?
先描述,再组织!
在 Linux 内核中,使用struct file结构体 来描述一个被进程打开的文件。
每个 open 系统调用成功时,内核都会创建一个 struct file 实例,并返回一个文件描述符(fd)与之关联。
struct file内部,会直接或间接地包含与被打开文件相关的属性和数据等信息。
多个struct file之间,用一个双向循环链表组织起来。
那么这样,对文件的管理,就转换成了 对链表的增删查改。
那这么多的文件,如果表示它们与进程之间的从属关系呢?(哪些文件属于哪个进程打开的)
在进程的task_struct中,有一个结构体指针——
struct files_struct* files
该指针指向一个struct files_struct结构体,该结构体中有一个成员struct file * fd_array[];,是一个struct file *的结构体指针 数组,那里面的元素不就指向一个个的struct file结构体嘛(就对应了该进程打开的所有文件)
那既然是数组的元素,就有下标啊。
每个文件的struct file*指针 的下标就是该文件的文件描述符。
进程每打开(open)一个文件,就会把对应的struct file结构体的指针存入fd_array数组中(找一个最小的且没被占用的下标位置),这也解释了为什么文件描述符是非负整数。
然后返回其对应的下标(即open的返回值),也是该文件的文件描述符。
所以操作系统内部,是使用文件描述符来唯一标识一个被特定进程打开的文件的,这也是为什么我们后面使用write、close这些系统调用必须要传文件描述符(这里也能推断出C语言中的FILE中必定封装了文件描述符fd)。
那为什么我们打开的文件,文件描述符是从3开始呢?也就明白了!
因为进程启动时候,默认打开了三个文件(标准输入、标准输出、标准错误),占用了0,1,2下标,所以我们再打开新的文件,下标就从3开始。
当然我们也可以打印出来标准输入、标准输出、标准错误对应的文件描述符来看看:
上面我们提到C语言中的FILE中必定封装了文件描述符fd
我们可以来看下FILE的定义
而:
它们的类型是FILE*,那我们通过->不就直接可以访问其中的文件描述符成员嘛!
所以,我们运行程序,前三个就应该是0,1,2
没有问题!
在C++中,cin、cout、cerr分别对应标准输入流对象、标准输出流对象、标准错误流对象,它们的类定义中,必定也有一个成员变量是文件描述符。
2.3 补充了解(缓冲区)
之前的文章中我们简单提过用户级缓冲区:
用户级缓冲区是在 FILE 结构体内部维护的。
那其实除了用户缓冲区还有内核缓冲区
同样这篇文章中我们提到:
我们使用printf打印一个字符串,并不是直接输出到显示器上的,而是先会暂存到缓冲区,等待合适的时机刷新到显示器“文件中”(inux下一切皆文件)。
现在应该再完善一点:
我们使用fwrite写入一个字符串到某个文件(以fwrite为例),并不是直接写入到文件的,而是先会暂存到用户缓冲区,即先从用户空间(比如一个字符数组存的字符串/一个常量字符串存在常量区,都在用户空间)到用户缓冲区,fwrite 最终会调用 write 系统调用,write系统调用会把数据从用户缓冲区拷贝到内核缓冲区,然后等待合适的时机,再刷新到文件中(比如内核缓冲区满了),这样可以减少磁盘IO次数,提高效率。
那如果使用的本身就是write这些系统调用,那就是先从用户空间拷贝到内核缓冲区,后面一样。
所以write这些接口,并不是直接把数据写入文件,其本质是一个拷贝函数,最终数据刷新到文件是合适的时机下由OS完成的。
两层缓存:C 库缓冲减少系统调用,内核页缓冲减少磁盘 I/O。当然这两层缓存都要放在内存中,内存是 CPU 可以直接访问的,这符合冯·诺依曼体系结构。
那同样地,读文或修改文件内容也都需要经过对应的缓冲区。
(先了解一下,后面我们还会谈缓冲区)
2.4 文件描述符的分配规则
其实上面已经讲完这个规则了:
新分配的文件描述符,永远是fd_array数组(也叫文件描述符表)中,最小的且没被占用的下标
我们也可以再来多做一些验证:
我们正常打开一个文件,文件描述符就从3开始分配,因为0,1,2被占用了。
那我们调用close可以关闭我们自己打开文件的描述符,当然也可以关闭0,1,2
那我现在把文件描述符0关闭,然后打开一个文件,分配的规则不是分配最小的且没被占用的下标嘛。
那现在我打开的这个文件就应该分配到0,因为现在0没被占用,并且最小
把2关掉
那就是2了。
那把1关掉呢?
那这次运行就应该打印1
但是!
这么什么都没打印呢?
🆗,1对应的是什么啊?
是标准输出(显示器)!
而printf为什么默认往显示器打印啊?
其实是因为printf底层固定是往文件描述符1对应的文件中打印的。
但是我们现在把1关闭了,然后新打开一个文件,所以
所以文件描述符1就不再指向标准输出,而是指向我们打开的log1.txt文件。
所以我们打印的信息就不会写入到显示器了,而是写到我们自己新打开的文件了。(我们把这个叫做重定向,后面会详细讲解重定向)
我们来看一下
怎么回事,没有啊
但是,为什么这个文件里也没有呢?
修改代码
最后的close注释掉
然后重新运行
这次文件里面就被写入了1。
怎么回事?为什么close注释掉就有了
或者可以这样
把close放开,但是close之前调用一下fflush
也可以(现在是一个追加写的模式)
那出现这种现象的原因是什么呢?
这个问题先留着,我们后面会解决
这篇文章先到这里,下一篇——重定向
3. 代码
#include<stdio.h>#include<sys/types.h>#include<unistd.h>#include<string.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(1);intfd1=open("log1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);printf("%d\n",fd1);// 关闭文件fflush(stdout);close(fd1);return0;}// int main()// {// printf("stdin->%d\n", stdin->_fileno);// printf("stdout->%d\n", stdout->_fileno);// printf("stderr->%d\n", stderr->_fileno);// int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// printf("%d\n", fd1);// printf("%d\n", fd2);// printf("%d\n", fd3);// printf("%d\n", fd4);// // 关闭文件// close(fd1);// close(fd2);// close(fd3);// close(fd4);// return 0;// }