ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux inode 从原理到排查:VFS 索引节点、耗尽定位与优化实践

Linux inode 从原理到排查:VFS 索引节点、耗尽定位与优化实践

有一次线上告警让我印象特别深:某台业务机磁盘还剩 8G 空间,但连touch /tmp/test这种最简单的操作都提示No space left on device,排查了一圈,df -h看分区一点问题没有,后来才反应过来去看df -i,果不其然,inode 已经 100% 用完了。就是从那次开始,我意识到很多搞后端和运维的朋友对“索引节点 inode”的理解浮在表面——知道它是 VFS 里的一个结构,但说不清它到底怎么管着文件、为什么小文件会把它耗尽、以及它和数据落盘之间是怎么配合的。

这篇是基于我自己的排查经历和读内核源码的笔记整理出来的,围绕 Linux VFS 中的核心对象 inode 展开,从它解决什么问题、结构体每个字段怎么理解、生命周期怎么流转,到线上 inode 耗尽的定位和清理方法,一次讲透。内容不算太底层到寄存器级别,但也不是“会用 df -i 就行”那种科普,适合做后端开发、SRE、系统运维、以及对内核机制感兴趣的读者阅读消化。

1. inode 到底在解决什么问题

想要彻底理解 inode,先得绕开一个常见误区:inode 不是“文件名对应的编号”,它本质上是 VFS 层为了统一管理“文件”这个抽象概念而设计的内核对象。把这个问题想清楚,后面所有字段都顺了。

1.1 从路径名到数据块:一次 open 背后的寻址链条

你在终端敲cat /data/logs/app.log时,内核要做的事比你想象的多得多。首先它会沿着路径逐级解析:先找到/这个目录的 dentry,再找data目录,再找logs目录,最后才定位到app.log这个目录项。每一步解析,最终都要落到一个关键问题:这个目录项对应的文件数据存在磁盘哪个位置、文件的元数据是什么。

路径解析拿到的是一个 dentry(目录项),而 dentry 内部挂着一个指向 inode 的指针。inode 里才存着真正的干货:文件的类型、权限、属主、大小、时间戳,以及一组指向数据块的指针。换句话说,路径名和 dentry 是“怎么找到这个文件”的入口,inode 才是“这个文件本身到底是什么”的载体。

这里有个特别容易让人疑惑的点:为什么不能直接让 dentry 保存所有文件信息,非要再套一层 inode?答案是硬链接。同一个文件可以被多个目录项指向,如果文件信息全部存在 dentry 里,每创建一个硬链接就得拷贝一份元数据,根本无法保证一致性。inode 独立出来之后,多个 dentry 通过指针共享同一个 inode,文件的元数据只有一份,这就从根上解决了问题。

再看数据块寻址。对于传统磁盘文件系统(比如 ext4),inode 里存着一组块指针,可以是直接块指针、间接块指针,或者 ext4 的 extent 树根。读文件时,VFS 通过 inode 找到逻辑块号,再经由文件系统的map_blocks这类回调转换成物理块号,最终落到块设备上。整个过程对用户态完全透明,这也是 VFS 设计的目的——统一不同类型文件系统的差异,让系统调用层只跟 inode、dentry、file 这几个抽象对象打交道。

1.2 inode 与 dentry:一对容易被绕晕的搭档

我见过不少人把 inode 和 dentry 混为一谈,实际上它们在 VFS 里的分工非常明确:

  • dentry(目录项):负责路径名解析和目录结构组织。它缓存的是“文件名 → inode 指针”的映射关系,解决的是“怎么找到文件”的问题。
  • inode(索引节点):负责保存文件元数据和数据块映射。它解决的是“文件是什么、数据在哪”的问题。

有个很直观的类比:如果整个文件系统是一座图书馆,dentry 就是书架上的索引卡,写着“某某书在哪个书架哪个位置”;inode 则是图书本身的登记信息,包括作者、页数、存放柜台号。一本书可以被多个索引卡引用(硬链接),但你真正翻开读内容时,看的始终是那本书本身。

在 dcache 设计里,dentry 缓存是 VFS 性能的关键。每次访问文件时,内核优先查 dcache,如果命中了 dentry,就免去了逐级磁盘目录项查找。但 dentry 缓存的生命周期和 inode 不一定同步:dentry 可能因为内存压力被回收,而 inode 如果还被别的 fd 引用着就还活着。这就是为什么ls -l一个已经删除但仍被进程打开的文件还能看到完整信息——因为 inode 还在内存里,只是对应的 dentry 已经从目录树里摘掉了。

2. struct inode 逐字段拆解,一次搞清楚

struct inode定义在include/linux/fs.h,字段非常多,初学者一看就头大。我按功能把它拆成几组,这样记起来就不乱。

2.1 元数据字段:权限、大小、时间戳从哪读

inode 最直观的那批字段,就是对应stat命令输出的信息:

  • ino_t i_ino:inode 号,文件系统内唯一。
  • umode_t i_mode:文件类型和权限位。文件类型包括普通文件、目录、符号链接、块设备、字符设备、socket、FIFO;权限位就是 rwx 那套。
  • kuid_t i_uid/kgid_t i_gid:属主和属组 ID。
  • loff_t i_size:文件大小,普通文件是字节数,目录则通常表示目录项数据的总大小。
  • struct timespec64 i_atime/i_mtime/i_ctime:访问时间、修改时间、状态变更时间。注意 ctime 不是文件创建时间,是 inode 元数据最后一次变更的时间。
  • unsigned int i_blocks:文件占用的磁盘块数,单位是 512 字节扇区。i_size是逻辑大小,i_blocks是物理占用,稀疏文件就是靠这两个字段的差异体现出“看着大、实际占用少”。

很多人会问 inode 自身存在哪?以 ext4 为例,文件系统在 mkfs 时就会规划好 inode 表,每个块组里有专门区域存放固定大小的磁盘 inode 结构,默认每个 inode 占 256 字节。内存中的struct inode比磁盘 inode 大得多,因为还要挂哈希链表、各种状态位、内存管理字段。VFS 在读取文件时会先把磁盘的 ext4_inode 读出来,再用它填充内存的 struct inode,这就是“磁盘 inode 和内存 inode”其实是两回事。

2.2 状态标志位:I_NEW、I_DIRTY、I_LOCK 都干嘛的

真正让内核开发者头大的不是元数据字段,而是unsigned long i_state和unsigned int i_flags这一堆位。

i_state是运行时状态,常见的有:

  • I_NEW:inode 刚分配出来,还没完成初始化。其他进程如果在这个状态下通过哈希表找到了它,会知道“这东西还没准备好”,于是自旋等待。初始化完成后要调用unlock_new_inode()清掉这个位,唤醒等待者。
  • I_DIRTY:表示 inode 的元数据或者关联数据是脏的,需要写回磁盘。细分的话还有I_DIRTY_DATASYNC、I_DIRTY_SYNC、I_DIRTY_PAGES几种,对应不同粒度的同步语义。
  • I_LOCK:正在对这个 inode 进行 I/O 操作,比如正在读它的数据块、正在写回。其他想访问的人得等锁释放。
  • I_FREEING、I_WILL_FREE:inode 正在被回收或即将被回收,dentry 层和文件层要避免再引用它。

i_flags则是文件属性标志,比如S_IMMUTABLE(不可变文件)、S_APPEND(只能追加)、S_NOATIME(不更新访问时间)这些。它和i_mode里的权限位是两回事,别混。前者是管理员通过 chattr 设置的文件属性,后者是通用的 rwx 权限。

调试时想查看 inode 状态,可以用/proc/sys/fs/inode-state,第 4 列是脏 inode 数量,能直观反映写回压力。我后文会专门讲怎么利用这个文件。

2.3 两个操作函数表:i_op 和 i_fop 的分工

结构体里有两个指针经常把人绕晕:

const struct inode_operations *i_op; // inode 操作 const struct file_operations *i_fop; // 文件操作

这俩的区别其实是 VFS 设计里最精髓的部分之一。inode_operations是针对 inode 本身的操作,是文件系统的“静态元数据操作”,比如:

  • create:在这个目录下创建一个新文件
  • lookup:查找目录项并返回对应的 inode
  • link/unlink:创建/删除硬链接
  • mkdir/rmdir/symlink:创建目录、删除目录、创建符号链接
  • getattr/setattr:读取/修改 inode 元数据
  • setxattr/getxattr:扩展属性操作

而file_operations是针对已经打开的文件“文件流”的操作,是打开之后才用到的,比如:

  • read/write:读写文件内容
  • open/release:打开/关闭文件
  • mmap:内存映射
  • iterate:读取目录项(对目录来说就是用 readdir 遍历)
  • fsync:强制落盘

用大白话说:i_op管的是“怎么在文件系统里创建/查找/删除一个文件”,i_fop管的是“文件打开之后怎么读写”。所以普通文件、目录、符号链接虽然都挂 inode,但它们的i_fop完全不同——目录的 read 没有意义,对应的是iterate遍历子项;符号链接的 read 读的是目标路径,而不是普通数据。

2.4 address_space:文件怎么和页缓存接上线的

inode 里还有一组特别重要的字段:

struct address_space *i_mapping; struct address_space i_data;

address_space是 inode 与页缓存之间的桥梁。文件快照落不落盘、读出来之后放哪,都由它管。每个文件内容对应的物理页面都挂在i_mapping管理的树结构上(现在用 xarray),内核以页为单位缓存文件数据。readpage、writepage、write_begin、write_end这些回调都注册在这个结构里,具体文件系统会提供自己的实现。

为什么 inode 要背着这么大一个映射结构?因为文件数据必须以页为单位进入页面缓存,而页属于物理内存层,和文件系统无关。VFS 通过struct address_space把“文件逻辑页号”映射到“页缓存里的 page”,文件系统再通过它把页写回对应磁盘位置。每次 read 一定是先查页缓存,命中就直接拷贝数据到用户空间;不命中才调底层的磁盘读函数把页从磁盘拉上来。

理解了i_mapping之后,很多问题就通了:为什么读同一个文件两次第二次特别快?因为第二次命中了页缓存。为什么用free看内存明明被程序占了很多,drop_caches一执行内存瞬间空出来?因为文件页缓存挂在 address_space 上,可以按 inode 精确回收。可以说 inode 把虚拟文件系统和物理页缓存焊在了一起。

3. inode 的完整生命周期,从读到写再到释放

前面都是静态看结构,这一章动态过一遍 inode 的“一生”:从磁盘上来,到内存里工作,最后被回收。理解了生命周期,“什么时候它占用多少内存、什么时候需要释放”就有数了。

3.1 首次打开文件:内存 inode 是怎么被“唤醒”的

第一次 open 一个文件时,内核并没有直接分配一个全新的 inode 对象。它会先走iget_locked()这套流程:根据文件系统实例(super_block)和 inode 号去全局哈希表里查找,如果能找到就直接返回,找不到才新建。

新建过程大致是这样:分配一个空的struct inode,设置 super_block 指针和 inode 号,标记为I_NEW放入哈希表,然后调用具体文件系统的回调去读磁盘。比如 ext4 会走ext4_iget,从 inode 表里读出 256 字节的磁盘 inode,填充内存结构中的大小、权限、时间戳、块映射等信息。填充完成后再调unlock_new_inode()清掉I_NEW,让其他等待者得以使用。

这个流程里有很关键的一点:同一个 inode 在内存中只会有一份。也就是说,同一个文件被打开 10 次,内核不会创建 10 个 struct inode,只会在同一个 inode 上增加引用计数。这也是 inode 缓存能够大幅提升性能的根本原因。不过每次 open 都会创建一个新的struct file,它才是真正对应 fd 的对象,file内部指向 dentry,dentry 指向 inode。

3.2 创建一个新文件:inode 分配流程

新建文件的路径是:用户调用 open 带 O_CREAT → VFS 在父目录的 dentry 上执行 lookup 发现不存在 → 调用父目录 inode 的i_op->create回调 → 具体文件系统分配新的磁盘 inode。

以 ext4 为例,它会从 inode 位图里找一个空闲位,初始化磁盘上的 ext4_inode 结构,分配 inode 号,然后把“父目录的数据块”更新,添加一个新的目录项(dentry),内容包含文件名和 inode 号。内存 inode 同样是先建好放进哈希表,等拿到磁盘 inode 数据后再填充。

这个过程中 inode 号和数据块都是文件系统的稀缺资源。ext4 的 inode 数量在 mkfs 时就是固定的,也就是说一个 ext4 文件系统最多能创建多少文件,从格式化那一刻就决定了。这也是后文“inode 耗尽”问题的根源。

这里顺便提一个日常误区:i_nlink和i_count。i_nlink是硬链接数,表示目录项中有多少个名字指向它,磁盘上真实存在;i_count是内存引用计数,表示当前有多少活跃引用。删除文件时先减i_nlink,只有硬链接数归零后才会真正触发数据块释放;而i_count只影响内存对象的释放时机。有时候你rm掉一个大文件,df显示空间没释放,就是因为还有进程用 fd 持有它,i_count不为零,内核认为“还有人要用”,所以数据块还留着。

3.3 脏 inode 回写:数据落盘的全部路径

inode 变成脏的有两条线:一是文件内容被写脏,Page Cache里的页面标记为 PG_dirty;二是元数据被改脏,比如 chmod、chown、touch,此时 inode 本身的字段变脏,标记I_DIRTY。

内核回写机制从什么维度触发?三个层面:

  • 周期性回写:内核线程flush(writeback 工作队列)周期性地扫描脏页和脏 inode,默认行为遵从/proc/sys/vm/dirty_writeback_centisecs(写回周期,默认 500 厘秒)和/proc/sys/vm/dirty_expire_centisecs(脏页过期时间,默认 3000 厘秒,即 30 秒)。也就是说,绝大多数脏数据不是写完立刻落盘,而是过一段时间被后台进程批量刷。
  • 内存压力触发:当系统内存紧张时,内核通过balance_dirty_pages强制回写,避免脏页无限膨胀。这也解释了为什么大文件拷贝时系统响应会卡——写脏速度超过回写速度,内核会阻塞写者。
  • 显式同步:用户调用sync、fsync、fdatasync,或者文件被关闭时可能触发同步(取决于具体文件系统挂载参数和 I/O 语义)。

写回时有个顺序问题很关键:数据页和 inode 元数据谁先落盘?一般做法是先写数据块,再写 inode 元数据(包括文件大小、时间戳更新)。因为如果先把 inode 改大再写数据,中途断电时用户会看到一个大小正确但内容缺失的文件;反过来先写数据再改 inode,最坏情况是 inode 记录的大小偏旧,数据块变成孤儿块,但不会出现“知道有内容却读不到”的逻辑错误。这也是日志文件系统能在崩溃恢复后尽量保持一致性的一个重要设计原则。

3.4 什么时候回收:缓存空间与命中率如何权衡

inode 是放内存缓存里复用的,但内存终究有限。内核回收 inode 主要通过两个入口:

  • iput()调用了iput_final,当i_count归零,且硬链接数也为零时,走销毁路径evict_inode,释放所有关联页缓存和数据块,真正删除文件。
  • 如果i_count归零但硬链接数不为零,则 inode 进入 LRU 列表,等待被回收。回收时需要先从哈希表摘除,再销毁内存对象。

控制 inode/dentry 缓存回收姿态的内核参数是/proc/sys/vm/vfs_cache_pressure,默认 100。值越大,回收越激进。如果机器上文件数量极多、经常出现 inode cache 占用过高,可以适当调高;但如果调到特别高,比如 300、500,每次操作文件都可能因为缓存被清而重新发起磁盘 I/O,性能反而下降。这个参数不是越大越好,需要自己压测权衡。

还要注意,一个 inode 被回收不等于文件被删除。它只是从内存中消失,磁盘上的 inode 和目录项还在,下次访问再从磁盘读回来。真正删除发生在i_nlink归零之后,那是另一个故事。

4. 实战排查:磁盘有空间却写不进文件

这一章节讲回开头那个事故。这种问题不是少数,邮件队列、容器镜像层、npm 缓存、Git 对象仓库这类小文件密集的场景,特别容易触发。

4.1 第一反应:确认是不是 inode 已耗尽

听到“磁盘写不进”的第一反应通常是查空间,但如果你已经有经验,会顺手把 inode 一起查了:

df -h /data df -i /data

df -i输出里有 Inodes、IUsed、IFree、IUse% 这几列。如果 IUse% 到了 100%,那问题就清楚了。同时可以使用/proc内核计数器确认:

cat /proc/sys/fs/inode-nr cat /proc/sys/fs/inode-state

inode-nr第一列是当前已分配的 inode 数量,第二列是空闲数量。inode-state还有脏 inode 数等更多维度。多个分区中只有一个分区 inode 耗尽时,/proc看到的是全局数据,只能说明系统整体压力,定位具体盘还是靠df -i。

再补充一个容易忽略的点:即使 inode 没到 100%,如果剩余 inode 太少,比如只剩几百个,某些应用频繁创建临时文件也会立刻撞墙。这类问题df -i显示 IUse% 接近 100%,但 df -h 一切正常,很容易被误判。

4.2 揪出 inode 消耗大户

确认是 inode 耗尽后,下一个问题就是:谁把 inode 吃光了?两个常用办法:

方法一:du --inodes按目录统计。

du --inodes -x /data 2>/dev/null | sort -nr | head -20

du --inodes是 GNU coreutils 新版本提供的参数,直接按目录统计 inode 占用,比 find 快几个数量级。

方法二:用 find 统计一级子目录文件数(老系统可用):

find /data -xdev -type f | cut -d/ -f3 | sort | uniq -c | sort -nr | head -20

注意这里-xdev参数很重要,表示不跨文件系统。因为每个文件系统有自己的 inode 表,跨文件系统统计会把多个盘的 inode 混在一起,误导判断。我见过有人忘了加-xdev,把/proc的数统计工给算进去,结果十几万的 inode 价值瞬间把真实目录淹没了。

统计出来之后,基本就是那几类元凶:邮件队列/var/spool/postfix/maildrop、Docker 的 overlay2 目录(尤其容器频繁创建删除小文件时)、npm/yarn 缓存、git 仓库.git/objects里大量小对象、以及没配置轮转或筛除的临时目录。

4.3 事故收尾和长期预防措施

处理已经耗尽的情况很简单,删掉临时文件即可,但如果这个目录是业务生产目录,建议顺序做:

  1. 先找到具体目录里的文件数量:find /data/path -type f | wc -l
  2. 确认这些文件确实可删后,按时间批量清理,比如删除 30 天前的:find /data/path -type f -mtime +30 -delete
  3. 如果文件过多导致 find 本身慢,可以先按一级子目录分批处理,避免一次扫全盘。

然后是最关键的预防措施。ext4 在 mkfs 时 inode 数量就固定了,想增加只能重建文件系统。所以格式化前需要根据业务场景估算 inode 密度。mkfs.ext4 默认大约每 16KB 空间分配一个 inode,也就是说块设备越大,inode 数量越多,但 inode 密度的“比例”是固定的。如果业务是海量小文件,建议用更小的-i参数,比如:

mkfs.ext4 -i 4096 /dev/sdb1

意思是每 4KB 空间分配一个 inode。也可以直接指定 inode 数量上限:

mkfs.ext4 -N 5000000 /dev/sdb1

用dumpe2fs -h /dev/sdb1可以提前查看Inode count和Inode size。对于无法重做的存量盘,只能靠清理和监控兜底:给 inode 使用率加告警,阈值设到 85% 左右就要报警,别等 100% 再处理。

如果是 xfs 文件系统,其实没有固定的 inode 表,它会动态分配 inode 并写入所在的分配组,基本不会遇到 inode 耗尽问题,但inode32和inode64挂载参数会影响 inode 分配的位置,这也算是个经验点。长期规划时,如果业务场景确认是小文件密集,优先考虑 xfs 这类动态 inode 的文件系统,比在 ext4 上算配额省心太多。

5. 观测 inode 的三板斧:从 /proc 到 tracepoint

前面讲了那么多理论,真正到定位问题时,靠的还是观测手段。这一章分享我常用的三个层面。

5.1 先用现成的 /proc 计数器

Linux 提供了一组可以直接读的内核计数器:

  • /proc/sys/fs/inode-nr:全局 inode 数量和空闲数量。
  • /proc/sys/fs/inode-state:除了历史数据,还包含脏 inode 数量。
  • /proc/sys/vm/vfs_cache_pressure:字典缓存回收的“积极程度”,默认 100。
  • /proc/sys/vm/dirty_writeback_centisecs和/proc/sys/vm/dirty_expire_centisecs:回写周期和脏数据过期时间。
  • /proc/meminfo中的Slab和SReclaimable:可以观察 inode/dentry 缓存在内存中的占用。如果 Slab 值异常大,往往就是 inode cache 或者 dentry cache 撑爆了。

有一个比较实用的组合思路:先用inode-state观察脏 inode 急剧增多,说明系统大量文件被修改且还没落盘;此时配合iotop看有没有回写线程在疯狂写盘。如果是内存压力大,看meminfo的 Slab 占了多大。这些信息组合在一起,基本能拼出 inode 层面的完整健康画像。

5.2 tracepoint:在内核里抓 inode 的动静

如果只是看计数器还不够细腻,可以用内核 tracepoint 直接跟踪。Linux 内核里 ext4、xfs 等文件系统都注册了大量 tracepoint,比如 ext4 有ext4_iget、ext4_dirty_inode、ext4_evict_inode等。用 ftrace 可以直接开启:

echo 'ext4:ext4_iget' > /sys/kernel/tracing/set_event echo 1 > /sys/kernel/tracing/tracing_on # 等几秒 cat /sys/kernel/tracing/trace

这样能看到每次 inode 被读取时的进程上下文、inode 号、设备等,对判断哪个进程在疯狂创建小文件非常有效。

如果习惯使用 BPF 工具,用 bpftrace 写一段小脚本统计 ext4 inode 分配事件:

bpftrace -e 'tracepoint:ext4:ext4_alloc_inode { @[comm] = count(); }'

当然,生产环境要小心开 tracepoint 的开销,先小范围采样、跑几秒钟就关掉。捕获到的数据往往能一针见血地告诉你:到底是哪个进程在批量创建文件,那个程序设计上又为什么这么爱吃 inode。

5.3 一些常用内核参数的调优经验

调优 inode 相关参数,我的经验排序是这样的:

  • 如果 inode cache 长时间占用过高,而业务又不常重复访问旧文件,适当调高vfs_cache_pressure是有收益的,比如 150-200 之间。但别一次跳到 500,回收太激进会导致所有文件操作都变成冷启动,磁盘 I/O 直线上升。
  • 如果频繁fsync导致小文件写性能差,可以适当调大dirty_expire_centisecs,让脏页面在内存里聚合更久再回写。但对数据库这类要求强一致的业务,这个参数不要动,否则可能导致崩溃恢复后数据丢失。
  • 如果创建/删除文件的吞吐是瓶颈,优先查是不是 dentry cache 抖动;这种情况下单纯调 inode 相关参数效果有限,更该关注的是线程模型和文件系统类型选择。

6. 几个绕不开的细节和常见误区

写到这里,把平时答疑中最常被问到的几个点集中列一下,都是和 inode 直接相关的。

6.1 inode 号能不能当全局唯一标识

inode 号只在同一个文件系统内唯一。不同分区、不同磁盘上的文件完全可以有相同的 inode 号。如果你在做 NFS、分布式存储、日志归因等需要唯一标识文件的工作,一定要同时带上设备号(st_dev)+ inode 号(st_ino)组合。stat命令能看到这两个值:

stat /data/logs/app.log

ls -i只显示 inode 号,容易给人“全局唯一”的错觉。我在做备份增量比对时就吃过亏:用 inode 号做唯一键,结果跨盘迁移后一堆文件被误判为“没变化”,白白漏掉了几百个文件。

6.2 硬链接与 inode 的引用关系

硬链接的本质就是多个目录项指向同一个 inode。所以:

  • ln a.txt b.txt不会复制数据,只是把 b.txt 的目录项加进来,同时让i_nlink加 1。
  • 删除其中一个,i_nlink减 1,但只要还有别的链接着,文件数据和 inode 就不会真正释放。
  • cp则完全不同:复制会创建全新的 inode,i_ino会变,文件内容相同但身份不同。
  • mv在同一文件系统内移动文件,只改目录项,不改变 inode;跨文件系统移动则必须做“复制+删除”,因为目标盘的 inode 表是另一套。

这条知识对理解“为什么移动文件很快,但复制很慢”“为什么df空间明明释放了,但 inode 占用还很高”这些现象都很重要。如果一个目录下硬链接多了,du默认不会重复统计底层数据,但 inode 计数仍然会每个目录项各算一个,容易把人绕晕。

6.3 那些不能只看 inode 的场景

inode 能解释很多问题,但也有一些场景不能只盯着它看。

  • tmpfs:/dev/shm、/tmp挂为 tmpfs 时,文件都在内存里,没有磁盘 inode 一说,但 VFS 仍然为每个文件构造内存 inode。这时候 inode 耗尽的含义变成了“内存不足”,df -i看到的数字可能很多,但这些 inode 占用的其实是内存和内核 slab。
  • 符号链接:软链接自身是一个 inode,数据区存的是目标路径字符串。所以软链接也会消耗 inode,大量软链接同样会导致 inode 耗尽。
  • 已删除但被进程占用的文件:df -h看空间没释放、但df -i的 inode 占用也还在,这时要找的是持有 fd 的进程,典型手段是lsof +L1。这类场景本质是 inode 的 i_count 没归零,导致完整的 inode 生命周期无法走完。

我自己在实操里最深刻的体会是:遇到文件系统问题,不要一上来就纠结某个单一指标,把df -h、df -i、/proc/meminfo的 Slab、inode-state几个数据一起看,基本上五分钟内能圈定方向。而想要把这套机制吃透,只靠运维手册是不够的,真正静下心去读一段 struct inode 的源码注释、跟着一次 open 到写回的调用链走一遍,比刷十篇博客都管用。inode 这个主题后续还能接着聊很多,比如与进程文件描述符表的联动、各种具体文件系统的 inode 布局差异、page cache 与回写机制更深层的协作,这些都是值得一头扎进去的深水区。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进