概览

当我们说一个应用程序从磁盘上读取文件时,通常分两步走:

操作系统(内核)先从磁盘上读取数据存到内核空间,再把数据从内核空间拷贝到用户空间。此后,用户应用程序才可以操作此数据。

所以,在这个过程中有两次数据读取操作:

  1. 第一步:从磁盘上读取
  2. 第二步:从内存中读取

缓冲的作用

提前读:每次读入当前块时,下一块也读入缓冲区,减少 I/O 次数 延迟写:磁盘块淘汰时,先存入缓冲区队列,避免每次修改磁盘块都写入磁盘

  1. 缓和 CPU 与 I/O 设备之间速度不匹配的矛盾
  2. 减少对 CPU 的中断频率,放宽对 CPU 中断响应时间的限制
  3. 解决基本数据单元大小(数据粒度)不匹配的问题
  4. 提高 CPU 和 I/O 设备之间的并行性

用户进程缓存区 Stdio Buffer

当一个用户进程要通过系统调用从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。

用户缓冲区处理的是用户空间和内核空间的数据传递,目的是减少系统调用的次数

调用read()申请的buffer就是用户缓冲区

每次调用read()的过程如下:

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回

内核缓存区 Buffer Cache

read是把数据从内核缓冲区复制到进程缓冲区。write是把进程缓冲区复制到内核缓冲区。

当然,write并不一定导致内核的写动作(不等价于写入磁盘),比如os可能会把内核缓冲区的数据积累到一定量后,再一次写入。这也就是为什么断电有时会导致数据丢失。

内核缓冲区处理的是内核空间和磁盘之间的数据传递,目的是减少访问磁盘的次数。

缓冲区的实现方式

单缓冲

在单缓冲区中,T 和 C 是可以并行的

当 T>C 时,处理器处理完还要等缓冲区装满数据才能将下一块数据从缓冲区传送到工作区,平均处理一块数据的时间为 T+M

当 T<C 时,缓冲区装满后,工作区还没完毕,要等 C 结束后才能将新数据块从缓冲区传送到工作区,平均处理一块数据的时间为 C+M

总结,单缓冲区处理每块数据的平均时间为 Max (C, T)+M

双缓冲

在双缓冲区中,C 和 M 可以与 T 并行

当 T>C+M 时,输入时间太长了,工作区处理完后,缓冲区还没满,所以平均处理时间只看 T

当 T<C+M 时,输入速度大于输出并处理的速度,所以两个缓冲区最终会满,不用担心缓冲区会空,所以平均处理时间只看传送并处理的时间 C+M

总结:双缓冲区处理每块数据的平均时间为 Max (C+M, T)

循环缓冲

循环缓冲将多个大小相等的缓冲区链接成一个循环队列

循环缓冲中设置 in 和 out 两个指针,in 指向第一个可以输入数据的空缓冲区,out 指向第一个可以提取数据的满缓冲区。

缓冲池

缓冲池由多个系统公用的缓冲区组成,适用于多个并发进程,缓冲区按其使用状况可以分为:

  1. 空缓冲队列
  2. 输入队列(由装满输入数据的缓冲区链接而成)
  3. 输出队列(由装满输出数据的缓冲区链接而成)

缓冲池中的缓冲区有以下 4 种工作方式:

  1. 收容输入,从空缓冲队列获取一个空缓冲区,输入数据后放到输入队列队尾
  2. 提取输入,从输入队列队首获取一个缓冲区,提取数据,用完后将空缓冲区挂到空缓冲队列的队尾
  3. 收容输出,从空缓冲队列获取一个空缓冲区,装满输出数据后放到输入队列队尾
  4. 提取输出,从输出队列队首获取一个缓冲区,提取数据,用完后将空缓冲区挂到空缓冲队列的队尾

方向指向缓冲池的操作是收容操作。 方向从缓冲池指向外部的是提取操作。