1. 引言
在 Linux 和 Unix 系统中,文件系统是操作系统最核心的组成部分之一。当我们谈论文件系统时,inode 是一个绕不开的关键概念。inode(index node,索引节点)是文件系统用来存储文件元数据的数据结构,它记录了文件的类型、权限、大小、时间戳以及数据块在磁盘上的位置等信息。
理解 inode 的工作原理,不仅有助于我们深入掌握文件系统的底层机制,还能帮助我们解决日常运维中遇到的"磁盘空间不足但文件却删不掉""文件系统损坏"等问题。本文将带你从 inode 的基本概念出发,逐步深入剖析基于 inode 的文件系统是如何工作的。
2. 什么是 inode
inode 是文件系统中用于描述文件或目录元数据的数据结构。每个文件或目录在创建时,文件系统都会为其分配一个唯一的 inode 编号,并建立对应的 inode 结构。
2.1 inode 中存储的信息
一个典型的 inode 包含以下信息:
- 文件类型:普通文件、目录、符号链接、设备文件等
- 权限位:读、写、执行权限(rwx)
- 属主信息:文件所有者 UID 和所属组 GID
- 文件大小:以字节为单位的文件长度
- 时间戳:访问时间(atime)、修改时间(mtime)、状态变更时间(ctime)
- 链接计数:指向该 inode 的硬链接数量
- 数据块指针:指向文件实际数据在磁盘上存储位置的指针数组
- 扩展属性:ACL 访问控制列表等
2.2 inode 中不存储的信息
需要特别注意的是,inode 中不包含文件名。文件名存储在目录项(dentry)中,目录项将文件名与 inode 编号关联起来。这也是为什么同一个 inode 可以被多个文件名(硬链接)引用。
3. inode 与文件系统的关系
3.1 文件系统的整体结构
一个基于 inode 的文件系统(如 ext4、xfs)在磁盘上的布局通常分为以下几个区域:
text
+------------------+
| 引导扇区 |
+------------------+
| 超级块 | <- 记录文件系统整体信息
+------------------+
| 块组描述符表 |
+------------------+
| 块位图 | <- 记录哪些数据块已被占用
+------------------+
| inode 位图 | <- 记录哪些 inode 已被占用
+------------------+
| inode 表 | <- 存放所有 inode 结构
+------------------+
| 数据块区域 | <- 存放文件实际内容
+------------------+

