万字长文爆肝:彻底弄懂Linux文件系统(Ext2),从Inode、Block到Dentry核心机制全解析

张开发
2026/4/20 1:27:28 15 分钟阅读

分享文章

万字长文爆肝:彻底弄懂Linux文件系统(Ext2),从Inode、Block到Dentry核心机制全解析
上篇文章深入理解Linux底层存储从物理磁盘架构到文件系统(inode/Block)原理目录前言1.宏观认识ext2文件系统2.宏观布局分而治之的块组(Block Group)3.微观解剖块组的内部3.1超级块Super Block3.2块组描述符GDT - Group Descriptor Table3.3数据块位图Block Bitmap3.4Inode 位图Inode Bitmap3.5Inode 表Inode Table3.6数据块区Data Blocks4.核心映射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 Group0Block 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 doesnt * know about, it should refuse to mount the filesystem. * * e2fscks requirements are more strict; if it doesnt know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesnt 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 TableInode表从哪里开始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一个新文件来推演一下系统是如何工作的[rootlocalhost linux]# touch abc [rootlocalhost 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彻底省去了查阅磁盘目录块的开销问题1Linux磁盘中存在真正的目录吗答案不存在只有文件只保存文件属性文件内容。问题2访问如何文件都要从根目录开始进行路径解析吗答案原则上是但是这样太慢了所以Linux会缓存历史路径结构问题3Linux目录的概念是怎么产生的答案打开的文件时目录的话由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 bs1M count5这是在干什么创建一个大小为 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可以找到文件的inodedentry路径属性这些核心的内容。回顾一个文件从创建到访问的全生命周期格式化为分区划定 Block Group建立 Super Block、Bitmaps 和 Inode Table 的基本秩序。挂载系统将格式化好的孤立分区通过内核挂载绑定到全局目录树的指定挂载点上实现统一访问。创建文件在 Inode Bitmap 中找空闲位分配 Inode在 Block Bitmap 找空闲位分配 Data Block将二者映射最后在父目录的 Data Block 中写入“文件名 - Inode 号”的记录。打开文件通过路径解析或内存的 Dentry 缓存快速命中目标 Inode将 Inode 信息加载到内存的 VFS 层供进程通过文件描述符fd进行读写。这就是 Linux 底层存储系统的基石严谨、克制且无比高效。本章完。

更多文章