[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
的发展体现在对这个基础模型的不断增强和优化上,以适应新的需求和性能挑战:
- 基础读写 (
read
,write
):最初的实现,通过文件描述符、缓冲区指针和长度来进行操作,并会更新文件的当前偏移量。 - 定位读写 (
pread
,pwrite
):为了满足多线程应用程序的需求,引入了pread
和pwrite
。它们允许在一个原子操作中指定读写的偏移量,并且不会修改文件描述符自身的当前偏移量。这避免了在多线程中lseek()
和read()
/write()
之间产生的竞争条件。 - 向量化I/O (
readv
,writev
):为了提高效率,引入了向量化或“分散-聚集”(Scatter-Gather)I/O。readv
可以将数据从文件读入到内存中多个不连续的缓冲区,而writev
可以将多个不连续缓冲区的数据一次性写入文件。这通过单次系统调用完成了多次读写才能完成的工作,极大地减少了内核态和用户态之间切换的开销。 - 现代异步I/O的接入点:随着Linux AIO(Asynchronous I/O)和最新的
io_uring
的发展,fs/read_write.c
中的核心逻辑(如vfs_read
,vfs_write
)也成为了这些现代高性能I/O接口最终调用的后端。 - 带标志位的扩展 (
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()
系统调用的内核路径如下:
- 系统调用入口:用户空间调用
read(fd, buf, count)
,触发一个软件中断,进入内核的系统调用处理函数(如sys_read
)。 - 获取
struct file
:内核使用文件描述符fd
,在当前进程的文件描述符表中查找,获取到一个struct file
对象。这个对象代表一个打开的文件实例。 - 核心分派:
sys_read
会调用fs/read_write.c
中的核心函数,如vfs_read()
。vfs_read()
会执行一些通用的检查(如文件是否可读)。 - 多态调用 (Polymorphism):最关键的一步发生了。
vfs_read()
会查找file->f_op
,这是一个指向struct file_operations
的指针。这个结构体包含了一系列函数指针(.read
,.write
,.read_iter
等)。vfs_read
会调用file->f_op->read
或file->f_op->read_iter
。- 如果打开的是一个ext4文件,那么
f_op
指向的是ext4_file_operations
,实际被调用的将是ext4_file_read_iter()
。 - 如果打开的是一个管道,
f_op
指向pipe_fops
,实际调用的是pipe_read()
。 - 如果打开的是一个字符设备(如
/dev/tty
),调用的将是该设备驱动提供的读函数。
- 如果打开的是一个ext4文件,那么
- 后端处理:具体的文件系统或驱动程序(如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和内存带宽。
- 阻塞模型:默认情况下,
read
和write
是阻塞的。如果磁盘繁忙,调用进程将被挂起,这对于需要高并发和低延迟的服务器应用来说是致命的。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是所有通用目的的文件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
函数)向文件写入数据。这是所有文件系统写入操作(无论是写入ramfs
、ext4
还是NFS)都必须经过的VFS抽象层。
实现原理分析
该代码的实现体现了VFS的层次化和抽象化设计,将写入操作分解为多个阶段:
顶层封装 (
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
则释放该锁。这保证了文件系统操作的原子性和一致性。
- 验证 (
数据结构转换 (
__kernel_write
): 这一层是一个便利的包装器。- 它的主要作用是将传统的、基于连续缓冲区(
const void *buf
,size_t count
)的写入请求,转换为现代内核I/O所使用的iov_iter
结构。 iov_iter
(I/O Vector Iterator) 是一个强大的抽象,它可以表示各种来源的数据(内核缓冲区、用户空间缓冲区、BIO向量等),而无需修改下层函数的接口。- 它还将写入大小限制在
MAX_RW_COUNT
之内,这是一个重要的安全措施,防止了整数溢出和向下层驱动传递过大的请求。
- 它的主要作用是将传统的、基于连续缓冲区(
核心分发 (
__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 | /* 调用者负责处理 file_start_write/file_end_write */ |
include/linux/fs.h
VFS写操作冻结保护:确保文件系统快照的一致性
本代码片段定义了一组VFS核心层的内联函数,它们共同构成了一个关键的同步机制:文件系统写操作冻结保护。其核心功能是允许内核中的代码(例如文件系统驱动、VFS层自身)在尝试向一个文件系统写入数据或元数据之前,获取一个共享的“写访问”锁。这个机制的主要目的是与**文件系统冻gil结(freeze)**操作进行互斥。当需要创建文件系统快照(snapshot)或进行其他需要磁盘状态绝对一致的操作时,一个“冻结者”可以请求冻结文件系统,它会等待所有已开始的写操作完成,并阻止任何新的写操作开始。这组函数就是写操作方用来遵循这一协议的接口。
实现原理分析
该机制的实现非常精巧,它使用了一个高性能的**每CPU读写信号量(per-cpu reader-writer semaphore)**来避免在多核系统中的锁争用。
- 模型: 该机制可以被理解为一个“多读者 vs. 单写者”的模型,但这里的角色与通常的读写锁有所不同:
- 文件系统写入者 (Writers): 扮演**读者(Readers)**的角色。许多进程可以同时对文件系统进行写操作,因此它们需要一个可以被共享的锁。
- 文件系统冻结者 (Freezer): 扮演**独占写入者(Exclusive Writer)**的角色。在任何时刻,只能有一个冻结者,并且当冻结者持有锁时,任何文件系统写入者都不能进入。
- 核心实现 (
__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上持有的读锁。
- 抽象层 (
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 | /** |