为什么 Python 将读取函数拆分为多个系统调用?

Why Python splits read function into multiple syscalls?

我测试了这个:

strace python -c "fp = open('/dev/urandom', 'rb'); ans = fp.read(65600); fp.close()"

具有以下部分输出:

read(3, "1^02P42321y60!<401112453!51"..., 65536) = 65536
read(3, "0-455061>Z06^Gy051^64363405710"..., 4096) = 4096

有两个请求字节数不同的读取系统调用。

当我使用 dd 命令重复相同的操作时,

dd if=/dev/urandom bs=65600 count=1 of=/dev/null

只使用请求的确切字节数触发一个读取系统调用。

read(0, "P.i6!6oA76325=2r`3\"0\n!4J6Q167"..., 65600) = 65600

我用谷歌搜索了这个,没有任何可能的解释。这与页面大小或任何 Python 内存管理有关吗?

为什么会这样?

我对发生这种情况的确切原因做了一些研究。

注意:我使用 Python 3.5 进行了测试。 Python 2 有一个不同的 I/O 系统,出于类似的原因具有相同的怪癖,但是在 Python 3 中使用新的 IO 系统更容易理解。

事实证明,这是由于 Python 的 BufferedReader,与实际系统调用无关。

您可以试试这个代码:

fp = open('/dev/urandom', 'rb')
fp = fp.detach()
ans = fp.read(65600)
fp.close()

如果您尝试跟踪这段代码,您会发现:

read(3, "]\"7V3$l14:6V36M51bdU5C375pWV"..., 65600) = 65600

我们的原始文件对象是 BufferedReader:

>>> open("/dev/urandom", "rb")
<_io.BufferedReader name='/dev/urandom'>

如果我们对此调用 detach(),那么我们将丢弃 BufferedReader 部分,只获取与内核对话的 FileIO。在这一层,它会一次读取所有内容。

所以我们正在寻找的行为在 BufferedReader 中。我们可以查看 Python 源代码中的 Modules/_io/bufferedio.c,特别是函数 _io__Buffered_read_impl。在我们的例子中,直到此时文件还没有被读取,我们发送到 _bufferedreader_read_generic.

现在,这就是我们看到的怪癖的来源:

while (remaining > 0) {
    /* We want to read a whole block at the end into buffer.
       If we had readv() we could do this in one pass. */
    Py_ssize_t r = MINUS_LAST_BLOCK(self, remaining);
    if (r == 0)
        break;
    r = _bufferedreader_raw_read(self, out + written, r);

本质上,这会将尽可能多的完整 "blocks" 直接读入输出缓冲区。块大小基于传递给 BufferedReader 构造函数的参数,它有一个由几个参数选择的默认值:

     * Binary files are buffered in fixed-size chunks; the size of the buffer
       is chosen using a heuristic trying to determine the underlying device's
       "block size" and falling back on `io.DEFAULT_BUFFER_SIZE`.
       On many systems, the buffer will typically be 4096 or 8192 bytes long.

所以这段代码将尽可能多地读取而不需要开始填充它的缓冲区。在这种情况下,这将是 65536 字节,因为它是小于或等于 65600 的 4096 字节的最大倍数。通过这样做,它可以将数据直接读取到输出中,避免填充和清空自己的缓冲区,这将是较慢。

完成后,可能还有更多内容需要阅读。在我们的例子中,65600 - 65536 == 64,因此它至少需要再读取 64 个字节。但它仍然显示 4096!是什么赋予了?好吧,这里的关键是 BufferedReader 的要点是最小化我们实际必须执行的内核读取次数,因为每次读取本身都有很大的开销。所以它只是读取另一个块来填充它的缓冲区(所以 4096 字节)并为您提供其中的前 64 个。

希望这能解释为什么会这样。

作为演示,我们可以试试这个程序:

import _io
fp = _io.BufferedReader(_io.FileIO("/dev/urandom", "rb"), 30000)
ans = fp.read(65600)
fp.close()

由此,strace 告诉我们:

read(3, "72{u'4R\fr\f~42052JF\n01s5]0\r6B"..., 60000) = 60000
read(3, "6_ 362}4Yl\ry5623O373034[=16=]0Y_32"..., 30000) = 30000

果然,这遵循相同的模式:尽可能多的块,然后再多一个。

dd,为了追求复制大量数据的高效率,会尝试一次读取更大的数量,这就是为什么它只使用一次读取。尝试使用更大的数据集,我怀疑您可能会发现多个读取调用。

TL;DR:BufferedReader 读取尽可能多的完整块 (64 * 4096),然后再读取一个额外的 4096 块来填充其缓冲区。

编辑:

正如@fcatho 指出的那样,更改缓冲区大小的简单方法是更改​​ open 上的 buffering 参数:

open(name[, mode[, buffering]])

( ... )

The optional buffering argument specifies the file’s desired buffer size: 0 means unbuffered, 1 means line buffered, any other positive value means use a buffer of (approximately) that size (in bytes). A negative buffering means to use the system default, which is usually line buffered for tty devices and fully buffered for other files. If omitted, the system default is used.

这适用于 Python 2 and Python 3