-
超级块(Superblock):保存文件系统的全局信息,如总块数、inode 总数、空闲块数等。
-
块组描述符表(Group Descriptor Table) :ext 系列文件系统会把磁盘划分为多个块组(Block Group) ,每个块组内部都包含自己的块位图、inode 位图、inode 表和数据块。块组描述符表就是一张数组,每个条目(Group Descriptor)对应一个块组,记录该块组的核心信息,主要包括:
- 块位图所在的块号(bg_block_bitmap)
- inode 位图所在的块号(bg_inode_bitmap)
- inode 表起始的块号(bg_inode_table)
- 该块组空闲块数、空闲 inode 数、目录数等统计信息
也就是说,内核要访问某个块组里的 inode 或数据块时,先查块组描述符表,拿到该块组的位图和 inode 表位置,才能进一步定位。
-
inode 表(inode Table):集中存放所有 inode 结构,每个 inode 有固定大小(ext4 默认为 256 字节)。
-
数据块(Data Blocks):真正存放文件内容的区域。
3.2 从文件名到数据的完整路径
3.3 目录文件的 inode 结构
目录在 Linux 中本质上也是一种文件 ,它同样拥有自己的 inode。区别在于:普通文件的 inode 数据块指针指向的是文件内容,而目录文件的 inode 数据块指针指向的是目录项(dentry)------即一张「文件名 → inode 编号」的映射表。
一个典型的目录数据块结构如下:
text
目录数据块
+----------------+----------------+
| 文件名 | inode 编号 |
+----------------+----------------+
| . | 262145 | <- 当前目录
| .. | 262144 | <- 上级目录
| readme.txt | 262146 |
| docs/ | 262147 | <- 子目录
| script.sh | 262148 |
+----------------+----------------+
可以看到,目录项中只保存文件名与 inode 编号的对应关系,并不保存文件的其他元数据。当我们访问某个文件时,内核正是通过目录项拿到 inode 编号,再进一步读取 inode 中的权限、大小、数据块指针等信息。
当我们在 shell 中执行 cat /home/user/readme.txt 时,内核经历了以下查找过程:
#mermaid-svg-mp0wwiKe90Qv7geC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mp0wwiKe90Qv7geC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mp0wwiKe90Qv7geC .error-icon{fill:#552222;}#mermaid-svg-mp0wwiKe90Qv7geC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mp0wwiKe90Qv7geC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mp0wwiKe90Qv7geC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mp0wwiKe90Qv7geC .marker.cross{stroke:#333333;}#mermaid-svg-mp0wwiKe90Qv7geC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mp0wwiKe90Qv7geC p{margin:0;}#mermaid-svg-mp0wwiKe90Qv7geC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mp0wwiKe90Qv7geC .cluster-label text{fill:#333;}#mermaid-svg-mp0wwiKe90Qv7geC .cluster-label span{color:#333;}#mermaid-svg-mp0wwiKe90Qv7geC .cluster-label span p{background-color:transparent;}#mermaid-svg-mp0wwiKe90Qv7geC .label text,#mermaid-svg-mp0wwiKe90Qv7geC span{fill:#333;color:#333;}#mermaid-svg-mp0wwiKe90Qv7geC .node rect,#mermaid-svg-mp0wwiKe90Qv7geC .node circle,#mermaid-svg-mp0wwiKe90Qv7geC .node ellipse,#mermaid-svg-mp0wwiKe90Qv7geC .node polygon,#mermaid-svg-mp0wwiKe90Qv7geC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mp0wwiKe90Qv7geC .rough-node .label text,#mermaid-svg-mp0wwiKe90Qv7geC .node .label text,#mermaid-svg-mp0wwiKe90Qv7geC .image-shape .label,#mermaid-svg-mp0wwiKe90Qv7geC .icon-shape .label{text-anchor:middle;}#mermaid-svg-mp0wwiKe90Qv7geC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mp0wwiKe90Qv7geC .rough-node .label,#mermaid-svg-mp0wwiKe90Qv7geC .node .label,#mermaid-svg-mp0wwiKe90Qv7geC .image-shape .label,#mermaid-svg-mp0wwiKe90Qv7geC .icon-shape .label{text-align:center;}#mermaid-svg-mp0wwiKe90Qv7geC .node.clickable{cursor:pointer;}#mermaid-svg-mp0wwiKe90Qv7geC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mp0wwiKe90Qv7geC .arrowheadPath{fill:#333333;}#mermaid-svg-mp0wwiKe90Qv7geC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mp0wwiKe90Qv7geC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mp0wwiKe90Qv7geC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mp0wwiKe90Qv7geC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mp0wwiKe90Qv7geC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mp0wwiKe90Qv7geC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mp0wwiKe90Qv7geC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mp0wwiKe90Qv7geC .cluster text{fill:#333;}#mermaid-svg-mp0wwiKe90Qv7geC .cluster span{color:#333;}#mermaid-svg-mp0wwiKe90Qv7geC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mp0wwiKe90Qv7geC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mp0wwiKe90Qv7geC rect.text{fill:none;stroke-width:0;}#mermaid-svg-mp0wwiKe90Qv7geC .icon-shape,#mermaid-svg-mp0wwiKe90Qv7geC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mp0wwiKe90Qv7geC .icon-shape p,#mermaid-svg-mp0wwiKe90Qv7geC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mp0wwiKe90Qv7geC .icon-shape .label rect,#mermaid-svg-mp0wwiKe90Qv7geC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mp0wwiKe90Qv7geC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mp0wwiKe90Qv7geC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mp0wwiKe90Qv7geC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户输入路径 /home/user/readme.txt
查找根目录 '/' 的 inode
读取根目录数据块,找到 'home' 目录项
获取 home 目录的 inode 编号
读取 home 目录数据块,找到 'user' 目录项
获取 user 目录的 inode 编号
读取 user 目录数据块,找到 'readme.txt' 目录项
获取 readme.txt 的 inode 编号
读取 inode 中的数据块指针
按指针读取文件数据
可以看到,文件名 → inode → 数据块 是文件访问的三层映射关系。
4. inode 的数据块寻址方式
inode 中存储数据块指针的方式是文件系统设计的关键。以经典的 ext 系列文件系统为例,采用多级索引结构:
4.1 直接指针
inode 中预留了 12 个直接指针,每个指针直接指向一个数据块。假设块大小为 4KB,则直接指针可寻址 12 × 4KB = 48KB 的数据。
4.2 间接指针
- 一级间接指针:指向一个索引块,该索引块中存放指向数据块的指针。4KB 的索引块可存放 1024 个指针(每个指针 4 字节),可寻址 1024 × 4KB = 4MB。
- 二级间接指针:指向一个索引块,该索引块中的指针再指向一级索引块,可寻址 1024 × 1024 × 4KB = 4GB。
- 三级间接指针:可寻址 1024 × 1024 × 1024 × 4KB = 4TB。
text
inode 结构
+----------------------------+
| 直接指针 0 ~ 直接指针 11 | -> 直接指向数据块
+----------------------------+
| 一级间接指针 | -> 指向索引块 -> 数据块
+----------------------------+
| 二级间接指针 | -> 指向索引块 -> 索引块 -> 数据块
+----------------------------+
| 三级间接指针 | -> 指向索引块 -> 索引块 -> 索引块 -> 数据块
+----------------------------+

