目录磁盘的理解存储结构相关概念CHS和LABCHSLAB公式转换文件系统初步”块“的概念“分区”iNode块组内部Super Block超级块GDTGroup Descriptor TableBlock Bitmap块位图Inode Bitmapinode 位图Inode Table索引节点表Data Blocks数据块疑问磁盘的理解存储结构相关概念拆开一个机械硬盘你会看到里面有几个关键部件盘片Platter像光盘一样闪闪发光的圆盘。数据就刻在上面。磁头Head悬在盘片上方极近的地方负责读取和写入数据。重点每个盘面都有一个磁头但所有的磁头都连在同一个机械臂上同进同退主轴Spindle带着盘片飞速旋转的马达。为了在这么大的盘片上定位数据工程师发明了三个概念磁道Track把盘片想象成操场跑道一圈一圈的同心圆就是磁道。扇区Sector把跑道切成一段一段的每一小段就是一个扇区。这是磁盘存储的最小物理单位通常是 512 字节。柱面Cylinder因为所有盘片的磁头是同进同退的所以当你把磁头停在某个位置时所有盘面上对应位置的磁道在空间上叠在一起形成了一个圆柱体这就是柱面。CHS和LABCHS如果让你去拿数据你需要告诉机械臂去第几个柱面C用第几个磁头H读第几个扇区S这就叫CHS 寻址。但这种方法很反人类因为你要同时管理三个数字而且外圈磁道和内圈磁道能切的扇区数量还不一样早期。⼀个细节传动臂上的磁头是共进退的LAB操作系统是个“大忙人”它不想管你里面有几个盘片、磁头怎么动。它希望用最简单的方式管理存储。所以操作系统把整个磁盘“拍扁”看成一根长长的“录音磁带”。磁带上密密麻麻排满了等大的格子。每个格子就是一个扇区。每个格子都有一个唯一的编号从 0 开始这个编号就叫 LBALogical Block Address逻辑块地址。操作系统只需要说“我要读第 1000 号扇区LBA1000”然后把它交给磁盘控制器。磁盘内部的固件伺服系统会自己用数学公式瞬间把 LBA 1000 转换成对应的 CHS柱面、磁头、扇区然后机械臂精准地去拿数据。公式转换假设磁盘有 100 个柱面4 个磁头每磁道 8 个扇区。想象你在操场上找自己的位置LBA你要先算前面有几个完整的小队。C 算前面有几排每个柱面排有4 × 8 32个扇区。LBA C × 32 H × 8 (S - 1)(注S 减 1 是因为物理扇区从 1 开始数而 LBA 从 0 开始数)反推C LBA ÷ 32算排在前面有几排H (LBA 剩下的余数) ÷ 8算这一排里前面有几层S (LBA 剩下的余数) % 8 1算这一层里第几个座所以OS 只认 LBA磁盘硬件自己转换成 CHS。文件系统初步”块“的概念物理磁盘存取的最小单位是扇区Sector通常只有 512 字节。如果你有一本 10MB 文件需要读 2 万次扇区。磁盘机械臂疯狂寻道效率低。块是操作系统读写磁盘的最小单位。就像图书馆的“书箱”借书还书都以“箱”为单位。这样一来读取一个 10MB 的文件只需搬运 2500 次10MB / 4KB效率直接提升 8 倍“分区”假设你有一块 1TB 的硬盘如果把它当成一个大整体来管理所有的文件、系统备份、用户数据全混在一起。一旦系统崩溃重装系统可能会把数据全毁掉而且寻找文件时面对 1TB 的空间头都要炸了。【引入分区】就像一个大图书馆如果只有一个大厅找书太累了。管理员决定用承重墙把大厅隔开分成科技区C盘、历史区D盘。分区就是把一个大硬盘切成几个逻辑上独立的区域。每个分区可以单独格式化、单独装系统、单独管理。操作系统只需要分别管理这几个独立的小区域复杂度大幅降低。iNode在分区里数据已经被打散成一个个“块”了。假如你写了一个 100KB 的 C 语言代码文件main.c它会被切成 25 个 4KB 的块散落在硬盘的各个角落。操作系统怎么知道这 25 个块属于main.c呢引入 inode】在图书馆里为了找到一本书你必须查“图书检索卡”。这张卡片上写着书名、作者、出版时间文件的属性。最重要的是卡片上写着这本书分别放在了第几排、第几个书架的哪几个箱子里指向文件数据块的指针。这张“图书检索卡”在 Linux 中就叫inode索引节点。一个文件对应唯一的一个 inode。inode 存储了文件的所有属性大小、权限、创建时间等但唯独没有文件名。inode 内部有一个指针数组指向了存储该文件内容的那些数据块Block。块组内部Super Block超级块struct ext2_super_block { __le32 s_inodes_count; // 整个分区有多少个 inode __le32 s_blocks_count; // 整个分区有多少个 4KB 块 __le32 s_free_inodes_count; // 还剩多少空闲 inode __le32 s_free_blocks_count; // 还剩多少空闲数据块 __le32 s_log_block_size; // 块大小比如 4KB // ... };详细解释如果你格式化了一个 300GB 的分区Super Block 里就记着“本分区总共有 7864 万个数据块总共有 196 万个 inode目前全都是空的。”协作情况它只管全局宏观数据。如果有进程想创建一个文件内核会先看一眼 Super Block确认“嗯还有空闲的 inode 和块”才允许创建。格式化是在磁盘上写入管理信息。格式化步骤1. 划分 Block Group 把 300GB 切成一块块的区域比如每个区 128MB划分为 Group 0, Group 1, Group 2... 2. 建立“总账本”写入 Super Block 在 Group 0 的开头写入 Super Block。记录“本分区总共有 7864 万个 4KB 块有 196 万个 inode。块大小是 4KB。”等信息。 3. 建立“索引目录”写入 GDT 写入 Group Descriptor Table告诉系统Group 0 的位图在哪个块Group 1 的位图在哪个块…… 4. 清空“状态位图”初始化 Bitmap 把 Block Bitmap 和 Inode Bitmap 全部用 0 填满。 全是 0 代表什么代表所有数据块都是空的所有 inode 也都是空闲的。 5. 创建“根目录” 在 Inode Table 里分配 1 号 inode 给根目录 /并在某个数据块里写入根目录的初始内容通常是 . 和 ..。GDTGroup Descriptor Tablestruct ext2_group_desc { __le32 bg_block_bitmap; // 块位图在这个块组的哪个块 __le32 bg_inode_bitmap; // inode位图在这个块组的哪个块 __le32 bg_inode_table; // inode表在这个块组的哪个块 __le16 bg_free_blocks_count; // 这个块组还剩多少空闲块 __le16 bg_free_inodes_count; // 这个块组还剩多少空闲inode // ... };详细解释因为一个大分区被切成了很多个 Block GroupGDT 就是用来管理这些 Block Group 的。它相当于一个“目录”告诉你 Block Group 0 的位图在哪、Block Group 1 的位图在哪。协作情况当系统要在某个组找 inode 时先查 GDT瞬间定位到那个组的 Inode Table 起始位置。Block Bitmap块位图内部结构它根本不是一个复杂的结构体它就是一段连续的字节byte。每一个比特位bit代表一个数据块Block。0代表该数据块是空闲的。1代表该数据块已经被占用了。详细解释假设本块组的 Block Bitmap 是0000 0001从右往左看说明 0号到6号块是空的7号块被占了。协作情况当你要往文件里写数据比如追加 4KB文件系统扫描这串 0101 的位图找到第一个为0的 bit把它改成1就说明这个块被分配给你了。不仅分配快还省空间1个字节就能管8个块。Inode Bitmapinode 位图内部结构和块位图一模一样也是一串连续的字节。每一个比特位bit代表一个 inode。0代表这个 inode 号码还没人用。1代表这个 inode 已经被分配出去了。详细解释Inode Table 是一个大数组比如能容纳 1024 个 inodeInode Bitmap 就是用来标记这 1024 个 inode 哪些被用掉了。协作情况当你touch abc新建文件时系统查 Inode Bitmap找到第一个为0的 bit把它标记为1并给你分配这个 inode 号比如 263466。Inode Table索引节点表struct ext2_inode { __le16 i_mode; // 文件类型和权限 (比如 0644) __le16 i_uid; // 所有者ID __le32 i_size; // 文件大小字节 __le32 i_atime; // 访问时间 __le32 i_mtime; // 修改时间 __le32 i_links_count; // 硬链接数 __le32 i_block[15]; // 核心记录文件数据存放在哪些 Block 里 // ... };详细解释这就是我们前面反复提的inode。在 4KB 的数据块里一个 128 字节的 inode 可以塞下 32 个。所以一个块组里的 Inode Table 可能占据了成百上千个数据块里面密密麻麻全是 inode。协作情况一旦分配了 inode 号系统就会在 Inode Table 里找到这个结构体把文件大小、权限、以及真正的数据存在哪几个块i_block数组全部记进去。Data Blocks数据块内部结构没有特定结构就是一块 4KB 的空白内存。存什么完全取决于你的文件。如果你存的是代码这里面就是代码字符。如果你存的是目录这里面存的就是{文件名, inode号}的列表这就是前面说的“目录也是文件”。协作情况Inode 里的i_block[15]记录着它们的编号。OS 拿着编号直接去 Data Blocks 里读数据。疑问1、为什么super block单独放在Block group内部而不是单独拿出来机械硬盘是极其脆弱的物理设备。如果一块磁道坏了或者磁盘表面刮花了那块区域的数据就彻底没了。 如果只放一份假设你把 Super Block 单独放在分区的开头。一旦硬盘开头那几个扇区出现了物理坏道这在早期硬盘中很常见整个分区就直接瘫痪了操作系统连这个分区有多大、有多少空闲空间都不知道根本无法挂载。 冗余备份Ext2 的设计者规定每一个 Block Group 的开头都要包含一份 Super Block 的拷贝。而且这些拷贝是保持同步的。 如果 Group 0 的 Super Block 所在的物理磁道坏了操作系统会尝试去 Group 1 读取备份的 Super Block。 只要有一份备份存活你的文件系统就还能被抢救回来。 (注现代文件系统如 ext4 做了优化虽然每个组都有备份空间但为了节省空间通常只在少数几个组里真正存储 Super Block 的完整拷贝这叫稀疏备份。)2、inode的编号在文件系统里面是唯一的吗是的inode 的编号在整个文件系统也就是一个独立的分区内是唯一的。“inode和数据块跨组编号的。所以在同一个分区内部inode编号和块号都是唯一的”inode 表是分开的Block Group 0有自己的一段Inode TableBlock Group 1也有自己的一段Inode Table。编号是全局递增的虽然它们分散在不同的组里但文件系统给它们统一编了号。比如Group 0的 inode 编号可能是1 ~ 1024。Group 1的 inode 编号紧接其后可能是1025 ~ 2048。计算公式如果已知一个 inode 号通过(inode号 - 1) / 每组inode数就能算出它在哪个块组通过(inode号 - 1) % 每组inode数就能算出它是这个组里的第几个 inode。怎么知道 inode 在哪一个分组里假设你在 Super Block 里查到每个块组Block Group固定包含 8192 个 inode。现在你手里拿着一个文件的 inode 编号12345。操作系统用脚趾头算一下就知道了分组号 (inode 编号 - 1) ÷ 每组 inode 数分组号 (12345 - 1) ÷ 8192 12344 ÷ 8192 1取整结论这个 inode 12345 存在Block Group 1里面。(注减 1 是因为 inode 编号是从 1 开始的而数组下标是从 0 开始的)问题二怎么知道在分组中位图的哪一个位置既然知道了它在 Group 1那它在 Group 1 的 Inode Bitmap 里具体是第几个比特位bit呢接着用刚才的余数算组内偏移量 (inode 编号 - 1) 每组 inode 数组内偏移量 12344 % 8192 4152结论它的确切位置是 Group 1 的 Inode Bitmap 里的第4152个 bit。进一步定位到具体结构体Inode Bitmap 里第 4152 个 bit 肯定被标记为了1表示已分配。Inode Table 里系统直接在 Group 1 的 Inode Table 中往后跳过 4151 个 inode 结构体每个 128 字节找到第 4152 个。这个结构体就是你要找的文件的属性档案