上一篇文章中,我们还遗留了一些问题,后面的文章会一一解决。

这篇文章,先来解决:文件描述符。

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>

int main()
{
    close(1);

    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 

    printf("%d\n", fd1);

    // 关闭文件
    fflush(stdout);
    close(fd1);

    return 0;
}

// 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;
// }

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