在Unix世界里,文件系统的底层机制并不复杂。从源码实现细节入手,我们来看看一个文件从创建到被打开,磁盘与内存之间究竟经历了哪些关键步骤。需要说明的是:本文基于经典V7 Unix实现进行解析,但其核心设计理念至今仍被现代操作系统沿用。
首先,当一个文件尚未被打开时,它在磁盘上需要以下三部分内容:一个目录项、一个磁盘Inode结构、以及若干磁盘块(n可以为0)。
目录项是一个轻量级数据结构,其中最关键的两个字段分别是:文件名和指向inode的指针。目录的执行权限本质上就是允许系统将文件名转换为inode编号,完成这项转换的核心函数是namei。具体实现细节可参考Unix源码,这里不再展开。
磁盘Inode结构在V7中定义如下:
struct dinode
{
unsigned short di_mode; /* 文件类型与权限位 */
short di_nlink; /* 硬链接数量 */
short di_uid; /* 文件所属用户ID */
short di_gid; /* 文件所属组ID */
off_t di_size; /* 文件字节大小 */
char di_addr[40]; /* 磁盘块地址列表 */
time_t di_atime; /* 最后访问时间 */
time_t di_mtime; /* 最后修改时间 */
time_t di_ctime; /* 创建时间 */
};
各字段含义已通过注释说明,但有几个要点值得深入理解:di_mode不仅包含权限信息,还标识文件类型(普通文件、目录、设备等);di_nlink记录有多少个目录项指向该inode;di_addr是inode的核心字段,它存储了文件数据所占用的磁盘块地址数组,V7使用40字节来存储这些地址,实际实现中每个块通常为512字节;三个时间戳分别对应访问时间、修改时间和属性变更时间。
磁盘块在物理层面是连续的扇区,逻辑上则按固定大小组织成块,便于文件系统管理。
当系统引用一个文件时(注意:此时尚未执行open系统调用),需要将磁盘inode加载到内存中。V7的内存inode结构定义如下:
struct inode
{
Char i_flag;
char i_count; /* 引用计数(当前活跃引用数) */
dev_t i_dev; /* 该inode所在的设备号 */
ino_t i_number; /* inode编号,与设备号唯一对应 */
unsigned short i_mode;
short i_nlink; /* 目录项数量 */
short i_uid; /* 属主用户ID */
short i_gid; /* 属主组ID */
off_t i_size; /* 文件大小 */
union {
struct {
daddr_t i_addr[NADDR]; /* 普通文件/目录的磁盘块地址 */
daddr_t i_lastr; /* 上次读取的逻辑块号(用于预读优化) */
};
struct {
daddr_t i_rdev; /* 设备号,复用i_addr[0]字段 */
struct group i_group; /* 多路复用组文件 */
};
} i_un;
};
与磁盘inode相比,内存版本增加了运行时所需的字段,其中最重要的就是引用计数i_count,它记录了当前有多少进程或内核路径指向该inode。当i_count归零时,该内存inode才可以被回收。
最后,当文件被真正打开时,还需要引入以下两层结构:
- 用户进程的u区中的
u_ofile数组项:该数组存储指向file结构的指针,所谓文件描述符,本质上就是该数组的下标索引。 - 系统文件表中的一项:即
struct file,V7中的定义如下:
struct file
{
char f_flag;
char f_count; /* 引用计数(当前引用该file结构的进程数) */
struct inode *f_inode; /* 指向内存inode的指针 */
union {
off_t f_offset; /* 读写指针偏移量 */
struct chan *f_chan; /* 多路复用通道指针 */
} f_un;
};
文件表结构最初的设计目的是为了支持偏移量共享(f_offset)。使用dup复制一个文件描述符与再次打开同一个文件,两者的本质区别就在于file结构的f_count:dup会使两个描述符指向同一个file结构,从而共享读写偏移量;而两次独立的open调用则会创建两个独立的file结构,各自维护自己的偏移量。
如果感兴趣,可以结合这些数据结构字段的变化,手动模拟文件打开、关闭、link、unlink等操作流程,这样对Unix文件系统原理的理解会更加透彻。虽然Unix源码非常精炼,但直接阅读仍有一定难度——本文只选取了最核心的线索进行分析。
此外,附上昨晚编写的一个find命令简易实现,支持-name、-user、-group参数。使用方法:Find 目录列表 可选参数。代码水平有限,希望给初学C语言的朋友提供一个参考范例,如有改进意见也欢迎交流指正。
