![](https://img.haomeiwen.com/i5128488/1cfd48a91b1e3025.jpg)
一、前言
在 Linux 操作系统中,将一切看作是「文件」。
比如,普通文件、目录文件、链接文件、字符设备文件(如键盘、鼠标、打印机等)、块设备文件(如硬盘、光驱等)、套接字等等。
内核则是利用文件描述符来访问文件。
二、文件描述符
2.1 基本认识
文件描述符(File descriptor,fd)在形式上是一个「非负整数」。每打开或创建一个文件,内核都会返回一个文件描述符,该文件描述符将最终对应上被打开或被创建的那个文件。
根据 POSIX 标准要求,每次打开文件时,必须使用当前进程中最小可用的文件描述符号码。
通常情况下,文件描述符为 0、1、2 的,有着特定的含义:
文件描述符 | 用途 | POSIX 名称 | stdio 流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
因此,此时再打开一个文件,它的文件描述符将会返回 3。以此类推。
2.2 文件描述符的限制
我们知道,进程启动时需要占用内存的,其中一部分内存分配给了文件描述符。因此,我们可以猜到每个进程可打开文件数是有限制的。你是否遇到过「Too many open files」的情况,很大可能就是因为打开文件数超过了进程最大可打开数所导致的。
每个操作系统最多可打开文件数是不同的,此处不展开介绍了,有兴趣自行了解。
ulimit
ulimit
主要是用来限制「进程」对资源的使用情况的,支持各种类型的限制。
# 查看进程允许打开的最大文件句柄数
$ ulimit -n
256
# 设置进程允许打开的最大文件句柄数
$ ulimit -n <num>
请注意,使用 ulimit -n
设置仅在当前进程生效,因此它属于进程级别的控制。
...
2.3 进程级别的文件描述符表
当一个 Linux 进程启动后,内核会创建一个进程控制块(Process Control Block,PCB),里面维护着一个「文件描述符表,File descriptor table」,用于记录当前进程所有可用的文件描述符,即当前进程所有打开的文件。
因此,文件描述符表是进程级别的,每个进程都会有一个。
文件描述符表的每个条目包含两个域:
- 控制标志(fd flags)
- 文件指针(file pointer):指向打开文件表对应条码的指针。
![](https://img.haomeiwen.com/i5128488/02d520db2c423414.png)
实际上,文件描述符就是文件描述符表的索引。
2.4 系统级别的打开文件表
系统内核对所有打开的文件都有一个系统级别的文件描述符表(Open file description table),也称为打开文件表(Open file table)。该表的每个条目称为文件句柄(File handle),它存储了与一个打开文件相关的全部信息。
- 当前文件偏移量(file offse)
- 打开文件时的标识(status flags)
- 文件访问模式
- 与信号驱动相关的设置
- 当前文件 inode 指针
- 文件类型和访问权限
- 指向该文件所持有的锁列表指针
- 还有文件的各种属性...
![](https://img.haomeiwen.com/i5128488/e2311e1602d532cf.png)
2.5 系统级别的 i-node 表
一个文件系统只有一个 i-node 表。想要真正读写文件,还要通过打开文件标的 i-node 指针进入 i-node 表。
i-node 表的每个条目包含了以下信息:
- 文件类型,例如普通文件、套接字或 FIFO 等。
- 文件大小
- 文件锁
- 文件时间戳,比如 mtime、atime、ctime。
![](https://img.haomeiwen.com/i5128488/81277256e368c083.png)
2.6 三个表之间的关系
文件描述表在每个进程中都会有且仅有一个,而打开文件表和 i-node 表,它们在整个文件系统中只有一个。
三者关系如下:
![](https://img.haomeiwen.com/i5128488/9168fff610143208.png)
关系图源自《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 操作。
2.7 小结
基于前面的介绍,可知:
- 同一进程的不同文件描述符可以指向同一文件。
- 不同进程可以拥有相同的文件描述符。
- 不同进程的同一文件描述符可以指向不同的文件(比较特殊的是文件描述符
0
、1
、2
分别对应标准输入、标准输出、标准错误)。 - 不同进程的文件描述符也可以指向同一文件。
未完待续...
网友评论