这种多级索引设计在文件较小时开销极低(直接指针即可覆盖),在文件较大时又能突破单块大小的限制,兼顾了性能与容量。
5. 硬链接与软链接
inode 机制直接决定了硬链接和软链接的行为差异。
5.1 硬链接
硬链接是指向同一个 inode 的多个目录项。创建硬链接时,系统只是新增一个目录项,将文件名指向已有的 inode,并将该 inode 的链接计数加 1。
bash
# 创建硬链接
ln /home/user/readme.txt /home/user/readme_hard.txt
# 查看 inode 编号,两者相同
ls -i /home/user/readme.txt /home/user/readme_hard.txt
硬链接的特点:
- 所有硬链接共享同一个 inode,修改任一链接的内容,其他链接同步变化。
- 只有链接计数降为 0 时,inode 和数据块才会真正被释放。
- 硬链接不能跨文件系统创建,也不能链接目录(防止循环引用)。
5.2 软链接(符号链接)
软链接是一个独立的文件,它拥有自己的 inode,但文件内容存储的是目标文件的路径字符串。
bash
# 创建软链接
ln -s /home/user/readme.txt /home/user/readme_soft.txt
# 查看 inode 编号,两者不同
ls -i /home/user/readme.txt /home/user/readme_soft.txt
软链接的特点:
- 软链接有自己的 inode,与目标文件相互独立。
- 目标文件被删除后,软链接变成"悬空链接"(dangling link)。
- 软链接可以跨文件系统,也可以链接目录。
6. 删除文件与 inode 的释放
当我们执行 rm 删除文件时,系统执行的操作是:
- 在目录中找到文件名对应的目录项。
- 删除该目录项,并将对应 inode 的链接计数减 1。
- 如果链接计数变为 0,则释放该 inode,并将其指向的数据块标记为空闲。
6.1 删除操作的底层细节
rm 命令最终会调用系统调用 unlink()(删除硬链接)或 unlinkat()。内核在删除文件时,会依次执行以下步骤:
- 定位目录项:根据路径逐级查找,在父目录的数据块中找到目标文件名对应的目录项。
- 删除目录项:将该目录项从目录数据块中移除(或标记为无效),并更新目录的 mtime。
- 链接计数减 1 :将对应 inode 的
i_nlink字段减 1。 - 判断是否释放 :若
i_nlink变为 0,说明没有任何目录项再引用该 inode,此时:- 将该 inode 标记为空闲,并更新 inode 位图;
- 释放 inode 指向的所有数据块,并更新块位图;
- 若文件有间接索引块,还需逐级释放索引块本身。
text
删除文件流程
+------------------+
| rm readme.txt |
+------------------+
|
v
+------------------+
| 查找目录项 | -> 在父目录数据块中找到 readme.txt
+------------------+
|
v
+------------------+
| 删除目录项 | -> 从目录数据块中移除
+------------------+
|
v
+------------------+
| 链接计数减 1 | -> i_nlink: 1 -> 0
+------------------+
|
v
+------------------+
| 释放 inode | -> 更新 inode 位图
+------------------+
|
v
+------------------+
| 释放数据块 | -> 更新块位图,标记为空闲
+------------------+
这里有一个常见的运维困惑:磁盘空间不足,但删除文件后空间并未立即释放。这通常是因为:
- 有进程仍持有该文件的文件描述符(fd),文件虽已从目录中移除,但 inode 仍被进程引用,数据块暂不释放。
- 文件被硬链接多次引用,删除一个链接后链接计数未归零。
bash
# 查看被删除但仍被进程占用的文件
lsof | grep deleted
6.2 删除目录时的特殊处理
删除目录与删除普通文件略有不同。rmdir 只能删除空目录 ,内核会先检查目录数据块中是否还有除 . 和 .. 之外的目录项:
- 若目录非空,
rmdir会返回ENOTEMPTY错误,拒绝删除。 - 若目录为空,则删除该目录的目录项,并将目录 inode 的链接计数减 1(此时通常从 2 变为 0,因为空目录只有
.和..两个引用)。
而 rm -r 会先递归删除目录内的所有子项,再删除目录本身。这也是为什么 rm -r 比 rmdir 更危险------它会级联释放大量 inode 和数据块。
7. 查看 inode 信息的常用命令
7.1 stat 命令
bash
stat /home/user/readme.txt
输出示例:
text
File: /home/user/readme.txt
Size: 1024 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 262145 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
Access: 2026-10-14 10:00:00.000000000 +0800
Modify: 2026-10-14 09:30:00.000000000 +0800
Change: 2026-10-14 09:30:00.000000000 +0800
7.2 ls -i 命令
bash
ls -i /home/user/
输出每个文件的 inode 编号。
7.3 df -i 命令
bash
df -i
查看文件系统的 inode 使用情况。当 inode 耗尽时,即使磁盘还有剩余空间,也无法创建新文件。
8. inode 耗尽问题与解决
8.1 问题现象
当文件系统中存在大量小文件时,inode 可能先于磁盘空间被耗尽。此时执行 touch newfile 会报错:
text
touch: cannot touch 'newfile': No space left on device
但 df -h 显示磁盘仍有大量剩余空间。
8.2 排查方法
bash
# 查看 inode 使用率
df -i
# 找出占用 inode 最多的目录
find / -xdev -type f | cut -d '/' -f 2 | sort | uniq -c | sort -nr | head
8.3 解决思路
- 清理无用的小文件、缓存文件、日志文件。
- 将大量小文件迁移到支持更大 inode 容量的文件系统。
- 重新格式化文件系统时,通过
mkfs.ext4 -i参数调整 inode 密度(每多少字节分配一个 inode)。
9. 总结
inode 是理解 Linux 文件系统的钥匙。本文从 inode 的定义出发,梳理了文件系统磁盘布局、多级索引寻址、硬链接与软链接的差异、文件删除与 inode 释放机制,以及 inode 耗尽的排查方法。
掌握 inode 的核心原理,能帮助你:
- 更准确地诊断磁盘空间与 inode 相关的问题。
- 深入理解硬链接与软链接的本质区别。
- 在文件系统设计与调优时做出更合理的决策。
希望本文能为你打开文件系统底层世界的大门。