[toc]

fs/read_write.c 文件读写VFS实现(File Read/Write VFS Implementation) read/write系统调用的通用入口

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是VFS(虚拟文件系统)最核心、最基础的组成部分,它为了解决一个操作系统设计的根本性问题而诞生:如何为所有类型的文件和设备提供一个统一的、标准的、可移植的读写接口

在VFS和fs/read_write.c所代表的抽象层出现之前,应用程序需要为每一种不同的文件系统编写不同的代码来进行读写,这是不可想象的。fs/read_write.c通过实现read(2), write(2)及其变体这一系列系统调用,解决了以下核心问题:

  • 抽象与统一:无论底层是ext4文件、一个管道(pipe)、一个终端设备(tty)还是一个socket,用户空间程序都可以使用相同的read()write()系统调用来进行I/O操作。read_write.c负责将这些通用的请求,分派给具体的文件系统或驱动程序去处理。
  • 可移植性:它遵循了POSIX标准,使得为Unix-like系统编写的程序(如cat, cp, bash等)可以无需修改地在Linux上编译和运行。
  • 集中化管理:将所有读写操作的入口集中在此处,便于进行通用的权限检查、文件位置(offset)管理、以及其他与VFS层相关的通用逻辑处理。

它的发展经历了哪些重要的里程碑或版本迭代?

read()write()系统调用是Unix的基石,自Linux诞生之初就已存在。fs/read_write.c的发展体现在对这个基础模型的不断增强和优化上,以适应新的需求和性能挑战:

  1. 基础读写 (read, write):最初的实现,通过文件描述符、缓冲区指针和长度来进行操作,并会更新文件的当前偏移量。
  2. 定位读写 (pread, pwrite):为了满足多线程应用程序的需求,引入了preadpwrite。它们允许在一个原子操作中指定读写的偏移量,并且不会修改文件描述符自身的当前偏移量。这避免了在多线程中lseek()read()/write()之间产生的竞争条件。
  3. 向量化I/O (readv, writev):为了提高效率,引入了向量化或“分散-聚集”(Scatter-Gather)I/O。readv可以将数据从文件读入到内存中多个不连续的缓冲区,而writev可以将多个不连续缓冲区的数据一次性写入文件。这通过单次系统调用完成了多次读写才能完成的工作,极大地减少了内核态和用户态之间切换的开销。
  4. 现代异步I/O的接入点:随着Linux AIO(Asynchronous I/O)和最新的io_uring的发展,fs/read_write.c中的核心逻辑(如vfs_read, vfs_write)也成为了这些现代高性能I/O接口最终调用的后端。
  5. 带标志位的扩展 (preadv2, pwritev2):进一步扩展了向量化I/O,增加了一个flags参数,允许传递一些修饰符,如RWF_HIPRI(高优先级I/O)。

目前该技术的社区活跃度和主流应用情况如何?

fs/read_write.c是内核中最稳定、最核心、被调用最频繁的文件之一。

  • 绝对核心:任何一个进行文件I/O的用户空间程序,最终都会通过这个文件中的代码进入内核的文件系统层。
  • 社区状态:其核心逻辑非常稳定。社区的活跃度主要体现在:
    • 为支持新的VFS特性(如近期的Folio转换)而进行的适配性修改。
    • 与新的I/O接口(如io_uring)进行更深度的集成和优化。
    • 修复在极端边界条件下发现的bug。

核心原理与设计

它的核心工作原理是什么?

fs/read_write.c的本质是一个VFS层的分派器(Dispatcher)。它本身不直接操作磁盘或任何硬件,而是作为一个通用的前端,将I/O请求路由给正确的后端实现。

一个典型的read()系统调用的内核路径如下:

  1. 系统调用入口:用户空间调用read(fd, buf, count),触发一个软件中断,进入内核的系统调用处理函数(如sys_read)。
  2. 获取struct file:内核使用文件描述符fd,在当前进程的文件描述符表中查找,获取到一个struct file对象。这个对象代表一个打开的文件实例。
  3. 核心分派sys_read会调用fs/read_write.c中的核心函数,如vfs_read()vfs_read()会执行一些通用的检查(如文件是否可读)。
  4. 多态调用 (Polymorphism):最关键的一步发生了。vfs_read()会查找file->f_op,这是一个指向struct file_operations的指针。这个结构体包含了一系列函数指针(.read, .write, .read_iter等)。vfs_read会调用file->f_op->readfile->f_op->read_iter
    • 如果打开的是一个ext4文件,那么f_op指向的是ext4_file_operations,实际被调用的将是ext4_file_read_iter()
    • 如果打开的是一个管道,f_op指向pipe_fops,实际调用的是pipe_read()
    • 如果打开的是一个字符设备(如/dev/tty),调用的将是该设备驱动提供的读函数。
  5. 后端处理:具体的文件系统或驱动程序(如ext4)在它的读函数中,会与页面缓存(Page Cache)进行交互,如果数据不在缓存中,则会发起块I/O请求从磁盘读取数据,最后通过copy_to_user()将数据从内核空间拷贝到用户空间的buf中。

它的主要优势体現在哪些方面?

  • 极致的抽象:这是VFS设计的精髓。它将“读一个文件”的通用概念与具体如何从ext4、XFS或NFS中读取数据的实现细节完全分离开来。
  • 可扩展性:添加一个新的文件系统或设备类型,只需实现自己的file_operations结构并注册即可,无需修改fs/read_write.c中的任何代码。
  • 集中化:通用的逻辑,如文件偏移量管理、权限检查、I/O计数等,都集中在VFS层处理,避免了代码重复。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

这些劣势是传统同步I/O模型固有的,fs/read_write.c是其实现者:

  • 系统调用开销:每一次read()write()都是一次上下文切换,对于需要进行大量小I/O的应用来说,这个开销会成为性能瓶颈。
  • 数据拷贝开销:在缓冲I/O模式下,数据需要在内核的页面缓存和用户空间的缓冲区之间进行一次拷贝,这会消耗CPU和内存带宽。
  • 阻塞模型:默认情况下,readwrite是阻塞的。如果磁盘繁忙,调用进程将被挂起,这对于需要高并发和低延迟的服务器应用来说是致命的。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是所有通用目的的文件I/O的首选且唯一的标准解决方案。

  • Shell工具cat, grep, cp, mv, dd等所有基础命令行工具都使用read/write
  • 编译器和链接器:读取源代码文件,写入目标文件和可执行文件。
  • Web服务器:读取静态文件(HTML, CSS, JS)并将其写入socket。
  • 文本编辑器:读取文件到内存缓冲区,并将用户的修改写回文件。

是否有不推荐使用该技术的场景?为什么?

更准确地说,是在哪些高性能场景下,有比直接、同步地调用read/write更好的替代方案:

  • 高性能网络服务器/数据库 (I/O多路复用):虽然底层仍然是read/write,但上层会使用epoll等机制来管理成千上万的非阻塞文件描述符,而不是为每个连接都创建一个阻塞的线程。
  • 内存映射I/O (mmap):对于需要对文件内容进行频繁、随机访问的场景(如数据库索引、共享库加载),mmap是更好的选择。它将文件直接映射到进程的虚拟地址空间,应用可以像访问内存一样访问文件,避免了read/write的系统调用和数据拷贝开销。
  • 极致I/O性能 (io_uring):对于需要榨干硬件性能的I/O密集型应用(如高性能数据库、存储中间件),io_uring是现代Linux的首选。它通过一个共享的环形缓冲区,实现了真正的、批处理的、极低开销的异步I/O,从根本上解决了read/write的性能瓶颈。

对比分析

请将其 与 其他相似技术 进行详细对比。

read/write (缓冲I/O) vs. mmap

特性 read/write mmap
编程模型 流式/系统调用模型。显式地从文件读取/写入数据流。 内存模型。将文件当作一块内存来直接读写指针。
数据流 Disk <-> Page Cache <-> App Buffer (涉及数据拷贝) Disk <-> Page Cache <-> App Address Space (无显式拷贝)
I/O触发 显式调用read/write 隐式,通过缺页中断(Page Fault)触发。
适用场景 顺序、流式读写大文件。 随机访问、需要多进程共享文件内容、文件作为数据结构(如数据库索引)。
复杂度 接口简单直观。 更复杂,需要处理内存同步(msync)、信号(SIGBUS)等。

