万字长文爆肝:彻底弄懂Linux文件系统(Ext2),从Inode、Block到Dentry核心机制全解析
上篇文章:深入理解Linux底层存储:从物理磁盘架构到文件系统(inode/Block)原理
目录
前言
1.宏观认识ext2文件系统
2.宏观布局:分而治之的块组(Block Group)
3.微观解剖:块组的内部
3.1超级块(Super Block)
3.2块组描述符(GDT - Group Descriptor Table)
3.3数据块位图(Block Bitmap)
3.4Inode 位图(Inode Bitmap)
3.5Inode 表(Inode Table)
3.6数据块区(Data Blocks)
4.核心映射:Inode 与 Data Block 映射
5.融会贯通:已知Inode号,文件的增删查改究竟在做什么?
6.目录与文件名
7.路径解析
8.路径缓存
9.挂载(Mount)的底层逻辑
第一步:凭空“捏”出一块虚拟磁盘
第二步:给裸磁盘注入“灵魂”(格式化)
第二步:给裸磁盘注入“灵魂”(格式化)
第三步:打造挂载点(入口)
第四步:完成挂载(接驳孤岛)
第五步:验证与卸载(过河拆桥)
10.总结
前言
在Linux的世界里,“一切皆文件”是至高无上的准则。但当我们通过终端敲下ls -l 或是调用open()函数打开一个文件时,操作系统底层究竟发生了什么?海量的数据是如何在物理磁盘上被有条不紊地组织起来的?
本文将以经典的Ext2 文件系统为例,带你硬核剖析Linux文件系统的底层架构。从宏观的块组(Block Group)划分,到微观的 Inode、Data Block 映射,再到内存中的 Dentry 路径缓存,我们将一步步揭开文件系统的神秘面纱。
1.宏观认识ext2文件系统
我们想要在硬盘上存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在Linux系统中,最常见的是ext2系列的文件系统。其最早期版本为ext2,后来发展出了ext3和ext4。虽然ext3和ext4对ext2进行了增强,但是核心没有发生变化,我们仍以ext2为例。
2.宏观布局:分而治之的块组(Block Group)
如上篇文章所言,磁盘的逻辑结构可以看作是一个由无数个“扇区”组成的一维数组。为了提高操作效率,文件系统将多个连续的扇区组合成了块(Block)(通常为4KB)。
然而,如果直接管理一个动辄几TB分区中的所有Block,管理成本将极其高昂。因此,Ext2文件系统将采取“分而治之”的策略:将整个分区划分为若干个大小相同的块组。
只要我们搞懂了一个块组的内部结构,就等于弄懂了整个文件系统的运作原理。
如下图所示,只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。
超详细快速解释上图,后文也会超深入讲解每一个区域:
第一层:Disk(物理磁盘层)
这是最宏观的视角,代表一整块物理硬盘。
MBR (Master Boot Record / 主引导记录):位于硬盘的绝对最前端(第 0 扇区)。它包含了极其关键的两样东西:一段体积很小的引导代码(告诉电脑怎么启动)和磁盘分区表(记录了硬盘被切成了几块)。
Partition 1 ~ 4:代表磁盘被划分出的几个逻辑区域。传统的 MBR 格式最多只支持划分 4 个主分区(图中恰好画了 4 个)。
第二层:Partition(分区层)
我们将图中的
Partition 2放大。当一个分区被格式化后,它内部的结构如下:
Boot Sector (引导扇区):每个分区的最开头也会预留一个扇区。虽然系统的总引导在 MBR,但如果这个分区安装了操作系统,这里就会存放该操作系统的具体引导程序(如 GRUB 的一部分)。
EXT2 File System:分区剩余的庞大空间,被格式化成了 EXT2 文件系统,用来真正存储日常数据。
第三层:File System(文件系统层)
EXT2 并没有把整个分区当成一个大仓库直接用,而是采用了分治思想。
Block Group 0 ~ N (块组):文件系统被等分成了成百上千个“块组”。
为什么要分组?想象一个极其巨大的图书馆,如果不分区域,找一本书要跑断腿。将磁盘切分成块组,可以保证文件的属性(inode)和文件真正的内容(Data Blocks)在物理位置上靠得很近,从而大大减少机械硬盘磁头的寻道时间,提高读写速度。
第四层:Block Group(块组层 - 核心机制)
我们将
Block Group放大,这里面藏着文件系统运转的全部秘密!一个块组内部被严格划分成了 6 个区域:
Super Block (超级块):文件系统的“大管家”。记录了整个文件系统的全局信息,比如:一共多少个 inode、一共多少个 block、每个 block 多大等。如果超级块坏了,整个文件系统就崩溃了(所以它通常会在其他块组里有备份)。
GDT (Group Descriptor Table / 块组描述符表):记录了当前这个块组内部各个区域的起始位置和大小。
Block Bitmap (数据块位图):一张“地图”。用 0 和 1 记录后面的
Data Blocks中,哪些块空着,哪些块已被占用。系统存新文件时,查这张图就能瞬间找到空闲的数据块。inode Bitmap (inode 位图):类似上面,用 0 和 1 记录
inode Table中哪些 inode 编号已经被使用了。inode Table (inode 表/索引节点表):存放所有文件属性的核心区域。(下一层详细讲)
Data Blocks (数据块):真正存放文件内容(如视频画面、文本字符、图片像素)的仓库区。
第五层:inode Table(索引节点表层 - 文件的灵魂)
图中最底部将蓝色的
inode Table进行了放大。在 Linux 中,文件名和文件内容是分离的。文件的真实内容存放在
Data Blocks里,而文件的“身份证/属性”存放在inode里。inode Table就像是一个大表格,每一行代表一个文件,包含以下关键列(图中有几个小拼写错误,这里已为你修正):
Inode Number (节点号):系统内部识别文件的唯一数字编号(Linux 底层不认文件名,只认这个号码)。
File Type (文件类型):
-代表普通文件,d代表目录(directory),l代表软链接,2代表软链接等。Permission (权限):图中的
Permissiontion是拼写错误。记录了读(r)、写(w)、执行(x)权限,如经典的644、755。Link count (硬链接数):有多少个文件名指向了这个 inode。当数值降为 0 时,系统才会真正删除这个文件。
UID / GID:该文件所属的用户 ID 和用户组 ID。
size (大小):文件的字节数。
pointer (数据块指针 - 最关键!):这是一组路标。它记录了当前这个文件的真实内容,到底存放在后面
Data Blocks区域的哪几个具体的数据块编号里。终极总结:Linux 是如何读取一个文件的?
假设你要读取
/home/test.txt:
操作系统根据路径找到
test.txt对应的Inode Number。系统拿着这个号码,去inode Bitmap确认该号码存在,然后去inode Table中调出它的那一行信息。
系统检查Permission(权限),看你是否有资格读它。
如果有权限,系统读取pointer(指针)列表。
顺着指针的指引,系统跑到后面的Data Blocks区域,把对应编号的数据块里的内容读出来,拼成一段文字展示在你的屏幕上。
在一个标准的Ext2分区中,最前方是1KB的启动扇区(Boot Block/Sector)(由PC标准规定,存储分区信息和启动信息,文件系统不可修改),紧接着就是连续排布的Block Group0,Block Group1等。
3.微观解剖:块组的内部
ext2文件系统会根据分区的大小划分为数个Block Group。每个块组内部都有着完全相同的结构组成。一个典型的块组可以划分为以下六个核心区域:
3.1超级块(Super Block)
存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:
Block 和 Inode 的总量与空闲量。
单个 Block 和 Inode 的大小(如4KB、128Bytes)。
最近一次挂载、写入和磁盘校验的时间。
最近一次检验磁盘的时间等其他文件系统的相关信息。
Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。为了防止物理扇区损坏导致数据全毁,Super Block在多个Block Group中都有备份。
我们一起来看看内核中有关super block的相关代码吧:
/* * Structure of the super block */ struct ext2_super_block { __le32 s_inodes_count; /* Inodes count */ __le32 s_blocks_count; /* Blocks count */ __le32 s_r_blocks_count; /* Reserved blocks count */ __le32 s_free_blocks_count; /* Free blocks count */ __le32 s_free_inodes_count; /* Free inodes count */ __le32 s_first_data_block; /* First Data Block */ __le32 s_log_block_size; /* Block size */ __le32 s_log_frag_size; /* Fragment size */ __le32 s_blocks_per_group; /* # Blocks per group */ __le32 s_frags_per_group; /* # Fragments per group */ __le32 s_inodes_per_group; /* # Inodes per group */ __le32 s_mtime; /* Mount time */ __le32 s_wtime; /* Write time */ __le16 s_mnt_count; /* Mount count */ __le16 s_max_mnt_count; /* Maximal mount count */ __le16 s_magic; /* Magic signature */ __le16 s_state; /* File system state */ __le16 s_errors; /* Behaviour when detecting errors */ __le16 s_minor_rev_level; /* minor revision level */ __le32 s_lastcheck; /* time of last check */ __le32 s_checkinterval; /* max. time between checks */ __le32 s_creator_os; /* OS */ __le32 s_rev_level; /* Revision level */ __le16 s_def_resuid; /* Default uid for reserved blocks */ __le16 s_def_resgid; /* Default gid for reserved blocks */ /* * These fields are for EXT2_DYNAMIC_REV superblocks only. * * Note: the difference between the compatible feature set and * the incompatible feature set is that if there is a bit set * in the incompatible feature set that the kernel doesn't * know about, it should refuse to mount the filesystem. * * e2fsck's requirements are more strict; if it doesn't know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn't understand... */ __le32 s_first_ino; /* First non-reserved inode */ __le16 s_inode_size; /* size of inode structure */ __le16 s_block_group_nr; /* block group # of this superblock */ __le32 s_feature_compat; /* compatible feature set */ __le32 s_feature_incompat; /* incompatible feature set */ __le32 s_feature_ro_compat; /* readonly-compatible feature set */ __u8 s_uuid[16]; /* 128-bit uuid for volume */ char s_volume_name[16]; /* volume name */ char s_last_mounted[64]; /* directory where last mounted */ __le32 s_algorithm_usage_bitmap; /* For compression */ /* * Performance hints. Directory preallocation should only * happen if the EXT2_COMPAT_PREALLOC flag is on. */ __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ __u16 s_padding1; /* * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set. */ __u8 s_journal_uuid[16]; /* uuid of journal superblock */ __u32 s_journal_inum; /* inode number of journal file */ __u32 s_journal_dev; /* device number of journal file */ __u32 s_last_orphan; /* start of list of inodes to delete */ __u32 s_hash_seed[4]; /* HTREE hash seed */ __u8 s_def_hash_version; /* Default hash version to use */ __u8 s_reserved_char_pad; __u16 s_reserved_word_pad; __le32 s_default_mount_opts; __le32 s_first_meta_bg; /* First metablock block group */ __u32 s_reserved[190]; /* Padding to the end of the block */ };3.2块组描述符(GDT - Group Descriptor Table)
作用:描述当前块组的属性信息。 整个分区被分成了多个块组,GDT 就如同一个目录,记录着本块组中 Inode Table(Inode表)从哪里开始,Data Blocks(数据块区)从哪里开始,以及本块组还有多少个空闲的 Inode 和 Block。GDT 同样会在多个组中进行备份。
// 磁盘级blockgroup的数据结构 /* * Structure of a blocks group descriptor */ struct ext2_group_desc { __le32 bg_block_bitmap; /* Blocks bitmap block */ __le32 bg_inode_bitmap; /* Inodes bitmap */ __le32 bg_inode_table; /* Inodes table block*/ __le16 bg_free_blocks_count; /* Free blocks count */ __le16 bg_free_inodes_count; /* Free inodes count */ __le16 bg_used_dirs_count; /* Directories count */ __le16 bg_pad; __le32 bg_reserved[3]; };3.3数据块位图(Block Bitmap)
作用:快速定位空闲的数据块。 它是一个位图(Bitmap),利用 0 和 1 来标记 Data Block 区域中,哪些数据块已经被占用,哪些处于空闲状态。有了它,系统分配存储空间时的时间复杂度可大幅降低。
3.4Inode 位图(Inode Bitmap)
作用:快速定位空闲的 Inode。 与 Block Bitmap 类似,每个 bit 代表 Inode Table 中的一个 Inode 是否空闲可用。
3.5Inode 表(Inode Table)
作用:存放文件属性,如:文件大小,所有者,最近修改时间等。 这是整个块组中最核心的数据结构之一。
Inode Table 中整齐地排列着当前块组分配的所有 Inode。需要注意的是:Inode 编号是以整个分区为单位进行全局划分的,不能跨分区。
3.6数据块区(Data Blocks)
作用:存放文件实际内容的区域。 这里就是一块块的 Block 集合。
根据不同的文件类型有以下几种情况:
对于普通文件,这里存放着文件代码、文本、视频字节流;
对于目录文件,该目录下的所有文件名和目录名存储在所在目录的数据块中,除了文件名外,ls -l命令看到的其他信息保存在该文件的inode中。
Block号按照分区划分,不可跨分区。
4.核心映射:Inode 与 Data Block 映射
文件 = 文件属性 + 文件内容。在Ext2中,属性存在Inode,内容存在Data Block。那么,这两者是如何关联的?
在Linux内核的 struct ext2_inode 结构体中, 存在一个极其关键的数组:__le32 i_block[EXT2_N_BLOCKS];
EXT2_N_BLOCKS宏定义通常为15,这15个指针就是连接Inode和Block的桥梁。
直接指针(0~11):前 12 个指针直接指向存放文件数据的 Block 号。如果文件很小(小于 12 * 4KB = 48KB),用这12个指针就足够了。
一级间接指针(12):如果文件较大,第 13 个指针指向一个 Block,这个 Block 里不存实际数据,而是存放其他 Block 的编号(即作为一个索引表)。
二级间接指针(13):指向一个一级索引表,该表里的指针再指向二级索引表,二级索引表再指向实际的数据块。
三级间接指针(14):同理,用于支持极其庞大的单体文件。
通过这种多级索引树的结构,极小容量的 Inode 成功实现了对超大体积文件的数据块管理。
内核代码:
/* * Structure of an inode on the disk */ struct ext2_inode { __le16 i_mode; /* File mode */ __le16 i_uid; /* Low 16 bits of Owner Uid */ __le32 i_size; /* Size in bytes */ __le32 i_atime; /* Access time */ __le32 i_ctime; /* Creation time */ __le32 i_mtime; /* Modification time */ __le32 i_dtime; /* Deletion Time */ __le16 i_gid; /* Low 16 bits of Group Id */ __le16 i_links_count; /* Links count */ __le32 i_blocks; /* Blocks count */ __le32 i_flags; /* File flags */ union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1; /* OS dependent 1 */ __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ __le32 i_generation; /* File version (for NFS) */ __le32 i_file_acl; /* File ACL */ __le32 i_dir_acl; /* Directory ACL */ __le32 i_faddr; /* Fragment address */ union { struct { __u8 l_i_frag; /* Fragment number */ __u8 l_i_fsize; /* Fragment size */ __u16 i_pad1; __le16 l_i_uid_high; /* these 2 fields */ __le16 l_i_gid_high; /* were reserved2[0] */ __u32 l_i_reserved2; } linux2; struct { __u8 h_i_frag; /* Fragment number */ __u8 h_i_fsize; /* Fragment size */ __le16 h_i_mode_high; __le16 h_i_uid_high; __le16 h_i_gid_high; __le32 h_i_author; } hurd2; struct { __u8 m_i_frag; /* Fragment number */ __u8 m_i_fsize; /* Fragment size */ __u16 m_pad1; __u32 m_i_reserved2[2]; } masix2; } osd2; /* OS dependent 2 */ }; #define EXT2_NDIR_BLOCKS 12 #define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS #define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) #define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) #define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1) //inode 的⼤⼩通常是 128 字节 或 256 字节5.融会贯通:已知Inode号,文件的增删查改究竟在做什么?
在了解了上述各种底层数据结构后,我们来思考一个直击灵魂的问题:已知文件 Inode 号的情况下,在指定分区内对文件进行增、删、查、改,底层到底是在干什么?
首先,我们得到核心结论:
格式化的本质:分区之后的格式化操作,本质上就是对分区进行逻辑分组,并在每个分组中写入 Super Block、GDT、Block Bitmap、Inode Bitmap 等管理信息。这些管理信息的集合,就是我们所说的“文件系统”。
精准定位:只要知道了一个文件的 Inode 号,系统就能通过简单的数学运算确定它属于哪一个 Block Group,进而在这个分组的 Inode Table 中精确定位到这块 128Bytes 大小的 Inode 结构。拿到 Inode,文件的属性和内容(通过指针寻找)就全部都有了!
下面,我们通过touch一个新文件来推演一下系统是如何工作的:
[root@localhost linux]# touch abc [root@localhost linux]# ls -i abc 263466 abc当我们在终端敲下这行命令时,内核在背后干了这些大事:
【增】创建文件(Create):
存储属性:内核先去遍历Inode Bitmap,找到一个为
0的空闲位置,将它置为1,分配出相应的 Inode 号(例如263466)。随后将文件的属主、权限、创建时间等记录在此时定位到的 Inode Table 中。存储数据:如果文件需要写入数据,内核去遍历Block Bitmap,找到空闲的
0置为1,获取空闲的数据块(例如 Block 300, 500, 800),将数据写入。记录分配情况:将分配到的块列表(300, 500, 800)依次写入 Inode 的
i_block数组中。添加到目录:内核将
(263466, "abc")这一对映射关系,追加写入到当前所在目录的 Data Block 之中!
【查】读取文件(Read):从父目录找到 Inode 号 -> 算出 Block Group -> 拿到 Inode -> 顺藤摸瓜读取 Data Block。
【改】修改文件(Update):顺着 Inode 找到对应的 Data Block,覆写原有数据。如果文件变大,再去申请新的 Block,并在 Block Bitmap 和 Inode 指针中更新记录。
【删】删除文件(Delete): 你觉得删除文件是把磁盘里的数据清零吗?并不是!
在父目录的 Data Block 中删除
(263466, "abc")这条映射记录。把 Inode 的属性信息置为无效,并在Inode Bitmap中把对应的位置
0。顺着指针找到用过的 Data Block,在Block Bitmap中把它们占用的位置
0。(这也是为什么文件删除瞬间就能完成,以及误删后有可能通过底层工具恢复数据的原因——只要对应的数据块没被新的数据覆盖,它就永远留在磁盘上!)
6.目录与文件名
问题:
我们访问文件,都是用的文件名,没用过inode号,目录是文件吗?如何理解?
答案:
在 Linux 的磁盘底层,根本没有所谓“文件夹”的特殊物理结构,目录也是一种普通的文件。
目录的 Inode:存放目录的权限、所有者、修改时间等属性。
目录的 Data Block:存放该目录下的文件名与对应 Inode 编号的映射关系表。
重点澄清:文件的 Inode 中并不包含文件名!文件名是存储在其父目录的数据块(Data Block)之中的。
验证说明代码:
// readdir.c #include <stdio.h> #include <string.h> #include <stdlib.h> #include <dirent.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <directory>\n", argv[0]); exit(EXIT_FAILURE); } DIR* dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅 if (!dir) { perror("opendir"); exit(EXIT_FAILURE); } struct dirent* entry; while ((entry = readdir(dir)) != NULL) { // 系统调⽤,⾃⾏查阅 // Skip the "." and ".." directory entries if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino); } closedir(dir); return 0; }运行结果:
所以,访问文件,必须打开当前目录,根据文件名,获得对应的inode号,然后进行文件访问。访问文件必须要知道当前工作目录,本质是必须能打开当前目录文件,查看目录文件的内容。
7.路径解析
当我们在终端访问/home/user/test.c时,系统是如何找到test.c的?
系统首先找到根目录
/的 Inode(其 Inode 号通常固定为 2,系统开机即知)。读取
/的 Data Block,在里面按字符串匹配找到home目录及对应的 Inode 号。根据
home的 Inode 号,读取home的 Data Block,匹配到user的 Inode 号。依此类推,最终找到
test.c的 Inode 号。拿到
test.c的 Inode,进而读取其 Data Block 获取最终的文件内容。
这个不断打开目录、匹配文件名、获取下级 Inode 的递归过程,就是路径解析。
注意:
此时,我们知道了访问文件必须要有目录 + 文件名 = 路径的原因
根目录固定文件名,inode号, 无需查找,系统开机之后就必须知道。
可是,路径是由谁提供的呢?
我们访问文件,都是指令/工具访问,本质是进程访问,进程有CWD! 进程提供路径。
我们open文件,提供了文件。
可是,最开始的路径从哪里来?Linux为什么要有根目录,根目录下为什么要有那么多缺省目录?为什么要有家目录,自己可以新建目录?
上面的所有行为,本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这就是天然的路径。
总结:系统 + 用户共同构建了Linux路径结构。
8.路径缓存
如果在深层级的目录中频繁访问文件,每次都从根目录/甚至当前目录去磁盘里逐级解析 Inode 和 Block,磁盘 I/O 将成为巨大的性能瓶颈。
为了解决这个问题,Linux 内核在内存中维护了一个专门的数据结构:struct dentry(目录项,Directory Entry)。
只要一个目录或文件被访问过,内核就会在内存中为其创建一个
dentry结构。dentry中保存了文件名(d_name)、对应的 Inode 指针(d_inode),以及父级dentry的指针(d_parent)。这些
dentry在内存中相互链接,形成了一棵庞大的目录树缓存(Dentry Cache)。
因此,当你第二次访问/home/user/test.c时,操作系统会直接从内存的 Dentry Hash 表中秒速定位到test.c的dentry,直接获取到其在磁盘上的 Inode,彻底省去了查阅磁盘目录块的开销!
问题1:Linux磁盘中,存在真正的目录吗?
答案:不存在,只有文件,只保存文件属性+文件内容。
问题2:访问如何文件,都要从根目录开始进行路径解析吗?
答案:原则上是,但是这样太慢了,所以Linux会缓存历史路径结构
问题3:Linux目录的概念是怎么产生的?
答案:打开的文件时目录的话,由OS自己在内存中进行路径维护。
struct dentry代码:
struct dentry { atomic_t d_count; unsigned int d_flags; /* protected by d_lock */ spinlock_t d_lock; /* per dentry lock */ struct inode* d_inode; /* Where the name belongs to - NULL is * negative */ /* * The next three fields are touched by __d_lookup. Place them here * so they all fit in a cache line. */ struct hlist_node d_hash; /* lookup hash list */ struct dentry* d_parent; /* parent directory */ struct qstr d_name; struct list_head d_lru; /* LRU list */ /* * d_child and d_rcu can share memory */ union { struct list_head d_child; /* child of parent list */ struct rcu_head d_rcu; } d_u; struct list_head d_subdirs; /* our children */ struct list_head d_alias; /* inode alias list */ unsigned long d_time; /* used by d_revalidate */ struct dentry_operations* d_op; struct super_block* d_sb; /* The root of the dentry tree */ void* d_fsdata; /* fs-specific data */ #ifdef CONFIG_PROFILING struct dcookie_struct* d_cookie; /* cookie, if any */ #endif int d_mounted; unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */ };注意:
每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构。
整个树形节点也同时会隶属于LRU(LeastRecentlyUsed,最近最少使用)结构中,进行节点淘汰
整个树形节点也同时会隶属于Hash,方便快速查找
更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何件,都在先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径。
9.挂载(Mount)的底层逻辑
看到这一章节,我们已经能够根据inode号在指定分区找文件了,也已经能根据目录文件内容,找到指定的inode了,在指定的分区内,我们可以为所欲为,但是,inode不能跨分区,Linux可以有多个分区,那么我们要怎么知道自己在哪一个分区?
答案就是:挂载(Mount)。
挂载的本质是:将一个格式化好的文件系统(分区),与当前全局目录树上的某一个空目录(挂载点)进行关联。
我们进行一个实验:用一个普通文件模拟出一块物理磁盘,并将其挂载到指定的目录/home/xxx404/linux-learning/test4_18中。
详细讲解每一步:
第一步:凭空“捏”出一块虚拟磁盘
$ dd if=/dev/zero of=./disk.img bs=1M count=5这是在干什么:创建一个大小为 5MB 的文件
disk.img,文件内容全部填充为 0。底层逻辑:在 Linux 中“一切皆文件”。既然磁盘对内核来说就是一个按块读写的字节设备,那我们完全可以生成一个固定大小的普通文件,用它来冒充“物理磁盘”。
dd就像一把刻刀,从/dev/zero(一个能无限吐出 0 的特殊设备)切了 5 块 1MB 的数据,写入到disk.img中。为什么这么做:我们需要一个“干净的、物理层面连续的裸空间”来供后续折腾,这比你去拔插一块真正的物理硬盘要方便和安全得多。
第二步:给裸磁盘注入“灵魂”(格式化)
$ mkfs.ext4 disk.img这是在干什么:将这块 5MB 的虚拟磁盘格式化为
ext4文件系统。底层逻辑:单纯的 5MB 全 0 文件对操作系统毫无意义。执行
mkfs.ext4就是在执行我们在第一、第二章讲过的操作:在这个文件里划分 Block Group,并在特定位置写入超级块(Super Block)、GDT、Inode Bitmap、Block Bitmap 以及 Inode Table。为什么这么做:没有建立好这些元数据结构的“裸盘”是装不了数据的,必须让其具备 Ext 文件系统的规则,才能被内核识别为合法的存储介质。
第二步:给裸磁盘注入“灵魂”(格式化)
$ mkfs.ext4 disk.img这是在干什么:将这块 5MB 的虚拟磁盘格式化为
ext4文件系统。底层逻辑:单纯的 5MB 全 0 文件对操作系统毫无意义。执行
mkfs.ext4就是在执行我们在第一、第二章讲过的操作:在这个文件里划分 Block Group,并在特定位置写入超级块(Super Block)、GDT、Inode Bitmap、Block Bitmap 以及 Inode Table。为什么这么做:没有建立好这些元数据结构的“裸盘”是装不了数据的,必须让其具备 Ext 文件系统的规则,才能被内核识别为合法的存储介质。
第三步:打造挂载点(入口)
$ mkdir -p /home/xxx404/linux-learning/test4_18这是在干什么:在当前系统中创建一个空的目录。
底层逻辑:在全局文件系统树中,开辟出一个空节点(新建一个目录的 Inode 和对应的
dentry)。为什么这么做:一块独立的磁盘(或本例中的
disk.img)就像一个孤岛,系统怎么访问里面的数据呢?必须在现有的大陆(全局根目录树/)上修一座桥。这个空目录,就是桥的入口,被称为“挂载点”。
第四步:完成挂载(接驳孤岛)
$ sudo mount -t ext4 ./disk.img /home/xxx404/linux-learning/test4_18/这是在干什么:把刚才做好的虚拟磁盘
disk.img,挂载到我们指定的目录下。底层逻辑:
Linux 发现你要挂载的是一个普通文件而不是物理块设备,它会自动在底层分配一个虚拟的回环设备(
loop device,例如/dev/loop0),把这个文件当成块设备来处理。内核在内存中创建一个
vfsmount数据结构,将/home/xxx404/linux-learning/test4_18/的dentry(内存中的目录节点)与disk.img内部文件系统自身的根 Inode(通常是 Inode 号为 2 的节点)强行绑定在一起。
为什么这么做:挂载完成后,当用户访问
/home/xxx404/linux-learning/test4_18/这个目录时,VFS(虚拟文件系统)会偷偷进行“狸猫换太子”,把读写请求透明地重定向到disk.img内部的文件系统中去!
第五步:验证与卸载(过河拆桥)
$ df -h # 此时你会发现输出结果多了一行类似如下的信息: # /dev/loop0 4.9M 24K 4.5M 1% /home/xxx404/linux-learning/test4_18 $ sudo umount /home/xxx404/linux-learning/test4_18这是在干什么:
df -h验证挂载是否成功,最后使用umount取消挂载。底层逻辑:执行卸载命令后,内核会把还在内存中未写入磁盘的数据(Dirty Page)强制刷入
disk.img,然后解除vfsmount的绑定关系,并释放/dev/loop0虚拟设备。为什么这么做:挂载是建立连接,卸载就是安全断开连接。卸载后,
/home/xxx404/linux-learning/test4_18又变回了一个普普通通的空目录,而刚才你写进去的文件,都完好无损地封存在disk.img这个“小黑盒”里了。
通过这一套硬核实战,你是否彻底明白了挂载的真谛?Linux 依靠这种强大的挂载树结构,将成百上千个不同的物理硬盘、U盘甚至虚拟磁盘,天衣无缝地缝合在了一起。对开发者而言,你永远只需要面对一个统一的/根目录。
注意:
/dev/loop0 在Linux系统中代表第⼀个循环设备(loop device)。循环设备,也被称为 回环设备或者loopback设备,是⼀种伪设备(pseudo-device),它允许将⽂件作为块设备 (block device)来使⽤。这种机制使得可以将⽂件(比如ISO镜像⽂件)挂载(mount)为 ⽂件系统,就像它们是物理硬盘分区或者外部存储设备⼀样
结论:分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。所以,可以根据访问目标文件的“路径前缀”准确判断我在哪一个分区。
10.总结
文件系统指的是,磁盘中的管理信息+内存当中管理文件相关的数据信息,共同营造一种对磁盘数据管理的解决方案。而普通用户和文件系统访问的入口,只有一个:文件描述符,通过文件描述符可以找到struct file,通过struct file可以找到文件的inode,dentry,路径,属性这些核心的内容。
回顾一个文件从创建到访问的全生命周期:
格式化:为分区划定 Block Group,建立 Super Block、Bitmaps 和 Inode Table 的基本秩序。
挂载系统:将格式化好的孤立分区,通过内核挂载绑定到全局目录树的指定挂载点上,实现统一访问。
创建文件:在 Inode Bitmap 中找空闲位分配 Inode,在 Block Bitmap 找空闲位分配 Data Block,将二者映射,最后在父目录的 Data Block 中写入“文件名 - Inode 号”的记录。
打开文件:通过路径解析或内存的 Dentry 缓存快速命中目标 Inode,将 Inode 信息加载到内存的 VFS 层,供进程通过文件描述符(fd)进行读写。
这就是 Linux 底层存储系统的基石,严谨、克制且无比高效。
本章完。
