文件描述符表
1. 文件描述符
1.1 前言
环境:Linux 2.6
在 Linux 中,有句话叫做「一切皆文件」,指的是在 Linux 中的设备,资源等几乎一切资源都抽象成了文件,然后只需要提供对文件进行操作的接口,就可以让我们用统一的方式来读取,写入等各种操作,从而来管理 Linux 中的各种资源和数据。这种设计模式不仅简化了 Linux 架构,还简化了开发人员对资源的操作
1.2 理解文件
当进程打开一个文件的时候,操作系统会为其分配一个文件描述符(先了解,下文讲),当我们获取这个文件描述符之后,就可以对这个文件进行读写等各种操作了
并且这个文件被打开后,在操作系统内核中会为其创建一个结构体 struct file
来进行管理(这和 C 语言的 FILE
结构体不一样),它记录了文件的状态,读写位置等文件信息,它是在内核空间中的,而 C 语言的 struct FILE
是在用户空间中的
并且在 struct file
中存在一个指向缓冲区的指针,可以将数据暂存在缓冲区中,而文件通常会被划分成若干个页(4KB),在打开文件 / 读取文件的时候,操作系统会读取相应的块到该文件的缓冲区中
1.3 文件描述附表
- 在
Linux
中,进程被描述成task_struct
进行管理(PCB),而task_struct
中有个指针struct files_struct* files
,该指针负责描述该进程的文件相关数据信息
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
...
struct files_struct *files; // 负责描述进程的文件信息数据
...
}
- 而
struct files_struct* files
指向的结构体中有一个成员struct file* fd_array
,就是「文件描述符表」(新版本的内核版本可能称为fdt
)
- 这个文件描述符表,的下标(准确来说是索引)就称为「文件描述符」,而文件描述符表下标对应的对象
fd_array[i]
就是struct file*
,是指向内核中用来描述被打开文件的结构体 - 并且在修改相关配置的情况下,一个进程默认可以打开 32 或者 64 个文件,在修改的情况下,可以达到 10w 个,也就是文件描述符表的大小可以达到的大小
注意:
- 每个进程都会有文件描述符表,并且子进程的创建也会拷贝父进程的文件描述符表,打开相应的文件
综上所述,这里画个图:
⭐实际上,Linux 在操作文件时,大部分是通过文件描述符表中的下标来进行操作的,而这个下标就是文件描述符 fd
,这个很重要,比如 wrtie
,read
,send
等系统调用接口都是对 fd
进行操作的。
1.4 打开文件时
当进程打开一个文件的时候,在内核中,会创建这个文件对应的 struct file
,然后在文件描述符表中分配一个没有被使用下标 chosenIndex
,然后让 fd_array[chosenIndex]
中填入这个 struct file
的地址,再将这个下标 chosenIndex
返回给用户
- 分配原则:文件描述符表会从头开始遍历,找到一个最小,没被使用的文件描述符并返回。
- 在 Linux 中,大部分对文件的操作,都是对文件描述符
fd
作操作
1.5 默认打开的三个文件
Linux 中所有的进程都会默认打开 3 个文件,分别是 stdin
(标准输入),stdout
(标准输出),stderr
(标准错误),它们所占用的文件描述符分别是 0,1,2
一般情况下,标准输入就是键盘,标准输出和标准错误都是和显示器绑定,所以向标准输出和标准错误中输出,都会在显示屏上显示
标准输出一般是接收程序的正常打印结果,标准错误一般是接收程序的错误或者异常结果
这里可以试试,我直接往 1 号和 2 号文件描述符中写入数据:
可以看出,向 1 号和 2 号打印的数据都往显示屏上打印了,
所以我们在程序中,打开文件获取到的文件描述符通常是从 3 开始
2. 重定向
2.1 瞅瞅
先了解什么是重定向:是一种改变输入源或者输出目标的方式,允许就输入或输出从默认的位置转移到其他位置。还是晦涩的话,这么理解就好了:本来应该写到 xxx 中,现在写到了 yyy 中
举例子:echo
是向显示屏中打印数据:
这直接打印到了屏幕上对吧
我们现在使用 >
,它在 Linux
命令行中是输出重定向(类似的>>
是追加重定向)
现在我将上面那段输出 重定向 到文件中:
可以看出本来是打印到显示屏上的,现在写到了 hello.c
文件中,这也就是重定向了
2.2 dup2
首先我们先了解关于重定向的一个接口 dup2
这个接口要求传入两个文件描述符,这个函数的作用简单来说可以这么理解:
- ⭐
fd_array[newfd] = fd_array[oldfd]
,就是将文件描述符表中,oldfd
对应的内容拷贝给newfd
对应的位置。也就是会有两个文件描述符指向同一个文件
比如下面这个代码,我打开了一个文件,然后执行 dup2(fd, 1)
,那么文件描述符表发生了啥 o.0?
int main()
{
int fd = open("hello.c", O_RDONLY);
if (fd < 0) return 0;
dup2(fd, 1);
return 0;
}
如下图,
- 打开文件的时候,在内核为这个文件创建
struct file
数据结构,然后为其分配文件描述符,并放到fd_array
中管理,再返回文件描述符 - 将
fd_array[1]
中的指针指向这个文件的struct file
中
⭐关键点就是将 fd_array[newfd]
的指针改成成指定的 struct file
然后看看下面代码演示一下:重定向之后,原本写在 1 号文件(也就是显示屏)中的数据现在写到了文件 hello.c
文件中。
其实到这里,重定向的实现原理就七七八八了,我们下面再梳理一下应该就晓得了
2.3 实现原理
- 当执行
dup2(fd, 1)
的时候,将文件描述符fd
的内容拷贝给1
号,这时候fd
和1
对应的指针都指向hello.c
- 文章开头说过,Linux 中大部分对文件的操作都是对文件描述符的操作
- ⭐在这个例子中,
write / read
同样是对文件描述符作操作,通俗点,就是操作系统只会对fd
下标操作,并不关心fd_array[fd]
里面是谁 - 所以当执行
write(1, str, strlen(str))
的时候,操作系统就直接往 1 号下标对应的文件进行写入了,它并不关心 1 号下标的文件是不是stdout
(强调) - 所以最终表现出来的情况就是「本该向显示屏中输出,现在输出到了
hello.c
文件中」
3. 一切皆文件
众所周知,电脑可以有很多外设,比如键盘,鼠标,声卡,音响,显示屏,网卡,磁盘…,而 Linux 怎么将这些看起来就离谱的硬件也抽象成文件的
这些外设,大都有自己的读操作和写操作,而 Linux 不应该专门为每个硬件设计相应的读写方法,甚至设计相应的数据结构,这样不便于维护和管理,而且很麻烦,如果统一起来就好了
- 实际上在操作系统和硬件之间,还有一层软件层 —— 驱动,每个硬件通常都有配置一个特定的驱动,驱动可以和硬件交互控制,也可以将操作系统的指令翻译成硬件可以理解的指令 / 命令
- 所以驱动可以理解成操作系统和硬件交互的桥梁
它可以为操作系统提供统一的接口(比如读写接口,下文也以读写接口举例子),借此操作系统可以实现对硬件的间接控制,以及完成两者之间的交互
还记得 struct file
结构体吗,前面说了这是一个操作系统内核中描述 / 管理文件的结构体,那么要怎么利用这个结构体来将底层硬件结合 / 统一起来?
可以在 struct file
中提供若干个函数指针,比如 ssize_t (*read)()
,ssize_t (*write)()
,然后这些函数指针直接指向驱动层提供的读写接口,指针不必关心自己到底指向了哪个函数,对于 Linux 来说,它只知道只要调用 struct file
中的 write
函数,就一定会执行对应驱动提供的写操作接口,从而以管理文件的方式,实现从底层硬件的控制