Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

文件 I/O 的内核缓冲 #2

Open
raxxarr opened this issue Sep 29, 2018 · 2 comments
Open

文件 I/O 的内核缓冲 #2

raxxarr opened this issue Sep 29, 2018 · 2 comments

Comments

@raxxarr
Copy link
Owner

raxxarr commented Sep 29, 2018

从最粗略的角度理解 Linux 文件 I/O 内核缓冲(buffer cache),啰嗦且不严谨。只为了直观理解。

当我们说一个程序读写磁盘上的文件时,通常指的是把磁盘设备上的数据块存储到用户空间内存中(或把用户空间内存的数据存储到磁盘设备上)。
read model without kernel
然而,程序与硬件的交互是交由操作系统内核来处理的,这样做的好处是,一方面可以为应用程序提供简单统一的接口,降低用户与硬件交互的复杂度;另一方面也提高了与硬件交互的安全性。
因此,上图的模型在用户空间与磁盘之间多了一层:内核空间

于是,一个读文件操作变成了:
read model with kernel

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

在这个过程中有两次数据读取操作:
read① 从磁盘上读取;
read② 从内存中读取。

我们知道,访问磁盘的速度要远远低于访问内存的速度,这是不同存储介质的物理特性和访问方式决定的,两者是毫秒和纳秒的区别,所以理论上 read① 的速度要远远慢于 read②。那么整个文件读取过程的时间瓶颈就出现在了对磁盘的读取上。要解决这个问题,就用上了 缓冲(buffer)

什么是缓冲

由于中文翻译的问题,我们有时候会把字面上相似,但实际差别较大的两个东西混为一谈,比如缓冲缓存,再比如伪类伪元素

缓冲(buffer),简单来说是为了解决速度不均匀的问题,而在生产和消费者之间设立的一个缓和区、平衡区。
比如我们经常在看在线视频时,开始会提示一小段时间的「缓冲」,这是因为视频播放要求均匀、持续的数据,而网络传输是时快时慢的,此时缓冲的作用就是等待生产者(网络)积累一定的数据后才给消费者去消费。这是生产者速度追不上消费者的情况。

video buffer

再比如汽车安全气囊,当汽车速度骤降时,人体会受到方向盘冲击,而安全气囊就是一个对人体向前速度的缓冲,以减少身体接触方向盘时的速度。这是生产者速度大于消费者的情况。

airbag buffer

由于我们将要关注的「内核缓冲区告诉缓存」既是一种缓冲机制,又发挥缓存的作用,所以这里特别容易搞糊涂 T T

内核缓冲

回到刚才的读文件场景。因为内核从磁盘读取数据的速度太慢,跟不上程序从内存中读数据的速度,所以它也设立了一个缓冲区,用来中和两者的速度差异。这就是文件 I/O 的内核缓冲,原名叫 Kernel Buffer Cache,一般翻译成「缓冲区高速缓存」(又是缓冲又是缓存的……我们先按缓冲去理解它的作用)。
它本质上就是图二中内核空间的一块内存,是读取过程中绕不开的中转站,只不过为了读写效率做了特别的工作,所以起了个特殊的名字。

那么内核缓冲做了什么事情呢?

  1. 读:数据预读
  2. 写:延时回写

数据预读(read_ahead)

数据预读指的是,当程序发起 read() 系统调用时,内核会比请求更多地读取磁盘上的数据,保存在缓冲区,以备程序后续使用。这种数据的预取基于一种预设:程序会重复地访问最近访问过的数据,且这种访问往往是顺序访问(比如对文件从前到后的顺序访问)。

因此当我们向内核请求读取数据时,内核会先从自己的 buffer cache 去寻找,如果命中数据,则不需要进行真正的磁盘 I/O,直接从内存中返回数据;如果缓存未命中,则内核会从磁盘中读取请求的 page,并同时读取紧随其后的几个 page(比如三个),如果文件是顺序访问的,那么下一个读取请求就会命中之前预读的缓存。
当然,预读算法非常复杂,这里只是一个简化的逻辑。当内核判断文件并非顺序读取时,也可能会放弃预读。

