1. 前言
在前面的两篇文章《Linux 虚拟文件系统(一)–初识inode》和《Linux 虚拟文件系统(二)--挂载文件系统》中,我介绍了 Linux VFS 的一些重要的数据结构。但是没有介绍我们的用户程序是如何跟文件系统打交道的。这一篇文章就来仔细说说这个问题。
2. 文件描述符
在 Linux 上用 C 语言写过代码的朋友们,应该清楚我们使用 clib 标准库中的 open 函数打开一个文件的时候。该函数返回的是一个整数值,一般被称为文件描述符。当需要对这个打开的文件进行读写操作时,则需要将这个文件描述符号作为参数传递给函数。
不知道你是否好奇一个整数值怎么就能代表一个打开的文件了呢?反正我是被这个疑惑困扰过好一段时间。
为了回答上面这个问题,我们来考察下进程描述符结构体 task_struct 的定义。为了减少干扰,我只保留跟我们目标相关的几个字段。
struct task_struct {
....
/* 文件系统信息: */
struct fs_struct *fs;
/* 打开的文件信息: */
struct files_struct *files;
/* 命名空间: */
struct nsproxy *nsproxy;
....
};
进程描述符中用保存 files_struct 了进程打开的文件信息。我们接着看下 files_struct 的定义。files_struct 保存了下一个文件描述符的值,file 字段保存所有打开的文件。其他字段我们暂时忽略。
struct files_struct {
...
struct fdtable fdtab;
// 下一个文件描述符数
unsigned int next_fd;
// 执行exec会关闭的文件集合
unsigned long close_on_exec_init[1];
// 初始化时打开的文件集合
unsigned long open_fds_init[1];
// 文件file 指针数组
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
...
};
内核支持每个进程打开 NR_OPEN_DEFAULT 个文件。如果超过这个值则需要对管理文件信息的各个成员进行扩容以储存更多的信息。主要的扩容对象是 fdtable。我们接着考察 fdtable 的定义。
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
咋一看,这个接口体的字段是不是跟 files_struct 的字段名称是不是有很多重复的,它们之间是不是存在什么联系?它们之间确实存在联系。你仔细看看,fdtable 的字段基本上是指针。而这些指针都是指向 files_struct 相同字段名的地址。
内核之所以要这么设计,并非完全多次一举。主要是为了扩容考虑。如果进程打开 的文件数量超过了 NR_OPEN_DEFAULT ,要进行扩容怎么办?那么是不是要对files_struct 中的字段进行扩容?但是里面的字段都不是指针,受限于 files_struct 自身大小没法为其分配更大的内容,除非修改内核源码将 NR_OPEN_DEFAULT 调大点。当然修改源码这种操作完全是不能接受的,为了兼容性能和可扩容,特意设计了 fdtable 结构体,里面的字段最开始指向 files_struct 结构体的内容。当文件数达到上限时,开辟一块更大的内存让 fdtable 里面的字段指向。从而达到扩容的目的。
说了这么多好像没说文件描述符是干嘛的,有点挂羊头卖狗肉了。其实很简单,文件描述符数就是文件在 files_struct 结构中的 fd_array 数组的下标(没扩容前),或者 fdtab 结构体中 fd 指针数组的下标(扩容前和扩容后都能找到对应的 file 结构)。
通过,clib 函数库传递进来的文件描述符参数,内核可以很快定位到对应的文件结构从而转发 I/O 操作。
4. 参考资料
- 《深入Linux内核架构》
- it.0voice.com/p/t_pc/cour…