gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-30 00:44:12 25 4
gpt4 key购买 nike

我测试过这个:

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

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

当我使用 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 内存管理有关吗?

为什么会这样?

最佳答案

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

注意:我使用 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);

本质上,这会将尽可能多的完整“ block ”直接读入输出缓冲区。 block 大小基于传递给 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 的要点是最小化我们实际必须执行的内核读取次数,因为每次读取本身都有很大的开销。所以它只是读取另一个 block 来填充它的缓冲区(所以 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

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

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

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

编辑:

正如@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 2Python 3 .

关于python - 为什么 Python 将读取函数拆分为多个系统调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32906360/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com