read/write vs. io_uring

特性 read/write io_uring
工作模式 同步阻塞 (默认) 或简单的非阻塞。 真异步、批处理
系统调用开销 每次操作一次 极低。一次io_uring_enter可以提交/完成任意数量的操作。
性能 适用于通用场景。 极致性能,专为高IOPS、低延迟设计。
功能 只支持读写等少数操作。 通用异步接口,支持文件I/O、网络I/O、定时器、fsync等几乎所有系统调用。
编程复杂度 简单。 相对复杂,需要管理提交队列(SQ)和完成队列(CQ)。

VFS Write Path: 核心内核写入实现

本代码片段是Linux内核中执行文件写入操作的核心路径。它定义了三个层次的函数:kernel_write(顶层API)、__kernel_write(内部实现)和__kernel_write_iter(现代迭代器接口)。其核心功能是提供一个统一的、健壮的、与具体文件系统无关的接口,供内核的其他部分(如xwrite函数)向文件写入数据。这是所有文件系统写入操作(无论是写入ramfsext4还是NFS)都必须经过的VFS抽象层。

实现原理分析

该代码的实现体现了VFS的层次化和抽象化设计,将写入操作分解为多个阶段:

  1. 顶层封装 (kernel_write): 这是最外层的、也是最“安全”的函数,供内核模块通用调用。

    • 验证 (rw_verify_area): 在进行任何写入之前,它首先调用rw_verify_area。这是一个关键的安全和验证步骤,它会执行多项检查,包括:
      a. 权限检查:通过安全模块(LSM,如SELinux)检查当前进程是否有权写入该文件。
      b. 文件大小限制:检查写入操作是否会超过进程的RLIMIT_FSIZE限制。
    • 同步/锁 (file_start_write/file_end_write): 它使用这对函数来包裹实际的写入操作。这对函数实现了与文件系统冻结(freezing)机制的同步。file_start_write会获取一个读锁,防止在写入过程中文件系统被冻结(例如为了创建快照)。file_end_write则释放该锁。这保证了文件系统操作的原子性和一致性。
  2. 数据结构转换 (__kernel_write): 这一层是一个便利的包装器。

    • 它的主要作用是将传统的、基于连续缓冲区(const void *buf, size_t count)的写入请求,转换为现代内核I/O所使用的iov_iter结构。
    • iov_iter (I/O Vector Iterator) 是一个强大的抽象,它可以表示各种来源的数据(内核缓冲区、用户空间缓冲区、BIO向量等),而无需修改下层函数的接口。
    • 它还将写入大小限制在MAX_RW_COUNT之内,这是一个重要的安全措施,防止了整数溢出和向下层驱动传递过大的请求。
  3. 核心分发 (__kernel_write_iter): 这是实际的VFS分发中心。

    • 检查: 它执行基本的健全性检查,如文件是否以写入模式打开(FMODE_WRITE)。它还有一个重要的检查,确保文件系统驱动没有同时定义write_iter和旧的write操作,以避免语义混乱。现代驱动应优先实现write_iter
    • kiocb (Kernel I/O Control Block): 它初始化一个kiocb结构。这个结构封装了I/O操作的所有上下文,如文件指针、位置等,是异步I/O的基础,在同步操作中也用于传递上下文。
    • 核心分发: 最关键的一行是 ret = file->f_op->write_iter(&kiocb, from);。它通过文件对象中的f_op指针,调用具体文件系统驱动(如ramfs, ext4等)所实现的write_iter函数,将控制权和数据传递给文件系统。
    • 收尾工作: 写入成功后,它负责更新文件位置指针*pos,通过fsnotify_modify通知inotify等子系统文件已被修改,并更新进程的I/O统计信息。

