如果不以磁盘块大小的倍数执行读/写操作,为什么性能会下降?

如果不以磁盘块大小的倍数执行读/写操作,为什么性能会下降?

恋爱中的Linux系统编程,第 3 章缓冲 I/O

回想一下第 1 章,块是代表文件系统上最小存储单元的抽象。在内核内部,所有 文件系统操作以块为单位进行。事实上,块是 I/O 的通用语言。因此,不能对小于块大小或不是块大小的整数倍的数据量执行 I/O 操作。如果您只想读取一个字节,那就太糟糕了:您必须读取整个块。想要写入 4.5 个块的数据吗?您需要写入 5 个块,这意味着完整读取该部分块,仅更新已更改的一半,然后写回整个块。

您可以看到这会导致什么结果:部分块操作效率低下。操作系统必须通过确保所有内容都发生在块对齐边界上并向上舍入到下一个最大块来“修复”您的 I/O。不幸的是,这不是用户空间应用程序通常的编写方式。大多数应用程序按照更高级别的抽象进行操作,例如字段和字符串,其大小与块大小无关。在最坏的情况下,用户空间应用程序一次可能只能读取和写入一个字节!这是很大的浪费。每个单字节写入实际上都是在写入整个块。

用户缓冲 I/O

必须向常规文件发出许多小型 I/O 请求的程序通常执行用户缓冲 I/O。这是指在用户空间中完成的缓冲,无论是由应用程序手动完成还是在库中透明地完成,而不是由内核完成的缓冲。正如第 2 章中所讨论的,出于性能原因,内核通过延迟写入、合并相邻 I/O 请求和预读来在内部缓冲数据。通过不同的方式,用户缓冲也旨在提高性能。

考虑一个使用用户空间程序 dd 的示例:

dd bs=1 count=2097152 if=/dev/zero of=pirate

由于 bs=1 参数,此命令将从设备 /dev/zero(提供无限的零流的虚拟设备)中以 2,097,152 个单字节块的形式将 2 MB 复制到文件盗版中。也就是说,它会通过大约200万个数据来复制数据读写操作——一次一个字节。

现在考虑相同的 2 MB 副本,但使用 1,024 字节块:

dd bs=1024 count=2048 if=/dev/zero of=pirate

此操作将相同的 2 MB 复制到同一个文件,但发出的问题减少了 1,024 倍读写操作。性能提升是巨大的,如表 3-1 所示。在这里,我记录了四个仅块大小不同的 dd 命令所花费的时间(使用三种不同的度量)。实时时间是总的挂钟时间,用户时间是在用户空间中执行程序代码所花费的时间,系统时间是代表进程在内核空间中执行系统调用所花费的时间。

表 3-1。块大小对性能的影响

Block size Real time User time System time

1 byte 18.707 seconds 1.118 seconds 17.549 seconds
1,024 bytes 0.025 seconds 0.002 seconds 0.023 seconds
1,130 bytes 0.035 seconds 0.002 seconds 0.027 seconds

与单字节块相比,使用 1,024 字节块可带来巨大的性能改进。然而,该表也表明如果操作不是以磁盘块大小的倍数执行,则使用更大的块大小(这意味着更少的系统调用)可能会导致性能下降尽管需要的调用较少,但 1,130 字节请求最终会生成未对齐的请求,因此效率低于 1,024 字节请求。

利用这种性能提升需要事先了解物理块大小。表中的结果显示块大小很可能是 1,024、1,024 的整数倍或 1,024 的除数。在 /dev/zero 的情况下,块大小实际上是 4,096 字节。

  1. 为什么“尽管需要的调用较少,但 1,130 字节请求最终会生成未对齐的请求,因此效率低于 1,024 字节请求”? (为什么与 1024 字节请求的性能不同?)

    • 发出的系统调用数count与之间的比率是 吗?bsdd

    • “读写操作”的次数是如何决定的?

  2. 如果“内核通过延迟写入、合并相邻 I/O 请求和预读来内部缓冲数据”,为什么我们需要用户缓冲区?内核缓冲区不是已经完成了用户缓冲区所做的工作吗?

  3. “文件系统操作以块的形式发生”是否意味着操作以块或块的任何整数倍的形式发生?

谢谢。

答案1

为什么“尽管需要的调用较少,但 1,130 字节请求最终会生成未对齐的请求,因此效率低于 1,024 字节请求”?

我将提供概念模型。内核可能有一些优化来减少这个问题(但不会让问题完全消失)。

如果块大小为 1024,那么您将拥有一系列块:

[1, 1024], [1025, 2048], [2049, 3076], [3077, 4096], ...

如果写入大小为 1130 的块,则对write()系统调用的第一次调用必须写入两个磁盘块才能满足一个请求。它首先将前 1024 个字节写入 block [1, 1024],留下 106 个字节未写入。然后它将读取第二个块 ( [1025, 2048]),将剩余的 106 字节复制到该块的前 106 字节中,然后将该块写回磁盘。

对系统调用的下一次调用write()必须再次读取第二个块 ( [1025, 2048]),将要写入的 1130 字节中的前 918 个字节 (1024-106) 复制到[1131, 2048]该块的字节中,然后将该块写回磁盘。然后它将读取第三个块 ( [2049, 3076]),将 1130 的最后 212 字节写入该块的前 212 字节,然后将该块写回磁盘。

这种模式仍在继续——尽管对 的调用较少write(),但内核必须重复读取/更新/写入现有块,而不是简单地写入块。

如果将 s 与块大小对齐write(),则不会出现“读取块,更新其中一部分,写回”的情况,它可以只写入块并继续前进,并且您不必读取/更新同一块以满足对write().

如果“内核通过延迟写入、合并相邻 I/O 请求和预读来内部缓冲数据”,为什么我们需要用户缓冲区?内核缓冲区不是已经完成了用户缓冲区所做的工作吗?

用户空间不能直接访问内核空间缓冲区。用户空间缓冲区对于使程序能够读取“块”而无需对每个字节进行系统调用(如 Love 所示效率低下)是必需的。

“文件系统操作以块的形式发生”是否意味着操作以块或块的任何整数倍的形式发生?

我认为这取决于设备以及用于与存储设备通信的协议。

相关内容