一、宏观认识
磁盘的空间一般都非常大,操作系统想要直接进行管理就比较困难,这时候就可以借鉴'分治'的管理思想。
Ext2 文件系统采用分治思想管理磁盘,将分区划分为 Block Group。每组包含超级块、块组描述符、位图、inode 表和数据块。inode 存储文件元数据并通过指针数组映射数据块,支持多级间接索引。路径解析依赖 dentry 结构缓存加速,软硬链接分别基于 inode 计数和路径引用。文件操作本质是对 inode 及数据块的增删改查。

磁盘的空间一般都非常大,操作系统想要直接进行管理就比较困难,这时候就可以借鉴'分治'的管理思想。
比如国家的管理:先划分省,再由省划分市,再由市划分乡镇...,这样只要管理好乡镇,就相当于管理好了市,管理好了市,就相当于管理好了省,以此类推,就管理好了整个国家。
操作系统对磁盘的管理也是这样,先将磁盘进行划区(Partition),然后对一个区引入文件系统进行管理,其他区复制该管理方式即可实现对整盘的管理。本质就是将区格式化为某种格式的文件系统。

上图中启动块(Boot Sector)的大小是确定的,为 1KB,由 PC 标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是 ext2 文件系统的开始。
前面讲到磁盘的最小存储单元是扇区(0.5KB),但如果以扇区为单位向磁盘读写数据就会导致效率太低,所以 Ext2 文件系统以数据块(常见为 4KB,8 个扇区)为单位进行数据的读写。而系统默认的一个 Block Group 包含 8192 个数据块,即 32MB。
以最小数据块为单位访问磁盘,不仅能一次性读写更多数据,还能利用数据的局部性原理提前缓存相关数据,从而显著提高内存缓存命中率和操作系统的整体效率。
所以,Ext2 文件系统将区又划分块进行管理,类似于省分治市。每个块都有相同的结构,最小数据块的大小在格式化时进行配置,常见的块大小有 1KB、2KB、4KB,部分架构下最大支持 8KB,Block Group 的大小根据数据块大小而异。通常情况下块的大小为 4KB(8 个扇区)。
注意:
文件系统最小存储单位为 数据块(假设 4KB),所以在 Block Group 中所有的结构都是以 最小数据块为单位进行存储的。即超级块,位图,inode 节点表,inode 节点,数据块的大小都是以 最小数据块(4KB)为单位的。
存放文件系统本身的结构信息,描述整个分区的文件系统信息。
记录的信息主要有:block 和 inode 的总量,未使用的 block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。
可见其重要性,如果 Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。所以,Super Block 没有放在区分里,而是放在块中,相当于有多个备份,这些 Super Block 中的数据保持一致。
/* * Structure of the super block */ // 超级块的结构定义
struct ext2_super_block {
__le32 s_inodes_count; // 索引节点(inode)总数
__le32 s_blocks_count; // 数据块总数
__le32 s_r_blocks_count; // 保留数据块总数(预留供超级用户/系统使用,防止磁盘占满导致系统异常)
__le32 s_free_blocks_count; // 空闲数据块数量(动态更新)
__le32 s_free_inodes_count; // 空闲索引节点(inode)数量(动态更新)
__le32 s_first_data_block; // 第一个数据块的编号(标记文件系统中数据块的起始位置)
__le32 s_log_block_size; // 数据块大小(以对数形式存储,实际大小=1024×2^该字段值)
__le32 s_log_frag_size; // 碎片大小(以对数形式存储,用于碎片管理)
__le32 s_blocks_per_group; // 每个块组包含的数据块数量
__le32 s_frags_per_group; // 每个块组包含的碎片数量
__le32 s_inodes_per_group; // 每个块组包含的索引节点(inode)数量
__le32 s_mtime; // 最后一次挂载文件系统的时间(时间戳格式)
__le32 s_wtime; // 最后一次对文件系统执行写操作的时间(时间戳格式)
__le16 s_mnt_count; // 累计挂载次数(挂载一次则自增 1)
__le16 s_max_mnt_count; // 最大允许挂载次数(达到该次数后,建议执行 fsck 文件系统检查)
// ...
__le16 s_inode_size; // 单个索引节点(inode)结构体的大小(字节数)
__le16 s_block_group_nr; // 该超级块所属的块组编号(用于备份超级块的识别)
char s_last_mounted[64]; // 最后一次挂载的目录路径(记录该文件系统上次挂载到系统中的哪个目录)
// ...
};
描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。
每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是 inode Table,从哪里开始是 Data Blocks,空闲的 inode 和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
/* * Structure of a blocks group descriptor */ // 块组描述符的结构定义
struct ext2_group_desc {
__le32 bg_block_bitmap; // 该块组中块位图所在的数据块编号(通过该编号可直接找到块位图)
__le32 bg_inode_bitmap; // 该块组中 inode 位图所在的数据块编号(通过该编号可直接找到 inode 位图)
__le32 bg_inode_table; // 该块组中 inode 表的起始数据块编号(通过该编号可直接找到 inode 表的起始位置)
__le16 bg_free_blocks_count; // 该块组中当前的空闲数据块数量(动态更新,创建/删除文件时变化)
__le16 bg_free_inodes_count; // 该块组中当前的空闲 inode 数量(动态更新,创建/删除文件时变化)
__le16 bg_used_dirs_count; // 该块组中当前已创建的目录数量(动态更新,创建/删除目录时变化)
__le16 bg_pad; // 填充字段(用于内存对齐,保证后续字段符合 CPU 访问规范,提升读写效率)
__le32 bg_reserved[3]; // 预留字段(为 Ext2 后续版本扩展预留空间,当前未使用)
};
Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
用一个比特位映射一个数据块(Data Blocks),以 0/1 标记对应的数据块的占用情况。
与块位图类似,Inode Bitmap 的每个 bit 表示一个 inode 是否空闲可用。
文件 = 属性 + 内容,属性数据就存储在 inode Table 中。
存放文件属性 如 文件大小,所有者,最近修改时间等当前分组所有 Inode 属性的集合 inode 编号以分区为单位,整体划分,不可跨分区,说明另一个分区的 inode 编号又从 1 开始。

