read()操作的工作流程
读文件操作 (read
) 的完整工作流程
假设一个用户程序已经通过open()
函数成功打开了一个文件,并获得了对应的文件描述符 fd
。现在,程序调用 read(fd, buf, n)
,意图从文件中读取 n
个字节到用户指定的缓冲区 buf
中。
第一步:用户程序发起调用
- 用户进程在用户态下,调用C库函数
read()
。这个库函数会打包好参数(文件描述符fd
、目标缓冲区地址buf
、要读取的字节数n
),准备请求操作系统服务。
第二步:系统调用与上下文切换
-
库函数通过一条特殊的陷入指令 (trap),将CPU的执行状态从用户态切换到内核态。
-
操作系统的系统调用处理程序接管控制权,根据传入的系统调用号,找到并开始执行内核中对应的
sys_read()
函数。
第三步:内核中定位文件和读取位置
-
内核首先需要知道“读哪里”。它会执行以下查找:
-
使用
fd
作为索引,在当前进程的**“进程级打开文件表”**中找到对应的表项。 -
通过该表项中的指针,找到在**“系统级打开文件表”**中的对应表项。
-
这个系统级表项非常关键,它包含了文件的当前读写位置(文件偏移量 a.k.a. file offset or pointer)。同时,它还包含一个指向该文件在内存中的**i-node(或v-node)**的指针。
-
第四步:检查内核I/O缓冲区(页高速缓存)—— 关键性能点
-
这是整个流程的核心决策点。操作系统不会立即去访问磁盘。
-
操作系统会根据上一步获取的**“文件偏移量”和要读取的字节数
n
,计算出需要读取的数据位于文件的哪一个或哪几个逻辑块 (logical block)** 中。 -
然后,它会去检查内核的 I/O缓冲区(也称页高速缓存,Page Cache),看看这些逻辑块是否因为之前的读写操作而已经存在于内存中。
-
情况A:缓存命中 (Cache Hit)
-
如果所有需要的数据块都已经在内核缓冲区中,这是最理想的情况。
-
系统将直接跳过第五步(物理I/O),避免了与慢速磁盘的任何交互。
-
-
情况B:缓存未命中 (Cache Miss)
- 如果需要的数据块(或其中一部分)不在缓冲区中,那么操作系统必须启动一次物理I/O操作,从磁盘上将数据加载进来。
-
第五步:启动物理I/O(仅在缓存未命中时发生)
-
地址转换: 操作系统通过查询内存中的文件i-node,将文件的逻辑块号转换为实际的物理磁盘块地址。
-
发出I/O请求: 内核向磁盘驱动程序发出一个读请求,指明要从哪个磁盘的哪个物理地址开始,读取多少个数据块。
-
DMA传输: 现代操作系统普遍使用DMA (Direct Memory Access) 方式进行数据传输。
-
CPU向DMA控制器下达指令,告诉它“把磁盘的X地址的数据,传送到内存的Y地址(即内核I/O缓冲区的地址)”。
-
之后,CPU就可以被释放去执行其他进程,无需再关心这个耗时的数据传输过程。
-
DMA控制器会全权负责将数据从磁盘搬运到内存的内核缓冲区。
-
-
I/O完成中断: 当DMA传输完成后,磁盘控制器会向CPU发送一个中断信号,通知操作系统“数据已经准备好了”。
第六步:数据从内核缓冲区复制到用户缓冲区
-
无论是缓存命中直接得到数据,还是缓存未命中后通过DMA从磁盘加载了数据,此时,要读取的数据都已经位于内核的I/O缓冲区中。
-
内核将从内核缓冲区中,根据用户请求的字节数
n
,将数据复制到用户程序在第一步调用时指定的buf
缓冲区中。
第七步:更新状态并返回
-
更新文件偏移量: 内核会更新系统级打开文件表中的文件偏移量,将其增加实际读取到的字节数,以便下一次
read
调用能从正确的位置开始。 -
更新访问时间: 可能会更新i-node中的“最后访问时间”。
-
返回用户态: 内核完成所有工作后,执行上下文切换,将CPU控制权交还给用户进程,并从内核态切换回用户态。
-
read()
函数返回,其返回值是实际读取到的字节数(可能小于请求的n
,例如读到了文件末尾)。