Linux系统编程—Ext系列文件系统

Linux系统编程—Ext系列文件系统 一、磁盘硬件要理解文件系统我们首先需要搞清楚它的底层载体 ——磁盘的工作原理。现代计算机主要使用两种存储设备机械硬盘HDD和固态硬盘SSD1.1 磁盘的物理结构机械磁盘是计算机中为数不多的机械设备相比内存有着容量大、价格低的优势但访问速度要慢上好几个数量级。从宏观的机房到微观的磁盘存储系统是一个层层封装的体系磁盘的内部结构可以拆解为盘片、磁头、主轴、传动臂等核心组件盘片 (Platter)磁盘的存储介质每个盘片有两个盘面都可以存储数据磁头 (Head)负责读取和写入盘片上的数据每个盘面对应一个磁头主轴 (Spindle)带动盘片高速旋转传动臂带动磁头移动定位到不同的磁道1.2 磁盘的存储结构为了管理数据磁盘将盘片的盘面划分为了磁道、扇区、柱面等逻辑单元扇区 (Sector)磁盘存储的最小物理单位传统磁盘的扇区大小为 512 字节新的磁盘也支持 4K 扇区磁道 (Track)盘片上的同心圆每个盘面被划分为多个磁道柱面 (Cylinder)所有盘面中相同半径的磁道共同组成的逻辑单元所有磁头可以同步访问同一个柱面的数据1.3 从 CHS 到 LBA早期的磁盘使用CHSCylinder/Head/Sector的三维寻址方式通过柱面号、磁头号、扇区号来定位一个扇区但 CHS 寻址有一个致命的缺陷它的地址位长度有限最大只能支持 8.4GB 的磁盘容量无法满足磁盘容量的快速增长。因此后来引入了LBALogical Block Address的线性寻址方式将整个磁盘的扇区抽象为一个一维数组每个扇区对应一个唯一的 LBA 地址操作系统只需要使用 LBA 就可以访问磁盘磁盘固件会自动完成 LBA 到 CHS 的转换。1转换公式CHS 转 LBA单个柱面的扇区总数 磁头数 * 每磁道扇区数 LBA 柱面号C * 单个柱面的扇区总数 磁头号H * 每磁道扇区数 扇区号S - 1注意扇区号 S 是从 1 开始的而 LBA 是从 0 开始的所以需要减 1LBA 转 CHS单个柱面的扇区总数 磁头数 * 每磁道扇区数 柱面号C LBA // 单个柱面的扇区总数 磁头号H (LBA % 单个柱面的扇区总数) // 每磁道扇区数 扇区号S (LBA % 每磁道扇区数) 1我们可以用 Python 代码来实现这个转换方便测试def chs_to_lba(c, h, s, heads, sectors_per_track): CHS地址转LBA地址 :param c: 柱面号 :param h: 磁头号 :param s: 扇区号 :param heads: 磁盘总磁头数 :param sectors_per_track: 每磁道扇区数 :return: LBA地址 sectors_per_cylinder heads * sectors_per_track lba c * sectors_per_cylinder h * sectors_per_track s - 1 return lba def lba_to_chs(lba, heads, sectors_per_track): LBA地址转CHS地址 :param lba: LBA地址 :param heads: 磁盘总磁头数 :param sectors_per_track: 每磁道扇区数 :return: (c, h, s) sectors_per_cylinder heads * sectors_per_track c lba // sectors_per_cylinder remaining lba % sectors_per_cylinder h remaining // sectors_per_track s (remaining % sectors_per_track) 1 return (c, h, s) # 测试假设磁盘有16个磁头每磁道63个扇区 heads 16 sectors_per_track 63 # CHS转LBA c, h, s 100, 5, 10 lba chs_to_lba(c, h, s, heads, sectors_per_track) print(fCHS({c},{h},{s}) - LBA: {lba}) # LBA转CHS c2, h2, s2 lba_to_chs(lba, heads, sectors_per_track) print(fLBA({lba}) - CHS: ({c2},{h2},{s2}))运行这段代码我们就可以验证两者的转换是完全可逆的2柱面的展开磁盘的三维结构我们可以把它展开成一维的结构就像山楂卷一样一层一层的柱面依次排列每个柱面里依次排列各个盘面的磁道每个磁道里依次排列扇区这样整个磁盘的扇区就变成了一个线性的数组这就是 LBA 的本质。二、文件系统有了磁盘的基础我们来看文件系统的核心概念这些是理解 Ext 系列的基础。2.1 块磁盘的最小读写单位是扇区512 字节但操作系统不会逐个扇区读写这样效率太低。操作系统会将连续的多个扇区组成一个块 (Block)块是文件存取的最小单位最常见的块大小是 4KB也就是 8 个连续的扇区。比如我们用stat 命令查看文件信息时就能看到块的信息[whbbite-alicloud stdio]$ stat main.c File: main.c Size: 488 Blocks: 8 IO Block: 4096 regular file分区的本质就是记录每个分区的起始和结束 LBA 地址这样每个分区就可以被看作是一个独立的逻辑磁盘各自安装不同的文件系统。2.3 Inode我们常说Linux 下的文件 内容 属性内容是文件的实际数据而属性就是文件的元信息比如权限、所有者、修改时间等。Linux 将这两部分分开存储存储文件元信息的区域就叫做Inode索引节点每个文件都对应一个唯一的 InodeInode 里存储了文件的所有属性以及指向文件内容块的指针。我们可以用ls -i查看文件的 Inode 号[whbbite-alicloud stdio]$ ls -li total 16 1052669 -rw-rw-r-- 1 whb whb 225 Oct 17 19:09 test.txt 1052007 -rw-rw-r-- 1 whb whb 488 Oct 17 19:06 main.cInode 的大小通常是 128 字节或 256 字节所有文件的 Inode 大小都是相同的不管文件本身的大小。Ext2 的 Inode 结构体定义如下struct ext2_inode { __le16 i_mode; /* 文件类型与权限 */ __le16 i_uid; /* 所有者UID */ __le32 i_size; /* 文件大小 */ __le32 i_atime; /* 访问时间 */ __le32 i_ctime; /* 创建时间 */ __le32 i_mtime; /* 修改时间 */ __le32 i_dtime; /* 删除时间 */ __le16 i_gid; /* 组ID */ __le16 i_links_count; /* 硬链接数 */ __le32 i_blocks; /* 占用的块数 */ // ... 其他属性 __le32 i_block[EXT2_N_BLOCKS];/* 指向数据块的指针数组 */ };注意文件名并不存在 Inode 里文件名是存在目录的内容里的三、Ext2Ext2 是 Ext 系列的经典版本也是 Ext3 和 Ext4 的基础它的核心设计一直沿用至今。3.1 Ext2 的结构Ext2 将整个分区划分为多个大小相同的块组每个块组的结构都是完全相同的这样做的好处是可以将数据和元数据分开存储减少磁头移动提升性能同时也可以做元数据的备份提升可靠性。整个分区的开头是启动块大小 1KB存储启动信息所有文件系统都不能修改它。启动块之后就是各个块组。3.2 块组的构成每个块组都由以下几个部分组成1. 超级块超级块存储了整个文件系统的全局信息比如块和 Inode 的总量、空闲块和 Inode 的数量、块大小、挂载时间等。超级块是文件系统的核心如果它损坏了整个文件系统就废了。为了可靠性超级块会在多个块组里做备份第一个块组必须有其他块组也会有备份防止某个块组损坏导致整个文件系统无法使用。超级块的结构体定义如下struct ext2_super_block { __le32 s_inodes_count; /* Inode总数 */ __le32 s_blocks_count; /* 块总数 */ __le32 s_free_blocks_count; /* 空闲块数 */ __le32 s_free_inodes_count; /* 空闲Inode数 */ __le32 s_log_block_size; /* 块大小的对数 */ // ... 其他全局信息 __le32 s_magic; /* 文件系统魔数用来识别Ext2 */ };2. 块组描述符表块组描述符表存储了每个块组的描述信息比如这个块组的块位图在哪里、Inode 位图在哪里、Inode 表在哪里空闲块和 Inode 的数量等。3. 块位图块位图用一个 bit 来标记对应的数据块是否被占用1 表示已占用0 表示空闲。这样内核可以快速找到空闲的块。4. Inode 位图和块位图类似用 bit 标记对应的 Inode 是否空闲用来快速分配 Inode。5. Inode 表存储这个块组所有的 Inode每个 Inode 的大小是固定的所以可以通过 Inode 号快速定位到它的位置。6. 数据块这就是真正存储文件内容的区域普通文件的内容、目录的内容都存在这里。3.3 Inode 的多级索引Inode 里有一个i_block数组大小是 15用来存储指向数据块的指针这就是 Inode 的多级索引机制前 12 个直接块直接指向数据块的块号小文件的所有数据块都可以存在这里不需要额外的索引第 13 个一级间接块指向一个索引块这个索引块里存储的是数据块的块号第 14 个二级间接块指向一个一级间接块的索引块第 15 个三级间接块指向一个二级间接块的索引块通过这种多级索引的方式Ext2 可以支持非常大的文件比如 4KB 块大小的情况下最大可以支持 2TB 的文件。3.4 如何工作我们以创建一个新文件为例来看 Ext2 是如何工作的分配 Inode内核先找到一个空闲的 Inode比如 263466把文件的属性信息写入这个 Inode分配数据块如果文件有内容内核找到空闲的数据块把内容写入这些块更新 Inode 的索引把这些数据块的块号写入 Inode 的i_block数组里更新目录在当前目录的数据块里添加一个条目(文件名, Inode号)把文件名和 Inode 关联起来这就是整个创建文件的过程是不是很清晰3.5 目录与路径很多人会好奇Linux 的目录是怎么工作的其实目录本身也是一个文件目录的属性存在它自己的 Inode 里而目录的内容就是一系列的(文件名, Inode号)的映射条目。我们可以写一个简单的程序来读取目录的内容验证这一点// readdir.c: 读取目录下的文件名与Inode号 #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) { // 跳过.和.. if (strcmp(entry-d_name, .) 0 || strcmp(entry-d_name, ..) 0) { continue; } printf(Filename: %-20s Inode: %lu\n, entry-d_name, (unsigned long)entry-d_ino); } closedir(dir); return 0; }编译运行这个程序我们就能看到根目录下的所有文件名和对应的 Inode 号whbbite:~/code/test/test$ ./readdir / Filename: mnt Inode: 1048577 Filename: tmp Inode: 1179650 Filename: sys Inode: 917506 Filename: libx32 Inode: 17 Filename: srv Inode: 786434 Filename: lib64 Inode: 16 Filename: sbin Inode: 18 Filename: dev Inode: 131073 Filename: swapfile Inode: 12 Filename: run Inode: 1048578 Filename: log.txt Inode: 20 Filename: proc Inode: 1179649 Filename: lostfound Inode: 11 Filename: etc Inode: 262145 Filename: lib Inode: 14 Filename: opt Inode: 917505 Filename: usr Inode: 1179651 Filename: lib32 Inode: 15 Filename: boot Inode: 655361 Filename: var Inode: 655365 Filename: bin Inode: 13 Filename: media Inode: 393217 Filename: home Inode: 786433 Filename: root Inode: 655362这就验证了目录的内容就是文件名和 Inode 的映射。那我们访问一个绝对路径比如/home/whb/test.c的时候系统是怎么做的这就是路径解析从根目录开始根目录的 Inode 号是固定的 2打开根目录的内容找到home对应的 Inode 号打开home目录的内容找到whb对应的 Inode 号打开whb目录的内容找到test.c对应的 Inode 号拿到test.c的 Inode就可以访问它的内容了为了加速路径解析Linux 内核会缓存路径的信息用dentry结构体来维护路径的树形结构这就是路径缓存避免每次访问文件都要从根目录开始一层层解析。3.6 挂载我们知道Inode 号是分区内唯一的不能跨分区那 Linux 怎么把多个分区整合到同一个目录树里这就是挂载。挂载的本质就是把一个分区的根目录关联到系统目录树的某个空目录上这样当你访问这个目录的时候系统就知道你要访问的是这个分区而不是原来的根分区。我们可以做一个简单的实验# 1. 创建一个5MB的空文件作为虚拟磁盘 $ dd if/dev/zero of./disk.img bs1M count5 # 2. 格式化为ext4文件系统 $ mkfs.ext4 disk.img # 3. 创建挂载点目录 $ mkdir /mnt/mydisk # 4. 挂载这个虚拟磁盘 $ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 5. 查看挂载结果 $ df -h /dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk这里的/dev/loop0就是回环设备它可以把一个普通文件当作块设备来使用这样我们就可以把镜像文件挂载成一个正常的分区了。四、从 Ext2 到 Ext4Ext2 虽然经典但它有一个很大的问题没有日志功能一旦系统意外断电就需要对整个分区做 fsck 检查这个过程非常慢而且有数据丢失的风险。于是 Ext 系列就演进到了 Ext3再到现在的 Ext4。4.1 三代文件系统对比我们先来看一下 Ext2、Ext3、Ext4 三代的核心特性与性能对比Ext4 在容量上带来了质的飞跃最大支持 1EB 的分区以及 16TB 的单个文件完全满足了如今大数据存储的需求。性能上的提升更是惊人Fsck 检查时间Ext4 相比 Ext2 快了 8 倍多因为 Ext4 支持元块组只需要检查有问题的块组而不是整个分区小文件创建速度Ext4 的速度是 Ext2 的 9 倍这得益于 Ext4 的多块分配、延迟分配等优化4.2 Ext3Ext3 在 Ext2 的基础上加入了\\日志 (Journal)\\功能它的核心思想是在修改文件系统的元数据之前先把要做的修改写入日志然后再执行实际的修改。这样如果系统意外断电重启的时候只需要重放日志就可以快速恢复文件系统的一致性不需要对整个分区做完整的 fsck大大提升了可靠性和恢复速度。Ext3 完全兼容 Ext2你甚至可以直接把 Ext2 的分区挂载为 Ext3不需要重新格式化。4.3 Ext4Ext4 是 Ext3 的增强版做了非常多的优化是目前 Linux 最常用的 Ext 系列文件系统大文件支持突破了 Ext3 的 2TB 限制支持最大 16TB 的文件1EB 的分区Extent 机制取代了原来的多级间接块用连续的块范围来表示大文件大大减少了元数据的开销提升了大文件的性能多块分配原来的 Ext3 每次只能分配一个块Ext4 可以一次性分配多个连续的块提升了写入性能延迟分配延迟块的分配直到数据刷到磁盘的时候才分配这样可以尽可能分配连续的块减少碎片持久预分配可以提前为文件预留空间避免碎片日志校验对日志做校验提升了日志的可靠性快速 fsck只需要检查空闲的块组大大缩短了 fsck 的时间这些优化让 Ext4 的性能相比 Ext3 有了质的提升尤其是在小文件和大文件的场景下。五、软硬链接Linux 下的链接分为硬链接和软链接它们的实现机制完全不同。5.1 硬链接硬链接的本质就是在目录里添加多个文件名它们都指向同一个 Inode。也就是说多个文件名共享同一个文件的内容和属性Inode 里有一个链接计数记录有多少个文件名指向它。当你删除一个硬链接的时候只是删除了目录里的这个文件名条目链接计数减 1只有当链接计数减到 0 的时候文件的内容和 Inode 才会被真正释放。5.2 软链接软链接是一个独立的文件它有自己的 Inode它的内容就是目标文件的路径。当你访问软链接的时候系统会自动跳转到目标文件。软链接就相当于 Windows 里的快捷方式它可以跨分区甚至可以指向不存在的文件。5.3 两者的区别特性硬链接软链接本质多个文件名指向同一个 Inode独立的文件内容是目标路径跨分区不支持支持指向目录不支持支持删除原文件只是链接计数减 1链接还能正常访问软链接失效变成死链接文件大小和原文件相同是目标路径的字符串长度六、文件系统的完整调用流程最后我们来看一下当我们打开一个文件的时候整个内核的调用流程是什么样的这能帮你把所有的知识点串起来整个流程从用户态的 open 系统调用开始经过 VFS 层找到对应的文件系统然后解析路径找到 Inode最后读取磁盘上的数据整个过程就是我们前面讲的所有知识点的组合。七、总结Ext 系列文件系统是 Linux 的基石从磁盘硬件的 CHS/LBA 寻址到块、Inode 的基础概念再到 Ext2 的块组设计Ext3 的日志Ext4 的性能优化整个体系层层递进非常优雅。理解了这些原理你不仅能搞懂 Linux 文件系统的工作方式也能在实际工作中更好地进行存储性能优化、问题排查比如为什么小文件多了磁盘会慢为什么 Ext4 的 fsck 这么快为什么删除大文件的时候系统会卡等等这些问题的答案都藏在这些底层原理里。参考资料[1]书籍《Linux 文件系统设计与实现》作者Robert Love详细讲解了 Linux 文件系统的底层架构、Ext 系列的实现原理及 VFS 层的工作机制。[2]Ext4 官方规范Ext4 Filesystem Specificationext4 磁盘布局 - ext4详细说明 Ext4 的磁盘布局、Extent 机制、日志实现等核心特性。[3] Linux Ext 文件系统深度解析从原理到演进与实战全维度扩展_ext3 ext4演进-CSDN博客[4] Linux日志文件系统(EXT4、XFS、JFS)及性能分析 - xxxxxxxx1x2xxxxxxx - 博客园[5] ext2 vs ext3 vs ext4 文件系统你应该用哪一个- MiniTool 分区向导