/* * Structure of an inode on the disk */ // 磁盘上的 inode 结构体定义(即 inode 表中单个节点的结构)
struct ext2_inode {
__le16 i_mode; // 文件类型与访问权限(如普通文件、目录、读写执行权限等,对应 chmod 命令修改的内容)
__le16 i_uid; // 文件所有者的用户 ID(通常 root 为 0,普通用户为递增数值)
__le32 i_size; // 文件大小(以字节为单位,仅对普通文件有效)
__le32 i_atime; // 最后一次访问文件的时间(如 cat 读取文件,不会修改该文件的内容,仅更新此时间)
__le32 i_ctime; // 文件元数据最后修改的时间(如修改文件权限、所有者,会更新此时间;创建文件时初始化)
__le32 i_mtime; // 文件内容最后修改的时间(如 vim 编辑保存文件,会更新此时间,对应 stat 命令查看的 mtime)
__le32 i_dtime; // 文件删除时间(文件被删除时记录此时间戳,未删除时为 0)
__le16 i_gid; // 文件所有者的用户组 ID(对应文件所属的用户组)
__le16 i_links_count; // 硬链接数量(文件的硬链接计数,当计数为 0 时,才会真正释放 inode 和数据块)
__le32 i_blocks; // 该文件占用的数据块总数(注意:此处块通常为 512 字节一个,与 Ext2 的文件系统块不同)
__le32 i_flags; // 文件标志位(用于控制文件的特殊行为,如是否允许追加写入、是否为只读等)
// ...
__le32 i_block[15]; // 数据块指针(直接 + 间接,共 15 个)
// 文件数据块的指针数组(共 15 个指针,分为 3 类:
// 1-12:直接指针,直接指向存储文件内容的数据块
// 13:一级间接指针,指向一个存储数据块编号的间接块
// 14:二级间接指针,指向一个存储一级间接块编号的块
// 15:三级间接指针,指向一个存储二级间接块编号的块)
// ...
};
***补充:***每个 inode 大小均为 128 字节,而一个最小的数据块为 4KB(4096 字节),则一个数据块可以存储 4096 / 128 = 32 个 inode。通过 inode 号就能索引到 inode table。
数据块:存放文件内容,也就是一个一个的 Block。
根据不同的文件类型有以下几种情况:对于普通文件,文件的数据存储在数据块中。对于目录文件,该目录下的所有文件名和目录名存储在所在目录的数据块中(即目录文件的文件内容为文件名和目录名),除了文件名外,ls -l 命令 看到的其它信息保存在该文件的 inode 中。Data Block 号按照分区划分,不可跨分区。
Data Blocks 的大小和最小存储单元数据块的大小一致为 4KB。
/* * Structure of a directory entry */ // 目录项的结构定义
struct ext2_dir_entry {
__le32 inode; // 该文件名对应的 inode 编号(通过该编号可找到对应的 ext2_inode 结构体,获取文件元数据)
__le16 rec_len; // 该目录项的总长度(字节数,用于跳过当前目录项,查找下一个目录项,支持变长对齐)
__le16 name_len; // 文件名的实际长度(字节数,最大不超过 255)
char name[]; // 文件名(变长数组,存储实际的文件名,长度由 name_len 指定,无结束符 '\0')
};
通过 inode 就可以索引到对应文件的数据块。
在 Ext2 文件系统中创建文件并写入数据时:
(1)操作系统先通过 inode 位图找到空闲 inode 节点并标记为已用,再通过块位图分配足够的空闲数据块并完成标记;
(2)随后将数据块编号存入 inode 的 i_block[15] 指针数组,建立 inode 与数据块的关联,把文件数据写入对应数据块并完善 inode 元数据;
(3)最后在所属目录的数据块中新增 ext2_dir_entry 结构体,完成「文件名 - inode 号」的映射,整个创建与写入流程就此完成。
struct ext2_inode {
// ...
__le32 i_block[15]; // 数据块指针(直接 + 间接,共 15 个)
// 文件数据块的指针数组(共 15 个指针,分为 3 类:
// 1-12:直接指针,直接指向存储文件内容的数据块
// 13:一级间接指针,指向一个存储数据块编号的间接块
// 14:二级间接指针,指向一个存储一级间接块编号的块
// 15:三级间接指针,指向一个存储二级间接块编号的块)
// ...
};

对于比较小的文件,inode 直接索引一个数据块进行存储;一级间接块索引表指针指向一个数据块,32 为下一个指针大小为 4 字节,而一个数据块为 4096 字节,则可以存储 4096 / 4 = 1024 个指针,可以索引 1024 个数据块存储文件内容数据,大小为 1024 * 4KB = 4MB;二级间接块索引表指针指向一个数据块,存储 1024 个指针,然后每个指针又指向一个数据块,则可以索引 1024 * 1024 = 1048576 个数据块,大小为 4GB。三级间接块索引表指针最终可以索引 1024^3 = 1073741824 个数据块,存储 4TB 数据。
inode table 中找到对应的 inode 节点。改操作分为两种场景,底层逻辑不同:
touch test.c // 创建文件
ls -i test.c // 查看文件 inode 号

创建一个新文件主要有以下 4 个操作:
删除操作的核心是处理 inode 的链接计数和数据块回收:
结论:分区之后的格式化操作,就是对分区进行分组,在每个分组中写入 SB、GDT、Block Bitmap、Inode Bitmap 等管理信息,这些管理信息统称:文件系统只要知道文件的 inode 号,就能在指定分区中确定是哪一个分组,进而在哪一个分组确定 是哪一个 inode。拿到 inode 文件属性和内容就全部都有了。
定位
inode table的过程完全不需要 inode 号、也不需要任何 inode 节点,**inode table**是文件系统格式化时就固定在分区的独立元数据区域。
系统通过 超级块(Super Block) 即可直接定位,inode 号仅用于定位 **inode table**内部的某个具体 inode 节点。
我们在访问文件时使用的都是文件名,没有用到 inode 号啊?
目录是不是文件呢?
Linux 中一切皆文件,目录当然也是一个文件,所以目录也有属性和内容。属性与普通文件相同;而其内容则是文件名与 inode 号的映射关系。
问题:打开当前工作目录文件,查看当前工作目录文件的内容,还得知道当前工作目录的上级目录文件的 inode,不也得知道当前工作目录的上级目录吗?
答:由于文件再带路径,所以系统会自动递归解析文件的完整路径,逐层查找目录项。
举个例子:访问
/home/user/test.txt,系统的底层操作是:先找到根目录/的 inode 号(固定为 2,系统保留),解析根目录的目录项,找到home对应的 inode 号;再通过home的 inode 号找到其目录数据块,解析目录项,找到user对应的 inode 号;最后通过user的 inode 号找到其目录数据块,解析目录项,找到 **test.txt**对应的 inode 号;拿到test.txt的 inode 号后,进行后续的增删改查操作。
问题:可是路径谁提供?
创建文件本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系 统指定的目录下新建,而这就是天然就有路径!
**注意:**所以,我们知道了:访问文件必须要有目录 + 文件名=路径的原因根目录固定文件名,inode 号,无需查找,系统开机之后就必须知道
通过上面我们就知道了,访问文件都要从根目录开始进行路径解析,但每次这样不是太慢了嘛!
所以,当我们打开目录时,Linux 会进行进行路径缓存,即操作系统在内存进行路径维护。
Linux 中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
struct dentry {
atomic_t d_count; // 目录项引用计数
unsigned int d_flags; // 目录项标志位(由 d_lock 自旋锁保护)
spinlock_t d_lock; // 每个目录项的专属自旋锁
struct inode *d_inode; // 该文件名所属的 inode 节点指针
// 若为 NULL,表示此为负目录项(对应不存在的文件/目录)
struct hlist_node d_hash; // 哈希查找链表的节点(用于目录项哈希表)
struct dentry *d_parent; // 指向父目录的目录项指针
struct qstr d_name; // 封装后的目录项文件名(含名、长度、哈希值)
struct list_head d_lru; // 最近最少使用链表的节点(用于缓存回收)
union {
struct list_head d_child; // 加入父目录子节点链表的节点
struct rcu_head d_rcu; // RCU 机制的回收头(用于安全释放目录项内存)
} d_u;
struct list_head d_subdirs; // 指向当前目录项的所有子目录/文件节点链表头
struct list_head d_alias; // 指向同一 inode 的别名目录项链表(硬链接专用)
unsigned long d_time; // 时间戳(由 d_revalidate 函数使用,验证目录项有效性)
struct dentry_operations *d_op; // 指向目录项的操作函数集指针
struct super_block *d_sb; // 指向目录项树的根超级块(所属文件系统的超级块)
void *d_fsdata; // 文件系统专属的私有数据
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; // 性能分析的 cookie 结构(若有)
#endif
int d_mounted; // 标记是否为文件系统挂载点
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; // 内嵌短文件名缓冲区(存储短文件名,内存优化)
};

