Linux内核分析之文件系统-00
This language version is unavailable; showing the other language.
33.1 VFS 核心对象 —— superblock、inode、dentry、file
VFS 的全部功能由四个对象承载,每个对象回答一个不同的问题:super_block 回答"这是哪个文件系统",inode 回答"这个文件是什么",dentry 回答"这个文件叫什么",file 回答"谁正在怎么用它"。本节逐字段拆解四者与它们的操作表。
33.1.1 super_block:文件系统实例
// include/linux/fs/super_types.h:132-210(节选)
struct super_block {
struct list_head s_list; /* Keep this first */ /* :133 */
dev_t s_dev; /* search index */
...
struct file_system_type *s_type; /* :139 归属的 fs 类型 */
const struct super_operations *s_op; /* :140 操作表 */
...
unsigned long s_flags; /* :144 挂载标志 */
unsigned long s_magic; /* :146 魔数(ls -l 视角) */
struct dentry *s_root; /* :147 根 dentry */
struct rw_semaphore s_umount; /* :148 */
int s_count;
atomic_t s_active; /* :150 挂载引用数 */
...
struct hlist_node s_instances; /* :172 挂到 fs_type->fs_supers */
struct sb_writers s_writers; /* :176 冻结/写计数 */
...
};
一个文件系统类型可对应多个 super_block(同一块盘挂两次、多个 NFS 导出)——s_instances 把实例链回 file_system_type->fs_supers 哈希。s_active 计数挂载引用,降零触发 kill_sb()(33.2 节)。s_writers 支撑文件系统冻结(FIFREEZE ioctl,快照/备份的前提)。7.0 把 super_block 与 super_operations 移入独立的 super_types.h(:214 起操作表:alloc_inode/destroy_inode/write_inode/put_super/sync_fs/freeze_fs/statfs/remount_fs),头文件分层本身就是一次可读性重构。
33.1.2 inode:文件的元数据本体
// include/linux/fs.h:766-850(节选)
struct inode {
umode_t i_mode; /* :767 类型+权限 */
unsigned short i_opflags;
...
kuid_t i_uid; /* :774 */
...
const struct inode_operations *i_op; /* :777 元数据操作表 */
struct super_block *i_sb; /* :778 回指所属 fs */
struct address_space *i_mapping; /* :779 页缓存挂点(36章) */
...
/* Filesystems may only read i_nlink directly. */
union {
const unsigned int i_nlink; /* :795 硬链接数 */
unsigned int __i_nlink;
};
loff_t i_size; /* :799 字节大小 */
time64_t i_atime_sec; /* :800 时间戳 sec+nsec 双字段 */
u32 i_atime_nsec;
...
u32 i_generation; /* :806 NFS 句柄稳定性 */
spinlock_t i_lock;
...
blkcnt_t i_blocks; /* :811 512B 块计数 */
...
};
inode 是"文件内容与元数据的化身"——与名字无关(一个 inode 可有 N 个名字=硬链接 i_nlink,可零个名字=已 unlink 但被打开/孤儿 inode)。i_mapping 把每个 inode 连到它的页缓存仓库(36 章主角);i_op 指向 inode_operations(fs.h:2001:lookup/create/mkdir/unlink/rename/getattr/setattr/setxattr/listxattr/permission)——目录项操作与权限检查都在这张表。注意 inode 是 VFS 抽象:ext4 的磁盘 inode、tmpfs 的内存 inode、procfs 的伪 inode 都填充同一结构,磁盘上是否存在是各实现自己的事。
33.1.3 dentry:名字的缓存
// include/linux/dcache.h:92-133(节选)
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* :94 */
seqcount_spinlock_t d_seq; /* :95 RCU-walk 序号 */
struct hlist_bl_node d_hash; /* :96 查找哈希链 */
struct dentry *d_parent; /* :97 父目录 */
union {
struct qstr __d_name;
const struct qstr d_name; /* :99-102 名字字符串 */
};
struct inode *d_inode; /* :104 指向的 inode(NULL=负缓存) */
...
const struct dentry_operations *d_op; /* :109 */
struct super_block *d_sb; /* :110 */
unsigned long d_time; /* :111 d_revalidate 用 */
void *d_fsdata; /* :112 fs 私有数据 */
...
struct lockref d_lockref; /* :116 锁+引用合并(16字节) */
union {
struct list_head d_lru; /* :119 LRU 链 */
...
};
struct hlist_node d_sib; /* :122 兄弟链 */
struct hlist_head d_children; /* :123 子链 */
union {
struct hlist_node d_alias; /* :127 inode 的别名链 */
...
} d_u;
};
dentry 是纯缓存——磁盘上没有 dentry 这个东西,它是"名字→inode"查找结果的内存缓存。设计要点:d_inode == NULL 的负缓存(查过不存在的名字也缓存,避免反复打文件系统);d_shortname 内联短名字(≤40B 免堆分配);d_lockref 把自旋锁与引用计数压进一个 16 字节结构(引用计数用特殊编码免原子指令,33.3 节 RCU-walk 的基础设施);d_alias 让同一 inode 的全部名字(硬链接)链起来。dcache 的裁剪由 d_lru 链上的 shrinker 驱动(内存压力时逐出,24 章 shrinker 家族)。
dentry_operations(d_revalidate/d_hash/d_compare/d_delete/d_release)是各文件系统挂钩点——NFS 用 d_revalidate 做服务器端有效性确认,overlayfs 用它实现"合并视图的目录缓存"(38 章)。
33.1.4 file:打开的会话
// include/linux/fs.h:1259-1330(节选)
struct file {
spinlock_t f_lock; /* :1260 */
fmode_t f_mode; /* :1261 读/写/执行模式 */
const struct file_operations *f_op; /* :1262 数据操作表 */
struct address_space *f_mapping; /* :1263 */
struct inode *f_inode; /* :1265 */
unsigned int f_flags; /* :1266 O_* 标志 */
...
struct fown_struct *f_owner; /* :1269 F_SETOWN */
union {
const struct path f_path; /* :1272 (mnt,dentry) 对 */
...
};
...
struct mutex f_pos_lock; /* :1276 位置锁(线程共享fd) */
loff_t f_pos; /* :1280 当前偏移 */
...
};
file 是每次 open 的会话:同一进程对同一文件 open 两次得到两个 struct file(各有 f_pos),fork/dup 则共享同一 file(f_pos 同步——f_pos_lock 解决多线程共享 fd 的偏移竞态)。f_path 记录打开时的 (vfsmount, dentry) 对——/proc/self/fd 读出的符号链接即它。f_op(file_operations,fs.h:1926)是数据面操作表:llseek:1929, read:1930, write:1931, read_iter:1932, write_iter:1933, iterate_shared:1936(目录读), poll:1937, mmap:1940, open:1941, fsync:1944, lock:1946, splice_read/write:1950-1951——33.4 节的 read 分发与 32 章 eventfd 的 poll 都在这张表上。
33.1.5 对象与操作表的分工总表
| 对象 | 生命周期 | 对应"什么" | 操作表 | 代表操作 |
|---|---|---|---|---|
| super_block | 挂载期间 | 文件系统实例 | super_operations | alloc_inode/sync_fs/freeze |
| inode | 引用计数(可晚于名字消失) | 文件内容与元数据 | inode_operations | lookup/create/unlink/permission |
| dentry | 纯缓存(LRU 逐出) | 名字条目 | dentry_operations | revalidate/compare |
| file | open..close | 打开会话 | file_operations | read/write/mmap/fsync |
"hard link 是 inode 的别名、open file 是 inode 的会话"——这一句覆盖了 VFS 最常被误解的语义:删除一个名字(unlink)只减 i_nlink,数据在最后一个名字与最后一个 file 都消失后才真正回收(ext4 的实现见 34 章)。
小结
VFS 用四对象三分工支撑全文件系统:super_block 管实例(s_active/s_writers/冻结)、inode 管内容与元数据(i_nlink 别名/i_mapping 页缓存挂点)、dentry 管名字缓存(负缓存/lockref/RCU 字段)、file 管会话(f_pos_lock/f_path);三张操作表(super/inode/file operations)把每个动作分发到具体实现。下一节看文件系统如何注册类型、挂载实例进 namespace 的挂载树。
33.2 文件系统注册与挂载
模块化的文件系统要经历两级注册:编译期向 VFS 注册类型(file_system_type),挂载时实例化 super_block 并挂进 namespace 的挂载树(vfsmount/mount)。本节沿 register_filesystem() → mount() → do_new_mount() → attach_mnt() 分析这条装配线,以及 modern fs_context(新挂载 API)的角色。
33.2.1 file_system_type:类型注册
// include/linux/fs.h:2269-2300(节选)
struct file_system_type {
const char *name;
int fs_flags; /* 位标志 */
#define FS_REQUIRES_DEV 1 /* 需要块设备 (ext4/xfs) */
#define FS_USERNS_MOUNT 8 /* 允许 user namespace 内挂载 (38章容器) */
#define FS_ALLOW_IDMAP 32 /* 支持 idmap 挂载 */
...
int (*init_fs_context)(struct fs_context *); /* 新挂载参数解析 */
const struct fs_parameter_spec *parameters;
void (*kill_sb) (struct super_block *); /* 实例析构 */
struct module *owner;
struct file_system_type * next; /* 全局注册链 */
struct hlist_head fs_supers; /* 本类型的全部实例 */
...
};
// fs/filesystems.c:72-94(节选)
int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;
...
fs->next = NULL;
...
spin_lock(&filesystems_lock);
p = find_filesystem(fs->name, strlen(fs->name)); /* 重名检查 */
if (p)
*p = fs; /* 尾插进全局链 */
else
res = -EBUSY;
spin_unlock(&filesystems_lock);
return res;
}
register_filesystem() 把类型挂进全局链(/proc/filesystems 的数据源),ext4 的 module_init 里调用、模块卸载时 unregister。fs_flags 决定挂载能力边界:FS_REQUIRES_DEV 要求块设备源(tmpfs/procfs 不设),FS_USERNS_MOUNT 是"非特权容器能不能挂它"的开关(38 章 overlayfs 设了它才可能在用户 namespace 中挂载)。
33.2.2 mount 系统调用:从类型到实例
// fs/namespace.c:3792-3860(节选)
static int do_new_mount(const struct path *path, const char *fstype,
int sb_flags, int mnt_flags,
const char *name, void *data)
{
...
type = get_fs_type(fstype); /* 按名查类型(可触发模块加载) */
if (!type)
return -ENODEV;
...
fc = fs_context_for_mount(type, sb_flags); /* 建 fs_context */
...
ret = parse_monolithic_mount_data(fc, data); /* 解析挂载选项 */
...
ret = vfs_get_tree(fc); /* 触发 get_tree → 建立 super_block+dentry 根 */
...
ret = do_new_mount_fc(fc, path, mnt_flags); /* :3759 建 mount 并挂树 */
...
}
mount(8) 的内核装配线:
mount("/dev/sda1", "/mnt", "ext4", flags, data)
└─ SYSCALL_DEFINE5(mount) namespace.c:4338
└─ path_mount() :4084
├─ do_new_mount() :3792
│ ├─ get_fs_type("ext4") 类型查找
│ ├─ fs_context_for_mount() 参数容器 (7.0 的挂载 API 重构:
│ │ 旧 s_flags/data 指针 → 结构化)
│ ├─ vfs_get_tree(fc)
│ │ └─ ext4_fill_super() 34 章的入口:
│ │ 读磁盘超级块 → 建 sb+s_root
│ └─ do_new_mount_fc() :3759
│ ├─ vfs_create_mount(fc) 建 struct mount
│ └─ do_add_mount() 权限/循环检查
│ └─ attach_mnt() :1050 挂进挂载树
v
挂载树: parentmnt ──> new mount(point=/mnt, sb=ext4实例)
fs_context 是挂载 API 的现代化(取代旧的 void *data 二进制 blob 传递):类型实现 init_fs_context + parameters 表,参数解析、重配置(mount -o remount)、fsopen()/fsmount() 新系统调用共享同一套结构——33.1 节 FS_BINARY_MOUNTDATA 标志就是"我还没迁移"的逃生口。
33.2.3 vfsmount 与 mount:挂载树的两层
// include/linux/mount.h:58 与 fs/mount.h:45(两层结构要点)
struct vfsmount {
struct dentry *mnt_root; /* 本挂载的根 dentry */
struct super_block *mnt_sb; /* super_block */
int mnt_flags;
} __randomize_layout;
struct mount { /* fs 内部扩展(不导出) */
struct hlist_node mnt_hash;
struct mount *mnt_parent; /* 挂载点所在的父 mount */
struct dentry *mnt_mountpoint; /* 挂载点 dentry */
struct vfsmount mnt; /* 对外可见部分 */
...
struct list_head mnt_instance; /* sb->s_mounts 链 */
...
};
为什么分两层:vfsmount 是稳定的历史 ABI(f_path.mnt 引用它),struct mount 是挂载树算法的真实节点(父指针、挂载点、子链、传播组)。路径查找(33.3 节)解析到挂载点时做 follow_automount/into:发现某 dentry 上叠了新 mount,就切换 vfsmount——/proc/mounts 的层级视图就是这棵树的遍历。挂载传播(shared/slave/private/unbindable,容器的 mount propagation)以 peer group 链实现于 struct mount 内——mount --make-shared 改的不是 flags 而是这些链。
umount 的对称操作:引用检查(busy 判定:有 file 打开/VMA 映射/子挂载)→ detach_mnt → s_active 降零时 kill_sb() → generic_shutdown_super()(super_types.h 声明,fs/super.c:清全部 inode/dentry、调 put_super)。
小结
文件系统的两级注册:register_filesystem 挂类型(fs_flags 定能力边界、fs_supers 收实例),mount() 经 fs_context 解析参数、vfs_get_tree 实例化 super_block 与根 dentry、attach_mnt 挂进 namespace 挂载树;vfsmount 对外稳定、struct mount 对内承树与传播组,路径查找在挂载点自动切换 vfsmount。下一节进入挂载树的最大消费者——路径查找的 RCU 快路径。
33.3 路径查找 —— namei 与 dcache
每次 open("/usr/bin/gcc") 都是一次路径查找——逐组件"取名字、查 dcache、必要时问文件系统"。它是 VFS 最热的路径,内核为它设计了两种遍历模式:RCU-walk(无锁乐观)与 ref-walk(持引用稳妥),失败自动降级。本节解剖 namei.c 的这条性能核心。
33.3.1 两级缓存命中:lookup_fast
// fs/namei.c:1839-1888(节选)
static struct dentry *lookup_fast(struct nameidata *nd)
{
struct dentry *dentry, *parent = nd->path.dentry;
int status = 1;
...
if (nd->flags & LOOKUP_RCU) {
struct dentry *d;
d = __d_lookup_rcu(parent, &nd->last, &nd->next_seq);
if (unlikely(!d))
return -ECHILD; /* 乐观失败信号 */
...
status = d_revalidate(d, nd->flags);
...
} else {
...
dentry = __d_lookup(parent, &nd->last); /* 锁定版哈希查找 */
...
}
...
}
__d_lookup_rcu 在 dentry 哈希链上无锁遍历:读 d_seq 序号(33.1.3 节的 seqcount)→ 取 d_name/d_inode → 再验 d_seq 未变——与 22.2.4 节 per-VMA 锁同款的"seq 比对"协议。-ECHILD 是 RCU-walk 的语言:"这里不安全,请降级重走"。
33.3.2 RCU-walk 与 ref-walk
路径查找的两种模式:
RCU-walk (乐观, 无锁):
- 全程只持 RCU 读临界区, dentry/inode 零引用计数
- 哈希链遍历 + seq 验证
- 遇到下列情况立即放弃, 返回 -ECHILD:
哈希未命中(要问文件系统) / 符号链接 / 权限检查
/ d_revalidate 需要睡眠(NFS)
- 代价: 降级后全部重走 → 但缓存热路径收益巨大
(多核扩展性: 零缓存行争抢)
ref-walk (稳妥, 持引用):
- 每 dentry 拿 d_lockref 引用, parent 逐级持锁
- 可以睡眠: 调 lookup_slow 问文件系统, 读盘等 IO
- 完成后逐级放引用
降级点: complete_walk()/unlazy_walk() (namei.c:1045)
在 RCU 模式下发现必须"慢下来"时, 把已走过的
组件重新以引用方式拿一遍 (RCU 保护下此操作安全)
性能账:/usr/lib/gcc/x86_64/... 这类深层路径,ref-walk 要拿十几次引用计数(跨核缓存行弹跳),RCU-walk 全程原子读——多核并发的路径查找因此从"锁串行"变成"全并行"。/proc/sys/vm/ 压力下 dcache 裁剪会让 RCU-walk 命中率下降、降级增多——perf 里 lookup_fast 与 lookup_slow 的占比是 dcache 健康度的直接指标。
33.3.3 主循环:link_path_walk
// fs/namei.c:2575(签名)与 2798
static int link_path_walk(const char *name, struct nameidata *nd)
/* 逐组件驱动: 取下一个组件名 → LOOKUP_FOLLOW 处理
链接 → lookup_fast/slow → 权限检查(i_op->permission)
→ 进入下一层 */
static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path) /* :2798 */
/* lookup 类操作(非 open)的顶层: 首组件是 '/' 时
从 nd->root 起, link_path_walk + complete_walk 收尾 */
int kern_path(const char *name, unsigned int flags, struct path *path) /* :3043 */
/* 内核模块可用的路径解析接口 */
主循环的每个组件处理五件事:分量截取(/ 切分)、符号链接(LOOKUP_FOLLOW,默认跟随,O_NOFOLLOW/末组件除外;链接深度 40 上限防环)、./..(.. 需 d_parent 且过挂载点回退——follow_dotdot 处理 33.2 节挂载树的向上穿越)、权限(每目录 may_lookup:执行位检查 + LSM 钩子)、最终组件(lookup_last/open_last_looknu:open 的创建语义 LOOKUP_CREATE|OPEN_FLAG_CREAT 在最后组件特判)。
.. 与挂载点的交互是路径语义最微妙处:从挂载点内部向上 .. 应该回到挂载点父目录而非被挂文件系统的真实父——follow_dotdot 检查 path_mounted()(namespace.c:2023 区的检查调用)并切换 vfsmount 后再取 d_parent。chroot 的"越狱防护"同样落在这条检查上。
33.3.4 缓存未命中:lookup_slow
// fs/namei.c:1926-1937
static noinline struct dentry *lookup_slow(const struct qstr *name,
struct dentry *dir,
unsigned int flags)
未命中时持父目录 i_rwsem 调 i_op->lookup()(文件系统把目录项从磁盘/网络取来、d_splice_alias 挂入 dcache)。写侧并发由目录 inode 的 i_rwsem 串行化(33.4 节的 s_vfs_rename_key 锁类处理跨目录 rename 的锁序)——"同目录 create 竞争被 i_rwsem 串行、不同目录并行"是 VFS 并发模型的根基。
小结
路径查找 = dcache 命中 + 必要的文件系统回源,内核以 RCU-walk(seq 验证、零引用、-ECHILD 降级)与 ref-walk(持引用、可睡眠)双模式实现"热路径全并行、冷路径可休眠";link_path_walk 逐组件处理链接、.. 与挂载点穿越、权限与最终组件语义;未命中走 lookup_slow 在 i_rwsem 保护下回源并 d_splice_alias 入缓存。下一节看拿到 struct file 之后的数据通路——read/write 的分发链。
33.4 文件读写 —— read/write 系统调用路径
VFS 的读写分发要同时伺候两代接口:老的 read/write 函数指针与新式的 read_iter/write_iter(迭代器 I/O,支持 async/直通/批量)。本节沿 vfs_read() 的分发链走到 file_operations 的边界——文件系统内部(ext4 的 extent 查找、页缓存交互)留给 34/36 章。
33.4.1 vfs_read:公共检查与分发
// fs/read_write.c:554-600(节选)
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
...
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count); /* 偏移合法性+锁检查 */
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT; /* 单次上限 2GB */
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); /* 新接口 */
else
ret = -EINVAL;
...
return ret;
}
// fs/read_write.c:483-510(新接口适配器, 节选)
static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = (ppos ? *ppos : 0);
iov_iter_ubuf(&iter, ITER_DEST, buf, len);
ret = call_read_iter(filp, &kiocb, &iter); /* → f_op->read_iter */
...
}
两代接口的并存:read 指针只被极少数老驱动使用;现代文件系统只实现 read_iter——它把"缓冲区描述"泛化为 iov_iter(用户/内核/管道/bvec 四种迭代器,splice 与 io_uring 复用同一接口),kiocb 承载 per-call 状态(位置、IOCB_NOWAIT、竞争标志)。new_sync_read 是"同步调用 → 迭代器接口"的适配器——sys_io_uring 则直接构造 kiocb 走 call_read_iter,跳过适配层。
公共检查的清单即 POSIX 语义:FMODE_READ(open 模式)、rw_verify_area(偏移+长度合法、强制锁检查)、MAX_RW_COUNT 截断。f_pos 的推进由调用方(ksys_pread64 传入显式位置;ksys_read 持 f_pos_lock 读写 f_pos——22.1.4 节 file 结构的 f_pos_lock 在此兑现)。
33.4.2 read_iter 之后的两个世界
f_op->read_iter 之内, 按文件类型分流:
[1] 常规文件 (ext4: ext4_file_read_iter):
generic_file_read_iter (36 章主入口)
├─ 命中页缓存: copy_page_to_iter
├─ 未命中: filemap_read 发起 IO
└─ DIO 标志: 绕页缓存直进块层 (35 章)
[2] 目录: iterate_shared → 文件系统的 readdir
(getdents64 系统调用; 偏移语义 = cookie)
[3] 设备/字符流: 驱动自己的 read_iter
(32 章 eventfd/timerfd 的读就是这种)
[4] splice 通道: splice_read → 把文件页挂进 pipe 环
(26.1.1 节 ops 多态的零拷贝消费)
写侧对称: vfs_write → write_iter
generic_file_write_iter
├─ 脏页路径: 写页缓存标 PG_dirty, 异步回写 (36.3)
└─ O_DIRECT: 直写块层
尾部: generic_write_sync (O_SYNC/O_DSYNC 强制 fsync)
写路径的一致性责任在 VFS 层只有一半(页缓存标脏),"何时落盘"交给回写子系统与 fsync(f_op->fsync,36.3 节的 journal commit 呼应)。generic_write_checks_count(fs.h:3050 声明)负责"超出 i_size 上限/文件系统限额(quota)/RLIMIT_FSIZE"的预检。
33.4.3 fsync 与位置语义速查
定位族: lseek (f_pos 修改) / pread/pwrite (显式位置, 不动 f_pos)
落盘族: fsync (数据+元数据) / fdatasync (仅数据, :1944 的 datasync 参数)
截断族: ftruncate → inode_operations->setattr
同步顺序族: O_SYNC 打开后每次 write 内联 fsync
(generic_write_sync 尾部调用 — 事务日志型负载慎用)
read 的"部分读"语义:
返回值 < count 合法 (EOF/信号/短读) — 调用方必须循环;
write 同理 (磁盘满/信号/配额) — 这是所有 IO 库
循环包装的存在理由 (与 26.2 节管道短写同源)
小结
VFS 读写层的形态是"薄公共层 + 迭代器分发":vfs_read/write 做模式/位置/长度检查后经 new_sync_read 适配到 read_iter/write_iter,kiocb+iov_iter 让同步 syscall、splice、io_uring 共享同一文件系统接口;数据面在 f_op 边界按文件类型分流——常规文件进页缓存/DIO、目录进 readdir、设备进驱动。位置语义由 f_pos/f_pos_lock 承载,部分读写是调用方必须循环处理的合法返回。下一节看读写之外的第三个竞争面——文件锁。
33.5 文件锁 —— flock 与 POSIX 锁
文件锁有三个语义不同的成员:flock(BSD,锁"打开的文件对象")、POSIX 记录锁(fcntl,锁"进程+字节区间")、租约(lease,锁"对文件访问的知情权")。三者在 fs/locks.c 统一为冲突检测框架,但生命周期与继承规则天差地别。本节对照三者的内核实现。
33.5.1 flock:文件对象级锁
// fs/locks.c:1133(要点)
static int flock_lock_inode(struct inode *inode, struct file_lock *request)
flock(fd, LOCK_EX|LOCK_SH|LOCK_UN) 语义:
- 锁的属主是 struct file (打开会话!), 不是进程
→ dup/fork 共享同一把锁 (同一 file)
→ 再 open 一次 = 新的独立锁持有者
- 解锁: LOCK_UN 或 close 该 fd (关联 file 销毁时)
- 默认阻塞; LOCK_NB → EWOULDBLOCK (31.2 节语义同源)
- 不防"没加锁的读写"! 纯协作式协议
内核结构: file_lock 挂 inode 的 flc_flock 链,
flock_lock_inode 在链上做冲突判定 + 排队睡眠
(flc_block 阻塞链 + 等待队列)
close 即解锁的规则有一半是坑:一个进程打开同一文件两次、第一次 close 会连带释放第二次的 flock(它们在同一 inode 的链上按 file 区分——但 fork 继承的引用也计数)。数据库式应用因此偏爱 POSIX 锁的进程语义。
33.5.2 POSIX 记录锁:进程+区间
// fs/locks.c:1458(签名)
int posix_lock_file(struct file *filp, struct file_lock *fl,
struct file_lock **conflock)
// fs/locks.c:2483
int fcntl_setlk(unsigned int fd, struct file *filp, unsigned int cmd,
struct flock *flock)
fcntl(F_SETLK/F_SETLKW, F_RDLCK|F_WRLCK|F_UNLCK) 语义:
- 属主是 (进程, inode) 对: 同进程重复加锁=替换,
**任一 fd close 全部解锁** (与 flock 相反的直觉!)
→ 多线程进程里 POSIX 锁几乎不可用
(线程 A close 测试用 fd, 线程 B 的锁全没了)
→ modern 修复: OFD 锁 (F_OFD_SETLK, open file
description 锁) — 属主改回 struct file,
flock 的生命周期 + POSIX 的区间语义
- 字节区间 [l_start, l_start+l_whence+l_len]:
冲突 = 区间重叠 && (一方是写 || 双方都写)
RDLCK 共享 / WRLCK 互斥 — 读锁可多持有
- F_GETLK 查询: 返回阻挡者的归属 (fcntl_getlk, :2355)
- F_SETLKW: 阻塞等待 (K=wait)
- 死锁检测: 阻塞路径上遍历等待图, 发现环则
挑一个牺牲者返回 EDEADLK
(内核唯一的"锁死锁检测" — 16.4 节 lockdep 是
编译期类, 这里是运行期)
实现: 区间锁需要"分裂/合并"算法 — 新锁插入时
与既有锁做重叠切割 (posix_lock_file, :1458),
解锁区间的空洞留给相邻锁补合
33.5.3 租约:写知情权
lease 语义 (F_SETLEASE, 需 CAP_LEASE 或文件打开权):
进程对文件"持有租约"后, 其他进程 open/截断该文件
会先收到信号 (SIGIO → 应答方转为写模式/释放),
有 lease_break_time (locks.c:97, 默认 45 秒) 宽限
用途: SAMBA/NFS 客户端缓存一致性 —
服务器借租约得知"还有人在缓存这文件吗",
别人要写时先通知本地缓存失效
实现: file_lease 结构 (locks.c:87 target_leasetype),
挂 inode 的 flc_lease 链; 冲突触发 lease_break →
信号通知 → 宽限计时 → 逾期强制剥夺
33.5.4 三锁对照表
| 维度 | flock | POSIX 锁 | OFD 锁 | 租约 |
|---|---|---|---|---|
| 属主 | struct file | 进程 | struct file | 进程+file |
| 粒度 | 整文件 | 字节区间 | 字节区间 | 整文件 |
| close 效果 | 仅该 file 的锁 | 进程全部锁释放 | 仅该 file | 视引用 |
| 网络文件系统 | 本地语义 | 可透传 NFS | NFSv4 对应 | 委托(delegation) |
| 典型用户 | 单实例守护进程 | 传统数据库 | 现代多线程应用 | 缓存服务 |
选型口诀:单实例互斥(防止 daemon 跑两份)用 flock;多线程应用必须 OFD 锁;跨机一致性协商才需要租约。
小结
文件锁的三个成员共享 fs/locks.c 的冲突框架而语义分立:flock 锁文件对象(fork 共享、close 释放)、POSIX 锁锁"进程+区间"(同进程任一 close 全释放——多线程陷阱,OFD 锁是修补)、租约提供"写前知情"(45 秒宽限,缓存一致性协议的内核支点)。POSIX 区间锁的分裂/合并算法与运行期死锁检测(EDEADLK)是这层唯一的"重"逻辑。VFS 框架至此五节齐备;下一章进入第一大实现——ext4 的磁盘布局与 extent 树。