本文基于 ~/6.18.33.2-microsoft-standard-WSL2 内核源码, 在WSL ubuntu24.04 上试验
涉及的核心文件:include/linux/fs.h、include/linux/dcache.h、include/linux/path.h、include/linux/mount.h、
fs/mount.h、include/linux/nsproxy.h、include/linux/fs_struct.h、include/linux/fdtable.h、
include/linux/sched.h、include/linux/buffer_head.h、fs/namei.c、fs/namespace.c、fs/dcache.c、fs/inode.c、
fs/read_write.c、mm/filemap.c、fs/ext4/ext4.h、fs/ext4/inode.c、fs/ext4/super.c、fs/ext4/readpage.c。
全景图:VFS 四大对象与两座"桥"
Linux 的虚拟文件系统(VFS)用四个核心结构体把"一个文件"拆成四个不同维度:
| 结构体 |
定义位置 |
它回答的问题 |
struct super_block |
include/linux/fs.h:1446 |
这个文件系统实例是谁?块大小、挂载在哪个设备? |
struct inode |
include/linux/fs.h:793 |
这个文件本身是谁?大小、权限、数据块在哪? |
struct dentry |
include/linux/dcache.h:92 |
这个路径名是什么?/a/b/c 的层级与缓存? |
struct file |
include/linux/fs.h:1211 |
这个打开会话是什么?当前读写偏移、操作函数表? |
它们之间的关系可以浓缩成一句话:
1
2
3
4
5
6
|
file ──(f_path.dentry)──▶ dentry ──(d_inode)──▶ inode ──(i_sb)──▶ super_block
└──(f_inode)───────────────┘ │(i_mapping) │(s_bdev)
▼ ▼
address_space block_device
│ │
页缓存(folio) ◀──▶ 磁盘 Block
|
而这一切要能"动起来",还需要另外两个维度把它们挂到进程上:
- 进程 → 文件:
task_struct->files(files_struct 文件描述符表)→ struct file;
- 进程 → 命名空间:
task_struct->nsproxy(nsproxy)→ mnt_namespace(挂载命名空间)→ mount 树 → super_block;
- 进程 → 当前路径:
task_struct->fs(fs_struct 的 root/pwd)决定相对路径、根目录从哪开始解析。
这两座"桥"——磁盘桥(inode ↔ block)和进程桥(进程 ↔ namespace)。
内核注释里对这四个对象的经典描述(include/linux/fs.h 附近):
VFS 把"文件名→文件内容"的解析拆成 dentry(名字)与 inode(内容/元数据)两层,
这样 open("/a/b/c") 时绝大部分路径组件只需要在内存 dcache 中命中,而不必访问磁盘。
struct super_block:文件系统的户口本
结构体逐段精读
include/linux/fs.h:1446 定义(节选关键字段):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
struct super_block {
struct list_head s_list; /* 所有 sb 挂在全局链表上 */
dev_t s_dev; /* 设备号,查找索引 */
unsigned char s_blocksize_bits;
unsigned long s_blocksize; /* 块大小,如 4096 */
loff_t s_maxbytes; /* 单文件最大尺寸 */
struct file_system_type *s_type; /* 指向 fs 类型(ext4/xfs/...) */
const struct super_operations *s_op; /* 生命周期操作表 */
...
struct dentry *s_root; /* 本文件系统根目录 dentry */
...
struct mount *s_mounts; /* 挂载在此 sb 上的 mount 链表 */
struct block_device *s_bdev; /* 底层块设备 */
struct file *s_bdev_file;
struct backing_dev_info *s_bdi; /* 回写设备信息 */
...
void *s_fs_info; /* 文件系统私有信息(ext4 用) */
...
char s_id[32]; /* 如 "sda1" */
uuid_t s_uuid;
...
struct user_namespace *s_user_ns; /* 解释 uid/gid 的用户命名空间 */
...
struct list_lru s_dentry_lru; /* 本 sb 的 dentry LRU */
struct list_lru s_inode_lru; /* 本 sb 的 inode LRU */
...
spinlock_t s_inode_list_lock;
struct list_head s_inodes; /* 本 sb 所有 inode 链表 */
spinlock_t s_inode_wblist_lock;
struct list_head s_inodes_wb; /* 需要回写的 inode */
} __randomize_layout;
|
要点:
- 一个"种类"的文件系统 ≠ 一个 super_block。
ext4 是一个 file_system_type,但每挂载一个 ext4 分区就有一个 super_block 实例。同类型的所有 sb 通过 file_system_type->fs_supers 哈希链表串起来(include/linux/fs.h:2704)。
s_blocksize 是磁盘 I/O 的最小单位,比如 4096 字节。它与 s_blocksize_bits(12)配合:1 << s_blocksize_bits == s_blocksize。
s_root 指向根目录 dentry,它又指向根 inode。ext4 挂载时用 ext4_iget(sb, EXT4_ROOT_INO, ...) 读入 inode 2(根目录)作为 s_root(见 fs/ext4/super.c:5518)。
s_fs_info 是"私有数据"指针。ext4 挂载时把它指向 struct ext4_sb_info,内核宏 EXT4_SB(sb) 就是 container_of 反推:
1
2
3
4
5
|
/* fs/ext4/ext4.h */
static inline struct ext4_sb_info *EXT4_SB(struct super_block *sb)
{
return sb->s_fs_info;
}
|
这是内核"面向对象"手法的核心:公共部分(super_block)在前,私有部分(ext4_sb_info)通过指针/内嵌关联。struct ext4_inode_info 也用了同样的手法,把 struct inode vfs_inode 内嵌为最后一个字段(fs/ext4/ext4.h:1114),这样 EXT4_I(inode) = container_of(inode, struct ext4_inode_info, vfs_inode)。
磁盘上的 superblock:ext4_super_block
内存 super_block 是"运行时描述",磁盘上的 ext4_super_block 是"持久化描述"。两者通过 buffer_head 桥接——挂载时 ext4 把磁盘第 2 个块(默认 s_sb_block=1,即块号 1)读进内存:
1
2
3
4
5
6
7
8
9
10
|
/* fs/ext4/super.c:5076 附近,ext4_read_super_block() */
bh = ext4_sb_bread_unmovable(sb, logical_sb_block);
...
es = (struct ext4_super_block *) (bh->b_data + offset);
sbi->s_es = es; /* 磁盘 superblock 副本常驻内存 */
sb->s_magic = le16_to_cpu(es->s_magic);
if (sb->s_magic != EXT4_SUPER_MAGIC) {
ext4_msg(sb, KERN_ERR, "VFS: Can't find ext4 filesystem");
goto out;
}
|
struct ext4_super_block 里躺着整个文件系统的"总账":块数、inode 数、每块组 inode 数、日志设备、UUID 等。注意 s_es(struct ext4_super_block *)直接指向 bh->b_data,也就是说磁盘 superblock 就住在页缓存的一个 buffer 里,改它=改页缓存,回写=写磁盘。
生命周期与操作表:super_operations
include/linux/fs.h:2459 的 struct super_operations 是 sb 的"方法表",ext4 的实现位于 fs/ext4/super.c(ext4_sops):
1
2
3
4
5
6
7
8
9
10
11
|
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); /* ext4_alloc_inode */
void (*destroy_inode)(struct inode *);
...
int (*write_inode) (struct inode *, struct writeback_control *wbc);
void (*evict_inode) (struct inode *); /* 驱逐 inode(回收时) */
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*statfs) (struct dentry *, struct kstatfs *);
...
};
|
alloc_inode 由 VFS 在需要新 inode 时调用;ext4 的实现是分配 struct ext4_inode_info(内含 vfs_inode)。这就是为什么每个 VFS inode 背后都"绑着"一个文件系统私有结构。
挂载:从 mount 系统调用到 sb 落地
完整链路(fs/namespace.c → fs/super.c → fs/ext4/super.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
mount("ext4", "/mnt", ...)
└─ do_mount → path_mount → do_new_mount
└─ vfs_get_tree(fc) // fs_context 携带 fs 类型、设备
└─ fc->fs_type->init_fs_context → ext4_init_fs_context
└─ get_tree_bdev(fc, ext4_fill_super)
└─ sget_fc(): 从 fs_supers 链上找已有 sb,找不到则分配新的
└─ ext4_fill_super():
├─ ext4_read_super_block() // 读磁盘 superblock(见 2.2)
├─ sb_set_blocksize(sb, blocksize)
├─ root = ext4_iget(sb, EXT4_ROOT_INO, ...) // 读根 inode
└─ sb->s_root = d_make_root(root)
└─ vfs_create_mount(fc) → struct mount { .mnt.mnt_sb = sb, .mnt.mnt_root = sb->s_root }
└─ attach_mnt(...) → 挂入 mnt_namespace 的 mounts 红黑树
|
struct vfsmount 只有三个关键字段(include/linux/mount.h:58):
1
2
3
4
5
6
|
struct vfsmount {
struct dentry *mnt_root; /* 被挂载子树的根 dentry */
struct super_block *mnt_sb; /* 指向 superblock */
int mnt_flags;
struct mnt_idmap *mnt_idmap;
};
|
vfsmount 就是"路径世界"与"文件系统实例"之间的铆钉:路径解析走到某个挂载点,就从 mount(vfsmount 的容器)跳到另一个 sb 的根。而 struct mount(fs/mount.h:43)是 vfsmount 的"管理外壳",多出 mnt_parent、mnt_mountpoint、mnt_ns(所属挂载命名空间)、mnt_mp(挂载点)等字段——命名空间隔离的就是 struct mount 这棵树。
struct inode:文件的身份证
内存 inode 精读
include/linux/fs.h:793:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
struct inode {
umode_t i_mode; /* 文件类型+权限(如 0100644) */
kuid_t i_uid;
kgid_t i_gid;
...
const struct inode_operations *i_op; /* 名字空间操作(create/lookup/...) */
struct super_block *i_sb; /* 所属文件系统 */
struct address_space *i_mapping; /* 页缓存映射 */
...
unsigned long i_ino; /* inode 号(磁盘上的编号) */
...
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev; /* 设备文件专用 */
loff_t i_size; /* 文件大小(字节) */
time64_t i_atime_sec; /* 访问时间 */
time64_t i_mtime_sec; /* 修改时间 */
time64_t i_ctime_sec; /* 状态改变时间 */
...
blkcnt_t i_blocks; /* 占用的磁盘块数(512B 为单位) */
...
struct rw_semaphore i_rwsem; /* inode 大锁 */
...
struct hlist_node i_hash; /* inode 哈希表节点 */
...
struct list_head i_lru; /* inode LRU */
struct list_head i_sb_list; /* 挂到 sb->s_inodes */
union {
struct hlist_head i_dentry; /* 指向本 inode 的所有 dentry */
struct rcu_head i_rcu;
};
...
atomic_t i_count; /* 引用计数 */
...
union {
const struct file_operations *i_fop; /* 文件操作表 */
void (*free_inode)(struct inode *);
};
...
struct address_space i_data; /* 内嵌的页缓存映射! */
...
void *i_private; /* fs 私有数据 */
} __randomize_layout;
|
几个必须记住的点:
i_ino 是磁盘上的 inode 编号(ext4 里就是 1、2、3…),ls -i 显示的就是它。
i_size 是逻辑大小,i_blocks 是物理占用的 512 字节块数(du 命令读它)。
i_mapping 默认指向内嵌的 i_data——inode_init_always() 里 inode->i_mapping = mapping;(fs/inode.c:295),其中 mapping 就是 &inode->i_data。i_data 是 struct address_space,它管着这个文件在页缓存里的所有 folio。i_mapping 就是"inode ↔ 内存页"的桥,而内存页又通过块映射指向磁盘块。
i_dentry 是"别名链表头":一个 inode 可以被多个名字引用(硬链接),每个名字对应一个 dentry,dentry 通过 d_alias 挂进来。这就是 d_alias 与 i_dentry 配对的原因(struct dentry 的 d_u.d_alias,include/linux/dcache.h:128)。
i_fop 是"文件操作表"(ext4 是 ext4_file_operations),open 时被拷贝/引用到 struct file->f_op。
磁盘上的 inode:ext4_inode
磁盘 inode 是 128/256 字节的定长记录,fs/ext4/ext4.h:787:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
...
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
...
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks ★ */
...
};
|
关键字段 i_block[EXT4_N_BLOCKS](15 个 32 位槽):传统间接块模式下它是指向数据块的指针数组;extent 模式下它承载扩展树(extent tree)的根节点。inode 与数据块的关联,全部藏在这 15 个字里。
内存里,struct ext4_inode_info(fs/ext4/ext4.h:1023)第一行就复制了它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct ext4_inode_info {
__le32 i_data[15]; /* unconverted —— 与磁盘 i_block 对齐的"原始数据" */
__u32 i_dtime;
ext4_fsblk_t i_file_acl;
ext4_group_t i_block_group; /* inode 所在块组,用于分配局部性 */
...
loff_t i_disksize; /* 磁盘上已生效的大小(区别于内存 i_size) */
struct rw_semaphore i_data_sem; /* 保护数据树修改(truncate vs get_block) */
struct inode vfs_inode; /* ★ VFS inode 内嵌在最后 */
struct jbd2_inode *jinode; /* 日志 */
...
struct ext4_es_tree i_es_tree; /* extent status tree(状态缓存) */
...
};
|
内存中 ext4_inode_info 与 struct inode 是"一块内存":container_of(inode, struct ext4_inode_info, vfs_inode)。这就是"VFS 通用 + 文件系统私有"的标准组合拳。
inode 如何从磁盘读进内存
ext4_iget()(fs/ext4/inode.c:5197 的 __ext4_iget)流程:
- VFS inode 缓存查找:
inode_lookup() 用 (sb, ino) 在全局 inode_hashtable 哈希(fs/inode.c:645 的 hash())里找;命中直接返回(同一个 inode 号在内存里只有一个 inode 对象)。
- 缓存未命中 →
alloc_inode 分配(ext4 分配 ext4_inode_info,ext4_alloc_inode)→ inode_init_always() 初始化 VFS 部分。
- 从磁盘读原始 inode:
__ext4_get_inode_loc()(fs/ext4/inode.c:4815)计算磁盘位置并读块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
static int __ext4_get_inode_loc(struct super_block *sb, unsigned long ino,
struct inode *inode, struct ext4_iloc *iloc,
ext4_fsblk_t *ret_block)
{
...
iloc->block_group = (ino - 1) / EXT4_INODES_PER_GROUP(sb);
gdp = ext4_get_group_desc(sb, iloc->block_group, NULL);
...
inodes_per_block = EXT4_SB(sb)->s_inodes_per_block;
inode_offset = ((ino - 1) % EXT4_INODES_PER_GROUP(sb));
iloc->offset = (inode_offset % inodes_per_block) * EXT4_INODE_SIZE(sb);
block = ext4_inode_table(sb, gdp); /* 块组描述符里记录的 inode 表起始块 */
...
block += (inode_offset / inodes_per_block); /* ★ ino → 磁盘块号 */
bh = sb_getblk(sb, block); /* 拿/建 buffer_head */
...
trace_ext4_load_inode(sb, ino);
ext4_read_bh_nowait(bh, REQ_META | REQ_PRIO, NULL, ...); /* 发起块读 */
wait_on_buffer(bh);
...
iloc->bh = bh;
return 0;
}
|
这段代码就是"inode 号 → 磁盘块“的标准算法:
1
2
3
4
|
块组号 = (ino - 1) / inodes_per_group
组内偏移 = (ino - 1) % inodes_per_group
磁盘块号 = inode_table(组) + 组内偏移 / inodes_per_block
块内偏移 = (组内偏移 % inodes_per_block) * inode_size
|
__ext4_iget 把 iloc.bh->b_data + iloc.offset 处的 struct ext4_inode *raw_inode 逐字段 memcpy/转换进 ext4_inode_info,再 ext4_set_aops() 挂上 ext4_aops/ext4_da_aops(见「读路径全链路」一节代码)。
inode 生命周期
1
2
3
4
5
6
|
open("/a") ──▶ iget_locked / ext4_iget ──▶ i_count++
│
└── 最后一个引用释放(close/unlink)
└─ iput() → 若 i_nlink==0 → 标记 I_FREEING
└─ evict_inode()(ext4_evict_inode: 释放数据块、清日志、从 sb->s_inodes 摘下)
└─ destroy_inode → free_inode → 归还 slab
|
inode 的引用计数 i_count 保证:文件被 unlink 后只要还有进程打开着,inode 就活着(数据不丢);全部关闭后才真正驱逐、释放磁盘块。
struct dentry:路径名的高速缓存
为什么需要 dentry
例如/usr/lib/libc.so.6 这个名字对应磁盘上 inode 的解析,如果每次都从磁盘目录项(dirent)读一遍,路径解析会慢得不可接受。于是内核设计了 dcache(dentry cache):把"父 dentry + 名字 → inode"的结果缓存成树。
struct dentry
include/linux/dcache.h:92:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
struct dentry {
/* RCU 查找热路径字段 */
unsigned int d_flags; /* 状态位(如 DCACHE_MOUNTED) */
seqcount_spinlock_t d_seq; /* 顺序锁 */
struct hlist_bl_node d_hash; /* dcache 哈希表节点 */
struct dentry *d_parent; /* 父目录 dentry */
union {
struct qstr __d_name; /* 仅供 fs/dcache.c 内部 */
const struct qstr d_name; /* 名字(qstr: name+len+hash) */
};
struct inode *d_inode; /* 名字指向的 inode,NULL=negative */
union shortname_store d_shortname; /* 短名字内联存储(不额外分配) */
...
const struct dentry_operations *d_op; /* 钩子 */
struct super_block *d_sb; /* 所属文件系统 */
...
struct lockref d_lockref; /* 锁+引用计数合体 */
...
union {
struct list_head d_lru; /* LRU 回收链 */
wait_queue_head_t *d_wait;
};
struct hlist_node d_sib; /* 兄弟链(挂父的 d_children) */
struct hlist_head d_children; /* 孩子链 */
union {
struct hlist_node d_alias; /* ★ 挂到 inode->i_dentry(硬链接别名) */
struct hlist_bl_node d_in_lookup_hash;
struct rcu_head d_rcu;
} d_u;
};
|
要点:
d_name 是 struct qstr(名字+长度+预计算 hash),避免路径解析中反复 strlen + hash。
d_parent / d_sib / d_children 把 dentry 组织成树,树根是 sb->s_root。
d_sb 让每个 dentry 知道自己属于哪个文件系统——跨设备跳转(.. 到挂载点边界)全靠它。
d_inode == NULL 的 dentry 叫 negative dentry:缓存"这个名字不存在”,避免对不存在的文件反复访问磁盘。
d_alias 与 d_children 是不同的链表:d_children 是"我作为父目录的孩子";d_alias 是"我作为 inode 的别名"。一个 inode 被硬链接 N 次,就有 N 个 dentry 的 d_alias 挂在 inode->i_dentry 上。
d_lockref 是"自旋锁+引用计数"原子合一,高频路径少一次锁开销。
dcache 的两重身份:树 + 哈希表
dentry 同时挂在两个结构上:
- 哈希表
dentry_hashtable(fs/dcache.c:114):键是 (父 dentry, 名字 hash),用于快速查找:
1
2
3
4
5
|
static inline struct hlist_bl_head *d_hash(unsigned long hashlen)
{
return runtime_const_ptr(dentry_hashtable) +
(hashlen & runtime_const(d_hash_mask));
}
|
1
2
3
4
5
6
7
8
9
10
|
/* fs/dcache.c:2425 d_hash_and_lookup() */
struct dentry *d_hash_and_lookup(struct dentry *dir, struct qstr *name)
{
name->hash = full_name_hash(dir, name->name, name->len);
if (dir->d_flags & DCACHE_OP_HASH) { /* 允许 fs 自定义 hash */
int err = dir->d_op->d_hash(dir, name);
...
}
return d_lookup(dir, name);
}
|
- 树结构:通过
d_parent + d_sib/d_children,配合 walk_component() 逐级下钻,形成从 s_root 到每个文件的路径树。
d_instantiate:dentry 与 inode 的"婚礼"
d_instantiate(dentry, inode)(fs/dcache.c)把两者绑起来:设置 dentry->d_inode = inode,并把 dentry 的 d_alias 挂进 inode->i_dentry。此后:
- 通过
dentry 找 inode:dentry->d_inode(d_inode(dentry) 宏);
- 通过 inode 找所有名字:遍历
inode->i_dentry 上的 d_alias。
dentry 与 mount 的交互
路径解析"走进"一个挂载点时,当前 dentry 会被标记 DCACHE_MOUNTED(include/linux/dcache.h:211),然后 follow_mount()(fs/namei.c)通过 __lookup_mnt()(fs/namespace.c)在 mount_hashtable 里找到挂载在此的 struct mount,把 nd->path 切换成新 vfsmount 的根:
1
2
3
4
5
|
走到 /mnt (dentry 有 DCACHE_MOUNTED)
→ __lookup_mnt(vfsmount, dentry) // 哈希键: (父vfsmount, dentry)
→ 命中 struct mount
→ nd->path.mnt = &mount->mnt
→ nd->path.dentry = mount->mnt.mnt_root // 跳到被挂载文件系统的根
|
这就是**“把 /mnt 换成另一个文件系统的根”**在代码里的样子,也是容器里看到不同目录树的根源(见「进程视角」一章)。
路径上的各级目录是怎么串起来的
把 open("/home/charles/vfslab/a.txt") 拆开看,这条路径的每一级并不是一个"字符串",而是一串对象,链在两个世界里同时存在:
① 磁盘世界:目录数据块里的"记录链"
每个目录在磁盘上就是一个 inode,它的数据块里按顺序躺着一条条定长/变长的目录项记录 ext4_dir_entry_2——每条记录 = (inode 号, 名字长度, 名字)。所以磁盘上的链是:
1
|
根目录 inode(2) 的数据块 ──"home"──▶ inode 57346 的数据块 ──"charles"──▶ inode 42 ...
|
解析时内核调 inode->i_op->lookup(ext4 是 ext4_lookup,fs/ext4/namei.c),它用 ext4_find_entry 顺着目录数据块把 (名字 → inode 号) 找出来,再 ext4_iget 把下一级的 inode 读进内存。
② 内存世界:dcache 树 + nameidata 游标
目录项被读进内存后,VFS 用一个 struct dentry 代表每一级名字,并织成树:
- 向上:
dentry->d_parent 指向父目录的 dentry(dcache.h:97);
- 横向:
dentry->d_sib 挂进父的 d_children 链表(dcache.h:122-123);
- 名字→inode:
dentry->d_inode(dcache.h:102),由 d_instantiate() 绑定;
- 回到文件系统:
dentry->d_sb(dcache.h:109)声明这一级属于哪个 super_block。
路径解析就是用一个"游标" struct nameidata 在这棵树里逐级下钻(fs/namei.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
path_init(): 起点 = current->fs->root (绝对路径) 或 current->fs->pwd (相对路径)
│
▼
link_path_walk() 循环, 对每个名字组件:
walk_component()
├─ lookup_fast(): 查 dcache 哈希表(父dentry+名字) → 命中则直接前进
└─ lookup_slow(): dcache 未命中 → 调父目录 inode->i_op->lookup
(ext4_lookup 读磁盘目录项) → d_instantiate 织入 dcache
├─ 若是挂载点(dentry 有 DCACHE_MOUNTED): follow_mount() 跳进被挂载 fs 的根
├─ 若是符号链接: inode->i_op->get_link() 插入一段新的组件链
└─ 前进: nd->path.dentry = 子 dentry, nd->inode = 子 inode
│
▼
open_last_lookups() → do_open(): 最后一级名字 → file->f_path 记住 (vfsmount, dentry)
|
③ 链的"断点":挂载点与 ..
- 挂载点:走到
/mnt 时 dentry 标记 DCACHE_MOUNTED,follow_mount() 在 mount_hashtable 里查到挂在这里的 struct mount,把游标切到 vfsmount->mnt_root——链从 ext4 的 dentry 树跳进了 tmpfs 的 dentry 树(super_block 换了,但路径看着还是连续的)。这正是「动手实验」一章 vfschain 实验里 dev=8:32 → dev=0:30 那一跳。
..(父目录):普通目录直接用 d_parent;但如果 .. 要跨出挂载边界(比如 tmpfs 根目录的 ..),内核会沿 mount->mnt_parent + mount->mnt_mountpoint 回到父文件系统里挂载点所在的 dentry——d_parent 只在一棵 super_block 的树内有效,跨树要靠 struct mount 的父子链(fs/mount.h:44-45)。
④ 一个 inode 可以有多条链(硬链接)
链的末端(inode)可以被多个 dentry 指向:/a/link 和 /b/link 两条链可以汇到同一个 inode(i_nlink=2,见 9.5 实验)。所以"路径"本质是**名字树(dentry)→ 对象(inode)**的映射,而不是一个扁平的字符串。
一句话:目录的磁盘数据块是"持久化的链",dcache 的 d_parent/d_children 是"内存里的链",link_path_walk 的 nd->path 是"沿着链走的游标",挂载点/符号链接是"链的跳转点",fs->root/fs->pwd 是"链的起点"。
一处目录最多能存多少条目?—— 磁盘上限与内存上限
“存目录项的地方"其实有两处,上限完全不同:
① 磁盘上:一个目录 inode 的数据块(持久上限,受 ext4 格式约束)
目录项记录 struct ext4_dir_entry_2(fs/ext4/ext4.h:2409)= 8 字节头部(4B inode + 2B rec_len + 1B name_len + 1B file_type)+ 名字,4 字节对齐:
| 项目 |
数值 |
依据 |
| 单个名字长度 |
≤ 255 字节 |
NAME_MAX(include/uapi/linux/limits.h:12)、EXT4_NAME_LEN |
| 单条记录最大 |
8+255 → 264 字节(对齐后) |
ext4_dir_rec_len |
| 一个 4K 块能放 |
1 字名 ~341 条 / 255 字名 ~15 条 |
4096 / 对齐后记录长 |
| 索引块容量 |
根 ~508、节点 ~510 条 dx_entry(每条 8B) |
dx_root_limit/dx_node_limit(fs/ext4/namei.c:577/588) |
| htree 深度 |
兼容 2 层 / largedir 3 层 |
EXT4_HTREE_LEVEL_COMPAT=2、EXT4_HTREE_LEVEL=3(ext4.h:2646) |
| 理论整目录上限 |
兼容模式 ~17 万条;largedir ≈ 508×510≈25.9 万叶子块 × 每块 ~333 条 ≈ 千万级 |
上面两行相乘 |
| 目录子目录数 |
≤ 65000(无 dir_nlink 特性时) |
EXT4_LINK_MAX(ext4.h:328)EXT4_DIR_LINK_MAX |
| 软限制 |
挂载选项 max_dir_size_kb,默认 0=不限 |
s_max_dir_size_kb(fs/ext4/namei.c:61) |
所以:老格式 ext3/ext4 目录到几万~几十万文件就会因 htree 打满而变慢/告警,largedir 特性(ext4 4.4+ 默认开启)把上限推到千万级。
② 内存里:dcache 没有条数上限,只有内存约束
- 每个
struct dentry 约 200~300 字节(短名内联在 d_shortname,长名单独分配)。
d_children 是链表、dcache 哈希表按内存量分配桶(alloc_large_system_hash("Dentry cache", ...),fs/dcache.c:3220),桶数只影响查找速度,不影响容量。
- 总量由内存压力 + shrinker 回收(
sb->s_dentry_lru),/proc/sys/fs/dentry-state 的前两列是 nr_dentry/nr_unused——第三列的 45 是回收用的"年龄阈值(秒)",不是条数上限,别被误导。
实测(本机,临时创建 2 万个空文件后删除):
1
2
3
4
5
6
7
|
$ cat /proc/sys/fs/dentry-state # 创建前: nr_dentry nr_unused age_limit want_pages nr_negative
18487 12543 45 0 2733 0
$ seq -f 'f%g' 1 20000 | xargs touch # 创建 20000 个文件
$ cat /proc/sys/fs/dentry-state # 创建后: 正好 +20010 个 dentry
38497 32555 45 0 2742 0
$ du -s /tmp/dirtest # 目录 inode 自己的数据块
492 /tmp/dirtest # ≈ 120 个 4K 块 (2万条 × ~24B)
|
结论一句话:磁盘侧有格式硬上限(块 × htree 深度,千万级封顶);内存侧没有"最多多少个"的硬上限,装多少取决于你有多少可回收内存——1 百万条 ≈ 两三百 MB 可回收的 dentry+inode 缓存。
struct file:进程与文件的会话
结构体精读
include/linux/fs.h:1211:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
struct file {
spinlock_t f_lock;
fmode_t f_mode; /* 打开模式(读/写/追加...) */
const struct file_operations *f_op; /* ★ 操作表(read/write/...) */
struct address_space *f_mapping; /* 页缓存映射(同 inode->i_mapping) */
void *private_data; /* fs/驱动私有 */
struct inode *f_inode; /* 背后的 inode */
unsigned int f_flags; /* O_* 标志 */
unsigned int f_iocb_flags;
const struct cred *f_cred; /* 打开者凭证 */
struct fown_struct *f_owner;
union {
const struct path f_path; /* ★ (vfsmount, dentry) */
struct path __f_path;
};
union {
struct mutex f_pos_lock; /* 位置锁 */
u64 f_pipe; /* 管道用 */
};
loff_t f_pos; /* ★ 当前读写位置 */
...
file_ref_t f_ref; /* 引用计数 */
} __randomize_layout;
|
struct file 是每个 open 一份的"会话状态”:f_pos(偏移)、f_mode、f_flags 都随会话;而 f_inode 指向共享的 inode(同一文件打开两次,有两个 file、一个 inode)。f_path 就是那个 (vfsmount, dentry) 对——打开瞬间路径解析的终点被永久记住。
进程 ↔ file:fdtable
task_struct->files 指向 struct files_struct(include/linux/fdtable.h:38):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
struct files_struct {
atomic_t count;
...
struct fdtable __rcu *fdt; /* 大表指针(表可能扩容) */
struct fdtable fdtab; /* 内嵌初始表 */
...
struct file __rcu * fd_array[NR_OPEN_DEFAULT]; /* 内嵌默认 fd 数组 */
};
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* fd 数组: 下标=fd 号 */
unsigned long *close_on_exec;
unsigned long *open_fds; /* 打开位图 */
...
};
|
于是:fd(一个整数)→ files_struct->fdt->fd[fd] → struct file *。而 task_struct 里这三件套(include/linux/sched.h:1189-1200):
1
2
3
4
5
6
|
/* Filesystem information: */
struct fs_struct *fs; /* 根/当前目录 */
/* Open file information: */
struct files_struct *files; /* 文件描述符表 */
/* Namespaces: */
struct nsproxy *nsproxy; /* 各类命名空间 */
|
open 的完整路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
open("/etc/hostname", O_RDONLY)
└─ do_sys_open → do_filp_open → path_openat(nd, ...) // fs/namei.c:4114
├─ path_init(nd, LOOKUP_DEFAULT) // fs/namei.c:2540
│ ├─ '/' 开头 → nd_jump_root → set_root // 取 current->fs->root
│ └─ 相对路径 → get_fs_pwd(current->fs, &nd->path) // 取 current->fs->pwd
├─ link_path_walk(...) // fs/namei.c:2441
│ └─ walk_component → lookup_fast(查 dcache) → 未命中 → lookup_slow
│ └─ dir->i_op->lookup → ext4_lookup → ext4_iget(读磁盘 inode)
│ └─ d_instantiate(dentry, inode) // 绑进 dcache
├─ open_last_lookups(...) // 最后一段名字
└─ do_open → alloc_empty_file → vfs_open
├─ file->f_path = nd->path
├─ file->f_inode = dentry->d_inode
├─ file->f_mapping = inode->i_mapping
├─ file->f_op = inode->i_fop → ext4_file_operations
└─ f_op->open → ext4_file_open
|
f_op 一经绑定,后续 read() 就直接调 file->f_op->read_iter(ext4 是 ext4_file_read_iter),中间不再经过路径解析。
inode/dentry/file 如何关联到磁盘 Block
磁盘关联分四层:
1
2
3
4
5
|
file 偏移(字节) ──(>>blkbits)──▶ 文件逻辑块号 lblk ──ext4_map_blocks──▶ 物理块号 pblk
│
(pblk << (blkbits-9)) 扇区
▼
bio → block layer → 磁盘
|
第一层:文件偏移 → 逻辑块号
1
|
lblk = file_offset >> inode->i_blkbits; /* i_blkbits: log2(块大小) */
|
页缓存索引则用 PAGE_SHIFT:index = iocb->ki_pos >> PAGE_SHIFT(mm/filemap.c:2621)。
第二层:逻辑块号 → 物理块号(ext4_map_blocks)
struct ext4_map_blocks(fs/ext4/ext4.h:258)是映射请求/结果:
1
2
3
4
5
6
|
struct ext4_map_blocks {
ext4_fsblk_t m_pblk; /* 物理块号(结果) */
ext4_lblk_t m_lblk; /* 逻辑块号(请求) */
unsigned int m_len; /* 块数 */
unsigned int m_flags; /* EXT4_MAP_MAPPED / EXT4_MAP_UNWRITTEN ... */
};
|
ext4_map_blocks()(fs/ext4/inode.c:698)是"lblk → pblk"的总入口,三段式:
① 先查 extent status tree(内存缓存):
1
2
3
4
5
6
7
8
9
10
11
12
|
/* Lookup extent status tree firstly */
if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es)) {
if (ext4_es_is_written(&es) || ext4_es_is_unwritten(&es)) {
map->m_pblk = ext4_es_pblock(&es) +
map->m_lblk - es.es_lblk; /* 在 extent 内偏移 */
map->m_flags |= ext4_es_is_written(&es) ?
EXT4_MAP_MAPPED : EXT4_MAP_UNWRITTEN;
retval = es.es_len - (map->m_lblk - es.es_lblk);
...
}
...
}
|
② 未命中缓存 → 查磁盘上的 extent 树(ext4_map_query_blocks → ext4_ext_map_blocks,fs/ext4/extents.c)。extent 树的叶子是 struct ext4_extent:ee_block(逻辑起点) + ee_len(长度) + ee_start(物理起点)。树根就住在 ext4_inode_info->i_data[15] 里——这就是磁盘 inode 的 i_block[15] 槽位的真身。树查找就是"从内存中的 i_data 根节点开始,逐级读磁盘索引块"的过程。
③ 写场景:EXT4_GET_BLOCKS_CREATE 标志 → 未分配则调 ext4_mb_new_blocks(mballoc)从位图里分配新块,并写回 extent 树 + 日志。
物理块号 pblk 是"文件系统块号",要换算成设备扇区:sector = pblk << (blkbits - 9)(4K 块 → 左移 3)。
第三层:物理块号 → 页缓存(address_space / folio / buffer_head)
struct address_space(include/linux/fs.h:506)是"文件 ↔ 页"的总管家:
1
2
3
4
5
6
7
8
9
10
11
|
struct address_space {
struct inode *host; /* 回指文件 inode */
struct xarray i_pages; /* ★ 页缓存: folio 的基数树 */
struct rw_semaphore invalidate_lock;
...
struct rb_root_cached i_mmap; /* mmap 的 VMA 红黑树 */
unsigned long nrpages; /* 缓存页数 */
pgoff_t writeback_index;
const struct address_space_operations *a_ops; /* ★ 方法表 */
...
};
|
- 每个 inode 内嵌一个
i_data(address_space),inode->i_mapping 默认指它(fs/inode.c:295)。
i_pages 是 XArray:键 = 页索引(文件偏移»PAGE_SHIFT),值 = struct folio *。
a_ops 是文件系统提供的页操作表。ext4 的 ext4_da_aops(fs/ext4/inode.c:4018):
1
2
3
4
5
6
7
8
9
|
static const struct address_space_operations ext4_da_aops = {
.read_folio = ext4_read_folio,
.readahead = ext4_readahead,
.writepages = ext4_writepages,
.write_begin = ext4_da_write_begin,
.write_end = ext4_da_write_end,
.dirty_folio = ext4_dirty_folio,
...
};
|
struct buffer_head(include/linux/buffer_head.h:60)是块级描述符:b_bdev(块设备)、b_blocknr(块号)、b_data(数据指针)、b_page(所在页)、b_state(uptodate/dirty/mapped 等位)。map_bh()(include/linux/buffer_head.h:408)就是把"逻辑块→物理块"结果写进 buffer_head 的瞬间:
1
2
3
4
5
6
7
8
|
static inline void
map_bh(struct buffer_head *bh, struct super_block *sb, sector_t block)
{
set_buffer_mapped(bh);
bh->b_bdev = sb->s_bdev; /* 设备 */
bh->b_blocknr = block; /* ★ 物理块号 */
bh->b_size = sb->s_blocksize;
}
|
而 ext4_get_block()(fs/ext4/inode.c:934)就是 ext4 对"给定逻辑块,帮我映射"回调的实现,内部走 _ext4_get_block → ext4_map_blocks → map_bh。
读路径全链路
以 read(fd, buf, 4096) 为例:
① 系统调用入口 fs/read_write.c:552:
1
2
3
4
5
6
7
8
9
|
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
...
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos); /* ext4 走这里 */
...
}
|
new_sync_read 构造 struct kiocb(携带 ki_filp、ki_pos)后调用 file->f_op->read_iter。
② VFS 通用读 mm/filemap.c:2717 的 filemap_read() → filemap_get_pages()(mm/filemap.c:2616):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static int filemap_get_pages(struct kiocb *iocb, size_t count,
struct folio_batch *fbatch, bool need_uptodate)
{
...
pgoff_t index = iocb->ki_pos >> PAGE_SHIFT; /* 偏移 → 页索引 */
...
filemap_get_read_batch(mapping, index, last_index - 1, fbatch); /* 查 XArray */
if (!folio_batch_count(fbatch)) {
DEFINE_READAHEAD(ractl, filp, &filp->f_ra, mapping, index);
...
page_cache_sync_ra(&ractl, last_index - index); /* 预读 */
...
}
...
if (!folio_test_uptodate(folio)) { /* 页不新鲜 → 真正读盘 */
err = filemap_update_page(iocb, mapping, count, folio, need_uptodate);
...
}
}
|
③ 缺页 → 文件系统读:filemap_read_folio() 调 mapping->a_ops->read_folio,也就是 ext4_read_folio(fs/ext4/inode.c:3392):
1
2
3
4
5
6
7
8
9
10
11
|
static int ext4_read_folio(struct file *file, struct folio *folio)
{
int ret = -EAGAIN;
struct inode *inode = folio->mapping->host;
...
if (ext4_has_inline_data(inode))
ret = ext4_readpage_inline(inode, folio);
if (ret == -EAGAIN)
return ext4_mpage_readpages(inode, NULL, folio); /* ★ 主线 */
return ret;
}
|
④ ext4 组装 bio ext4_mpage_readpages()(fs/ext4/readpage.c:211)——这里就是"lblk→pblk→bio"三步合一的地方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
int ext4_mpage_readpages(struct inode *inode,
struct readahead_control *rac, struct folio *folio)
{
...
struct block_device *bdev = inode->i_sb->s_bdev; /* ★ sb 定位设备 */
...
while (page_block < blocks_per_folio) {
...
map.m_lblk = block_in_file; /* 逻辑块号 */
map.m_len = last_block - block_in_file;
if (ext4_map_blocks(NULL, inode, &map, 0) < 0) /* ★ 查映射 */
...
...
if (first_hole != blocks_per_folio) {
... /* 空洞部分清零 */
}
...
if (bio == NULL) {
bio = bio_alloc(bdev, bio_max_segs(nr_pages),
REQ_OP_READ, GFP_KERNEL);
...
bio->bi_iter.bi_sector = first_block << (blkbits - 9); /* ★ pblk→扇区 */
bio->bi_end_io = mpage_end_io;
...
}
length = first_hole << blkbits;
if (!bio_add_folio(bio, folio, length, 0)) /* 页加入 bio */
goto submit_and_realloc;
...
}
...
if (bio)
submit_bio(bio); /* ★ 交给块层 */
return 0;
}
|
⑤ 块层 → 磁盘:submit_bio() → 电梯算法/I/O 调度 → block_device 驱动的请求队列 → 磁盘。bio->bi_iter.bi_sector 是 512B 扇区号,所有块设备 I/O 都以它为最终单位。
至此,“文件偏移"走完了一条完整的数据通道:
file->f_pos → kiocb->ki_pos → 页索引 → folio(XArray) → (ext4_map_blocks) pblk → bio(bi_sector) → 磁盘
写路径(延迟分配 delalloc 的威力)
写路径同样从 file->f_op->write_iter(ext4_file_write_iter)进入,但数据不立即落盘:
1
2
3
4
5
6
7
8
9
10
|
write() → ext4_file_write_iter → ext4_buffered_write_iter
└─ generic_perform_write
├─ ext4_da_write_begin
│ ├─ grab_cache_page_write_begin (拿 folio)
│ └─ ext4_da_get_block_prep → ext4_map_blocks(CREATE)
│ └─ 只做"延迟分配": 在 extent status tree 记 EXT4_MAP_DELAYED
│ (物理块先不分配!只记账)
├─ copy_page_from_iter (用户数据拷进页)
└─ ext4_da_write_end → ext4_mark_inode_dirty
└─ folio_mark_dirty: folio 打上 DIRTY 标记, 挂入写回队列
|
真正分配物理块发生在回写(writeback)时:ext4_writepages()(fs/ext4/inode.c)→ mpage_map_and_submit_extent() → 此时才 ext4_map_blocks(CREATE) 真分配 → ext4_bio_write_folio()(fs/ext4/page-io.c:457)组装写 bio 提交。这个设计让多个小写入合并成连续的大块分配,大幅减少磁盘碎片——代价是断电可能丢未落盘数据,因此有 sync/fsync 和日志(jbd2)兜底。
三条旁路
- Direct I/O(
O_DIRECT):绕过页缓存,ext4_dio_write_iter 直接 ext4_map_blocks → bio。
- DAX(持久内存):
ext4_dax_aops,noop_dirty_folio,CPU 直接访问持久内存映射,零拷贝。
- mmap:
mmap 文件 = 把 address_space->i_mmap 红黑树里的 VMA 与页缓存 folio 建立映射,缺页时走 read_folio 拉数据,写回仍走同一套 writeback。
进程视角:namespace / fs_struct / files_struct 如何组织
进程视角的"三件套”
include/linux/sched.h:1189 起:
1
2
3
4
5
6
|
/* Filesystem information: */
struct fs_struct *fs; /* 根目录+当前目录 */
/* Open file information: */
struct files_struct *files; /* fd 表 */
/* Namespaces: */
struct nsproxy *nsproxy; /* 命名空间集合 */
|
命名空间(namespace)与文件系统相关的有两个:
- mount namespace(
mnt_namespace):隔离"挂载树"——不同 mnt ns 的进程看到不同的目录树结构、不同的挂载点。
- user namespace(
user_ns):隔离 uid/gid 映射——super_block 的 s_user_ns 用它解释磁盘上的 uid/gid(fs.h:1562 注释:“Owning user namespace and default context in which to interpret filesystem uids, gids, quotas…")。
nsproxy:命名空间的总管
include/linux/nsproxy.h:32:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
* ...
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
refcount_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns; /* ★ 挂载命名空间 */
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct time_namespace *time_ns;
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns;
};
|
注意注释里的关键设计:nsproxy 被"共享所有命名空间"的线程/进程共用(引用计数),只要有一个命名空间被 clone/unshare,nsproxy 整体复制一份。fork() 时 copy_namespaces()(kernel/nsproxy.c:147)的快速路径:
1
2
3
4
5
6
7
|
if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWPID | CLONE_NEWNET |
CLONE_NEWCGROUP | CLONE_NEWTIME)))) {
...
get_nsproxy(old_ns); /* 没有新命名空间 → 直接共享 */
return 0;
}
|
mnt_namespace:隔离的挂载树
fs/mount.h:10:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct mnt_namespace {
struct ns_common ns; /* 通用命名空间头(ns_type/inum) */
struct mount * root; /* ★ 本命名空间挂载树根 */
struct {
struct rb_root mounts; /* 全部 mount 的红黑树 */
struct rb_node *mnt_last_node;
struct rb_node *mnt_first_node;
};
struct user_namespace *user_ns;
...
unsigned int nr_mounts; /* 挂载数 */
...
};
|
struct mount(fs/mount.h:43)是"一棵树节点 + 一个 vfsmount”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
struct mount {
struct hlist_node mnt_hash; /* mount_hashtable 节点 */
struct mount *mnt_parent; /* 父挂载 */
struct dentry *mnt_mountpoint; /* 挂载点 dentry(在父文件系统里) */
struct vfsmount mnt; /* ★ vfsmount(mnt_root, mnt_sb) */
union {
struct rb_node mnt_node; /* ns->mounts 红黑树节点 */
...
};
...
struct list_head mnt_mounts; /* 子挂载链表 */
struct list_head mnt_child; /* 链入父的 mnt_mounts */
...
struct mnt_namespace *mnt_ns; /* ★ 所属挂载命名空间 */
struct mountpoint *mnt_mp; /* 挂载点 */
...
int mnt_id; /* 挂载 ID(/proc/self/mountinfo) */
...
};
|
于是**“挂载"就是三件事**:
mnt_parent / mnt_mountpoint 把新 mount 挂到父文件系统的某个 dentry 上;
mnt_mounts / mnt_child 维护父子树;
mnt_ns 声明它属于哪个命名空间。
而 struct mount 与 struct vfsmount 的关系是同一块内存:real_mount(mnt) = container_of(mnt, struct mount, mnt)(fs/mount.h:112)。
全局与命名空间的"哈希/树"双视图
- 全局:
mount_hashtable(fs/namespace.c:86)以 (parent vfsmount, mountpoint dentry) 为键,用于路径解析时"这个 dentry 上有没有挂载"的快速查询。
- 命名空间内:
mnt_namespace->mounts 红黑树,ns->root 是树根。/proc/self/mountinfo 就是遍历当前进程命名空间的这棵红黑树生成的。
fs_struct:路径解析的"起点”
include/linux/fs_struct.h:9:
1
2
3
4
5
6
7
|
struct fs_struct {
int users;
seqlock_t seq; /* 保护 root/pwd 的读写 */
int umask;
int in_exec;
struct path root, pwd; /* ★ 根目录、当前目录 */
} __randomize_layout;
|
struct path 只有两个指针(include/linux/path.h:8):
1
2
3
4
|
struct path {
struct vfsmount *mnt; /* 在哪棵挂载树上 */
struct dentry *dentry; /* 哪个目录项 */
} __randomize_layout;
|
path 是"路径"的最小表示:(vfsmount, dentry) 唯一确定"从哪个文件系统的哪个目录"出发。
路径解析的起点(fs/namei.c:2540 path_init):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* Absolute pathname -- fetch the root */
if (*s == '/' && !(flags & LOOKUP_IN_ROOT)) {
error = nd_jump_root(nd); /* → set_root(): nd->root = current->fs->root */
...
return s;
}
/* Relative pathname -- get the starting-point it is relative to. */
if (nd->dfd == AT_FDCWD) {
...
get_fs_pwd(current->fs, &nd->path); /* ★ 相对路径从 current->fs->pwd 出发 */
nd->inode = nd->path.dentry->d_inode;
}
|
set_root()(fs/namei.c:999):
1
2
3
4
5
6
7
8
|
static int set_root(struct nameidata *nd)
{
struct fs_struct *fs = current->fs;
...
get_fs_root(fs, &nd->root); /* nd->root = fs->root */
nd->state |= ND_ROOT_GRABBED;
...
}
|
绝对路径以 fs->root 为根,相对路径以 fs->pwd 为起点——这就是 chroot 改变"根"、chdir 改变"当前目录"后,路径解析行为随之改变的本质。注意:fs_struct 不是命名空间(它可以被 clone(CLONE_FS) 共享),但命名空间切换会同步重定向 fs->root/pwd 到新命名空间对应的 mount(见下节)。
CLONE_NEWNS:创建新挂载命名空间时发生了什么
unshare -m / clone(CLONE_NEWNS) → copy_namespaces()(kernel/nsproxy.c:147)→ create_new_namespaces() → copy_mnt_ns()(fs/namespace.c:4108):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
struct mnt_namespace *copy_mnt_ns(u64 flags, struct mnt_namespace *ns,
struct user_namespace *user_ns, struct fs_struct *new_fs)
{
...
if (likely(!(flags & CLONE_NEWNS))) {
get_mnt_ns(ns); /* 不创建新 ns → 引用共享 */
return ns;
}
old = ns->root;
new_ns = alloc_mnt_ns(user_ns, false);
...
/* First pass: copy the tree topology */
copy_flags = CL_COPY_UNBINDABLE | CL_EXPIRE;
...
new = copy_tree(old, old->mnt.mnt_root, copy_flags); /* ★ 克隆整棵挂载树 */
...
new_ns->root = new;
...
/* Second pass: switch the tsk->fs->* elements and mark new vfsmounts
* as belonging to new namespace. */
p = old;
q = new;
while (p) {
mnt_add_to_ns(new_ns, q); /* 挂进新命名空间的红黑树 */
new_ns->nr_mounts++;
if (new_fs) {
if (&p->mnt == new_fs->root.mnt) { /* 重定向 fs->root */
new_fs->root.mnt = mntget(&q->mnt);
rootmnt = &p->mnt;
}
if (&p->mnt == new_fs->pwd.mnt) { /* 重定向 fs->pwd */
new_fs->pwd.mnt = mntget(&q->mnt);
pwdmnt = &p->mnt;
}
}
...
}
ns_tree_add_raw(new_ns);
return new_ns;
}
|
要点:
copy_tree() 克隆的是 struct mount(挂载树节点),不是 struct super_block!新命名空间里的每个 mount 仍然指向同一个 super_block / inode——文件系统是全局共享的,隔离的只是"挂载拓扑视图"。这就是为什么容器里 mount 自己的东西不影响宿主,但对同一文件的修改双方都看得见。
- 如果
unshare -m 的进程有私有 fs_struct(CLONE_FS 已拆),新挂载树的对应节点会被写回 fs->root.mnt / fs->pwd.mnt,保证"当前目录"在新视图里依旧成立。
命名空间的"身份证":ns_common 与 /proc/pid/ns/mnt
每个命名空间都以 struct ns_common 开头(include/linux/ns_common.h:40):
1
2
3
4
5
6
7
8
|
struct ns_common {
u32 ns_type; /* CLONE_NEWNS / CLONE_NEWPID ... */
struct dentry *stashed; /* 本 ns 在 nsfs 里缓存的 dentry */
const struct proc_ns_operations *ops; /* 输出/比较用 */
unsigned int inum; /* ★ 命名空间序号(唯一) */
refcount_t __ns_ref;
...
};
|
mnt_namespace 的 ns 字段就是它。/proc/<pid>/ns/mnt 是一个 magiclink,其 inode 号(ls -l /proc/self/ns/mnt 看到的 4026531840 之类)就是 ns->ns.inum——两个进程 ns/mnt 的 inode 相同 ⇔ 它们共享同一个 mount namespace。这也是容器逃逸检测与安全审计常用的比对手段。
完整链条:进程 → 命名空间 → sb → inode → 磁盘
把第 2~7 章串起来,一个进程访问 /etc/hostname 的完整"关系链":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
task_struct
├─ files → files_struct → fd[3] → struct file
│ ├─ f_path.dentry → dcache 里的 dentry("hostname")
│ │ ├─ d_parent → ... → d_parent → s_root dentry
│ │ ├─ d_sb → super_block(根文件系统)
│ │ └─ d_inode → inode(hostname)
│ ├─ f_inode → 同上 inode
│ ├─ f_mapping → inode->i_data(address_space)
│ │ └─ i_pages(XArray) → folio → bio → 磁盘
│ └─ f_op → ext4_file_operations
├─ fs → fs_struct { root, pwd } ← 路径解析起点
└─ nsproxy → mnt_ns → mnt_namespace { root, mounts 红黑树 }
└─ mount { mnt_parent, mnt_mountpoint }
└─ vfsmount { mnt_root = s_root, mnt_sb = super_block }
│
├─ s_bdev → block_device → bio 扇区
└─ s_fs_info → ext4_sb_info
└─ s_es → 磁盘 superblock 副本
inode ──i_sb──▶ super_block
inode ──i_mapping/i_data──▶ address_space ──a_ops──▶ ext4_da_aops
inode ──EXT4_I──▶ ext4_inode_info ──i_data[15]──▶ extent 树 ──▶ pblk ──▶ 扇区
|
完整关系图:从进程到磁盘扇区的一整条链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
┌────────────────────────── 进程(task_struct) ──────────────────────────┐
│ │
fs ───┴─── files nsproxy ──┴──
│ │ │
┌──────────▼─────┐ ┌──▼────────────────┐ ┌────────▼────────┐
│ fs_struct │ │ files_struct │ │ mnt_namespace │
│ root / pwd │ │ fdtable[]:fd───┐ │ │ root │
└──┬─────────────┘ └─────────────────┼─┘ │ mounts(rbtree) │
│ ▼ └───┬────────────┘
│ ┌──────────────────────┐ │
│ │ struct file │ │
│ │ f_path(vfsmount, │◀─────────────────────────────────┤
│ │ dentry) │ mount { mnt_ns = mnt_namespace }
│ │ f_inode │ │
│ │ f_mapping │ vfsmount { mnt_root, mnt_sb } ─┼──▶ super_block
│ │ f_pos / f_op │ │
│ └──┬───────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌──────────────────────────┐ │
│ │ dentry │ │ inode │ │
│ │ d_parent │ │ i_ino / i_size / i_uid │ │
│ │ d_children │ │ i_sb ──────────────────────────────────────────┘
│ │ d_inode ───┼─▶ i_mapping ─┐ │
│ │ d_alias │ │ i_data(address_space) │
│ └────────────┘ │ │ │
│ └────────────┼─────────────┘
│ ▼
│ ┌───────────────────────────┐
│ │ address_space │
│ │ host → inode │
│ │ i_pages: XArray │
│ │ index → folio │
│ │ a_ops → ext4_da_aops │
│ └─────────────┬─────────────┘
│ │ read_folio / writepages
│ ▼
│ ┌───────────────────────────┐
│ │ ext4_map_blocks │
│ │ lblk → pblk │
│ │ (extent 树/状态树) │
│ └─────────────┬─────────────┘
│ ▼
│ ┌───────────────────────────┐
│ │ bio { bi_sector = pblk<<k }│
│ └─────────────┬─────────────┘
│ ▼
│ ┌───────────────────────────┐
│ │ block layer → 磁盘 │
│ │ super_block.s_bdev 定位设备│
│ └───────────────────────────┘
|
- file → path → dentry → inode → sb(打开文件会话)
- inode → i_mapping/i_data → address_space → folio → bio → 磁盘(数据通道)
- inode → i_sb → sb → s_bdev / s_fs_info(ext4_sb_info)(设备与私有数据)
- task → nsproxy → mnt_ns → mount 树 → vfsmount → sb(命名空间视图)
动手实验:在 6.18.33.2 (WSL2 ubuntu24.04) 上试验
inode 的字段 ↔ 磁盘块的映射(vfsmap.c)
目的:把 struct inode 的关键字段(fstat 可见)与内核 ext4_map_blocks() 的"逻辑块→物理块"结果(FIEMAP 可见)打印出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
/* vfsmap.c — 把"文件逻辑块 → 磁盘物理块"的映射打出来
* 即内核 ext4_map_blocks() 在用户态可见的等价结果 (FS_IOC_FIEMAP) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <linux/fiemap.h>
#include <linux/fs.h>
int main(int argc, char **argv)
{
if (argc < 2) { fprintf(stderr, "用法: %s <文件>\n", argv[0]); return 1; }
int fd = open(argv[1], O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) < 0) { perror("fstat"); return 1; }
unsigned blk = st.st_blksize; /* 文件系统块大小(如 4096) */
printf("== struct inode 的部分字段 (fstat) ==\n");
printf(" i_ino = %lu\n", (unsigned long)st.st_ino);
printf(" i_size = %lld 字节\n", (long long)st.st_size);
printf(" i_blocks = %lld 个 512B 块 (物理占用)\n", (long long)st.st_blocks);
printf(" i_nlink = %lu\n", (unsigned long)st.st_nlink);
printf(" 块大小 = %u 字节 (st_blksize, 即 i_blkbits=log2(%u))\n", blk, blk);
printf(" i_atime = %ld i_mtime = %ld i_ctime = %ld\n",
(long)st.st_atime, (long)st.st_mtime, (long)st.st_ctime);
printf("\n== ext4_map_blocks 的结果: 逻辑块 → 物理块 (FS_IOC_FIEMAP) ==\n");
printf(" 逻辑块号(lblk) 物理块号(pblk) 长度(块) 标志\n");
struct fiemap *fm = calloc(1, sizeof(*fm) + 128 * sizeof(struct fiemap_extent));
if (!fm) { perror("calloc"); return 1; }
fm->fm_start = 0;
fm->fm_length = st.st_size;
fm->fm_extent_count = 128;
if (ioctl(fd, FS_IOC_FIEMAP, fm) < 0) { perror("FS_IOC_FIEMAP"); return 1; }
struct fiemap_extent *ex = (struct fiemap_extent *)(fm + 1);
for (unsigned i = 0; i < fm->fm_mapped_extents; i++) {
printf(" %12llu %14llu %8llu 0x%x%s\n",
ex[i].fe_logical / blk, ex[i].fe_physical / blk,
ex[i].fe_length / blk, ex[i].fe_flags,
(ex[i].fe_flags & FIEMAP_EXTENT_LAST) ? " <- 最后一块" : "");
}
printf(" (pblk 换算扇区: pblk << %u = 第 %llu 个 512B 扇区)\n",
__builtin_ctz(blk / 512), (unsigned long long)(ex[0].fe_physical / 512));
free(fm);
close(fd);
return 0;
}
|
真实输出(对 6 字节的小文件 a.txt):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
$ ./vfsmap a.txt
== struct inode 的部分字段 (fstat) ==
i_ino = 57411
i_size = 6 字节
i_blocks = 8 个 512B 块 (物理占用)
i_nlink = 1
块大小 = 4096 字节 (st_blksize, 即 i_blkbits=log2(4096))
i_atime = 1788079167 i_mtime = 1788079167 i_ctime = 1788079167
== ext4_map_blocks 的结果: 逻辑块 → 物理块 (FS_IOC_FIEMAP) ==
逻辑块号(lblk) 物理块号(pblk) 长度(块) 标志
0 2505969 1 0x1 <- 最后一块
(pblk 换算扇区: pblk << 3 = 第 20047752 个 512B 扇区)
|
对照内核代码:
| 打印的字段 |
内核来源 |
说明 |
i_ino = 57411 |
struct inode::i_ino(fs.h:814) |
磁盘上的 inode 编号 |
i_blocks = 8 |
struct inode::i_blocks(fs.h:839) |
6 字节文件也占 1 个 4K 块 = 8 个 512B 单位 |
i_blkbits = 12 |
struct inode::i_blkbits(fs.h:837) |
i_blocksize() = 1<<12 = 4096 |
lblk 0 → pblk 2505969 |
ext4_map_blocks()(fs/ext4/inode.c:698) |
文件第 0 块落在磁盘第 2505969 个 4K 块 |
| 扇区号 20047752 |
bio->bi_iter.bi_sector(fs/ext4/readpage.c:364) |
2505969 << 3 = 20047752,块层以 512B 扇区为单位 |
小提示:i_blocks=8 意味着"物理占用"比"逻辑大小"大——文件系统按块分配,这是 du 与 ls -l 大小不一致的原因。
file / dentry / inode 的三角关系(vfsdup.c)
目的:直观看到"两次 open = 两个 struct file(各自 f_pos),共享一个 inode;/proc/self/fd 显示的是 file->f_path 记住的 dentry;unlink 只删 dentry,inode 靠 file 引用存活"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
/* vfsdup.c — 演示 struct file / struct dentry / struct inode 的区分 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
static void show_fd_path(int fd)
{
char link[64], buf[256];
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
ssize_t n = readlink(link, buf, sizeof(buf) - 1);
if (n < 0) { perror("readlink"); return; }
buf[n] = 0;
printf(" /proc/self/fd/%d -> %s\n", fd, buf);
}
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "a.txt";
int wfd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(wfd, "0123456789ABCDEF", 16); /* 写长一点方便演示 f_pos */
close(wfd);
int fd1 = open(path, O_RDONLY);
int fd2 = open(path, O_RDONLY);
struct stat s1, s2;
fstat(fd1, &s1);
fstat(fd2, &s2);
printf("1) 同一文件 open 两次:\n");
printf(" fd1=%d fd2=%d\n", fd1, fd2);
printf(" inode(fd1)=%lu inode(fd2)=%lu → 共享同一个 struct inode? %s\n",
(unsigned long)s1.st_ino, (unsigned long)s2.st_ino,
(s1.st_ino == s2.st_ino) ? "YES" : "NO");
printf(" 但它们是两个 struct file: 各自有独立的 f_pos\n");
char b1[16] = {0}, b2[16] = {0}; /* 留足空间给 NUL */
read(fd1, b1, 8); /* fd1 的 f_pos: 0 -> 8 */
read(fd2, b2, 8); /* fd2 的 f_pos: 0 -> 8 (互不影响) */
printf(" fd1 读到 \"%s\" (f_pos 0→8)\n", b1);
printf(" fd2 读到 \"%s\" (自己的 f_pos 也从 0 开始)\n", b2);
printf("\n2) struct file->f_path 记住的 dentry (经 /proc/self/fd):\n");
show_fd_path(fd1);
show_fd_path(fd2);
printf("\n3) unlink 之后: dentry 被删, inode 仍靠 file 引用存活\n");
char tmp[64];
snprintf(tmp, sizeof(tmp), "%s.del", path);
int fd3 = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd3, "ghost", 5);
unlink(tmp);
printf(" unlink(\"%s\") 后 /proc/self/fd/%d 仍然可读, 显示:\n", tmp, fd3);
show_fd_path(fd3);
struct stat s3;
fstat(fd3, &s3);
printf(" fstat 仍然成功 → inode #%lu 还活着 (i_count>0)\n",
(unsigned long)s3.st_ino);
close(fd3);
printf(" close(fd3) 后 inode 才被 evict\n");
close(fd1); close(fd2);
return 0;
}
|
真实输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$ ./vfsdup a.txt
1) 同一文件 open 两次:
fd1=3 fd2=4
inode(fd1)=57411 inode(fd2)=57411 → 共享同一个 struct inode? YES
但它们是两个 struct file: 各自有独立的 f_pos
fd1 读到 "01234567" (f_pos 0→8)
fd2 读到 "01234567" (自己的 f_pos 也从 0 开始)
2) struct file->f_path 记住的 dentry (经 /proc/self/fd):
/proc/self/fd/3 -> /home/charles/vfslab/a.txt
/proc/self/fd/4 -> /home/charles/vfslab/a.txt
3) unlink 之后: dentry 被删, inode 仍靠 file 引用存活
unlink("a.txt.del") 后 /proc/self/fd/5 仍然可读, 显示:
/proc/self/fd/5 -> /home/charles/vfslab/a.txt.del (deleted)
fstat 仍然成功 → inode #57408 还活着 (i_count>0)
close(fd3) 后 inode 才被 evict
|
对照内核代码:
- 两个 fd 的 inode 相同 →
fstat 走的 fd → files_struct->fdt->fd[fd] → file → f_inode(fs/stat.c),同一 inode 对象。
- 各自的
f_pos → 每个 struct file 独占的 f_pos 字段(fs.h:1233),所以两次 read 都从 0 开始、互不影响。
/proc/self/fd/3 -> /home/charles/vfslab/a.txt → 这是 file->f_path(fs.h:1224)里的 dentry 树被 d_path()(fs/d_path.c)序列化成字符串。
(deleted) → unlink 只把 dentry 从目录中摘除(d_delete),inode 的 i_count 因 open 的 file 引用还大于 0,所以 inode 与数据都活着;直到最后一个 file close,iput() 才把 i_nlink==0 的 inode 送进 evict_inode()(fs/inode.c)真正回收。这也是"删不掉正在被读的文件"(Linux 语义)的原因——对比 Windows 的"文件被占用无法删除"。
直接测量页缓存(vcache.c)
目的:用 posix_fadvise(POSIX_FADV_DONTNEED)(无需 root,按文件丢弃缓存)和 /proc/meminfo 的 Cached,精确看到"页缓存 = 文件大小"——这就是 struct address_space::i_pages(XArray)里装的 folio。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/* vcache.c — 直接测量"页缓存"(address_space->i_pages) 的占用
* posix_fadvise(POSIX_FADV_DONTNEED) = 内核 mapping->invalidate_folio 一路 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
static long meminfo_cached_kb(void)
{
FILE *f = fopen("/proc/meminfo", "r");
char line[128];
long kb = -1;
while (fgets(line, sizeof(line), f))
if (sscanf(line, "Cached: %ld kB", &kb) == 1) break;
fclose(f);
return kb;
}
int main(int argc, char **argv)
{
if (argc < 2) { fprintf(stderr, "用法: %s <文件>\n", argv[0]); return 1; }
int fd = open(argv[1], O_RDONLY);
struct stat st;
fstat(fd, &st);
printf("文件: %s (inode %lu, 大小 %.1f MB)\n", argv[1],
(unsigned long)st.st_ino, st.st_size / 1048576.0);
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); /* 从"不在缓存"开始 */
long c0 = meminfo_cached_kb();
printf("\n[1] 清缓存后 Cached 基线 : %ld kB\n", c0);
char buf[65536];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) ; /* 读一遍 → 填页缓存 */
long c1 = meminfo_cached_kb();
printf("[2] cat 一遍后 Cached : %ld kB (+%ld kB ≈ 文件大小 → 全部进页缓存)\n",
c1, c1 - c0);
lseek(fd, 0, SEEK_SET);
while ((n = read(fd, buf, sizeof(buf))) > 0) ; /* 再读一遍 */
long c2 = meminfo_cached_kb();
printf("[3] 再 cat 一遍后 Cached : %ld kB (增量≈0 → 命中缓存, 不再读盘)\n", c2);
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); /* 逐出该文件所有 folio */
long c3 = meminfo_cached_kb();
printf("[4] fadvise(DONTNEED) 后 Cached : %ld kB (-%ld kB ≈ 文件大小 → 页被逐出)\n",
c3, c2 - c3);
close(fd);
return 0;
}
|
真实输出(对 512MB 文件):
1
2
3
4
5
6
7
8
|
$ dd if=/dev/zero of=big.bin bs=1M count=512 && sync
$ ./vcache big.bin
文件: big.bin (inode 57412, 大小 512.0 MB)
[1] 清缓存后 Cached 基线 : 386436 kB
[2] cat 一遍后 Cached : 910724 kB (+524288 kB ≈ 文件大小 → 全部进页缓存)
[3] 再 cat 一遍后 Cached : 910724 kB (增量≈0 → 命中缓存, 不再读盘)
[4] fadvise(DONTNEED) 后 Cached : 386436 kB (-524288 kB ≈ 文件大小 → 页被逐出)
|
三个数字都精确等于 512 MiB = 524288 kB——页缓存就是"文件内容在内存里的完整副本",这一份副本就是 address_space->i_pages 里那些 folio,由 filemap_read/filemap_get_pages(mm/filemap.c:2616)在缺页时经 ext4_read_folio 填充,posix_fadvise(DONTNEED) 则触发 invalidate_folio 逐出。
延迟分配 delalloc(写 ≠ 立刻占磁盘)
目的:看到 write() 之后物理块还没有分配(数据在页缓存里),sync 回写时才分配——这是前面「写路径(延迟分配 delalloc 的威力)」一节讲过的 ext4 延迟分配(ext4_da_aops)。
1
2
3
4
5
6
7
8
|
$ echo "data-for-delalloc" > d.bin
$ filefrag -v d.bin # sync 之前
ext: logical_offset: physical_offset: length: flags:
0: 0..0: 0..0: 0: last,unknown_loc,delalloc,eof
$ sync # 回写
$ filefrag -v d.bin # sync 之后
ext: logical_offset: physical_offset: length: flags:
0: 0..0: 2506049..2506049: 1: last,eof
|
delalloc 标志、物理块为 0 —— 正是 ext4_da_write_begin 只记账(extent status tree 里标 EXT4_MAP_DELAYED)不分配(fs/ext4/inode.c:698 的 ext4_map_blocks 走 EXT4_GET_BLOCKS_CREATE 才分配),sync 触发 ext4_writepages() 回写时块 2506049 才真正被 mballoc 分配。
硬链接 = 两个 dentry,一个 inode(d_alias)
目的:直观看到 struct inode::i_dentry 别名链表(fs.h:867)与 dentry::d_alias(dcache.h:128)。
1
2
3
4
5
6
7
|
$ ln d.bin d.hard
$ ls -li d.bin d.hard
57408 -rw-r--r-- 2 charles charles 18 Aug 30 16:42 d.bin
57408 -rw-r--r-- 2 charles charles 18 Aug 30 16:42 d.hard
$ stat -c '%n inode=%i nlink=%h' d.bin d.hard
d.bin inode=57408 nlink=2
d.hard inode=57408 nlink=2
|
两个名字(dentry)、同一个 i_ino=57408、i_nlink=2:内核里这两个 dentry 的 d_alias 都挂在 inode 的 i_dentry 链表上;i_nlink 就是"还有几个名字引用这个 inode",unlink 一个名字它就减 1。
多 extent 文件与空洞
目的:看一个文件的数据块在磁盘上不连续地分布(这正是"文件逻辑块 ≠ 物理块连续"的直观证据,也是 ext4_map_blocks 要查 extent 树的原因)。
1
2
3
4
5
6
7
8
9
|
$ dd if=/dev/zero of=multi.bin bs=1M count=16 # 写第 0~16MB
$ dd if=/dev/zero of=multi.bin bs=1M count=16 seek=32 conv=notrunc # 写 32~48MB
$ dd if=/dev/zero of=multi.bin bs=1M count=16 seek=64 conv=notrunc # 写 64~80MB
$ sync && filefrag -v multi.bin
File size of multi.bin is 83886080 (20480 blocks of 4096 bytes)
ext: logical_offset: physical_offset: length: flags:
0: 0..4095: 2600960..2605055: 4096:
1: 8192..12287: 2605056..2609151: 4096:
2: 16384..20479: 2609152..2613247: 4096: last,eof
|
解读:文件逻辑块 04095 在物理块 26009602605055;逻辑块 819212287 接在物理块 2605056(正好接续——ext4 尽力做了局部性分配);逻辑块 1638420479 在物理块 2609152。中间的逻辑空洞(40968191、1228816383)在页缓存里读出来是 0,在 ext4_mpage_readpages(fs/ext4/readpage.c:330)里就是"first_hole 部分清零、不发 bio"。
命名空间的"身份证"(vfsns.c)
目的:用 readlink /proc/self/ns/mnt 拿到 ns_common::inum(ns_common.h:44),验证"共享命名空间的进程 inum 相同",并演示读取他人 ns 需要 ptrace 权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
/* vfsns.c — 命名空间身份证: ns_common.inum */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static long ns_inum(const char *path)
{
char buf[128];
ssize_t n = readlink(path, buf, sizeof(buf) - 1);
if (n < 0) return -1; /* 权限受限等 */
buf[n] = 0;
unsigned long v = 0;
sscanf(buf, "%*[^:]:[%lu]", &v); /* 解析 "mnt:[4026532211]" */
return (long)v;
}
static void print_ns_inum(const char *label, const char *path)
{
long v = ns_inum(path);
if (v < 0)
printf(" %-8s: (读取失败, 权限受限) %s\n", label, path);
else
printf(" %-8s: %lu (%s)\n", label, v, path);
}
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0); /* fork 子进程会继承 stdout 缓冲, 关掉避免错乱 */
printf("== 本进程 (PID %d) 各命名空间的 inum ==\n", getpid());
print_ns_inum("mnt", "/proc/self/ns/mnt");
print_ns_inum("pid", "/proc/self/ns/pid");
print_ns_inum("net", "/proc/self/ns/net");
print_ns_inum("uts", "/proc/self/ns/uts");
long my_mnt = ns_inum("/proc/self/ns/mnt");
printf("\n== 与其他进程比较 ==\n");
print_ns_inum("PID 1", "/proc/1/ns/mnt");
printf(" (读取他人 ns 需要 ptrace 权限 → 无权限时 EACCES, 这也是安全机制之一)\n");
pid_t shell = fork();
if (shell == 0) {
execl("/bin/sh", "sh", "-c",
"echo \" 子 shell (PID $$): mnt inum = $(readlink /proc/self/ns/mnt)\"",
(char *)NULL);
_exit(1);
}
waitpid(shell, NULL, 0);
pid_t child = fork();
if (child == 0) {
long v = ns_inum("/proc/self/ns/mnt");
printf("\n== fork() 的子进程 (PID %d) ==\n", getpid());
printf(" mnt inum = %lu → 与父进程共享 mount ns? %s\n",
v, v == my_mnt ? "YES" : "NO");
printf(" pid inum = %lu (PID namespace 共享, 但 PID 编号不同)\n",
ns_inum("/proc/self/ns/pid"));
fflush(NULL);
_exit(0);
}
waitpid(child, NULL, 0);
printf("\n== 打开 /proc/self/ns/mnt: 它是 nsfs 文件系统里的一个 inode ==\n");
system("ls -l /proc/self/ns/mnt");
return 0;
}
|
真实输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
$ ./vfsns
== 本进程 (PID 5024) 各命名空间的 inum ==
mnt : 4026532211 (/proc/self/ns/mnt)
pid : 4026532213 (/proc/self/ns/pid)
net : 4026531833 (/proc/self/ns/net)
uts : 4026532212 (/proc/self/ns/uts)
== 与其他进程比较 ==
PID 1 : (读取失败, 权限受限) /proc/1/ns/mnt
(读取他人 ns 需要 ptrace 权限 → 无权限时 EACCES, 这也是安全机制之一)
子 shell (PID 5026): mnt inum = mnt:[4026532211]
== fork() 的子进程 (PID 5028) ==
mnt inum = 4026532211 → 与父进程共享 mount ns? YES
pid inum = 4026532213 (PID namespace 共享, 但 PID 编号不同)
== 打开 /proc/self/ns/mnt: 它是 nsfs 文件系统里的一个 inode ==
lrwxrwxrwx 1 charles charles 0 Aug 30 16:42 /proc/self/ns/mnt -> mnt:[4026532211]
|
三个教学点:
- 所有进程(含 fork 的子进程、新开的 shell)的 mnt inum 都是
4026532211 → 它们共享同一个 mnt_namespace(也就是共享同一棵 mount 树,copy_mnt_ns 的快速路径 get_mnt_ns(ns),fs/namespace.c:4121)。
- mnt=4026532211 ≠ 4026531840(初始 ns 的 inum) → 这台 WSL2 本身就跑在"私有"的 mount namespace 里(WSL 的初始化机制),容器场景下
docker run 的容器进程会得到一个全新的 inum——用 readlink /proc/1/ns/mnt 比对 inode 号即可检测容器/逃逸。
- 读取
/proc/1/ns/mnt 报 EACCES → 对他人 ns 文件的访问受 ptrace_may_access 检查(fs/proc/namespaces.c 的 proc_ns_get_link 走 ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)),这是容器逃逸防护的一环。
strace 看路径解析与 negative dentry
目的:看 open 的路径解析入口——AT_FDCWD 表示相对路径从 current->fs->pwd 出发(fs/namei.c:2590 的 get_fs_pwd),以及大量 ENOENT 正是 dcache 里的 negative dentry(dcache.h:216 DCACHE_MISS_TYPE)在起作用——这些名字被缓存为"不存在",内核无需反复查磁盘。
1
2
3
4
5
6
7
8
9
|
$ strace -e trace=openat cat /etc/hostname 2>&1 | head -8
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/C.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT ...
openat(AT_FDCWD, "/usr/lib/locale/C.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/C.UTF-8/LC_MEASUREMENT", O_RDONLY|O_CLOEXEC) = -1 ENOENT ...
|
(小实验:cd /etc && strace -e trace=openat cat hostname,会看到 openat(AT_FDCWD, "hostname", ...)——相对路径,起点是 pwd。)
沿着路径逐级走一遍(vfschain.c)
目的:把"路径上的各级目录是怎么串起来的"变成看得见的东西——从 / 开始用 openat(O_PATH) 逐级下钻(这正是内核 link_path_walk() 的缩影),打印每一级的 inode 与设备号;再用 openat("..") 反向走回根,验证 d_parent 链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
/* vfschain.c — 把一条路径的每一级"串"出来 */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#define MAXLVL 64
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "/home/charles/vfslab/a.txt";
if (path[0] != '/') { fprintf(stderr, "需要绝对路径\n"); return 1; }
char tmp[1024];
snprintf(tmp, sizeof(tmp), "%s", path);
char *comps[MAXLVL];
int n = 0;
char *save = NULL;
for (char *c = strtok_r(tmp, "/", &save); c; c = strtok_r(NULL, "/", &save))
comps[n++] = c;
printf("路径: %s\n", path);
printf("== 正向: 从 / 逐级 openat 下钻 (内核 link_path_walk 的缩影) ==\n");
int dirfds[MAXLVL]; /* 各级目录 fd 保留, 供反向 ".." 验证 */
int ndirs = 0;
int fd = open("/", O_PATH | O_DIRECTORY);
dirfds[ndirs++] = fd;
struct stat st;
fstat(fd, &st);
unsigned prev_maj = major(st.st_dev), prev_min = minor(st.st_dev);
printf(" [0] \"/\" %*s inode=%-8lu dev=%u:%u 目录\n", 14, "",
(unsigned long)st.st_ino, major(st.st_dev), minor(st.st_dev));
for (int i = 0; i < n; i++) {
int is_last = (i == n - 1);
int nfd = openat(fd, comps[i], O_PATH | (is_last ? 0 : O_DIRECTORY));
if (nfd < 0) { perror(comps[i]); return 1; }
fstat(nfd, &st);
int newdev = (major(st.st_dev) != prev_maj || minor(st.st_dev) != prev_min);
printf(" [%d] \"%s\"%*s inode=%-8lu dev=%u:%u %s%s\n",
i + 1, comps[i],
(int)(14 - (int)strlen(comps[i])), "",
(unsigned long)st.st_ino, major(st.st_dev), minor(st.st_dev),
S_ISDIR(st.st_mode) ? "目录" : "文件",
newdev ? " ← 设备号变了!" : "");
if (S_ISDIR(st.st_mode))
dirfds[ndirs++] = nfd; /* 注意: 不 close, 否则 fd 号会被复用 */
prev_maj = major(st.st_dev);
prev_min = minor(st.st_dev);
fd = nfd;
}
printf("\n== 反向: 用 openat(\"..\") 走回 / (dentry->d_parent 链) ==\n");
int cur = (S_ISDIR(st.st_mode)) ? ndirs - 1 : ndirs - 2;
for (int i = cur; i >= 0; i--) {
fstat(dirfds[i], &st);
int match = 0;
if (i > 0) {
int p = openat(dirfds[i], "..", O_PATH | O_DIRECTORY);
if (p >= 0) {
struct stat ps, qs;
fstat(p, &ps);
fstat(dirfds[i - 1], &qs);
match = (ps.st_ino == qs.st_ino);
close(p);
}
}
printf(" [%d] inode=%-8lu dev=%u:%u %s\n",
i, (unsigned long)st.st_ino,
major(st.st_dev), minor(st.st_dev),
match ? "\"..\" 正好命中上一级 → d_parent 链成立"
: (i == 0 ? "(根)" : "\"..\" 检查"));
}
return 0;
}
|
真实输出——同一文件系统内(全部 dev=8:32,ext4):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
$ ./vfschain /home/charles/vfslab/a.txt
路径: /home/charles/vfslab/a.txt
== 正向: 从 / 逐级 openat 下钻 (内核 link_path_walk 的缩影) ==
[0] "/" inode=2 dev=8:32 目录
[1] "home" inode=57346 dev=8:32 目录
[2] "charles" inode=42 dev=8:32 目录
[3] "vfslab" inode=52633 dev=8:32 目录
[4] "a.txt" inode=57411 dev=8:32 文件
== 反向: 从最后一级用 openat("..") 走回 / (dentry->d_parent 链) ==
[2] inode=42 dev=8:32 ".." 正好命中上一级 → d_parent 链成立
[1] inode=57346 dev=8:32 ".." 正好命中上一级 → d_parent 链成立
[0] inode=2 dev=8:32 (根)
|
真实输出——跨过挂载点(/mnt/wsl 是 tmpfs):
1
2
3
4
5
6
7
8
9
10
11
|
$ ./vfschain /mnt/wsl
路径: /mnt/wsl
== 正向: 从 / 逐级 openat 下钻 (内核 link_path_walk 的缩影) ==
[0] "/" inode=2 dev=8:32 目录
[1] "mnt" inode=49153 dev=8:32 目录
[2] "wsl" inode=1 dev=0:30 目录 ← 设备号变了!
== 反向: 从最后一级用 openat("..") 走回 / (dentry->d_parent 链) ==
[2] inode=1 dev=0:30 ".." 正好命中上一级 → d_parent 链成立
[1] inode=49153 dev=8:32 ".." 正好命中上一级 → d_parent 链成立
[0] inode=2 dev=8:32 (根)
|
两个输出的解读:
- 正向链:每一级是一个 dentry(名字)→ inode(
d_inode),inode 号逐级变化:2 → 57346 → 42 → 52633 → 57411。内核里这些 dentry 通过 d_parent(向上)、d_sib/d_children(横向)织成一棵树;每一级的内容(下一级名字→inode 号)来自父目录 inode 的磁盘数据块(ext4_dir_entry_2 记录,经 ext4_lookup 读取)。
- 跨挂载点:走到
wsl 时 dev 从 8:32(ext4)变成 0:30(tmpfs),inode=1 是 tmpfs 的根——内核在 DCACHE_MOUNTED 的 dentry 上经 follow_mount()/__lookup_mnt() 把游标切进了另一个 super_block 的 dentry 树。
- 反向
.. 链:每一级 openat("..") 命中的 inode 与正向记录的上一级完全一致——普通目录走 d_parent;而 tmpfs 根的 .. 能跨回 ext4 的 /mnt,靠的是 mount->mnt_parent + mnt_mountpoint(fs/mount.h:44-45)。
- 顺带一提:程序里"不 close 目录 fd,否则 fd 号被复用导致反向全打到同一个文件"这个坑,就是真实内核里"引用计数/句柄生命周期"问题的缩小版。
fd 表与 mountinfo
目的:看进程的 files_struct(fd 表)和它视角下的挂载树(mnt_namespace->mounts 红黑树的文本化输出)。
1
2
3
4
5
6
7
8
9
10
11
|
$ ls -l /proc/self/fd
total 0
lr-x------ 1 charles charles 64 ... 0 -> /dev/null
lrwx------ 1 charles charles 64 ... 1 -> socket:[25202]
lrwx------ 1 charles charles 64 ... 2 -> socket:[25204]
lr-x------ 1 charles charles 64 ... 3 -> /proc/5077/fd
$ awk 'NR<=2 || $5=="/" {print NR": "$0}' /proc/self/mountinfo
1: 67 71 0:30 / /mnt/wsl rw,relatime shared:1 - tmpfs none rw
2: 68 71 0:32 / /usr/lib/wsl/drivers ro,nosuid,nodev,noatime - 9p drivers ro,...
3: 71 59 8:32 / / rw,relatime - ext4 /dev/sdc rw,discard,errors=remount-ro,data=ordered
|
mountinfo 每行字段与内核结构对照(fs/proc_namespace.c 的 show_mountinfo):
| 字段 |
值 |
内核来源 |
| 挂载 ID |
71 |
mount::mnt_id |
| 父挂载 ID |
59 |
mount::mnt_parent->mnt_id |
| 主:次设备号 |
8:32 |
sb->s_dev(/dev/sdc = 8:32) |
| 挂载内根 |
/ |
vfsmount::mnt_root 相对路径 |
| 挂载点 |
/ |
mount::mnt_mountpoint |
| 挂载选项 |
rw,relatime |
vfsmount::mnt_flags |
| 文件系统类型 |
ext4 |
sb->s_type->name |
| 来源 |
/dev/sdc |
sb->s_bdev |
| super 选项 |
rw,data=ordered... |
sb->s_op->show_options(ext4 实现) |
根 inode = 2,验证 EXT4_ROOT_INO
1
2
3
4
|
$ stat -c '根目录 / inode=%i (源码 EXT4_ROOT_INO=2)' /
根目录 / inode=2 (源码 EXT4_ROOT_INO=2)
$ stat -c '/home inode=%i' /home
/home inode=57346
|
ext4 固定 inode 1=坏块、2=根目录(fs/ext4/ext4.h:313 EXT4_BAD_INO=1、EXT4_ROOT_INO=2),挂载时 ext4_iget(sb, EXT4_ROOT_INO, ...)(fs/ext4/super.c:5518)读入的就是它,成为 sb->s_root。
unshare -m - run program in new namespaces
1
2
|
$ unshare -m true
unshare: unshare failed: Operation not permitted
|
这正是 copy_namespaces()(kernel/nsproxy.c:147)里的权限检查:
1
2
|
} else if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
|
普通用户没有 CAP_SYS_ADMIN,创建新 mount namespace 被拒。
顺带说明:本环境连 sudo 也用不了——/proc/self/status 显示 NoNewPrivs: 1、CapEff: 0、Seccomp: 2,内核级禁止 setuid 提权(与密码无关)。所以下面这些 root 实验是通过docker 特权容器执行的(本账号在 docker 组,docker run --privileged 的容器内 uid=0 且拥有 CAP_SYS_ADMIN,等于一条文档化的 root 通道;/dev/sdc 用 bind mount 带进容器,宿主机二进制用 LD_LIBRARY_PATH + 宿主动态加载器直接跑,零安装)。
在有 root 的机器/容器上,
1
2
3
4
5
6
7
8
9
|
$ docker run --rm --privileged alpine:3.20 sh -c '
> ls -l /proc/self/ns/mnt
> unshare -m sh -c "ls -l /proc/self/ns/mnt; mount -t tmpfs none /mnt; \\
> grep \" /mnt \" /proc/self/mounts"
> grep " /mnt " /proc/1/mounts'
lrwxrwxrwx 1 root root 0 ... /proc/self/ns/mnt -> mnt:[4026532222] ← 容器自己的 mnt ns
lrwxrwxrwx 1 root root 0 ... /proc/self/ns/mnt -> mnt:[4026532362] ← unshare -m 又新建一个
none /mnt tmpfs rw,relatime 0 0 ← tmpfs 只在新 ns 里可见
← 外面(/proc/1/mounts)看不到它
|
三个 inum 对照(宿主进程是 4026532211,见 9.7 实验):
| 视角 |
mnt ns inum |
说明 |
| 宿主 shell |
4026532211 |
本机 WSL2 的 mnt ns |
| docker 容器 |
4026532222 |
每个容器默认拿到新 mnt ns(copy_mnt_ns 克隆整棵挂载树) |
| 容器里 unshare -m |
4026532362 |
copy_namespaces + copy_mnt_ns + copy_tree 再克隆一次 |
tmpfs 挂载只出现在 unshare 之后那个命名空间的 mounts 里,外面看不到——这就是"挂载视图隔离"的字面意思(fs/namespace.c:4108 的 copy_mnt_ns 第二遍会把 fs->root/fs->pwd 同步重定向到新树)。
root 实验(已在本机用特权容器中执行)
以下实验原本需要 CAP_SYS_ADMIN / 块设备读权限,本环境已通过 docker run --privileged + bind-mount /dev/sdc 执行
① debugfs 读磁盘 inode:imap 精确定位 + 公式验证
1
2
3
|
$ debugfs -R "imap <57411>" /dev/sdc
Inode 57411 is part of block group 7
located at block 12933, offset 0x0200
|
用 dumpe2fs -h 的常量亲手算一遍(__ext4_get_inode_loc 的同一套公式):
| 步骤 |
计算 |
结果 |
| 常量 |
Block size=4096, Inodes per group=8192, Inode size=256(dumpe2fs -h) |
inodes_per_block = 4096/256 = 16 |
| 块组 |
(57411-1) / 8192 |
7 ✓ |
| 组内偏移 |
(57411-1) % 8192 |
66 |
| 表内块 |
66 / 16 |
4 → 块 = 12929 + 4 = 12933 ✓ |
| 块内偏移 |
(66 % 16) × 256 |
0x200 ✓ |
(12929 来自 dumpe2fs 完整输出的 Group 7: Inode table at 12929-13440。)公式与 debugfs 结果完全一致。
② 直接把 inode 的原始字节 dd 出来并解码
inode 57411 位于"磁盘块 12933 + 偏移 0x200":dd if=/dev/sdc bs=4096 skip=12933 count=1 | dd bs=1 skip=$((0x200)) count=256,得到 256 字节,对照 struct ext4_inode(fs/ext4/ext4.h:787)逐字段解码:
1
2
3
4
5
6
7
8
9
10
11
12
|
偏移 字节(小端) 字段 解码值 对照 fstat
0x00 a4 81 i_mode 0x81a4 = 0100644 0644 普通文件 ✓
0x02 e8 03 i_uid 1000 uid=1000 ✓
0x04 10 00 00 00 i_size_lo 16 16 字节 ✓
0x18 e8 03 i_gid 1000 gid=1000 ✓
0x1A 01 00 i_links_count 1 nlink=1 ✓
0x1C 08 00 00 00 i_blocks_lo 8 8×512B ✓
0x20 00 00 08 00 i_flags 0x00080000 EXT4_EXTENTS_FL ✓
0x28 0a f3 01 00 ... i_block[15] extent 树根: magic=0xF30A, entries=1, max=4, depth=0
0x34 00 00 00 00 ee_block 0 (逻辑块 0)
0x38 01 00 00 00 ee_len/start_hi 1 块 (长度 1)
0x3C ca b5 39 00 ee_start_lo 0x39b5ca = 3784906 (物理块号)
|
磁盘上 inode 就是一块按 struct ext4_inode 排布的字节——内存里的 struct inode 是它的运行时翻译。
③ 三路验证:原始字节 == debugfs stat == filefrag
用新建文件(fresh.bin,10KB = 3 块)做端到端对照,三方完全一致:
1
2
3
4
5
6
|
$ filefrag -v fresh.bin # 内核内存视图
0: 0..2: 2458911..2458913: 3: last,eof
$ debugfs -R "stat <213294>" /dev/sdc # 磁盘视图(debugfs)
EXTENTS: (0-2):2458911-2458913
$ dd ... | od # 原始字节: extent: ee_block=0, ee_len=3, ee_start=0x0025851f
0025851f = 2458911 ✓
|
注:对 a.txt(inode 57411)原始块读到 ee_start=0x39b5ca(3784906),而 filefrag/debugfs 显示 3782090——挂载中的 rw 文件系统上,磁盘 inode 块可能滞后于内核视图(日志/回写时序),这也是为什么内核要先读进内存再校验、以及 debugfs 这类工具只适合离线/只读检查的原因。
④ drop_caches:清空全系统页缓存
1
2
3
4
5
6
7
8
9
10
|
$ dd if=/dev/zero of=big.bin bs=1M count=512 && sync
$ grep Cached /proc/meminfo
Cached: 864964 kB
$ docker run --rm --privileged alpine:3.20 sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ grep Cached /proc/meminfo
Cached: 176196 kB ← 回落 ~689MB (不止我们的 512MB, 全系统的页都被清了)
$ time cat big.bin > /dev/null
real 0m0.126s
$ grep Cached /proc/meminfo
Cached: 700536 kB ← 冷读又把它读回页缓存
|
echo 3 > /proc/sys/vm/drop_caches 等价于触发各 super_block 的 s_dentry_lru/s_inode_lru 和 address_space 的逐出路径(fs/drop_caches.c),比 posix_fadvise(DONTNEED)(9.3)范围更大。
⑤ 挂载传播:private → shared → slave
在私有命名空间里(不影响宿主)用 mountinfo 的 optional 字段观察 mnt_share/mnt_slave 链表(fs/mount.h:68-71)的效果:
1
2
3
4
5
6
7
|
$ mount -t tmpfs none /mnt
$ grep " /mnt " /proc/self/mountinfo
165 482 0:84 / /mnt rw,relatime - tmpfs none rw ← 无 shared/slave 字段 = private
$ mount --make-shared /mnt
165 482 0:84 / /mnt rw,relatime shared:230 - tmpfs none rw ← 加入共享组 230
$ mount --make-slave /mnt
165 482 0:84 / /mnt rw,relatime - tmpfs none rw ← 变为 slave(无 master 时无字段)
|
mountinfo 每行第 6 个字段(shared:N / master:N / unbindable)直接对应 mount::mnt_share(循环链表)与 mnt_slave(+mnt_master 指针)的拓扑。
总结
- super_block 是文件系统实例的"户口本",内存版(
fs.h:1446)与磁盘版(ext4_super_block)通过 buffer_head 桥接,s_bdev/s_fs_info 分别连向设备和私有数据。
- inode 是文件本体的"身份证",
i_ino 定位磁盘 inode 表(__ext4_get_inode_loc 的三段除法),i_data/i_mapping 把文件接到页缓存,i_block[15]/i_data[15] 承载 extent 树,实现 lblk→pblk(ext4_map_blocks)。
- dentry 是路径名的缓存:
d_parent/d_children 组成树,d_hash 提供哈希查询,d_inode/d_alias 把名字接到 inode,d_sb 把每个名字归属到文件系统。
- file 是进程的打开会话:
f_path 记住解析结果,f_pos 记住当前位置,f_op 承载读写方法;files_struct 用一张表把进程 fd 和 file 串起来。
- 磁盘关联是一条四层通道:
偏移 >> blkbits → lblk → ext4_map_blocks → pblk → (pblk<<k) 扇区 → bio → submit_bio,中间页缓存(address_space/XArray/folio)与 buffer_head 是内存侧的两大缓冲。
- namespace 隔离的是"视图"而非"数据":
nsproxy->mnt_ns 里的 mount 树(copy_mnt_ns/copy_tree 克隆)决定你看到哪些挂载,但 super_block/inode 全局共享;fs_struct 的 root/pwd 决定路径解析的起点;ns_common.inum 则是命名空间的身份证,/proc/pid/ns/mnt 的 inode 号可用来判断两个进程是否共享挂载视图。
super_block 回答"哪个文件系统",inode 回答"哪个文件",dentry 回答"哪个名字",file 回答"哪个会话";页缓存与块映射把这一切接到磁盘,mnt_namespace 与 fs_struct 把这一切挂到进程。