fca*_*tho 5 python dd system-calls
我测试了这个:
strace python -c "fp = open('/dev/urandom', 'rb'); ans = fp.read(65600); fp.close()"
Run Code Online (Sandbox Code Playgroud)
具有以下部分输出:
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
Run Code Online (Sandbox Code Playgroud)
对于具有不同请求字节数的读取系统调用有两个调用.
当我使用dd命令重复相同时,
dd if=/dev/urandom bs=65600 count=1 of=/dev/null
Run Code Online (Sandbox Code Playgroud)
只使用请求的确切字节数触发一个读取系统调用.
read(0, "P.i\246!\356o\10A\307\376\2332\365=\262r`\273\"\370\4\n!\364J\316Q1\346\26\317"..., 65600) = 65600
Run Code Online (Sandbox Code Playgroud)
我没有任何可能的解释来搜索这个.这与页面大小或任何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()
Run Code Online (Sandbox Code Playgroud)
如果您尝试使用此代码,您会发现:
read(3, "]\"\34\277V\21\223$l\361\234\16:\306V\323\266M\215\331\3bdU\265C\213\227\225pWV"..., 65600) = 65600
Run Code Online (Sandbox Code Playgroud)
我们的原始文件对象是BufferedReader:
>>> open("/dev/urandom", "rb")
<_io.BufferedReader name='/dev/urandom'>
Run Code Online (Sandbox Code Playgroud)
如果我们调用detach()它,那么我们扔掉BufferedReader部分,然后获取FileIO,这就是与内核对话的内容.在这一层,它会立即读取所有内容.
所以我们正在寻找的行为是在BufferedReader中.我们可以查看Modules/_io/bufferedio.cPython源代码,特别是函数_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);
Run Code Online (Sandbox Code Playgroud)
从本质上讲,这将尽可能多地将完整的"块"读入输出缓冲区.块大小基于传递给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.
Run Code Online (Sandbox Code Playgroud)
因此,此代码将尽可能多地读取而无需开始填充其缓冲区.在这种情况下,这将是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()
Run Code Online (Sandbox Code Playgroud)
有了这个,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
Run Code Online (Sandbox Code Playgroud)
果然,这遵循相同的模式:尽可能多的块,然后再一个.
dd为了高效率地复制大量数据,我们会尝试一次读取更多的数据,这就是为什么它只使用一次读取.尝试使用更大的数据集,我怀疑你可能会发现多次调用.
TL; DR:BufferedReader读取尽可能多的完整块(64*4096),然后读取一个4096的额外块以填充其缓冲区.
编辑:
正如@fcatho指出的那样,更改缓冲区大小的简单方法是更改以下buffering参数open:
Run Code Online (Sandbox Code Playgroud)open(name[, mode[, buffering]])(...)
可选的缓冲参数指定文件所需的缓冲区大小:0表示无缓冲,1表示行缓冲,任何其他正值表示使用(大约)该大小(以字节为单位)的缓冲区.负缓冲意味着使用系统默认值,通常为tty设备进行行缓冲,并为其他文件进行完全缓冲.如果省略,则使用系统默认值.