《Linux系统编程》13.Ext系列文件系统
Yupureki:个人主页✨个人专栏:《C》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》《个人在线OJ平台》Yupureki的简介:目录1. 引入文件系统1.1 块的概念1.1.1 硬件层面的“扇区”1.1.2 文件系统层面的“块”1.1.3 Linux 内核中的“块”抽象1.2 ‘分区的概念1.2.1 分区的作用1.2.2 Linux 中的分区管理1.3 inode‘的概念1.3.1文件由什么组成1.3.2 inode 里存了什么1.3.3 一个类比2. ext2文件系统2.1 宏观认识2.2 分块组2.2.1 超级块2.2.2 块描述符表2.2.3 块位图2.2.4 inode位图2.2.5 inode表2.2.6 数据块Data Block2.3 目录与文件名2.3.1 路径解析2.3.2 路径缓存3. 文件系统的底层操作3.1 创建文件例如 touch /home/user/newfile.txt3.2 读取文件例如 cat /home/user/newfile.txt3.3 删除文件例如 rm /home/user/newfile.txt3.4 进入文件夹例如 cd /home/user3.5 核心机制总结1. 引入文件系统1.1 块的概念1.1.1 硬件层面的“扇区”磁盘硬件的最小寻址单位是扇区sector传统为 512 字节现代硬盘多采用 4096 字节4K的物理扇区。但扇区对操作系统上层而言粒度太小效率不高。1.1.2 文件系统层面的“块”文件系统在格式化时会将连续的若干个扇区组成一个逻辑单位称为块block或簇cluster。例如 ext2 文件系统默认块大小为 4096 字节即 8 个 512 字节扇区。这也是我们所称的”一页的大小块是文件系统进行数据分配、读写的最小单位优点减少元数据开销提高 I/O 效率。缺点块太大可能造成内部碎片小文件浪费空间。在 ext2/ext3/ext4 中块大小可以在格式化时指定1K、2K、4K它直接影响单个文件最大容量通过多级指针寻址文件系统最大总容量空间利用率1.1.3 Linux 内核中的“块”抽象Linux 将磁盘、分区等设备视为块设备block device并提供统一的通用块层block layer。上层文件系统发起基于块的读写请求如读一个 4K 块。通用块层将这些请求构建成 I/O 调度队列合并相邻请求再通过块设备驱动程序转换为底层的扇区操作。因此在 Linux 中“块”既指文件系统管理的最小单位也是内核 I/O 交互的基本单位。1.2 ‘分区的概念其实磁盘是可以被分成多个分区partition的以Windows观点来看你可能会有一块磁盘并且将它分区成CDE盘。那个CDE就是分区。分区从实质上说就是对硬盘的一种格式化。1.2.1 分区的作用分区是将一块物理磁盘如/dev/sda划分为多个逻辑上独立的区域。每个分区可以格式化为不同的文件系统ext4、XFS、NTFS 等独立挂载、备份、修复用于不同用途系统分区、数据分区、交换分区1.2.2 Linux 中的分区管理内核识别Linux 内核通过驱动读取分区表为每个分区生成设备文件如/dev/sda1、/dev/nvme0n1p2。用户空间工具fdisk/gdisk创建、删除、查看分区表分别用于 MBR/GPTparted支持两种格式的高级工具lsblk查看磁盘和分区树状关系分区与文件系统的关系分区只是一个线性地址空间必须在其上创建文件系统即格式化才能存储文件。例如mkfs.ext4 /dev/sda1会将分区 1 格式化为 ext4并建立超级块、块组、inode 表等结构1.3 inode‘的概念Linux 中的 inode可以把它想象成文件的身份证或索引节点。它存储了文件的元数据即“关于数据的数据”但不包含文件名和文件内容本身。1.3.1文件由什么组成在 Linux 中一个文件通常由三部分组成目录项记录文件名和对应的 inode 编号。文件名只是给人看的入口内核实际上通过 inode 编号识别文件。inode记录文件的属性信息以及数据块的位置。数据块存储文件的实际内容。1.3.2 inode 里存了什么通过stat命令可以查看 inode 信息。它包含文件大小字节数文件类型普通文件、目录、设备文件、链接等权限读、写、执行属主和属组UID/GID时间戳atime访问时间mtime内容修改时间ctime状态改变时间权限、属主等变动链接数有多少个文件名指向这个 inode。指针指向存储文件内容的磁盘块的地址这是 inode 最核心的作用它将文件名和物理存储位置关联起来。inode 不包含文件名。文件名存储在目录文件中目录文件本质上是一个“文件名与 inode 编号的映射表”。1.3.3 一个类比想象一个大型仓库仓库管理员是操作系统。货物是文件的数据内容。货架格子是磁盘数据块。入库单就是inode。入库单上写着货物体积大小、谁送的所有者、谁可以搬走权限、送货时间时间戳最重要的是它记录了货物存放在哪个货架格子的编号指针。货物标签就是文件名。你在仓库外面看到的标签如report.pdf它只是一个指向“入库单编号”的指引。如果你把货物标签撕掉删除文件名只要入库单还在仓库管理员就能根据入库单找到货物。但如果入库单也被销毁了删除 inode货物就成了无法被找到的“垃圾”数据恢复的难点所在。2. ext2文件系统我们想要在硬盘上储文件必须先把硬盘格式化为某种格式的文件系统才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在Linux系统中最常见的是ext2系列的文件系统。其早期版本为ext2后来又发展出ext3和ext4。ext3和ext4虽然对ext2进行了增强但是其核心设计并没有发生变化我们仍是以较老的ext2作为演示对象。2.1 宏观认识现代一整块磁盘几乎都是1000GB起步这么大块的资源我们如何处理?很简单利用分治的办法我假设把磁盘分为几个几乎一样的盘就如同Windows上面的C盘D盘E盘等如果我管理好了一个盘就相当于也能把其他盘管理好把其他盘管理好了就把整个磁盘管理好了那一个盘有几百GB还是不好管理怎么办?很简单我再把一个盘分为几个几乎一样的快组假设一个快组只有30GB如果我管理好了一个快组就相当于也能把其他快组管理好把其他快组管理好了就把整个盘管理好了这就是分治的思想2.2 分块组那一个区有30GB我们怎么管理?那么我们就需要对应的组件来管理这个块组组件作用引导块位于分区第一个块通常只有第一个块组包含存放引导加载程序。超级块描述整个文件系统的全局信息大小、块数、inode数等。块组描述符表记录每个块组的位置信息位图、inode表起始块等。块位图用比特位标记组内哪些数据块已使用。inode 位图用比特位标记组内哪些 inode 已使用。inode 表连续存放一组 inode 结构体。数据块实际存储文件内容和目录项。2.2.1 超级块存放文件系统本身的结构信息描述整个分区的文件系统信息。记录的信息主要有block和inode的总量未使用的block和inode的数量一个block和inode的大小最近一次挂载的时间最近一次写入数据的时间最近一次检验磁盘的时间等其他文件系统的相关信息。SuperBlock的信息被破坏可以说整个文件系统结构就被破坏了超级块在每个块组的开头都有一份拷贝第一个块组必须有后面的块组可以没有。为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作就必须保证文件系统的superblock信息在这种情况下也能正常访问。所以一个文件系统的superblock会在多个blockgroup中进行备份这些super block区域的数据保持一致结构体ext2_super_block。包含关键信息s_inodes_count文件系统总 inode 数s_blocks_count总块数s_first_data_block第一个数据块编号s_log_block_size块大小以 1024 字节为单位的对数s_blocks_per_group每组的块数s_inodes_per_group每组的 inode 数s_magic魔数 0xEF53用于识别 ext22.2.2 块描述符表块组描述符表描述块组属性信息整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息如在这个块组中从哪里开始是inodeTable从哪里开始是DataBlocks空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。每个块组对应一个描述符ext2_group_desc记录该组的bg_block_bitmap块位图所在的块号bg_inode_bitmapinode 位图所在的块号bg_inode_tableinode 表起始块号bg_free_blocks_count组内空闲块数bg_free_inodes_count组内空闲 inode 数所有组描述符依次存放在超级块后面的块中通常只占用少量块。挂载时系统读取这些描述符获得各组的位置信息。2.2.3 块位图一个块有 8* 块大小字节个比特。每个比特代表一个数据块0 表示空闲1 表示已用。2.2.4 inode位图类似于块位图标记本组 inode 是否被分配。2.2.5 inode表一组连续的块每个块容纳多个 inode 结构体ext2_inode。每个 inode 大小为 128 字节ext2 标准存储文件元数据i_mode文件类型和权限i_uid/i_gid所有者i_size文件大小i_atime/i_mtime/i_ctime时间戳i_blocks已占用块数512 字节为单位i_links_count硬链接数数据块指针数组核心12 个直接指针i_block[0..11]1 个单间接指针i_block[12]1 个双间接指针i_block[13]1 个三间接指针i_block[14]这种多级索引结构允许文件大小灵活扩展且小文件只需直接指针效率高。2.2.6 数据块Data Block存放实际内容对于普通文件是数据对于目录则是目录项Directory Entry的集合。目录项结构ext2_dir_entry_2inode文件的 inode 编号rec_len本条目录项的长度对齐到 4 字节name_len文件名长度file_type文件类型普通、目录、链接等name文件名变长目录项简单地说就是一个“文件名 → inode 号”的映射表按顺序存储。2.3 目录与文件名问题:既然inode是描述文件的但我们用的仍是路径文件名哪有什么乱七八糟的inode,为什么不用?目录也是文件吗?如何理解答案:目录也是文件但是磁盘上没有目录的概念只有文件属性文件内容的概念。是文件。可以把目录文件理解成一个“电话本”里面记录了“人名文件名”和“电话号码inode 编号”。你按人名查找但真正拨号时用的是电话号码。访问文件必须打开当前目录根据文件名获得对应的inode号然后进行文件访问所以访问文件必须要知道当前工作目录本质是必须能打开当前工作目录文件查看目录文件的内容2.3.1 路径解析问题打开当前工作目录文件查看当前工作目录文件的内容当前工作目录不也是文件吗我们访问?当前工作目录不也是只知道当前工作目录的文件名吗要访问它不也得知道当前工作目录inode吗答案1所以也要打开当前工作目录的上级目录额.上级目录不也是目录吗不还是上面的问题吗答案2所以类似”递归需要把路径中所有的目录全部解析出口是/根目录。最终答案3而实际上任何文件都有路径访问目标文件比如/home/whb/code/test/test/test.c都要从根目录开始依次打开每一个目录根据目录名依次访问每个目录下指定的目录直到访问到test.c。这个过程叫做Linux路径解析。一个例子:当你执行cat /home/user/test.txt时系统内核经历了这样的过程路径解析从根目录/开始内核读取根目录的目录文件这也是一个文件在里面找到home对应的 inode 编号。获取 inode根据编号找到/home的 inode再从它的目录数据块中找到user的 inode 编号。重复最后找到test.txt的 inode 编号。使用 inode内核拿到test.txt的 inode 后才从 inode 中读取权限、大小以及数据块指针然后根据指针去磁盘读取文件内容。结论文件名只是给人看的“标签”系统真正操作的唯一标识是 inode 编号。文件名到 inode 的映射关系就存储在目录文件里。2.3.2 路径缓存为什么需要路径缓存路径解析比如访问/home/user/test.txt需要从根目录开始读取其数据块找到home对应的 inode。根据home的 inode读取其数据块找到user的 inode。再根据user的 inode读取其数据块找到test.txt的 inode。每一步都可能涉及磁盘读取目录数据块而磁盘 I/O 是系统最慢的操作之一。如果每次访问文件都要走一遍完整路径性能会极差。因此Linux 在内核中维护了一个目录项缓存dentry cache通常简称为dcache。它缓存了路径分量文件名与 inode 的映射关系以及目录项之间的树形关系。注意·每个文件其实都要有对应的dentry结构包括普通文件。这样所有被打开的文件就可以在内存中形成整个树形结构整个树形节点也同时会隶属于LRU(LeastRecentlyUsed最近最少使用)结构中进行节点淘汰整个树形节点也同时会隶属于Hash方便快速查找更重要的是这个树形结构整体构成了Linux的路径缓存结构打开访问任何文件都在先在这棵树下根据路径进行查找找到就返回属性inode和内容没找到就从磁盘加载路径添加dentry结构缓存新路径3. 文件系统的底层操作我们可以将文件系统底层操作总结为四个典型场景。这些操作都涉及路径解析依赖 dentry 缓存、inode 管理元数据与数据块指针、目录项修改在父目录的数据块中增删映射以及磁盘位图更新分配或释放 inode 和数据块。下面以 ext2 文件系统为背景按操作逐一描述。3.1 创建文件例如touch /home/user/newfile.txt1. 路径解析内核从根目录开始利用dentry 缓存dcache逐层查找/→home→user。若缓存未命中则读取相应目录的数据块将目录项加载到 dcache。最终获得父目录/home/user的 dentry 和对应的 inode。2. 在父目录中检查重名父目录的 dentry 指向其 inode通过 inode 找到目录数据块。在目录数据块中遍历目录项检查newfile.txt是否已存在。若存在则返回错误。3. 分配 inode从inode 位图中查找空闲位标记为已用并更新块组描述符中的空闲 inode 计数。初始化新的 inode 结构体ext2_inode设置权限、时间戳、链接数初始为 1、文件类型普通文件等。数据块指针数组全部清零尚未分配数据块。4. 在父目录中添加目录项在父目录的数据块中找到一个空闲位置可能扩大目录文件大小添加一条新目录项{ inode号, 文件名, 文件类型 }。如果目录数据块空间不足需要为目录文件分配新的数据块通过块位图分配并更新目录 inode 的指针。5. 更新元数据父目录的 inode 大小增加若新增了数据块时间戳更新。将新 inode 写入磁盘inode 表对应位置。将修改过的父目录数据块和 inode 标记为脏等待回写。6. 缓存更新在 dcache 中创建新的 dentry 对象指向新文件的 inode并插入父目录的哈希表。3.2 读取文件例如cat /home/user/newfile.txt1. 路径解析同样从根目录开始利用 dcache 逐层查找路径分量最终获得目标文件的 dentry。若 dcache 中已有该文件的 dentry则直接使用否则从磁盘读取目录项并创建 dentry。2. 获取 inode通过 dentry 得到文件的 inode 对象可能已在 inode 缓存中。检查进程权限读权限。3. 读取数据块根据 inode 中的直接指针、间接指针等计算出文件内容所在的磁盘块号。通过页缓存page cache读取数据块若数据已在页缓存中直接返回。否则从磁盘读取到页缓存再拷贝到用户空间。4. 更新访问时间修改 inode 的atime访问时间标记 inode 为脏稍后回写磁盘。3.3 删除文件例如rm /home/user/newfile.txt1. 路径解析解析得到目标文件的 dentry 和 inode以及父目录的 dentry 和 inode。2. 检查权限与链接数确认进程有父目录的写权限且文件未被锁定等。3. 从父目录中移除目录项在父目录的数据块中找到newfile.txt对应的目录项将其标记为“空闲”。可以调整相邻空闲项的rec_len合并空间或暂时保留以便后续重用。4. 减少 inode 链接数将目标文件 inode 的i_links_count减 1。5. 释放资源若链接数降为 0如果链接数变为 0则释放文件占用的所有数据块根据 inode 中的数据块指针数组将对应的块位图位清零并更新块组描述符的空闲块计数。释放 inode将 inode 位图对应位清零更新空闲 inode 计数。将 inode 标记为已删除在 inode 表中清除或标记无效。6. 更新父目录元数据父目录的 inode 大小可能减小若删除的目录项导致数据块变空可能释放目录数据块。父目录的mtime和ctime更新。7. 缓存清理从 dcache 中移除该文件的 dentry标记为无效或直接删除。若 inode 已释放也从 inode 缓存中移除。3.4 进入文件夹例如cd /home/user1. 路径解析内核解析路径/home/user利用 dcache 逐层查找最终获得目录user的 dentry。2. 权限检查检查该目录的执行权限x若无权限则拒绝。3. 进程上下文更新内核修改当前进程的当前工作目录cwd指针将其指向目标目录的 dentry。不会对磁盘做任何修改所有操作都在内存中完成。4. 缓存影响如果路径解析过程中某些分量不在 dcache 中会触发磁盘读取目录项并将它们加入 dcache后续访问同一目录即可命中缓存。3.5 核心机制总结操作涉及的关键底层组件主要步骤创建文件父目录 inode、目录数据块、inode 位图、块位图、dcache分配 inode → 添加目录项 → 更新位图 → 创建 dentry读取文件目标文件 inode、数据块指针、页缓存、dcache路径解析 → 通过 inode 定位数据块 → 从页缓存或磁盘读取删除文件父目录 inode、目录数据块、inode 位图、块位图、dcache删除目录项 → 减少链接数 → 若链接数为 0 则释放 inode 和数据块进入文件夹目标目录 dentry、dcache、进程 cwd路径解析 → 权限检查 → 更新进程当前目录指针纯内存操作关键思想文件名与 inode 通过目录项解耦所有路径操作都经过 dcache 加速实际磁盘访问仅限于元数据和数据块读写的必要时机。理解这些底层细节就能更好地把握 Linux 文件系统的性能特点和故障排查思路。