注意:
• 每个文件其实都要有对应的 dentry 结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构;
• 整个树形节点也同时会隶属于 LRU (Least Recently Used,最近最少使用) 结构中,进行节点淘汰;
• 整个树形节点也同时会隶属于 Hash,方便快速查找;
• 更重要的是,这个树形结构,整体构成了 Linux 的路径缓存结构,打开访问任何文件,都在先在这 棵树下根据路径进行查找,找到就返回属性 inode 和内容,没找到就从磁盘加载路径,添加 dentry 结构,缓存新路径。
有个问题,inode 号是以分区为单位进行划分的,仅在同一分区不冲突,即分区 1,分区 2,分区 3...,中就会都有一个 inode n(n 号节点),那这些相同的 inode 号会不会相互影响?
答:当然不会,因为 Linux 将不同的磁盘分区挂载到不同目录下,形成统一的文件路径体系,所以根据路径就能够区分 inode 号来自哪个分区。
无论哪个分区的文件,最终都会有一个唯一的绝对路径,Linux 通过这个路径先定位到文件所属的分区,再在该分区内通过 inode 编号找到对应的 inode 结构体,后续再通过 inode 的索引机制访问数据块。路径成为了跨分区识别 inode 的'唯一标识',从根本上解决了 inode 编号重复的问题。
硬链接本质为让多个文件名与一个 inode 关联,创建硬链接后,所有链接文件的 inode 编号、文件内容、权限完全一致,内核会记录该 inode 的'硬链接数'(即绑定的文件名数量)。
文件 abc 与文件 def 的 inode 相同。
注意:
当我们删除硬链接的文件时,只有当硬链接数变为 0 时,系统才会释放该 inode 对应的磁盘空间(真正删除文件内容)。
软链接是 Linux 中基于文件路径的引用方式,核心是创建一个特殊的小文件,该文件本身仅存储目标文件 / 目录的绝对 / 相对路径,而非目标的 inode 号或数据,访问软链接时,系统会自动根据存储的路径跳转到目标文件 / 目录,类似 Windows 的快捷方式,是 Linux 中跨分区、跨文件系统引用文件的核心方式。
ln -s abc abc.s // 创建 abc 的软链接 abc.s
ls -li // 查看文件信息

软链接是创建一个新文件,inode 不同。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online