ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux系统篇(二十)——文件(四):吃透 Linux 磁盘与文件系统:一张原理图讲清块、inode、分区底层逻辑

2026/8/19 14:19:02 拓冰建站 浏览量
Linux系统篇(二十)——文件(四):吃透 Linux 磁盘与文件系统:一张原理图讲清块、inode、分区底层逻辑 前言刚接触 Linux 存储时我一直有一堆百思不解的疑问 为什么磁盘要先分区、再格式化才能存文件ls -li输出的 inode 号到底代表什么4KB 块、块组、超级块、位图之间是什么从属关系Windows 靠文件名找文件Linux 为什么非要单独搞一套 inode 机制网上零散教程只讲命令很少把磁盘硬件→分区→文件系统→块组→inode→数据块这条完整链路串起来。直到整理了这张完整底层思维导图从磁盘最底层扇区一路拆解到文件属性、文件名存储逻辑才打通 Linux 文件存储的全部脉络。本文结合原理图从零拆解 Linux EXT 系列文件系统完整底层架构看懂这篇磁盘、inode、软硬链接、格式化原理全都一通百通。一、磁盘最底层硬件最小单位扇区OS 统一封装为「块」1. 硬件层面扇区物理硬盘出厂最小读写单元是扇区传统大小 512 字节现在新型磁盘 4K 扇区操作系统不能直接以扇区管理数据效率太低。2. OS 抽象块BlockLinux 文件系统格式化时会把连续多个扇区封装成块标准默认 4KB可手动调整是文件系统读写的最小单位所有文件、目录、日志最终都按 4KB 块分配存储空间哪怕文件只有 1 字节也会占用一整个 4KB 块这就是磁盘空间会有微小浪费的根本原因。注意磁盘就是一个三维数组我们把它看待成为一个一维数组数组下标就是LBA每个元素都是扇区每个扇区都有LBA那么8个扇区一个块每一个块的地址我们也能算出来。知道LBA块号 LBA/8知道块号LBA块号*8 n.n是块内第几个扇区二、磁盘分层管理磁盘→分区→文件系统三层架构一块裸盘不能直接存文件必须经历分区 → 格式化创建文件系统两步操作原理图清晰画出层级1.Disk整块磁盘磁盘头部存放 MBR 主引导记录同时记录分区表把整块大磁盘切割成多个独立分区Partition1/2/3/4分区隔离不同业务数据损坏互不影响。2.Partition分区单个分区内包含引导扇区Boot Sector分区只是空间划分没有存储文件的规则裸分区无法存放文件必须格式化。柱面是分区的最小单位我们可以利用参考柱面号码的方式来进行分区其本质就是设置每个区的起始柱面和结束柱面号码。此时我们可以将硬盘上的柱面分区进行平铺将其想象成一个大的平面3.File SystemEXT 文件系统格式化就是给分区写入 EXT2/3/4 文件系统规则把分区空间切割成多个块组Block Group这是 Linux 文件系统的核心管理单元。三、EXT2文件系统核心块组 Block Group 内部完整结构每个分区会平均拆分成若干块组所有块组结构完全一致均衡分散数据避免磁盘局部损耗单个块组内部由 6 部分组成位图思想传统的标记方式比如用一个字节/布尔值记录一个磁盘块的占用状态管理大容量磁盘时会产生极大的内存开销。而位图利用一个二进制位对应一个元素的状态比如管理1000万个磁盘块仅需要约1.2MB的内存空间相比传统方案空间压缩了近8倍在早期内存资源极其紧张的计算机环境下大幅降低了内存占用成本。本质上是为了节省空间和时间1.Super Block 超级块Super Block 超级块文件系统全局信息仓库总块数、总 inode 数量、块大小、空闲块 / 空闲 inode 统计、挂载记录。为防止损坏超级块会在每个块组做冗余备份防止超级块损坏接导致分区无法挂载。超级块结构体定义/* * 超级块结构体定义 */ struct ext2_super_block { __le32 s_inodes_count; /* inode 总数量 */ __le32 s_blocks_count; /* 磁盘块总数量 */ __le32 s_r_blocks_count; /* 预留的超级用户可用块总数 */ __le32 s_free_blocks_count; /* 空闲磁盘块总数 */ __le32 s_free_inodes_count; /* 空闲inode总数 */ __le32 s_first_data_block; /* 第一个数据块的块编号 */ __le32 s_log_block_size; /* 块大小的对数实际块大小 1024 (s_log_block_size) */ __le32 s_log_frag_size; /* 片大小的对数 */ __le32 s_blocks_per_group; /* 每个块组包含的磁盘块数量 */ __le32 s_frags_per_group; /* 每个块组包含的片数量 */ __le32 s_inodes_per_group; /* 每个块组包含的inode数量 */ __le32 s_mtime; /* 上次挂载的时间 */ __le32 s_wtime; /* 上次写入的时间 */ __le16 s_mnt_count; /* 累计挂载次数 */ __le16 s_max_mnt_count; /* 两次强制磁盘检查之间的最大挂载次数 */ __le16 s_magic; /* Ext2文件系统魔数用于校验文件系统合法性 */ __le16 s_state; /* 文件系统状态 */ __le16 s_errors; /* 遇到错误时的处理行为 */ __le16 s_minor_rev_level; /* 次版本号 */ __le32 s_lastcheck; /* 上次执行磁盘检查的时间 */ __le32 s_checkinterval; /* 两次磁盘检查的最大允许间隔时间 */ __le32 s_creator_os; /* 创建该文件系统的操作系统 */ __le32 s_rev_level; /* 主版本号 */ __le16 s_def_resuid; /* 可以使用预留块的默认用户ID */ __le16 s_def_resgid; /* 可以使用预留块的默认组ID */ /* * 以下字段仅在EXT2_DYNAMIC_REV版本的超级块中生效 * 旧的兼容版本文件系统不会识别这些扩展字段 * 如果你想使用这些扩展特性必须将s_rev_level设置为1 * 同时必须将s_minor_rev_level设置为非0值 * 否则内核会直接忽略这些扩展字段 */ __le32 s_first_ino; /* 第一个非预留的inode编号 */ __le16 s_inode_size; /* inode结构体的大小 */ __le16 s_block_group_nr; /* 该超级块所在的块组编号 */ __le32 s_feature_compat; /* 兼容特性标志位 */ __le32 s_feature_incompat; /* 不兼容特性标志位内核无法识别时拒绝挂载 */ __le32 s_feature_ro_compat; /* 只读兼容特性标志位 */ __u8 s_uuid[16]; /* 文件系统的唯一UUID标识 */ char s_volume_name[16]; /* 卷名 */ char s_last_mounted[64]; /* 上次挂载的目录路径 */ __le32 s_algorithm_usage_bitmap; /* 压缩算法使用位图 */ /* 性能提示仅在COMPAT_PREALLOC特性开启时生效 */ __u8 s_prealloc_blocks; /* 为文件预分配的块数量 */ __u8 s_prealloc_dir_blocks; /* 为目录预分配的块数量 */ __u16 s_padding1; /* 日志相关字段仅在EXT3_JOURNAL特性开启时生效 */ __u8 s_journal_uuid[16]; /* 日志文件系统的UUID */ __le32 s_journal_inum; /* 日志文件的inode编号 */ __le32 s_journal_dev; /* 日志文件所在的设备号 */ __le32 s_last_orphan; /* 待删除孤立inode链表的起始位置 */ __le32 s_hash_seed[4]; /* 目录哈希算法的种子值 */ __u8 s_def_hash_version; /* 默认使用的目录哈希算法版本 */ __u8 s_reserved_char_pad; __u16 s_reserved_word_pad; __le32 s_default_mount_opts; /* 默认挂载选项 */ __le32 s_first_meta_bg; /* 第一个元数据块组的编号 */ __u32 s_reserved[190]; /* 预留的填充字段用于扩展到块末尾 */ };2.GDT 块组描述符表GDT 块组描述符表记录每个块组的位图、inode 表、数据块起始位置操作系统通过 GDT 快速定位任意块组资源。// 磁盘级blockgroup的数据结构 /* * 块组描述符的结构体定义 */ struct 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节点总数 */ __le16 bg_used_dirs_count; /* 当前块组内已使用的目录类inode总数 */ __le16 bg_pad; /* 内存对齐填充字段 */ __le32 bg_reserved[3]; /* 预留扩展字段 */ };3.Data Blocks 数据块Data Blocks 数据块真正存放文件实际内容的区域普通文件存文本 / 二进制数据目录文件存放「文件名 - inode 号」映射表软链接存放目标文件路径。4.Block Bitmap 块位图Block Bitmap 块位图二进制位图每 1bit 标记一个数据块0 空闲1 已占用。新建文件时系统读取位图分配空闲块删除文件对应 bit 置 0 回收空间。为了实现高速的分配和回收操作文件系统会把完整的块位图加载到内存中。删除文件时只需要在内存里把对应数据块的标识位直接置0不需要擦除磁盘上的实际数据操作速度极快。这也就是为什么我们在下载应用时相对较慢而在删除应用时却很快的原因。5.inode Table inode 表Linux 灵魂inode Table inode 表是整块思维导图的核心红框存储全部文件的属性集合单个 inode 固定 128 字节存储文件元数据文件类型、权限、属主、大小、时间戳、数据块指针关键inode 不存储文件名只存文件内容对应的磁盘块编号磁盘上每一个独立文件 / 目录 / 软链接都独占一个唯一 inode 号ls -li 命令输出的第一列数字就是 inode 编号。ls -li磁盘上存储的inode索引节点核心结构体的C语言源码定义​ /* * 磁盘上存储的inode结构 */ struct ext2_inode { __le16 i_mode; // 文件模式 __le16 i_uid; // 文件所有者的用户ID低16位 __le32 i_size; // 文件总字节大小 __le32 i_atime; // 文件访问时间 __le32 i_ctime; // 文件创建/属性修改时间 __le32 i_mtime; // 文件内容修改时间 __le32 i_dtime; // 文件删除时间 __le16 i_gid; // 文件所属用户组ID低16位 __le16 i_links_count; // 文件硬链接计数 __le32 i_blocks; // 文件占用的磁盘块总数 __le32 i_flags; // 文件标志位 union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1; // 操作系统相关保留字段1 __le32 i_block[EXT2_N_BLOCKS]; // 指向数据块的指针数组 __le32 i_generation; // 文件版本号用于NFS网络文件系统 __le32 i_file_acl; // 文件访问控制列表 __le32 i_dir_acl; // 目录访问控制列表 __le32 i_faddr; // 文件碎片地址 union { struct { __u8 l_i_frag; // 碎片编号 __u8 l_i_fsize; // 碎片大小 __u16 i_pad1; __le32 l_i_uid_high; // 用户ID高16位 __le32 l_i_gid_high; // 用户组ID高16位 __le32 l_i_reserved2[2]; } linux2; struct { __u8 h_i_frag; // 碎片编号 __u8 h_i_fsize; // 碎片大小 __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; // 碎片编号 __u8 m_i_fsize; // 碎片大小 __u16 m_pad1; __u32 m_i_reserved2[2]; } masix2; } osd2; // 操作系统相关保留字段2 } __attribute__((packed)); // 相对于磁盘块的常量定义 #define EXT2_NDIR_BLOCKS 12 // 直接索引块数量 #define EXT2_IND_BLOCK 13 // 一级间接索引块 #define EXT2_DIND_BLOCK 14 // 二级间接索引块 #define EXT2_TIND_BLOCK 15 // 三级间接索引块 #define EXT2_N_BLOCKS 16 // 索引块总数量 备注EXT2_N_BLOCKS 15 ​6.inode Bitmap inode 位图inode Bitmap inode 位图和块位图逻辑一致每 1bit 标记一个 inode 是否空闲负责分配回收文件属性节点。 注意文件名属性并未纳入到inode数据结构内部inode的大小一般是128字节或者256任何文件的内容大小可以不同但是属性大小一定是相同的因此创建一个文件的流程主要有以下4个操作存储属性内核先找到一个空闲的i节点假如是263466。内核把文件信息记录到其中。存储数据该文件需要存储在某些磁盘块假如内核找到了三个空闲块300,500800。将内核缓冲区的第一块数据复制到300下一块复制到500以此类推。记录分配情况文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加文件名到目录新的文件名abc。linux如何在当前的目录中记录这个文件内核将入口263466abc添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。7.跨组但不跨区文件的inode结构体与它实际存储内容的Data Blocks是跨组编号的。这意味着一个文件的 inode 属性可能在 Block Group 0 中而它的内容数据块可能分布在 Block Group 1。如果我的文件比较大一个group里面的block放不下这个文件内容是可以跨组保存的。我们的每一个组都是固定的所以我们可以直接用这个inode的编号去除以这个组的大小并且模上这个组的大小就可以知道我的这个inode在哪个组的哪一个位置。但是inode 和数据块绝对不能跨分区因此在同一个分区内部inode 编号和块号都是唯一的而不同分区之间的 inode 编号则是各自独立的。四、Linux 核心设计文件名与文件数据彻底分离inode 机制这是和 Windows 文件存储最大的区别也是很多人搞不懂软硬链接的根源原理图专门拆解了这套逻辑1. inode 只保管文件本体属性文件大小、权限、创建时间、磁盘数据块地址全部存在 inode 中和文件名完全解绑。系统定位文件本质是先找到 inode再通过 inode 读取磁盘数据块拿到文件内容。2. 文件名只存放在父目录的数据块里目录本身也是特殊文件有自己的 inode 和数据块目录的数据块内存储一张映射表文件名 - inode编号。 也就是说文件名只是 inode 的别名真正唯一标识文件的是 inode 号。 举个场景我们执行cat code.c时流程是读取当前目录的 inode找到目录数据块在目录映射表匹配字符串code.c拿到对应 inode 编号读取该 inode获取文件内容所在数据块读取数据块输出文件内容。所以访问文件必须打开当前目录根据文件名获得对应的inode号然后进行文件访问所以访问文件必须要知道当前工作目录本质是必须能打开当前工作目录文件查看目录文件的内容1.路径解析而上级目录也有当前的目录那就会一直追溯到根目录 而这个过程就叫做路径解析2.路径缓存文件访问依赖路径解析绝对路径默认从根目录/逐层读取各级目录文件匹配文件名拿到目标 inode 才能访问文件。路径访问性能优化全程从根目录逐层解析速度较慢Linux 通过缓存历史路径结构提升查找效率。于是就有了路径缓存操作系统会把历史访问过的文件路径构建成一棵树以便于后面访问快速查找。Linux中在内核中维护树状路径结构的内核结构体叫做struct dentrystruct dentry { atomic_t d_count; unsigned int d_flags; /* 受 d_lock 保护 */ spinlock_t d_lock; /* 每个 dentry 的锁 */ struct inode *d_inode; /* 名称所属的索引节点 - NULL 表示负向不存在 */ /* * 接下来的三个字段会被 __d_lookup 访问。将它们放在这里 * 以便它们都能放入同一个缓存行中。 */ struct hlist_node d_hash; /* 查找哈希链表 */ struct dentry *d_parent; /* 父目录 */ struct qstr d_name; struct list_head d_lru; /* LRU 链表 */ /* * d_child 和 d_rcu 可以共享内存 */ union { struct list_head d_child; /* 父级链表的子项 */ struct rcu_head d_rcu; } d_u; struct list_head d_subdirs; /* 我们的子目录 */ struct list_head d_alias; /* 索引节点别名链表 */ unsigned long d_time; /* 被 d_revalidate 使用 */ struct dentry_operations *d_op; struct super_block *d_sb; /* dentry 树的根超级块 */ void *d_fsdata; /* 文件系统特定数据 */ #ifdef CONFIG_PROFILING struct dcookie_struct *d_cookie; /* cookie如果有的话 */ #endif int d_mounted; unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 小名称内联存储 */ };注意每个文件都有对应的dentry结构包括普通文件。这样所有被打开的文件就可以在内存中形成整个树形结构整个树形节点也同时会隶属于LRU(Least Recently Used最近最少使用)结构中进行节点淘汰整个树形节点也同时会隶属于Hash方便快速查找更重要的是这个树形结构整体构成了Linux的路径缓存结构打开访问任何文件都先在这棵树下根据路径进行查找找到就返回属性inode和内容没找到就从磁盘加载路径添加dentry结构缓存新路径3.inode和datablock映射inode内部存在 __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */EXT2_N_BLOCKS 15就是用来进行inode和block映射的这样文件内容属性就都能找到了。1. 直接块指针inode 里 12 个直接指针指针直接指向普通数据块这个数据块里存的就是文件真实内容文字、图片、程序数据。 → 这种指针最终指向文件内容。如果文件内容太多了该如何存储呢inode 只给了 12 个直接指针假设一块 4KB 12 × 4KB 49152 字节48KB如果文件超过 48KB12 个直接指针根本不够存所有数据块的地址存不下大文件。2. 一级 / 二级 / 三级间接块指针图里带横线的索引表块第一步inode 的间接指针 → 索引表块数据块索引表这个索引表里只存一堆子指针块号完全没有文件内容只是中转目录。第二步索引表里的子指针 → 普通数据块走到这一层才是存放文件内容的地方。3.举二级间接流程inode 二级指针 → 一级索引表全是指针无内容 → 二级索引表全是指针无内容 → 普通数据块真正文件内容这些一级、二级、三级间接指针本质上就是为了解决文件内容太多的问题的4.文件查找的过程五.挂载分区我们已经能够根据inode号在指定分区找文件了也已经能根据目录文件内容找指定的inode了。问题inode不是不能跨分区吗Linux不是可以有多个分区吗我怎么知道我在哪一个分区# 1. 制作一个大的磁盘块就当作一个分区 $ dd if/dev/zero of./disk.img bs1M count5 # 2. 格式化写入文件系统 $ mkfs.ext4 disk.img # 3. 建立空目录 $ sudo mkdir /mnt/mydisk # 4. 查看可以使用的分区 $ df -h # 5. 将分区挂载到指定的目录 $ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 6. 再次查看注意新增了 loop0 $ df -h # 7. 卸载分区 $ sudo umount /mnt/mydisk注意/dev/loop0在Linux系统中代表第一个循环设备loop device。循环设备也被称为回环设备或者loopback设备是一种伪设备pseudo-device它允许将文件作为块设备block device来使用。这种机制使得可以将文件比如ISO镜像文件挂载mount为文件系统就像它们是物理硬盘分区或者外部存储设备一样。ls /dev/loop* -l六、软硬链接底层差异从 inode 一眼看懂结合原理图 inode 机制软硬链接的区别一目了然1.硬链接多个不同文件名指向同一个 inode 编号目录映射表里多条记录绑定同一个 inodeinode 内链接计数 1删除其中一个文件名仅计数 - 1计数归 0 才释放 inode 与数据块。 限制不能跨分区、不支持目录创建硬链接。2.软链接符号链接拥有独立全新 inode自身是单独文件数据块仅存储目标文件路径字符串访问时读取路径再跳转原文件原文件删除后软链接直接失效无计数机制支持跨分区、链接目录。明明没有手动进行硬链接可为什么硬链接数还会发生变化呢其实每一个目录创建后都会有当前目录和上级目录自动进行了硬链接七、格式化、分区实操底层本质总结梳理看完整套架构就能理解日常磁盘操作底层做了什么分区fdisk修改磁盘 MBR 分区表切割磁盘空间边界不修改分区内部数据格式化mkfs.ext4给分区写入 EXT 文件系统划分块组、初始化超级块、位图、空白 inode 表清空原有数据挂载mount读取分区超级块加载块组信息将文件系统关联到系统目录入口rm 删除文件仅把 inode 位图、块位图对应 bit 置为空闲不擦除磁盘数据这是删除文件仍能数据恢复的底层原理。八.文件系统总结结尾总结Linux 文件系统是一套分层、解耦的精密设计硬件扇区封装为块磁盘切割分区隔离空间分区拆分为块组均衡管理再通过 inode 分离「文件属性」和「文件名」。这张思维导图把从磁盘硬件到文件访问的完整链路全部打通弄懂块、块组、inode、目录映射这套底层逻辑再去学习磁盘扩容、数据恢复、软硬链接、磁盘 IO 调优都会事半功倍彻底告别只会敲命令不懂底层原理的阶段。解耦 把两件强绑定、改一个就必须动另一个的东西拆分开让两者互相独立、互不依赖。耦合两件事捆死在一起 解耦拆开各司其职改动其中一边不影响另一边。带来巨大好处给文件重命名只改目录里的名字映射不动 inode、不动文件真实数据几乎零开销硬链接同一个 inode可以对应 N 个不同文件名新增名字完全不用复制文件内容移动同分区文件只是修改父目录映射文件本体 inode 和数据块一点不变删除文件名只是删掉一条映射inode 和数据还在只要还有别的硬链接文件依然可用。核心把「文件标识名」和「文件真实本体」解耦两者独立变化互不牵制。