基于 inode 的文件系统详解

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 删除文件时,系统执行的操作是:

  1. 在目录中找到文件名对应的目录项。
  2. 删除该目录项,并将对应 inode 的链接计数减 1。
  3. 如果链接计数变为 0,则释放该 inode,并将其指向的数据块标记为空闲。

6.1 删除操作的底层细节

rm 命令最终会调用系统调用 unlink()(删除硬链接)或 unlinkat()。内核在删除文件时,会依次执行以下步骤:

  1. 定位目录项:根据路径逐级查找,在父目录的数据块中找到目标文件名对应的目录项。
  2. 删除目录项:将该目录项从目录数据块中移除(或标记为无效),并更新目录的 mtime。
  3. 链接计数减 1 :将对应 inode 的 i_nlink 字段减 1。
  4. 判断是否释放 :若 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 相关的问题。
  • 深入理解硬链接与软链接的本质区别。
  • 在文件系统设计与调优时做出更合理的决策。

希望本文能为你打开文件系统底层世界的大门。

相关推荐
kmomo..44 分钟前
Linux SPI 驱动开发学习笔记:从框架到实战
linux·驱动开发·学习
小此方1 小时前
Linux网络(二十二):深入解析IP协议:从地址寻址、头部结构到内核机制与TTL环路控制
linux·网络·tcp/ip
Ruiery1 小时前
Linux 6.6内核内存管理深度解析(四):大页(HugeTLB / THP)— 为什么一个页表项要管 2MB
linux·运维·服务器
Lsetea1 小时前
OpenSSL报certificate rejected:error 28与CA辅助信任属性排查
linux·https·ssl证书·openssl·证书链
沫璃染墨1 小时前
从零入门计算机网络系列(五):UDP套接字编程实战——从核心API到客户端与服务器通信
linux·网络·c++·vscode·网络协议·udp
Margrop1 Margrop1 小时前
别再挨个试清华、中科大、阿里源了!国家队下场的“CERNET 镜像聚合站”,凭什么让你的下载直接起飞?
linux·开源镜像·cernet
一条破秋裤1 小时前
05_设备树自定义属性_写入读取验证
linux·网络·数据库
此生决int1 小时前
Linux系统(07)——进程概念(下)
linux·运维·服务器
迷途之人不知返2 小时前
【基础IO】-3-系统级别的文件操作
linux