夜雨聆风学习资料网

ARTICLE · 1275224

Linux 6.12 源码深度剖析: vfs_write

Linux 6.12 源码深度剖析: vfs_write

Linux 6.12 内核深度解析:VFS 核心枢纽 vfs_write 的实现

1. 📌 技术点速览

        在 Linux 操作系统中,一切皆文件。无论是普通文件、套接字、管道,还是字符/块设备,用户态发起的写操作最终都会汇聚到内核的虚拟文件系统层(Virtual File System, VFS)。vfs_write 函数正是 VFS 层处理数据写入的核心枢纽。

+-----------------------------------------------------------------+|                      User Space (用户态)                         ||   write() / pwrite() / writev()                                 |+-----------------------------------------------------------------+                                |  (System Call / syscall / svc)                                v+-----------------------------------------------------------------+|                      VFS Layer (内核态)                         ||   sys_write() -> ksys_write() -> vfs_write()                    ||   - 权限与边界校验 (FMODE_WRITE, access_ok, rw_verify_area)      ||   - 写入屏障与冻结保护 (file_start_write)                        ||   - 动态分发 (f_op->write / f_op->write_iter)                   |+-----------------------------------------------------------------+                                |        +-----------------------+-----------------------+        | (Regular File)        | (Socket)              | (Device)        v                       v                       v+---------------+       +---------------+       +---------------+| Ext4 / XFS    |       | Network Stack |       | TTY / GPU     |+---------------+       +---------------+       +---------------+

  vfs_write 处于系统调用分发层与底层具体文件系统/设备驱动层之间,属于内核的 VFS (虚拟文件系统层) 模块。它主要解决以下三个核心问题:

  1. 统一抽象与多态分发:屏蔽底层介质(磁盘、网络、内存、硬件寄存器)的差异,为用户态提供统一的 write 接口,并在内核中通过面向对象的设计(file_operations)将写请求分发给具体的存储介质或驱动。

  2. 安全边界与合规性防御:在数据真正触达底层文件系统之前,进行严格的权限检查(是否可写)、地址空间合法性校验(防止恶意用户态指针越界访问内核空间)以及文件锁/配额限制。

  3. 文件系统一致性保护(Freeze Protection):通过超级块(Superblock)写入锁机制,防止在文件系统执行快照或冻结(Freeze)时发生数据写入,确保文件系统元数据与数据的一致性。


2. 🗺️ 软件功能架构图

        以下架构图展示了从用户态发起 write 系统调用,通过 vfs_write 进行安全校验与冻结保护,最终跨模块分发到具体文件系统(如 Ext4)及页缓存(Page Cache)的完整流程。


3. 🔍 核心源码硬核解析 (基于 Linux 6.12)

        在 Linux 6.12 内核中,vfs_write 的核心实现位于 fs/read_write.c。下面我们对该函数进行逐行剖析。

