《Linux/UNIX系统编程手册(全2册)》第13章文件I/O缓冲,本章描述了这两种类型的缓冲,并讨论了其对应用程序性能的影响。本章还讨论了可以屏蔽或影响缓冲的各种技术,以及直接I/O技术--在某些需要绕过内核缓冲的场景中非常有用。本节为大家介绍文件I/O的内核缓冲:缓冲区高速缓存。
第13章 文件I/O缓冲
出于速度和效率考虑,系统I/O调用(即内核)和标准C语言库I/O函数(即stdio函数)在操作磁盘文件时会对数据进行缓冲。本章描述了这两种类型的缓冲,并讨论了其对应用程序性能的影响。本章还讨论了可以屏蔽或影响缓冲的各种技术,以及直接I/O技术--在某些需要绕过内核缓冲的场景中非常有用。
13.1 文件I/O的内核缓冲:缓冲区高速缓存(1)
read()和write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存(kernel buffer cache)之间复制数据。例如,如下调用将3个字节的数据从用户空间内存传递到内核空间的缓冲区中:
write()随即返回。在后续某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘。(因此,可以说系统调用与磁盘操作并不同步。)如果在此期间,另一进程试图读取该文件的这几个字节,那么内核将自动从缓冲区高速缓存中提供这些数据,而不是从文件中(读取过期的内容)。
与此同理,对输入而言,内核从磁盘中读取数据并存储到内核缓冲区中。read()调用将从该缓冲区中读取数据,直至把缓冲区中的数据取完,这时,内核会将文件的下一段内容读入缓冲区高速缓存。(这里的描述有所简化。对于序列化的文件访问,内核通常会尝试执行预读,以确保在需要之前就将文件的下一数据块读入缓冲区高速缓存中。更多关于预读的内容请参考13.5节。)
采用这一设计,意在使read()和write()调用的操作更为快速,因为它们不需要等待(缓慢的)磁盘操作。同时,这一设计也极为高效,因为这减少了内核必须执行的磁盘传输次数。
Linux内核对缓冲区高速缓存的大小没有固定上限。内核会分配尽可能多的缓冲区高速缓存页,而仅受限于两个因素:可用的物理内存总量,以及出于其他目的对物理内存的需求(例如,需要将正在运行进程的文本和数据页保留在物理内存中)。若可用内存不足,则内核会将一些修改过的缓冲区高速缓存页内容刷新到磁盘,并释放其供系统重用。
更确切地说,从内核2.4开始,Linux 不再维护一个单独的缓冲区高速缓存。相反,会将文件I/O缓冲区置于页面高速缓存中,其中还含有诸如内存映射文件的页面。然而,正文的讨论采用了"缓冲区高速缓存(buffer cache)"这一术语,因为这是UNIX 实现中历史悠久的通称。
缓冲区大小对I/O系统调用性能的影响
无论是让磁盘写1000次,每次写入一个字节,还是一次写入1000个字节,内核访问磁盘的字节数都是相同的。然而,我们更属意于后者,因为它只需要一次系统调用,而前者则需要调用1000次。尽管比磁盘操作要快许多,但系统调用所耗费的时间总量也相当可观:内核必须捕获调用,检查系统调用参数的有效性,在用户空间和内核空间之间传输数据(详情参见3.1节)。
为BUF_SIZE(BUF_SIZE 指定了每次调用read()和write() 时所传输的字节数)设定不同的大小来运行程序清单4-1,可以观察到不同大小的缓冲区对执行文件I/O 所产生的影响。表13-1所示为在Linux ext2文件系统上复制大小为100MB的文件,该程序在使用不同BUF_SIZE 值时所需要的时间。有关本表中的信息,需要注意以下几点。
总用时和总CPU时间这两列含义很明显。而用户CPU和系统CPU两列是将总CPU用时分解为在用户模式下执行代码所需的时间和执行内核代码所需的时间(比如,系统调用)。
表中测试结果得自于2.6.30普通(vanilla)内核下,块大小为4096字节的ext2文件系统。
所谓普通内核(vanilla kernel),意指未打补丁的主线(mainline)内核。与之形成鲜明对比的是大多数发行商所提供的内核,常包含各种补丁来修复错误和添加新功能。
每行显示的结果为在给定缓冲区大小下运行20次的均值。在这些测试以及本章后续提及的其他测试里,在程序每次的执行间隔中,会卸载并再次重新装配文件系统,以确保文件系统的缓冲区高速缓存为空。计时则由shell命令time完成。
表13-1:复制100MB大小的文件所需时间
BUF_SIZE | 时间(秒) | |||
总用时 | 总CPU用时 | 用户CPU用时 | 系统CPU用时 | |
1 | 107.43 | 107.32 | 8.20 | 99.12 |
2 | 54.16 | 53.89 | 4.13 | 49.76 |
4 | 31.72 | 30.96 | 2.30 | 28.66 |
8 | 15.59 | 14.34 | 1.08 | 13.26 |
16 | 7.50 | 7.14 | 0.51 | 6.63 |
32 | 3.76 | 3.68 | 0.26 | 3.41 |
64 | 2.19 | 2.04 | 0.13 | 1.91 |
续表
BUF_SIZE | 时间(秒) | |||
总用时 | 总CPU用时 | 用户CPU用时 | 系统CPU用时 | |
128 | 2.16 | 1.59 | 0.11 | 1.48 |
256 | 2.06 | 1.75 | 0.10 | 1.65 |
512 | 2.06 | 1.03 | 0.05 | 0.98 |
1024 | 2.05 | 0.65 | 0.02 | 0.63 |
4096 | 2.05 | 0.38 | 0.01 | 0.38 |
16384 | 2.05 | 0.34 | 0.00 | 0.33 |
65536 | 2.06 | 0.32 | 0.00 | 0.32 |
因为采用不同的缓冲区大小时,数据的传输总量(因此招致磁盘操作的数量)是相同的,表13-1所示为发起read()和write()调用的开销。缓冲区大小为1字节时,需要调用read()和write()1亿次,缓冲区大小为4096个字节时,需要调用read()和write() 24000次左右,几乎达到最优性能。设置再超过这个值,对性能的提升就不显著了,这是因为与在用户空间和内核空间之间复制数据以及执行实际磁盘I/O 所花费的时间相比,read()和write() 系统调用的成本就显得微不足道了。
从表 13-1 的最后一行中可以粗略估算出在用户空间与内核空间之间传输数据以及执行文件I/O 的总耗时。因为此时系统调用的次数相对较少,所以它们所花费的时间相对于总耗时和CPU 时间可以忽略不计。据此可认为,系统CPU 时间主要是测量用户空间与内核空间之间数据传输所消耗的时间。而总耗时则是对与磁盘传输数据所需时间的估算。(正如下面将提到的,时间主要花在了对磁盘的读操作上。)
总之,如果与文件发生大量的数据传输,通过采用大块空间缓冲数据,以及执行更少的系统调用,可以极大地提高I / O 性能。
13.1 文件I/O的内核缓冲:缓冲区高速缓存(2)
表13-1度量了一系列因素:执行read()和write()系统调用所需的时间、内核空间和用户空间缓冲区之间传输数据所需的时间、内核缓冲区与磁盘之间传输数据所需的时间。再进一步考虑一下最后一个要素,显然,将输入文件的内容传输到缓冲区高速缓存是不可避免的。然而,当数据从用户空间传输到内核空间后,write()调用立即返回。由于测试系统上的RAM大小(4 GB) 远超欲复制文件的大小(100MB),据此推断,当程序完成时,输出文件实际尚未写入磁盘。因此,再进一步做个实验,运行一个程序,使用不同大小的缓冲区,以write() 随意向文件中写入一些数据。运行结果如表13-2所示。
同样,表13-2中数据是来自于内核2.6.30,以及块大小为4096字节的ext2文件系统,并且每行显示为运行了20次后的均值。本节并未列出测试程序(filebuff/ write_bytes.c)的代码清单,但可从随本书一起发行的源码中获取。
表13-2:写一个100MB大小的文件所需要的时间
BUF_SIZE | 时间(秒) | |||
总用时 | 总CPU用时 | 用户CPU用时 | 系统CPU用时 | |
1 | 72.13 | 72.11 | 5.00 | 67.11 |
2 | 36.19 | 36.17 | 2.47 | 33.70 |
续表
BUF_SIZE | 时间(秒) | |||
总用时 | 总CPU用时 | 用户CPU用时 | 系统CPU用时 | |
4 | 20.01 | 19.99 | 1.26 | 18.73 |
8 | 9.35 | 9.32 | 0.62 | 8.70 |
16 | 4.70 | 4.68 | 0.31 | 4.37 |
32 | 2.39 | 2.39 | 0.16 | 2.23 |
64 | 1.24 | 1.24 | 0.07 | 1.16 |
128 | 0.67 | 0.67 | 0.04 | 0.63 |
256 | 0.38 | 0.38 | 0.02 | 0.36 |
512 | 0.24 | 0.24 | 0.01 | 0.23 |
1024 | 0.17 | 0.17 | 0.01 | 0.16 |
4096 | 0.11 | 0.11 | 0.00 | 0.11 |
16384 | 0.10 | 0.10 | 0.00 | 0.10 |
65536 | 0.09 | 0.09 | 0.00 | 0.09 |
表13-2显示为使用不同大小的缓冲区调用write()从用户空间向内核缓冲区高速缓存传输数据所花费的成本。缓冲区越大,与表13-1中数据的差异就越明显。例如,对于一个65536字节大小的缓冲区,在表13-1中总耗时为2.06秒,而表13-2中仅为0.09秒。这是因为在后者的情况下并未执行实际的磁盘I/O操作。换言之,表13-1中采用大缓冲区时的耗时绝大部分花在了对磁盘的读取上。
正如13.3节所述,若强制在数据传输到磁盘前阻塞输出操作,则调用write()所需的时间会显著上升。
最后,值得注意的是,表13-2(以及表13-3)中信息仅仅代表了对文件系统评价基准的形式之一,还不完善。此外,文件系统不同,结果可能也会有所不同。对文件系统的度量还有各种其他标准,比如多用户、高负载下的性能表现,创建和删除文件的速度,在一个大型目录下搜索一个文件所需的时间,存储小文件所需的空间,或者在遭遇系统崩溃时对文件完整性的维护。只要I/O或是其他文件系统操作的性能至关重要,那么在目标平台上针对特定应用的测试基准就不可替代。