因此,预读提供了以下好处:

  1. 减少了 I/O 时间对进程的影响。 因为进程的读取操作和真正的 I/O 可能发生在不同的时空,数据是预取的,当进程需要它的时候早已经在内存中准备好了,对于这个进程来说,I/O 时间是不存在的,但是对于整个系统来说,I/O 时间是一个必要成本,因为总要从磁盘读数据,只是发生的时间早晚罢了;
  2. 提供了缓存。 当进程对文件重复访问时,buffer cache 提供了缓存,把本来应该发生的 I/O 省掉了,这个和第一点不同,是结结实实得省掉了一次 I/O 时间;
  3. 减少了磁盘处理器的命令数,因为每个命令多读了几个相邻扇区,或者说,把小块的 I/O 变成了大块的 I/O,提升了磁盘性能;

回写

回写指的是,当程序发起 write() 系统调用时,内核并不会直接把数据写入到磁盘文件中,而仅仅是写入到缓冲区中,几秒后才会真正将数据刷新到磁盘中。对于系统调用来说,数据写入缓冲区后,就返回了,因此一个 read() / write() 并非真正执行 I/O 操作,它只代表数据在用户空间 / 内核空间传递的完成。
延迟往磁盘写入数据的一个最大的好处就是,可以合并更多的数据一次性写入磁盘。也就是上面说的,把小块的 I/O 变成大块 I/O,减少磁盘处理命令次数,提高提盘性能。
另一个好处是,当其它进程紧接着访问该文件时,内核可以从直接从缓冲区中提供更新的文件数据。

因此,Linux 内核以 buffer cache 为介质,通过预读和回写的机制,提高了文件 I/O 速度,和磁盘访问效率。

内核缓冲区有多大?

Linux 内核对 buffer cache 并没有设定固定的大小上限,内核会分配尽可能多的内存给 buffer cache,它只受到可用内存总量限制。当可用内存不足,内核会将脏页(修改过的缓存)回写入磁盘,预读算法也有相应的策略去回收不必要的缓存。

示例

我们通过一个简单程序的过程图来对内核缓冲做一个粗略的演示:
下图是一个复制程序,把文件 A 的内容拷贝到文件 B,过程简化了很多,只列出了关注点。
buffer-cache-graph

用户的缓冲:C 库 I/O 缓冲

由于缓冲可以将小块 I/O 合并成大块 I/O 操作这一特性,C 库的 I/O 函数(比如 stdio 库中的 fpringf()、fgets()、fputs()等)也提供了缓冲机制。
我们知道,这些库函数是用户程序和 read()、write() 这些 system call 之间的桥梁,它的 buffer 并不会对磁盘读写产生什么影响,而在于可以减少系统调用的开销。

系统调用的开销大吗?可以看下面一张图:
sys_call
这是《The Linux Programming Interface》展示的一个系统调用 execve() 的执行过程,它还是需要蛮多的步骤,内核必须捕获调用、检查调用参数的有效性、在内核态和用户态之间传输数据。

虽然单个系统调用开销看起来并不巨大,但是试想一个场景:向磁盘写入 1000 字节,是写入 1000 次,每次写入 1 字节呢,还是一次性写入 1000 字节比较好呢。这两者的一个最大差别就是前者需要调用 write() 系统调用 1000 次,而后者只需要调用一次。那么累积起来,系统调用的消耗也是可观的。

上面书中有一个对比,复制 100MB 的数据,设立一个缓冲区,缓冲区大小为 1 字节的用时为 107.43 秒(total cpu 107.32 / user cpu 8.20 / sys cpu 99.12),而当缓冲区大小为 4096 字节时,这个成绩是 2.05 秒(total cpu 0.38 / user cpu 0.01 / sys cpu 0.38)(缓冲区再大性能提升就不显著了,因为 anyway 读写 I/O 的时间和用户空间、内核空间拷贝数据的时间开销是大头,少量的系统调用的开销对比起来就微乎其微了)。

因此,在用户这边,对 I/O 操作也设置一个缓冲区以减少系统调用,也是一个不错的选择。

当然,缓冲是一种策略,一种解决问题的方式,只是 Linux kernel、glibc 为解决特定的问题实现了它。我们用户在编写程序解决问题的时候,也可以将这种思想作为一种选择。

@raxxarr
Copy link
Owner Author

raxxarr commented Dec 23, 2019

修正措辞:「缓冲是一种机制」
因为 Raymond 在 「The Art of UNIX Programming」中说:提供机制而不是策略,因为策略是频繁变化的,而机制长存。

@robinx2012
Copy link

感谢分享,有一个疑惑请教下:
磁盘控制器的驱动程序也是可以分配内存的,那么DMA是将磁盘数据直接从磁盘拷贝到驱动分配的内存还是内核缓冲区的内存呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants