文件I/O是操作系统不可或缺的部分,也是实现数据持久化的手段。对于Linux来说,其“一切皆是文件”的思想,更是突出了文件在Linux内核中的重要地位
1. 文件、文件描述符和文件表
Linux内核将一切视为文件,那么Linux的文件是什么呢?其既可以是事实上的真正的物理文件,也 可以是设备、管道,甚至还可以是一块内存。狭义的文件是指文件系统中的物理文件,而广义的文件 则可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核 中,对外提供一致的文件操作接口。
从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就 是一个文件句柄。那么何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描述符后,可以通过它得到用于管理文件的真正的数据结构。
使用文件描述符即句柄,有两个好处:一是增加了安全性,句柄类型对用户完全透明,用户无法通过任何hacking的方式,更改句柄对应的内部结果,比如Linux内核的文件描述符,只有内核才能通过该值得到对应的文件结构;二是增加了可扩展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时发生变化,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代 码。
Linux的每个进程都会维护一个文件表,以便维护该进程打开文件的信息,包括打开的文件个数、每个打开文件的偏移量等信息。
2. 内核文件表的实现
内核中进程对应的结构是task_struct,进程的文件表保存在task_struct->files中。其结构代码如下所示:
源码地址: https://github.com/torvalds/linux/blob/master/include/linux/fdtable.h
/*https://github.com/torvalds/linux/blob/master/include/linux/fdtable.h * Open file table structure */struct files_struct {
/* * read mostly part */ /* count 为文件表files_struct的引用计数*/ atomic_t count; bool resize_in_progress; wait_queue_head_t resize_wait; /*文件描述符表*/ /* 为什么两个fdtable呢? 这是内核的一种优化策略, fdt为指针,而fdtab为普通变量,一般情况下, fdt是指向fdtab的,当需要它时候,才会真正动态申请内存,因为默认大小的文件表足以应付大多数情况,因此这样就可以避免频繁的内存申请。 这也是内核的常用技巧之一,在创建时,使用普通的变量或者数组,然后让指针指向它, 作为默认情况使用, 只有当进程使用量超过默认值时候,才会动态申请内存*/ struct fdtable __rcu *fdt; struct fdtable fdtab; /* * written part on a separate cache line in SMP */ /*使用____cacheline_aligned_in_smp可以保证file_lock是以cache line 对齐的,避免了false sharing */ spinlock_t file_lock ____cacheline_aligned_in_smp; /*适用于查找下一个空闲的fd*/ unsigned int next_fd; /*保存执行exec需要关闭的文件描述符的位图*/ unsigned long close_on_exec_init[1]; /*保存打开的文件描述符的位图*/ unsigned long open_fds_init[1]; unsigned long full_fds_bits_init[1]; /*fd_array为一个固定大小的file结构数组, struct file是内核用于文件管理的结构, 这里使用默认大小数组, 就是为了可以涵盖大多数情况, 避免动态分配*/ struct file __rcu * fd_array[NR_OPEN_DEFAULT];};
下面看看files_struct是如何使用默认的fdtab和fd_array的,init是Linux的第一个进程,它的文件表是一个全局变量,代码如下:
init_files.fdt和init_files.fdtab.fd都分别指向了自己已有的成员变量,并以此作为一个默认值。后面的进程都是从init进程fork出来的。fork的时候会调用dup_fd,而在dup_fd中其代码结构如下: