4. inode 数据结构:struct inode 的定义、关键字段与状态标志
inode(索引节点)是 VFS 层对文件元数据的核心抽象。每个文件、目录、符号链接在磁盘上都对应一个唯一的 inode 编号,在内存中则对应一个 struct inode 实例。理解 inode 的结构与状态转换,是进行缓存调优、减少物理 IO 的前提。
4.1 struct inode 核心定义
内核源码 include/linux/fs.h 中定义了 struct inode。以下列出与缓存、IO 路径最相关的字段,省略了锁、回调函数等辅助成员:
struct inode {
umode_t i_mode; /* 文件类型与权限位 */
unsigned short i_opflags;
kuid_t i_uid; /* 属主 UID */
kgid_t i_gid; /* 属组 GID */
unsigned int i_flags; /* 文件系统标志 (FS_*) */
unsigned long i_ino; /* inode 编号,全局唯一 */
atomic_t i_count; /* 引用计数,决定是否可回收 */
unsigned int i_nlink; /* 硬链接计数 */
dev_t i_rdev; /* 设备号(块/字符设备) */
loff_t i_size; /* 文件大小(字节) */
struct timespec64 i_atime; /* 最后访问时间 */
struct timespec64 i_mtime; /* 最后修改时间 */
struct timespec64 i_ctime; /* 最后状态变更时间 */
struct inode_operations *i_op; /* inode 操作函数表 */
const struct file_operations *i_fop;/* 默认文件操作 */
struct super_block *i_sb; /* 所属超级块 */
struct address_space *i_mapping; /* 页缓存映射,核心 IO 入口 */
struct address_space i_data; /* 内嵌 address_space */
struct list_head i_dentry; /* 关联 dentry 链表 */
struct hlist_node i_hash; /* 用于 inode 哈希表 */
struct list_head i_lru; /* LRU 链表,用于回收 */
struct list_head i_sb_list; /* 超级块内 inode 链表 */
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
};
unsigned long i_state; /* inode 状态标志 (I_*) */
unsigned int dirtied_when; /* 脏页时间戳 */
struct rcu_head i_rcu; /* RCU 销毁 */
};
调优关注点:
i_count、i_state、i_mapping 和 i_lru 是缓存回收与 IO 触发的关键字段。引用计数归零且无脏数据时,inode 可被回收;i_state 中的 I_DIRTY 标志直接决定是否触发回写 IO。
4.2 关键字段详解
| 字段 | 类型 | 作用与调优意义 |
|---|---|---|
i_ino |
unsigned long |
文件系统内唯一标识。哈希查找 inode 的 key,哈希桶大小影响查找效率。 |
i_count |
atomic_t |
引用计数。内核路径(如 dentry 引用、打开文件描述符)每增加一次引用则 +1。计数为 0 时 inode 可被 LRU 回收。 |
i_nlink |
unsigned int |
硬链接数。为 0 时文件已被删除,inode 进入释放流程。 |
i_size |
loff_t |
文件逻辑大小。读 IO 时判断是否超出 EOF;写 IO 时若增大则触发块分配。 |
i_mapping |
struct address_space * |
指向页缓存树(radix tree / xarray)。所有基于文件的页缓存都挂在此处,是缓存命中与缺失的分水岭。 |
i_dentry |
struct list_head |
链接所有指向此 inode 的 dentry。当 dentry 被回收时,通过此链表解除关联。 |
i_lru |
struct list_head |
挂入全局 inode LRU 链表,用于内存回收时驱逐未使用的 inode。 |
i_state |
unsigned long |
位掩码状态标志,决定 inode 是否脏、是否正在回写、是否正在被释放。 |
4.3 inode 状态标志:I_* 位掩码
i_state 字段使用位掩码表示 inode 的运行时状态。这些标志直接影响内核是否发起磁盘 IO:
| 标志 | 值 | 含义 | IO 影响 |
|---|---|---|---|
I_DIRTY_SYNC |
1 | inode 元数据(时间戳、大小等)被修改,但不需要写回所有数据。 | 触发元数据回写 IO |
I_DIRTY_DATASYNC |
2 | 数据相关元数据变更(如文件大小变化),需要数据同步。 | 触发数据与元数据回写 |
I_DIRTY_PAGES |
4 | inode 对应的页缓存中有脏页。 | 触发页面回写 IO |
I_DIRTY_TIME |
8 | 仅 atime 等时间戳被更新,可延迟回写。 | 可合并或跳过回写 |
I_DIRTY |
(1|2|4|8) | 上述脏标志的合集,表示 inode 整体脏。 | 回写调度入口判断 |
I_NEW |
16 | inode 刚分配,尚未初始化完成。 | 阻塞其他访问,避免未就绪 IO |
I_WILL_FREE |
32 | inode 即将被释放,正在清理缓存。 | 禁止新的 IO 请求 |
I_FREEING |
64 | inode 正在被销毁,所有引用已释放。 | 回收页缓存,释放内存 |
I_CLEAR |
128 | inode 数据已从页缓存清除,仅剩结构体。 | 最终销毁前状态 |
I_SYNC |
256 | inode 正在被回写,其他回写请求需等待。 | 序列化回写,避免重复 IO |
I_REFERENCED |
512 | inode 最近被访问过,LRU 回收时获得一次豁免。 | 延长缓存驻留时间 |
调优关键:
I_DIRTY_PAGES 与 I_SYNC 的组合决定了回写并发度。当大量 inode 同时处于 I_DIRTY_PAGES | I_SYNC 状态时,回写线程可能成为 IO 瓶颈。通过 /proc/sys/vm/dirty_ratio 和 dirty_background_ratio 可以控制脏页堆积阈值,间接影响 I_DIRTY_PAGES 的持续时间。
4.4 inode 状态转换与 IO 触发路径
下图展示了 inode 从创建到回收的核心状态转换,以及每个阶段是否产生磁盘 IO:
4.5 调优实践:通过 i_state 诊断 IO 压力
在调优场景中,可以通过 /proc/sys/fs/inode-state 和 slabtop 观察 inode 缓存用量,但更细粒度的状态分布需要借助 eBPF 或 tracepoint:
# 查看系统 inode 总数与空闲数
$ cat /proc/sys/fs/inode-state
123456 7890 0 0 0 0 0
# 使用 bpftrace 跟踪 I_DIRTY_PAGES 状态变化
$ bpftrace -e '
kprobe:mark_inode_dirty {
$ino = (struct inode *)arg0;
if ($ino->i_state & 4) {
printf("inode %lu 进入脏页状态\n", $ino->i_ino);
}
}'
避坑提示: 不要盲目增大
vfs_cache_pressure 来回收 inode。当 inode 处于 I_DIRTY_PAGES 或 I_SYNC 状态时,强制回收会导致回写风暴,反而增加瞬时 IO 延迟。应先观察 i_state 分布,再调整脏页阈值。
4.6 关键字段与缓存调优的映射关系
| 调优目标 | 涉及字段 | 可调参数 |
|---|---|---|
| 减少元数据回写 IO | i_state (I_DIRTY_SYNC/I_DIRTY_TIME) |
dirty_expire_centisecs、lazytime 挂载选项 |
| 提高 inode 缓存命中率 | i_lru、i_count |
vfs_cache_pressure、nr_inodes 上限 |
| 降低脏页堆积延迟 | i_mapping、i_state (I_DIRTY_PAGES) |
dirty_ratio、dirty_background_ratio |
| 避免回写拥塞 | i_state (I_SYNC) |
nr_requests、writeback_ratio |