3. 索引节点对象:struct inode 结构体详解、inode 唯一标识与状态
索引节点(inode)是 VFS 中用于描述文件或目录元数据的核心对象。每个文件、目录、符号链接等在 VFS 层都对应一个唯一的 struct inode 实例。它不包含文件名,也不包含文件数据内容,而是存储权限、大小、时间戳、数据块指针等元信息。
3.1 struct inode 核心字段解析
VFS 的 struct inode 定义在 include/linux/fs.h 中,字段数量众多,面试中常考察以下几类关键字段:
| 分类 | 字段名 | 说明 |
|---|---|---|
| 唯一标识 | i_ino |
inode 号,在同一个文件系统内唯一 |
| 唯一标识 | i_generation |
inode 世代号,用于 NFS 等场景避免重用冲突 |
| 文件类型与权限 | i_mode |
文件类型(普通/目录/链接等)及 rwx 权限位 |
| 所有者 | i_uid / i_gid |
文件所属用户 ID 和组 ID |
| 时间戳 | i_atime / i_mtime / i_ctime |
访问时间、修改时间、状态变更时间 |
| 大小与块 | i_size / i_blocks |
文件字节大小和占用的磁盘块数 |
| 引用计数 | i_count |
内核中对该 inode 的引用计数 |
| 硬链接数 | i_nlink |
指向该 inode 的目录项数量 |
| 操作函数表 | i_op / i_fop |
inode 操作集和文件操作集指针 |
| 文件系统关联 | i_sb |
指向所属超级块对象的指针 |
| 地址空间 | i_mapping |
页缓存映射,用于文件数据读写 |
面试高频点:
i_count 与 i_nlink 的区别。前者是内核内存中的活跃引用数,后者是磁盘上的硬链接计数。当 i_nlink == 0 且 i_count == 0 时,inode 才会被真正释放。
3.2 inode 唯一标识:i_ino 与 i_generation
在 VFS 层面,inode 的全局唯一性由 (i_sb, i_ino) 二元组保证。同一个超级块(即同一个挂载的文件系统实例)内,i_ino 必须唯一。不同文件系统之间,i_ino 可能重复,因此比较 inode 是否相同时必须同时比较超级块指针。
i_generation 字段用于解决 inode 号重用带来的安全问题。当文件被删除后,其 inode 号可能被新文件复用。NFS 客户端如果缓存了旧的文件句柄,通过 i_generation 可以检测到 inode 已被重新分配,从而避免访问到错误的数据。
面试题: 如何判断两个文件描述符指向同一个文件?答案:比较
struct file 中的 f_inode 指针是否相同,或者比较 f_inode->i_sb 和 f_inode->i_ino 是否相等。
3.3 inode 状态机:从创建到释放
inode 在内核中存在多种状态,理解状态转换对分析文件系统行为至关重要。主要状态包括:
- NEW:刚分配内存,尚未与磁盘同步,
i_state包含I_NEW标志。 - DIRTY:inode 元数据已被修改,但尚未写回磁盘,
i_state包含I_DIRTY标志。 - CLEAN:inode 与磁盘内容一致,无待写回数据。
- FREEING:正在被释放,
i_nlink == 0且引用计数降为 0。 - LOCKED:正在被 I/O 操作使用,防止并发访问。
注意:
I_DIRTY 又细分为 I_DIRTY_SYNC(时间戳变更)、I_DIRTY_DATASYNC(数据相关元数据变更)和 I_DIRTY_PAGES(页缓存脏页),写回策略不同。
3.4 inode 操作集:i_op 与 i_fop
每个 inode 都关联两个操作函数表,这是 VFS 实现多态的关键:
i_op(struct inode_operations *):包含create、lookup、link、unlink、mkdir、rmdir、rename、setattr、getattr等操作。这些操作主要针对目录项和 inode 元数据本身。i_fop(struct file_operations *):包含read、write、open、release、llseek、mmap、fsync等操作。这些操作针对文件内容的数据读写。
关键区别: 目录 inode 的
i_fop 通常指向 dir_file_operations,其 read 操作返回目录项列表;而普通文件 inode 的 i_fop 指向 def_blk_fops 或具体文件系统的文件操作集。
3.5 面试常见问题与避坑
| 问题 | 核心要点 |
|---|---|
| inode 与 dentry 的关系 | dentry 是目录项缓存,指向 inode;多个 dentry 可指向同一个 inode(硬链接) |
| i_count 何时增加 | 打开文件、获取 dentry、page cache 引用等场景会增加 |
| i_nlink 何时变为 0 | 最后一个硬链接被删除时,文件并未立即释放,直到所有打开的文件描述符关闭 |
| 如何遍历 inode 的脏页 | 通过 i_mapping->nrpages 和 find_get_pages 遍历页缓存 |
| inode 号在什么情况下会重复 | 不同文件系统之间可以重复;同一文件系统内不会重复 |
3.6 代码示例:获取 inode 信息
以下代码展示了在 VFS 层面如何通过文件描述符获取 inode 的核心元数据:
#include <linux/fs.h>
#include <linux/dcache.h>
static void show_inode_info(struct inode *inode)
{
if (!inode)
return;
pr_info("inode: ino=%lu, mode=%o, nlink=%u, size=%lld\n",
inode->i_ino,
inode->i_mode,
inode->i_nlink,
inode->i_size);
pr_info(" uid=%u, gid=%u, blocks=%lu\n",
i_uid_read(inode),
i_gid_read(inode),
inode->i_blocks);
pr_info(" atime=%ld, mtime=%ld, ctime=%ld\n",
inode->i_atime.tv_sec,
inode->i_mtime.tv_sec,
inode->i_ctime.tv_sec);
pr_info(" state=0x%lx, count=%d\n",
inode->i_state,
atomic_read(&inode->i_count));
}
/* 在 open 系统调用钩子中调用 */
static int my_open(struct inode *inode, struct file *file)
{
show_inode_info(inode);
return 0;
}
面试提示: 注意
i_uid_read 和 i_gid_read 是内核封装函数,用于处理不同架构下 uid/gid 的读写一致性。直接访问 inode->i_uid 在某些内核版本中已被废弃。