引言
在《Linux 系统编程手册》5.4 节,关于文件描述符和打开的文件关系是这样描述的:「内核为所有打开的文件维护了一个系统级的描述表(open file description table),有时也称之为打开文件表(open file table),并将表中的每个条目称为打开文件句柄(open file handle)。而针对每个进程,内核又为其维护了打开文件的描述符表(open file descriptor table)」。
后来在这篇文章 Linux 文件句柄的这些技术内幕,只有 1% 的人知道 中,看到了这样的的描述:「简单来说,每个进程都有一个打开的文件表(fdtable)。表中的每一项是struct file类型,包含了打开文件的一些属性比如偏移量,读写访问模式等,这是真正意义上的文件句柄」。
那么这里提到的「打开的文件表」又是什么呢,怎么又变成了每个进程都有的呢?《Linux 系统编程手册》中不是说「打开文件表(open file table)」是独立于进程的系统级表吗?是不是觉得有点困惑和矛盾呢?
为了能够解答困惑,加深对文件描述符的理解,特地深扒了下 Linux 内核的相关源码。接下来,我们将会看到上文提到的描述表(open file description table)、打开文件表(open file table)、打开文件句柄(open file handle)这三种抽象的数据结构具体是怎么实现的?以便能够更好地理解书中的概念。
文件描述符与打开的文件关系
在区分这些概念前,我们先来看看 open()
系统调用的 man page
中提到的一段说明:
A call to open() creates a new open file description, an entry in the
system-wide table of open files. The open file description records
the file offset and the file status flags (see below). A file
descriptor is a reference to an open file description; this reference
is unaffected if pathname is subsequently removed or modified to
refer to a different file…
在看完上面的介绍后,结合《Linux 系统编程手册》提到的名词,我们可以作出这样的映射:
- open file description: 打开的文件句柄(open file handle),它才会关联到真正地文件 inode
- table of open files: 就是书中提到的系统级描述表(open file table)
- file descriptor: 其实就是一个针对文件句柄的引用
好啦,下面来看看与文件描述符相关的实现细节,并了解几个重要的系统调用 实现。
实现细节
以下代码摘自 Linux Kernel 5.4,考虑到内核代码非常复杂,处理细节也很多,这里并没有把每个函数或数据结构所有代码都贴出来,只保留了一些和本文焦点有关的代码行。
Linux 内核中相关的数据结构
每个进程都关联指向了一个 files_struct
,即打开的文件信息:
1 | struct task_struct { |
那么,打开的文件信息长什么样呢?
1 | struct files_struct { |
而关于 fdtable (这个可以理解为进程独立的打开文件的描述符表(open file descriptor table))的定义如下:
1 | struct fdtable { |
再来看看文件句柄(即 open file description)是什么?它维护了和打开文件有关的重要信息:
1 | struct file { |
画了一个图,方便了解上述数据结构关系:
三个重要的系统调用实现
open
open()
系统调用会分配新的文件句柄(file description),用来维护与打开文件相关的元信息(如偏移量、路径、操作方法等),并会给进程返回一个文件描述符(其实就是个小整数)。它对应的实现流程如下:
1 | // fs/open.c |
dup
dup()
系统调用实际上是会分配一个新的文件描述符,但是底层还是会指向传入的文件描述符关联的文件句柄(file description)。
1 | // fs/file.c |
close
close()
系统调会回收文件描述符,同时会给文件描述符指向的文件句柄(file description)的引用计数减 1,并在需要的时候进行回收。该系统调用的实现流程总结如下:
其对应的代码实现如下:
1 | SYSCALL_DEFINE1(close, unsigned int, fd) |
总结
本文介绍了下文件描述符和打开文件的关系,并简要讲解了 Linux 内核中关于这些抽象概念的具体实现。同时,还简单介绍了下 open()
, dup()
和 close()
系统调用的实现。相信在看完这些后,能够更加深入和清晰地理解这三个概念:系统级打开文件表(open file table)/描述表(open file description table)、文件句柄(open file handle)/文件描述(file description)以及文件描述符(file descriptor)。
最后,回答下本文开头提到的问题。其实,站在进程的角度来看,作者在 Linux 文件句柄的这些技术内幕,只有 1% 的人知道 中提到「每个进程都有一个打开的文件表(fdtable)」这样的说法其实也没什么问题。只是此处打开的文件表(fdtable)和《Linux 系统编程》中提到的系统级打开文件表(open file table)并非一个概念。作者提到的打开的文件表(fdtable)其实正是抽象的进程文件描述符(file descriptor)表。当然,我们没必要为此纠结,咬文嚼字也没什么意义,不过对于困惑的东西还是能够搞清楚才好。