
在 Linux 操作系统中,将一切看作是「文件」。
比如,普通文件、目录文件、链接文件、字符设备文件(如键盘、鼠标、打印机等)、块设备文件(如硬盘、光驱等)、套接字等等。
内核则是利用文件描述符来访问文件。
文件描述符(File descriptor,fd)在形式上是一个「非负整数」。每打开或创建一个文件,内核都会返回一个文件描述符,该文件描述符将最终对应上被打开或被创建的那个文件。
根据 POSIX 标准要求,每次打开文件时,必须使用当前进程中最小可用的文件描述符号码。
通常情况下,文件描述符为 0、1、2 的,有着特定的含义:
| 文件描述符 | 用途 | POSIX 名称 | stdio 流 |
|---|---|---|---|
| 0 | 标准输入 | STDIN_FILENO | stdin |
| 1 | 标准输出 | STDOUT_FILENO | stdout |
| 2 | 标准错误 | STDERR_FILENO | stderr |
因此,此时再打开一个文件,它的文件描述符将会返回 3。以此类推。
我们知道,进程启动时需要占用内存的,其中一部分内存分配给了文件描述符。因此,我们可以猜到每个进程可打开文件数是有限制的。你是否遇到过「Too many open files」的情况,很大可能就是因为打开文件数超过了进程最大可打开数所导致的。
每个操作系统最多可打开文件数是不同的,此处不展开介绍了,有兴趣自行了解。
ulimit
ulimit 主要是用来限制「进程」对资源的使用情况的,支持各种类型的限制。
# 查看进程允许打开的最大文件句柄数
$ ulimit -n
256
# 设置进程允许打开的最大文件句柄数
$ ulimit -n <num>
请注意,使用 ulimit -n 设置仅在当前进程生效,因此它属于进程级别的控制。
当一个 Linux 进程启动后,内核会创建一个进程控制块(Process Control Block,PCB),里面维护着一个「文件描述符表,File descriptor table」,用于记录当前进程所有可用的文件描述符,即当前进程所有打开的文件。
因此,文件描述符表是进程级别的,每个进程都会有一个。
文件描述符表的每个条目包含两个域:

实际上,文件描述符就是文件描述符表的索引。
系统内核对所有打开的文件都有一个系统级别的文件描述符表(Open file description table),也称为打开文件表(Open file table)。该表的每个条目称为文件句柄(File handle),它存储了与一个打开文件相关的全部信息。

一个文件系统只有一个 i-node 表。想要真正读写文件,还要通过打开文件标的 i-node 指针进入 i-node 表。
i-node 表的每个条目包含了以下信息:

文件描述表在每个进程中都会有且仅有一个,而打开文件表和 i-node 表,它们在整个文件系统中只有一个。
三者关系如下:

关系图源自《The Linux Programming Interface》一书。图示说明如下:
- 在进程 A 中,文件描述符 1 和 20 都指向了打开文件表中的索引为 23 的句柄,这可能是调用了 dup()、dup2()、dcntl()、或者对同一个文件多次调用了 open() 函数形成的。
- 进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个打开文件表句柄,可能是因为调用 fork() 后出现的,子进程会继承父进程的打开文件描述符表,也就是子进程继承了父进程的打开文件。或者是某进程通过 Unix 域套接字将一个打开的文件描述符传递给另一个进程。或者不通过进程独自调用 open() 函数打开同一个文件是正好分配到与其他进程打开该文件描述符一样。
- 进程 A 的文件描述符 0 和进程 B 的文件描述符 3 分别指向了不同的打开文件句柄,但这些句柄均指向 i-node 表相同的条目(即同一个文件),发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。
从图中可知,我们可以将文件描述符理解为进程中文件描述符表的「索引」,或者把文件描述符表看作为一个数组,那么文件描述符就是数组的下标。
当进行 I/O 操作时,会传入 fd 作为参数,先从当前进程文件描述符表查找该 fd 对应的条目,得到文件指针。根据文件指针,在打开文件表取出对应的那个已经打开的文件句柄,得到 inode 指针。根据 inode 指针,在 i-node 表中找到对应条目,从而定位到文件真正的位置,然后进行 I/O 操作。
基于前面的介绍,可知:

