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 树。