我测试了这个:
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内存管理有关吗?
为什么会这样?
答案 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()
,那么我们扔掉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);
基本上,这将尽可能多地将完整的“块”读入输出缓冲区。块大小基于传递给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设备进行行缓冲,并为其他文件进行完全缓冲。如果省略,则使用系统默认值。