代码分析

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
/* 调用者负责处理 file_start_write/file_end_write */
// __kernel_write_iter: 核心的、基于iov_iter的写入分发函数。
ssize_t __kernel_write_iter(struct file *file, struct iov_iter *from, loff_t *pos)
{
struct kiocb kiocb;
ssize_t ret;

// 检查文件是否以写入模式打开,如果不是,这是一个内核bug。
if (WARN_ON_ONCE(!(file->f_mode & FMODE_WRITE)))
return -EBADF;
// 检查文件是否可写。
if (!(file->f_mode & FMODE_CAN_WRITE))
return -EINVAL;
/*
* 如果驱动同时定义了新的write_iter和旧的write,会产生混乱,因此也报错。
* 现代驱动应该只实现 write_iter。
*/
if (unlikely(!file->f_op->write_iter || file->f_op->write))
return warn_unsupported(file, "write");

// 初始化一个用于同步I/O的kiocb。
init_sync_kiocb(&kiocb, file);
// 设置写入位置。
kiocb.ki_pos = pos ? *pos : 0;
// 核心:通过函数指针调用底层文件系统驱动的write_iter方法。
ret = file->f_op->write_iter(&kiocb, from);
if (ret > 0) { // 如果写入成功
if (pos)
// 更新调用者的文件位置指针。
*pos = kiocb.ki_pos;
// 发送文件被修改的通知 (例如给inotify)。
fsnotify_modify(file);
// 增加当前进程的I/O统计(写入字符数)。
add_wchar(current, ret);
}
// 增加当前进程的I/O统计(写系统调用次数)。
inc_syscw(current);
return ret;
}

/* 调用者负责处理 file_start_write/file_end_write */
// __kernel_write: 将传统的 (buffer, count) 接口转换为 iov_iter 接口的包装器。
ssize_t __kernel_write(struct file *file, const void *buf, size_t count, loff_t *pos)
{
// 定义一个内核向量(kvec),指向输入的缓冲区。
struct kvec iov = {
.iov_base = (void *)buf,
// 写入大小不能超过MAX_RW_COUNT的限制。
.iov_len = min_t(size_t, count, MAX_RW_COUNT),
};
struct iov_iter iter;
// 从kvec初始化一个iov_iter,方向为ITER_SOURCE(写操作的数据源)。
iov_iter_kvec(&iter, ITER_SOURCE, &iov, 1, iov.iov_len);
// 调用核心的iter函数。
return __kernel_write_iter(file, &iter, pos);
}
// ... (关于为autofs特殊导出的注释) ...
EXPORT_SYMBOL_GPL(__kernel_write);

// kernel_write: 供内核模块使用的顶层、安全的写入API。
ssize_t kernel_write(struct file *file, const void *buf, size_t count,
loff_t *pos)
{
ssize_t ret;

// 步骤1: 验证区域,进行安全检查和文件大小限制检查。
ret = rw_verify_area(WRITE, file, pos, count);
if (ret)
return ret;

// 步骤2: 获取写锁,防止文件系统被冻结。
file_start_write(file);
// 步骤3: 调用内部实现函数执行写入。
ret = __kernel_write(file, buf, count, pos);
// 步骤4: 释放写锁。
file_end_write(file);
return ret;
}
EXPORT_SYMBOL(kernel_write);

include/linux/fs.h

VFS写操作冻结保护:确保文件系统快照的一致性

本代码片段定义了一组VFS核心层的内联函数,它们共同构成了一个关键的同步机制:文件系统写操作冻结保护。其核心功能是允许内核中的代码(例如文件系统驱动、VFS层自身)在尝试向一个文件系统写入数据或元数据之前,获取一个共享的“写访问”锁。这个机制的主要目的是与**文件系统冻gil结(freeze)**操作进行互斥。当需要创建文件系统快照(snapshot)或进行其他需要磁盘状态绝对一致的操作时,一个“冻结者”可以请求冻结文件系统,它会等待所有已开始的写操作完成,并阻止任何新的写操作开始。这组函数就是写操作方用来遵循这一协议的接口。

实现原理分析

