2026/10/24 2:13:09

【Linux系统编程】Ext 系列文件系统(2):Ext2文件系统

【Linux系统编程】Ext 系列文件系统(2):Ext2文件系统 文章目录1. 引入2. ext2 文件系统2.1 宏观认识2.2 Block GroupData BlocksBlock Bitmapinodeinode Tableinode Bitmapinode和datablock的映射GDTGroup Descriptor Table块组描述符表Super Block超级块2.3 磁盘格式化补充2.4 目录文件2.5 路径解析2.6 路径缓存2.7 挂载分区1. 引入上一篇文章我们提到过扇区sector是磁盘硬件读写的最小单位。传统 HDD机械硬盘 通常是 512 字节现在很多磁盘是 4K 物理扇区。块block是操作系统/文件系统访问磁盘的基本单位大小由文件系统决定常见是 4KB。比如 ext4 的块大小通常为 4K。磁盘也叫块设备。引入块的概念其实硬盘是典型的“块”设备操作系统读取硬盘数据的时候其实是不会一个个扇区地读取这样效率太低而是一次性连续读取多个扇区即一次性读取一个”块”block。硬盘的每个分区是被划分为一个个的”块”。一个”块”的大小是由格式化的时候确定的并且不可以更改最常见的是4KB即连续八个扇区组成一个 ”块”。”块”是文件存取的最小单位。引入分区的概念磁盘可以抽象为一个三维数组三维数组底层本质也是一个一维数组数组下标就是LBA每个元素都是扇区• 每个扇区都有LBA那么8个扇区一个块每一个块的地址我们也能算出来。• 知道LBA块号 LBA/8• 知道块号LBA块号*8 n. (n是块内第几个扇区)所以站在文件系统的角度就可以将磁盘看成是一个块数组——block array[n];假设现在有一个磁盘800GB这个容量是比较大的但是如果直接这样使用的话其实是不太好管理的。就好比我们的国家中国地域辽阔为了方便管理所以就划分成了一个个的省省下面又分成市-县-乡镇-村所以磁盘这里也是类似的道理一整块磁盘太大了所以可以对磁盘进行一个分区比如将上面的磁盘分成这样四个区然后我们来简单补充理解一下OS是怎么把数据写给磁盘的一句话总结操作系统通过设备驱动程序读写设备控制器的控制、状态和数据寄存器来访问外设同时还会结合 DMA、中断等机制完成高效数据传输。通俗点讲操作系统主要通过驱动程序读写外设的寄存器来指挥外设但搬大量数据时通常还会让 DMA 帮忙、靠中断通知。2. ext2 文件系统所有的准备工作都已经做完是时候认识下文件系统了。我们想要在磁盘上存储文件必须先把磁盘格式化为某种格式的文件系统才能存储文件。文件系统的目的就是组织和管理磁盘中的文件。在Linux 系统中最常见的是 ext2 系列的文件系统。其早期版本为 ext2后来又发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进行了增强但是其核心设计并没有发生变化我们这里就以 ext2 作为演示对象。2.1 宏观认识物理磁盘可以被抽象成这样的扇区数组块block是操作系统/文件系统访问磁盘的基本单位大小由文件系统决定常见是 4KB。比如 ext4 的块大小通常为 4K。磁盘也叫块设备一个块包含多个扇区一整块磁盘太大了所以可以对磁盘进行一个分区国-省只要能管理一个分区就能管理所有分区也就能管理所有磁盘文件。所有分区的信息一般可以保存到第一个扇区中但是一个分区可能也有几百GB也并不小。所以就可以在分区里面继续划分省-市ext2文件系统将整个分区划分成若干个同样大小的块组 (Block Group)那现在问题就转换为如何管理一个小组了分治思想上图中启动块Boot Block/Sector的大小是确定的为1KB由PC标准规定用来存储磁盘分区信息和启动信息任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。那下面我们就来研究一下一个块组2.2 Block Groupext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。文件内容属性内容和属性都是数据都要被存储。Linux中文件的内容和属性是分开存储的。块block是操作系统/文件系统访问磁盘的基本单位大小由文件系统决定常见是 4KB。比如 ext4 的块大小通常为 4K。磁盘也叫块设备Data BlocksData Blocks 数据块真正存放文件内容目录也是一种文件它的数据块里存放“文件名 → inode 号”的目录项后面介绍我们图是这样画的但其实Data Blocks 占据了绝大部分的空间。Data Blocks中的每一个数据块都有一个编号从0开始比如说一个分组的Data Blocks中有这么多的数据块这些数据块可能共同存放了多个文件的内容数据。并且某一时刻可能有的数据块被占用了有的还未被使用那如何辨别呢这就是下面我们要介绍的Block BitmapBlock Bitmap 块位图用一个二进制位表示本块组中一个数据块是否已分配。每一个比特位的位置就代表了不同的数据块该比特位的值是1 表示已用0 表示空闲。分配数据块时先查它。下一个问题文件的内容存放在数据块中并且我们说Linux中文件的内容和属性是分开存储的。那么文件的属性元信息存到哪里呢inodeinode 是 Unix/Linux 文件系统如 ext2/ext3/ext4里的核心概念本质是一个结构体全称 index node索引节点。inode中就几乎包含了上面列出的所有的文件属性但是不包含文件名。一个 inode 通常保存inode中有一个唯一的标识符叫做inode号也唯一标识了该inode对应的文件我们可以来看一下ext2文件系统中inode的实现因为inode结构体的大小是固定的当然属性的内容可以不一样inode Tableinode Table 是 ext2/ext3/ext4 文件系统中真正存放 inode 记录 的磁盘区域。你可以把它理解成一个按 inode 号索引的固定大小数组每个元素是一条磁盘 inode 记录。基本结构每个块组都有自己的 inode Table。整个文件系统的 inode 总数在格式化时就确定了不能动态增加。每个 inode 记录大小固定ext2/ext3 默认 128 字节ext4 默认 256 字节inode Table 由连续的数据块组成内部按固定大小切成一个个槽位。每个槽位对应一个 inode 号。所以 inode Table 不是“动态插入”的表而是一块预先划分好的连续磁盘空间。所以创建一个文件初始时内容是空的但也要先创建对应的inode放入inode table中。那如何知道inode table哪些位置是空闲的哪些位置已被占用呢inode Bitmap同样用位图表示本块组中每个 inode 是否已分配。创建文件时先在这里找空闲 inode。总结一下inode Bitmap用一个二进制位标记 inode Table 中每个 inode 是否已分配。inode Table实际存放 inode 记录。创建文件时先在 inode Bitmap 找空闲位得到 inode 号然后写 inode Table 对应槽位。删除文件时释放 inode 号把 inode Bitmap 对应位清零并清空或标记 inode Table 中的记录。所以inode Bitmap 管分配状态inode Table 存实际内容。上面我们说inode中有一个唯一的标识符叫做inode号也唯一标识了该inode对应的文件ls后加个-i 选项就可以查看文件的inode号那现在我们知道文件内容是存到data blocks中的文件属性是存到inode table中的。现在两者是分离的。那查找一个文件的时候通过inode号找到inode就可以找到文件的属性那如何找到文件内容呢inode和datablock的映射ext2 inode 结构体中有一个数组用来存放数据块的块号。这些块号指向真正存放文件内容的数据块。这样就能进一步找到文件的内容了。小文件用直接块大文件逐级使用间接块GDTGroup Descriptor Table块组描述符表每个块组对应一个描述符记录该块组的块位图、inode 位图、inode 表的位置以及空闲块数、空闲 inode 数、目录数等。所有描述符合起来就是 GDT。Super Block超级块存放文件系统本身的结构信息描述整个分区的文件系统信息。记录的信息主要有bolck 和 inode的总量未使用的block和inode的数量一个block和inode的大小最近一次挂载的时间最近一次写入数据的时间最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏可以说整个文件系统结构就被破坏了那么有个问题既然super block描述的是整个分区的文件系统信息那为什么存在一个块组里面超级块在多个块组的开头都有一份拷贝。为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作就必须保证文件系统的super block信息在这种情况下也能正常访问。所以一个文件系统的super block会在多个block group中进行备份这些super block区域的数据保持一致。2.3 磁盘格式化磁盘格式化简单说就是在磁盘或分区上建立文件系统让操作系统能够按文件、目录的方式存取数据。它不是把磁盘物理上“擦干净”而是写入一套管理规则和元数据。作用在分区内部创建文件系统磁盘格式化会做这些事确定文件系统参数块大小inode 数量、inode 大小每块组多少块、多少 inode划分块组把分区分成多个块组写入元数据超级块 Super Block块组描述符表 GDT块位图 Block Bitmapinode 位图 inode Bitmapinode 表 inode Table建立根目录创建 / 目录的 inode 和目录项写入文件系统标识UUID、卷标、特性标志、挂载状态等所以格式化就是创建/重新初始化 超级块、GDT、位图、inode 表这些结构的过程。补充在同一个分区同一个文件系统内块号和 inode 号都是全局唯一的。但要注意“全局”的范围仅限当前文件系统/分区内部。不同分区之间可以重复。那么问题来了我现在拿到一个inode号怎么知道是在哪个分区呢“inode号”本身只是一个文件系统内部的局部标识必须和“设备号”组合才能在整个系统中唯一定位一个文件。一个“块设备”对应一个设备号而“分区”通常会被内核注册为一个独立的块设备因此也拥有自己的设备号。每个 inode 在内核中都有一个指针 i_sb直接指向它所属文件系统的超级块superblock。超级块是文件系统的“户口本”记录了该文件系统的全部元信息其中就包括它所在的设备号s_dev。因此内核只需从 inode 出发通过 i_sb 就能知道它属于哪个文件系统、位于哪个设备上。2.4 目录文件上面我们还提到文件名是不在inode中的那文件名在哪呢我们平时直接使用的都是文件名而不是什么inode号目录也是文件目录的内容目录的属性目录同样也有这些属性当然也是存到inode中。内容当然也要存到data blocks中。那目录的内容是什么呢目录也是一种文件它的数据块里存放“文件名 → inode 号”的目录项我们可以来验证一下这里我已经写好了一份代码#includestdio.h#includestring.h#includestdlib.h#includedirent.h#includesys/types.h#includeunistd.hintmain(intargc,char*argv[]){if(argc!2){fprintf(stderr,Usage: %s directory\n,argv[0]);exit(EXIT_FAILURE);}DIR*diropendir(argv[1]);// 系统调用自行查阅if(!dir){perror(opendir);exit(EXIT_FAILURE);}structdirent*entry;while((entryreaddir(dir))!NULL)// 系统调用自行查阅{// Skip the . and .. directory entriesif(strcmp(entry-d_name,.)0||strcmp(entry-d_name,..)0){continue;}printf(Filename: %s, Inode: %lu\n,entry-d_name,(unsignedlong)entry-d_ino);}closedir(dir);return0;}这段代码是一个遍历指定目录、列出其中所有文件不含子目录内容及其 inode 号的小工具。功能上类似 ls -i 目录 的简化版。来运行看一下所以文件名不在inode中保存而是在该文件所在的目录中保存。所以我们平时用的都是文件名如何通过文件名找到文件的内容和属性呢目录里面不是存了文件名 → inode 号的映射嘛所以通过文件名就可以找到文件的inode号那进而就可以在inode table中找到文件的inode那文件的属性和内容不就都可以拿到了嘛。所以同一个目录下不能存在同名文件因为文件名是键要通过文件名去找inode号。所以创建一个新文件主要有以下4个操作存储属性内核先找到一个空闲的i节点。内核把文件信息记录到其中。存储数据假设该文件需要存储在三个磁盘块内核找到了三个空闲块300,500800。将内核缓冲区的第一块数据复制到300下一块复制到500以此类推。当然如果只是 touch abc 创建一个空文件通常不会立即分配数据块而是写入数据时再分配记录分配情况文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加文件名到目录新的文件名abc。linux如何在当前的目录中记录这个文件内核将入口263466abc添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。现在我们就在原理上明白之前我们讲权限的时候为什么在一个目录下创建文件需要w权限了查看目录的内容需要r权限2.5 路径解析所以访问一个文件时要在其所在目录的数据块中查找“文件名 → inode 号”的目录项最终得到目标文件的 inode 号再通过 inode 访问文件数据。比如这里我们要访问cloud.cpp那就需要读取目录文件server的内容可是server也是一个文件啊那要访问server得内容不就需要能够读取它得所在目录——cloud得内容嘛同样cloud也是一个文件那就…就需要这样逐级往上直到根目录。根目录是确定的我们看到它的inode号是2所以我们得出一个结论访问文件时内核要通过路径名逐级解析目录在每个目录的数据块中查找“文件名 → inode 号”的目录项最终得到目标文件的 inode 号再通过 inode 访问文件数据。这就是为什么我们之前访问文件或者open打开一个文件必须要用文件路径为什么进程的PCB中要有CWD。但是这样的话是不是每访问一个文件都要根据路径进行逐级解析这样不慢吗2.6 路径缓存问题1Linux磁盘中存在真正的目录吗答案磁盘上没有“真正的目录”只有文件。目录是一种特殊文件它的数据块里存放目录项。问题2访问任何文件都要逐级进行路径解析答案原则上是但是这样太慢所以Linux会缓存历史路径结构问题2Linux目录的概念怎么产生的答案目录是文件系统定义的一种特殊文件用于组织文件和子目录。它在磁盘上有 inode 和数据块数据块中存放目录项。Linux中在内核中维护树状路径结构的内核结构体叫做 struct dentry每个文件其实都要有对应的dentry结构包括普通文件。file结构体我们已经很熟悉了我们看到它里面有个成员变量——struct dentry *f_dentry;这样所有被打开的文件就可以在内存中形成一个树形结构。• 整个树形节点也同时会隶属于LRU(Least Recently Used最近最少使用)结构中内存紧张时进行节点淘汰• 整个树形节点也同时会隶属于Hash方便快速查找•更重要的是这个树形结构整体构成了Linux的路径缓存结构打开访问任何文件都在先在这棵树下根据路径进行查找找到就返回属性inode和内容没找到就从磁盘加载路径添加dentry结构缓存新路径那这样的话做路径解析的时候第一次通常是慢的需要读磁盘、建缓存。第二次、第 n 次的时候只要 dentry 缓存没有被 LRU 淘汰路径解析会优先从 dentry 哈希表和树结构中进行速度极快。这完美展示了 Linux VFS 的设计哲学用一棵树dentry tree描述层级用哈希表d_hash加速查找用 LRU 链表d_lru管理生命周期用 d_inode 连接文件本体的 inode。这就是 Linux 能够高效处理海量文件路径的底层原因2.7 挂载分区分区写入文件系统无法直接使用需要和指定的目录关联进行挂载才能使用。“分区写入文件系统”mkfsMake File System它的作用正是在一个裸分区上建立起文件系统所需的结构如超级块、inode 表、根目录等让操作系统能够在这个分区上管理和存取文件。这一步操作通常也被称为格式化。 后分区里有了超级块、inode 表、根目录形成一棵自包含的目录树。“无法直接使用”这棵树不在系统单根目录树里没有路径能到达它。“需要和指定的目录关联”使用mount命令选一个系统树里的目录作为挂载点如 /home。“进行挂载才能使用”mount 建立 vfsmountvfsmount 是 Linux VFS 中用来描述一次挂载行为的核心结构体。你可以把它理解为一个“连接器”或“挂载记录”它负责将一个文件系统的根目录与系统目录树中的一个挂载点关联起来路径解析经过挂载点时切换到该文件系统文件才可通过路径访问。一图总结