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

时间:2015-10-02 11:38:06

标签: python system-calls dd

我测试了这个:

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

使用以下部分输出:

read(3, "\211^\250\202P\32\344\262\373\332\241y\226\340\16\16!<\354\250\221\261\331\242\304\375\24\36\253!\345\311"..., 65536) = 65536
read(3, "\7\220-\344\365\245\240\346\241>Z\330\266^Gy\320\275\231\30^\266\364\253\256\263\214\310\345\217\221\300"..., 4096) = 4096

对于具有不同请求字节数的read syscall,有两个调用。

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

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

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

read(0, "P.i\246!\356o\10A\307\376\2332\365=\262r`\273\"\370\4\n!\364J\316Q1\346\26\317"..., 65600) = 65600

我在没有任何可能的解释的情况下搜索了这个。这与页面大小或任何Python内存管理有关吗?

为什么会这样?

1 个答案:

答案 0 :(得分:9)

我对这究竟发生的原因做了一些研究。

注意:我使用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, "]\"\34\277V\21\223$l\361\234\16:\306V\323\266M\215\331\3bdU\265C\213\227\225pWV"..., 65600) = 65600

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

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

如果我们在这上面调用detach(),那么我们扔掉Bu​​fferedReader部分,然后获取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);

基本上,这将尽可能多地将完整的“块”读入输出缓冲区。块大小基于传递给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字节,因为它是4096字节的最大倍数小于或等于65600.通过这样做,它可以直接将数据读入输出并避免填充和清空自己的缓冲区,这将是慢。

完成后,可能会有更多内容要阅读。在我们的例子中,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, "\357\202{u'\364\6R\fr\20\f~\254\372\3705\2\332JF\n\210\341\2s\365]\270\r\306B"..., 60000) = 60000
read(3, "\266_ \323\346\302}\32\334Yl\ry\215\326\222\363O\303\367\353\340\303\234\0\370Y_\3232\21\36"..., 30000) = 30000

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

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

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

编辑:

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

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

(...)

     

可选的缓冲参数指定文件所需的缓冲区大小:0表示无缓冲,1表示行缓冲,任何其他正值表示使用(大约)该大小(以字节为单位)的缓冲区。负缓冲意味着使用系统默认值,通常为tty设备进行行缓冲,并为其他文件进行完全缓冲。如果省略,则使用系统默认值。

这适用于Python 2Python 3