该机制的实现非常精巧,它使用了一个高性能的**每CPU读写信号量(per-cpu reader-writer semaphore)**来避免在多核系统中的锁争用。

  1. 模型: 该机制可以被理解为一个“多读者 vs. 单写者”的模型,但这里的角色与通常的读写锁有所不同:
    • 文件系统写入者 (Writers): 扮演**读者(Readers)**的角色。许多进程可以同时对文件系统进行写操作,因此它们需要一个可以被共享的锁。
    • 文件系统冻结者 (Freezer): 扮演**独占写入者(Exclusive Writer)**的角色。在任何时刻,只能有一个冻结者,并且当冻结者持有锁时,任何文件系统写入者都不能进入。
  2. 核心实现 (__sb_start_write, __sb_end_write):
    • __sb_start_write调用了percpu_down_read_freezable。这是整个机制的核心。
      • percpu_: 表示这个锁的计数器在每个CPU上都有一个本地副本。当一个任务想获取写访问权时,它只在它当前运行的CPU上对计数器进行操作。这极大地减少了多核之间的缓存同步开销,性能非常高。
      • down_read: 这是获取一个“读”锁(共享锁)的术语。
      • _freezable: 这是此函数的特殊之处。在尝试获取读锁之前,它会检查当前是否有一个“冻结”请求正在等待。如果有,当前任务将会进入睡眠状态,直到文件系统被“解冻(thaw)”。
    • __sb_end_write调用percpu_up_read,它简单地释放在当前CPU上持有的读锁。
  3. 抽象层 (sb_*_write, file_*_write):
    • sb_*_write函数是对核心实现的简单封装,提供了面向超级块(struct super_block)的API。
    • file_*_write函数则提供了更高一层的、面向文件对象(struct file)的API。它们增加了一个重要的检查:!S_ISREG(file_inode(file)->i_mode)。这意味着这个冻结保护机制只对普通文件(regular files)生效。对管道、套接字或字符设备等非普通文件的写操作不会触发此锁,因为它们不影响底层块设备上文件系统的一致性状态。

代码分析

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
/**
* file_start_write - 为普通文件I/O获取超级块的写访问权。
* @file: 我们要写入的文件。
*/
static inline void file_start_write(struct file *file)
{
// 关键检查:只对普通文件(regular file)应用冻结保护。
if (!S_ISREG(file_inode(file)->i_mode))
return;
// 调用下一层的sb_start_write。
sb_start_write(file_inode(file)->i_sb);
}

// file_start_write_trylock - file_start_write的非阻塞版本。
static inline bool file_start_write_trylock(struct file *file)
{
if (!S_ISREG(file_inode(file)->i_mode))
return true;
return sb_start_write_trylock(file_inode(file)->i_sb);
}

/**
* file_end_write - 释放对普通文件超级块的写访问权。
* @file: 我们写入过的文件。
*/
static inline void file_end_write(struct file *file)
{
if (!S_ISREG(file_inode(file)->i_mode))
return;
sb_end_write(file_inode(file)->i_sb);
}

/**
* sb_start_write - 获取对一个超级块的写访问权。
* @sb: 我们要写入的超级块。
* 描述:
* 当一个进程要向文件系统写入数据或元数据时,它应该将操作包裹在
* sb_start_write() - sb_end_write()对中,以实现与文件系统冻结的互斥。
* 此函数增加写入者计数。如果文件系统已被冻结,此函数会等待直到解冻。
*/
static inline void sb_start_write(struct super_block *sb)
{
__sb_start_write(sb, SB_FREEZE_WRITE);
}

/**
* sb_end_write - 释放对一个超级块的写访问权。
* @sb: 我们写入过的超级块。
*/
static inline void sb_end_write(struct super_block *sb)
{
__sb_end_write(sb, SB_FREEZE_WRITE);
}

/*
* 这些是内部函数,请使用sb_start_{write,pagefault,intwrite}等上层API。
*/
// __sb_end_write - 释放锁的核心实现。
static inline void __sb_end_write(struct super_block *sb, int level)
{
// 释放当前CPU上的读锁。
percpu_up_read(sb->s_writers.rw_sem + level-1);
}

// __sb_start_write - 获取锁的核心实现。
static inline void __sb_start_write(struct super_block *sb, int level)
{
// 尝试获取当前CPU上的读锁。如果文件系统正在被冻结,此函数会使当前任务睡眠。
percpu_down_read_freezable(sb->s_writers.rw_sem + level - 1, true);
}