/*  * 源码路径: fs/read_write.c * 核心职责: VFS 层的写操作总入口,负责安全校验、冻结保护、多态分发及 I/O 统计。 */ssize_t vfs_write(structfile *file, constchar __user *buf, size_t count, loff_t *pos){    ssize_t ret;    /*      * 1. 基础权限校验:     *    检查文件描述符在打开时是否申请了写权限 (FMODE_WRITE)。     *    如果文件是以只读方式 (O_RDONLY) 打开的,直接拒绝,返回 -EBADF (Bad file descriptor)。     */    if (!(file->f_mode & FMODE_WRITE))        return -EBADF;    /*      * 2. 写入可行性校验:     *    检查该文件类型是否支持写入 (例如,某些特殊的伪文件系统或只读挂载点会清除 FMODE_CAN_WRITE)。     *    若不支持,返回 -EINVAL (Invalid argument)。     */    if (!(file->f_mode & FMODE_CAN_WRITE))        return -EINVAL;    /*      * 3. 内存安全防御 (关键防御点):     *    access_ok 是一个与架构紧密相关的宏 (x86/arm64 均有各自的高效实现)。     *    它用于确保用户态传入的指针 `buf` 及其长度 `count` 完全处于用户空间地址范围内。     *    防止恶意用户传入内核空间地址(如 0xffff888000000000),利用内核权限向内核内存写入数据(Meltdown/漏洞防御)。     *    若校验失败,返回 -EFAULT (Bad address)。     */    if (unlikely(!access_ok(buf, count)))        return -EFAULT;    /*      * 4. 区间与锁校验:     *    rw_verify_area 会检查:     *    - 写入区间是否超出系统限制 (如超过 rlimit 中的 RLIMIT_FSIZE)。     *    - 是否触发了强制锁 (Mandatory Locking) 冲突。     *    - 检查 pos 指针是否溢出。     */    ret = rw_verify_area(WRITE, file, pos, count);    if (ret)        return ret;    /*      * 5. 单次写入上限截断:     *    MAX_RW_COUNT 定义为 INT_MAX & PAGE_MASK。     *    限制单次系统调用写入的最大字节数,防止单次大 I/O 导致内核态长时间不释放 CPU,保障调度公平性。     */    if (count > MAX_RW_COUNT)        count =  MAX_RW_COUNT;    /*      * 6. 文件系统冻结保护 (Freeze Protection):     *    file_start_write 会获取当前文件系统超级块 (superblock) 的写入锁 (sb_writers)。     *    如果此时系统管理员正在对该文件系统执行冻结操作 (如 fsfreeze,用于做 LVM 快照),     *    当前写线程会在此处阻塞,直到文件系统被解冻 (Thaw)。     *    这体现了 VFS 模块与 Superblock 模块之间的紧密协同。     */    file_start_write(file);    /*      * 7. 多态分发核心逻辑:     *    Linux 内核采用 C 语言实现了面向对象的多态。     *    - 优先使用旧版直驱接口 `file->f_op->write` (如某些简单字符设备驱动)。     *    - 若无 `write`,则检查是否实现了现代的异步/迭代接口 `file->f_op->write_iter` (主流文件系统如 Ext4, XFS 均采用此接口)。     *      通过 `new_sync_write` 将同步写请求包装为 `kiocb` 和 `iov_iter`,然后调用 `write_iter`。     *    - 若两者皆无,说明该文件不支持写,返回 -EINVAL。     */    if (file->f_op->write)        ret = file->f_op->write(file, buf, count, pos);    else if (file->f_op->write_iter)        ret = new_sync_write(file, buf, count, pos);    else        ret = -EINVAL;    /*      * 8. 写入成功后的后续处理:     *    - fsnotify_modify: 触发 inotify/fanify 事件,通知用户态监听该文件的 watcher (如 tail -f 或 systemd)。     *    - add_wchar: 将成功写入的字节数累加到当前进程的 `task_struct->ioac.write_bytes` 中,用于 /proc/PID/io 统计。     */    if (ret > 0) {        fsnotify_modify(file);        add_wchar(current, ret);    }    /*      * 9. 系统调用计数与释放锁:     *    - inc_syscw: 增加当前进程的写系统调用次数计数器 (syscw)。     *    - file_end_write: 释放超级块的写入锁,允许其他冻结操作进行。     */    inc_syscw(current);    file_end_write(file);    return ret;}

深入解析:new_sync_write 的桥梁作用

       现代 Linux 内核(包括 6.12)为了统一同步与异步 I/O(AIO/io_uring),推荐文件系统只实现 .write_iter。当用户调用同步 write 时,VFS 必须通过 new_sync_write 进行适配转换:

/* 源码路径: fs/read_write.c (原理简化版) */staticssize_tnew_sync_write(struct file *filp, constchar __user *buf, size_t len, loff_t *ppos){    struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };    struct kiocb kiocb;    struct iov_iter iter;    ssize_t ret;    /* 1. 初始化内核 I/O 控制块 (kiocb),绑定文件指针与当前偏移量 */    init_sync_kiocb(&kiocb, filp);    kiocb.ki_pos = (ppos ? *ppos : 0);    /* 2. 初始化迭代器 (iov_iter),将用户态单块内存 buf 包装为迭代器格式 */    iov_iter_ubuf(&iter, ITER_SOURCE, (void __user *)buf, len);    /* 3. 调用具体文件系统的 write_iter 接口 */    ret = call_write_iter(filp, &kiocb, &iter);    BUG_ON(ret == -EIOCBQUEUED); // 同步写不允许返回异步队列状态    /* 4. 更新文件偏移量 */    if (ret > 0 && ppos)        *ppos = kiocb.ki_pos;    return ret;}

4. ⚙️ 系统运行背景与上下文

  vfs_write 它是 Linux 内核中多模块协同作战的典型代表。以下是它与架构、内存、调度及通知系统的跨模块交互细节:

4.1 架构层协同:x86 与 arm64 的 access_ok 差异

在 vfs_write 的入口处,access_ok(buf, count) 是保障内核安全的第一道防线。

  • x86_64 架构: x86_64 采用虚拟地址空间划分(用户态:0x0000000000000000 至 0x00007fffffffffff)。access_ok 在 x86 下的本质是检查 (unsigned long)addr + size <= TASK_SIZE_MAX。如果越界,说明指针指向了内核空间(高位为 1 的地址),直接拦截。

  • arm64 架构: arm64 拥有两个独立的虚拟地址空间页表基址寄存器(TTBR0_EL1 控制用户空间,TTBR1_EL1 控制内核空间)。在 arm64 上,access_ok 同样通过检查地址是否超出 TASK_SIZE 来实现。此外,arm64 硬件支持 PAN (Privileged Access Prevention,特权访问禁止) 技术。当内核态试图直接访问用户态数据时,如果未显式开启访问权限,硬件会触发 Page Fault。vfs_write 下游的 copy_from_user 会在执行前通过 uaccess_enable_not_uao() 临时允许内核访问用户空间,拷贝完成后立即关闭,防止内核漏洞被利用。

4.2 内存管理模块 (MM) 协同:Page Cache 与脏页标记

当 vfs_write 调用到具体文件系统(如 Ext4)的 write_iter 时,通常会进入缓存 I/O (Buffered I/O) 流程:

  1. 页分配:Ext4 跨模块调用内存管理系统的 grab_cache_page_write_begin,在 Page Cache 中查找或分配对应的物理页(Page Frame)。

  2. 数据拷贝:调用 copy_page_from_iter_atomic,将用户态 buf 中的数据拷贝到内核 Page Cache 页中。

  3. 脏页标记:数据拷贝完成后,调用 __set_page_dirty_nobuffers 将该页标记为“脏页 (Dirty Page)”。此时数据仅存在于内存中,并未落盘。

  4. 异步刷盘:内存管理系统的后台线程(writeback 机制)会定期或在内存紧张时,将这些脏页通过块设备驱动写入物理磁盘。

4.3 锁与一致性协同:sb_writers 机制

file_start_write(file) 内部调用了 __sb_start_write(file_inode(file)->i_sb, SB_FREEZE_WRITE)。

  • 这是 VFS 与超级块管理模块的协同。

  • Linux 支持三级冻结:SB_FREEZE_WRITE(禁止普通写)、SB_FREEZE_PAGEFAULT(禁止页缺页写)、SB_FREEZE_FS(禁止文件系统元数据修改)。

  • sb_writers 采用的是 Per-CPU 读写信号量 (percpu-rwsem)。在正常高并发写入时,各个 CPU 仅累加本地的 Read 计数器,无锁竞争,性能极高。当执行 fsfreeze 时,冻结线程会获取 Write 锁,此时会合并所有 CPU 的计数器并阻塞后续的 vfs_write。


5. 💡 10年老兵避坑指南/实战案例

        在超大规模生产环境中,围绕 vfs_write 及其跨模块协同,常常会遇到各种诡异的性能瓶颈与死锁问题。以下整理了两个经典实战案例及排查方案。

5.1 案例一:文件系统冻结导致的“神秘”进程 D 状态死锁

生产背景

某云原生数据库节点在执行每日备份时,会调用 fsfreeze -f /data 对挂载点进行冻结以创建一致性 LVM 快照。然而,在冻结期间,数据库主进程突然出现大量 D 状态(Uninterruptible Sleep)线程,导致业务完全停摆。即使解冻(fsfreeze -u)也无法恢复。

根因剖析

通过 cat /proc/PID/stack 查看 D 状态线程的内核调用栈:

[<0>] percpu_down_read+0x51/0x90[<0>] __sb_start_write+0x2e/0x70[<0>] vfs_write+0xa5/0x1c0[<0>] ksys_write+0x5f/0xe0[<0>] do_syscall_64+0x5b/0x110[<0>] entry_SYSCALL_64_after_hwframe+0x6c/0x76

    可以看出,线程阻塞在 vfs_write 内部的 file_start_write(即 __sb_start_write)。 为什么解冻无法唤醒它们? 因为该数据库在写入时,持有了一个自定义的全局事务锁(Transaction Lock),然后调用了 write。而另一个线程在等待该事务锁。当 write 因为 fsfreeze 阻塞在 sb_writers 读锁上时,它并没有释放数据库的事务锁。这导致了跨模块死锁:

  • 数据库事务锁被写线程 A 持有。

  • 写线程 A 在 VFS 层等待 fsfreeze 释放写锁。

  • 备份脚本(试图执行解冻或后续操作)因为需要获取数据库事务状态而被阻塞。

避坑与调优

  1. 应用层规避:在调用 write 等可能阻塞在 VFS 层的系统调用前,绝对不要持有应用层的高开销全局锁。

  2. 超时机制:对于关键业务,避免使用可能永久阻塞的同步 write,改用 io_uring 或非阻塞 I/O,并对 fsfreeze 的持有时间进行严格监控。


5.2 案例二:高并发下 sb_writers 导致的 CPU 暴涨与 Cache Line 抖动

生产背景

        在一台 128 核的 arm64 服务器上运行超高并发的 KV 存储服务,小数据块(4KB)同步写入非常频繁。在业务高峰期,系统 CPU 利用率飙升,但 I/O 吞吐量(IOPS)却上不去。通过 perf top 观察发现,大量 CPU 时间消耗在 __sb_start_write 和 percpu_down_read 中。

根因剖析

虽然 sb_writers 使用了 percpu-rwsem 以优化读锁性能,但在 128 核的高并发环境下,频繁调用 file_start_write 和 file_end_write 依然会带来不可忽视的开销:

  1. Per-CPU 内存屏障开销:在 arm64 架构下,percpu_down_read 内部为了保证内存一致性,会执行 smp_mb()(强内存屏障指令)。在高核数下,频繁的内存屏障会导致 CPU 流水线停顿(Pipeline Stall)。

  2. 频繁的系统调用:每次 4KB 的写入都伴随着一次 vfs_write 的完整生命周期,包括两次 sb_writers 的状态修改。

解决方案与排查工具

1、合并 I/O (Batching):在应用层引入 Buffer,将多次小写入合并为一次大写入(如 64KB),或者改用 writev(向量化写入),大幅减少进入 vfs_write 的次数。

2、使用 io_uring:io_uring 支持内核线程(SQPOLL)代理写入,能够极大地减少系统调用上下文切换及相关的锁开销。

3、ftrace 追踪脚本: 使用 trace-cmd 或 bpftrace 监控 vfs_write 的耗时分布,找出是否存在异常延迟:

# 使用 bpftrace 统计 vfs_write 的耗时直方图 (单位: 微秒)bpftrace -e 'kprobe:vfs_write {    @start[tid] = nsecs;}kretprobe:vfs_write /@start[tid]/ {    @latency_us = hist((nsecs - @start[tid]) / 1000);    delete(@start[tid]);}'
如果直方图在几毫秒处出现双峰,且伴随有 __sb_start_write 的高延迟,则基本可以断定是由于文件系统冻结锁竞争或底层页缓存分配瓶颈导致的。

相关学习资料