You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
上面书中有一个对比,复制 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 为解决特定的问题实现了它。我们用户在编写程序解决问题的时候,也可以将这种思想作为一种选择。
The text was updated successfully, but these errors were encountered:
当我们说一个程序读写磁盘上的文件时,通常指的是把磁盘设备上的数据块存储到用户空间内存中(或把用户空间内存的数据存储到磁盘设备上)。
然而,程序与硬件的交互是交由操作系统内核来处理的,这样做的好处是,一方面可以为应用程序提供简单统一的接口,降低用户与硬件交互的复杂度;另一方面也提高了与硬件交互的安全性。
因此,上图的模型在用户空间与磁盘之间多了一层:内核空间。
于是,一个读文件操作变成了:
操作系统(内核)先从磁盘读取数据到内核空间的内存(read①),再把数据从内核空间内存拷贝到用户空间内存(read②)。此后,用户应用程序才可以操作此数据。
在这个过程中有两次数据读取操作:
read① 从磁盘上读取;
read② 从内存中读取。
我们知道,访问磁盘的速度要远远低于访问内存的速度,这是不同存储介质的物理特性和访问方式决定的,两者是毫秒和纳秒的区别,所以理论上 read① 的速度要远远慢于 read②。那么整个文件读取过程的时间瓶颈就出现在了对磁盘的读取上。要解决这个问题,就用上了 缓冲(buffer)。
什么是缓冲
由于中文翻译的问题,我们有时候会把字面上相似,但实际差别较大的两个东西混为一谈,比如缓冲和缓存,再比如伪类和伪元素。
缓冲(buffer),简单来说是为了解决速度不均匀的问题,而在生产和消费者之间设立的一个缓和区、平衡区。
比如我们经常在看在线视频时,开始会提示一小段时间的「缓冲」,这是因为视频播放要求均匀、持续的数据,而网络传输是时快时慢的,此时缓冲的作用就是等待生产者(网络)积累一定的数据后才给消费者去消费。这是生产者速度追不上消费者的情况。
再比如汽车安全气囊,当汽车速度骤降时,人体会受到方向盘冲击,而安全气囊就是一个对人体向前速度的缓冲,以减少身体接触方向盘时的速度。这是生产者速度大于消费者的情况。
由于我们将要关注的「内核缓冲区告诉缓存」既是一种缓冲机制,又发挥缓存的作用,所以这里特别容易搞糊涂 T T
内核缓冲
回到刚才的读文件场景。因为内核从磁盘读取数据的速度太慢,跟不上程序从内存中读数据的速度,所以它也设立了一个缓冲区,用来中和两者的速度差异。这就是文件 I/O 的内核缓冲,原名叫 Kernel Buffer Cache,一般翻译成「缓冲区高速缓存」(又是缓冲又是缓存的……我们先按缓冲去理解它的作用)。
它本质上就是图二中内核空间的一块内存,是读取过程中绕不开的中转站,只不过为了读写效率做了特别的工作,所以起了个特殊的名字。
那么内核缓冲做了什么事情呢?
数据预读(read_ahead)
数据预读指的是,当程序发起 read() 系统调用时,内核会比请求更多地读取磁盘上的数据,保存在缓冲区,以备程序后续使用。这种数据的预取基于一种预设:程序会重复地访问最近访问过的数据,且这种访问往往是顺序访问(比如对文件从前到后的顺序访问)。
因此当我们向内核请求读取数据时,内核会先从自己的 buffer cache 去寻找,如果命中数据,则不需要进行真正的磁盘 I/O,直接从内存中返回数据;如果缓存未命中,则内核会从磁盘中读取请求的 page,并同时读取紧随其后的几个 page(比如三个),如果文件是顺序访问的,那么下一个读取请求就会命中之前预读的缓存。
当然,预读算法非常复杂,这里只是一个简化的逻辑。当内核判断文件并非顺序读取时,也可能会放弃预读。
因此,预读提供了以下好处:
回写
回写指的是,当程序发起 write() 系统调用时,内核并不会直接把数据写入到磁盘文件中,而仅仅是写入到缓冲区中,几秒后才会真正将数据刷新到磁盘中。对于系统调用来说,数据写入缓冲区后,就返回了,因此一个 read() / write() 并非真正执行 I/O 操作,它只代表数据在用户空间 / 内核空间传递的完成。
延迟往磁盘写入数据的一个最大的好处就是,可以合并更多的数据一次性写入磁盘。也就是上面说的,把小块的 I/O 变成大块 I/O,减少磁盘处理命令次数,提高提盘性能。
另一个好处是,当其它进程紧接着访问该文件时,内核可以从直接从缓冲区中提供更新的文件数据。
因此,Linux 内核以 buffer cache 为介质,通过预读和回写的机制,提高了文件 I/O 速度,和磁盘访问效率。
内核缓冲区有多大?
Linux 内核对 buffer cache 并没有设定固定的大小上限,内核会分配尽可能多的内存给 buffer cache,它只受到可用内存总量限制。当可用内存不足,内核会将脏页(修改过的缓存)回写入磁盘,预读算法也有相应的策略去回收不必要的缓存。
示例
我们通过一个简单程序的过程图来对内核缓冲做一个粗略的演示:
下图是一个复制程序,把文件 A 的内容拷贝到文件 B,过程简化了很多,只列出了关注点。
用户的缓冲:C 库 I/O 缓冲
由于缓冲可以将小块 I/O 合并成大块 I/O 操作这一特性,C 库的 I/O 函数(比如 stdio 库中的 fpringf()、fgets()、fputs()等)也提供了缓冲机制。
我们知道,这些库函数是用户程序和 read()、write() 这些 system call 之间的桥梁,它的 buffer 并不会对磁盘读写产生什么影响,而在于可以减少系统调用的开销。
系统调用的开销大吗?可以看下面一张图:
这是《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 为解决特定的问题实现了它。我们用户在编写程序解决问题的时候,也可以将这种思想作为一种选择。
The text was updated successfully, but these errors were encountered: