简体   繁体   English

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

[英]Why Python splits read function into multiple syscalls?

I tested this: 我测试了这个:

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

With the following partial output: 具有以下部分输出:

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

There are two calls for read syscall with different number of requested bytes. 有两个具有不同请求字节数的read syscall调用。

When I repeat the same using dd command, 当我使用dd命令重复相同的操作时,

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

just one read syscall is triggered using the exact number of bytes requested. 仅使用所请求的确切字节数触发了一个读系统调用。

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

I have googled this without any possible explanation. 我已经用谷歌搜索了,没有任何可能的解释。 Is this related to page size or any Python memory management? 这与页面大小或任何Python内存管理有关吗?

Why does this happen? 为什么会这样?

I did some research on exactly why this happens. 我对确切为什么会做一些研究。

Note: I did my tests with Python 3.5. 注意:我使用Python 3.5进行了测试。 Python 2 has a different I/O system with the same quirk for a similar reason, but this was easier to understand with the new IO system in Python 3. 出于类似的原因,Python 2具有不同的I / O系统并具有相同的功能,但是使用Python 3中的新IO系统更容易理解。

As it turns out, this is due to Python's BufferedReader, not anything about the actual system calls. 事实证明,这是由于Python的BufferedReader引起的,与实际的系统调用无关。

You can try this code: 您可以尝试以下代码:

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

If you try to strace this code, you will find: 如果尝试跟踪此代码,则会发现:

read(3, "]\"\34\277V\21\223$l\361\234\16:\306V\323\266M\215\331\3bdU\265C\213\227\225pWV"..., 65600) = 65600

Our original file object was a BufferedReader: 我们的原始文件对象是BufferedReader:

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

If we call detach() on this, then we throw away the BufferedReader portion and just get the FileIO, which is what talks to the kernel. 如果我们对此调用detach() ,那么我们将丢弃BufferedReader部分,而仅获得FileIO,这是与内核对话的内容。 At this layer, it'll read everything at once. 在这一层,它将立即读取所有内容。

So the behavior that we're looking for is in BufferedReader. 因此,我们正在寻找的行为在BufferedReader中。 We can look in Modules/_io/bufferedio.c in the Python source, specifically the function _io__Buffered_read_impl . 我们可以在Python源代码中查看Modules/_io/bufferedio.c ,特别是函数_io__Buffered_read_impl In our case, where the file has not yet been read from until this point, we dispatch to _bufferedreader_read_generic . 在我们这里,直到现在还没有读取过文件,我们将调度到_bufferedreader_read_generic

Now, this is where the quirk we see comes from: 现在,这是我们看到的古怪之处:

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);

Essentially, this will read as many full "blocks" as possible directly into the output buffer. 本质上,这将直接将尽可能多的完整“块”直接读入输出缓冲区。 The block size is based on the parameter passed to the BufferedReader constructor, which has a default selected by a few parameters: 块大小基于传递给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.

So this code will read as much as possible without needing to start filling its buffer. 因此,此代码将尽可能多地读取,而无需开始填充其缓冲区。 This will be 65536 bytes in this case, because it's the largest multiple of 4096 bytes less than or equal to 65600. By doing this, it can read the data directly into the output and avoid filling up and emptying its own buffer, which would be slower. 在这种情况下,这将是65536字节,因为它是小于或等于65600的4096字节的最大倍数。这样做,它可以将数据直接读到输出中,而避免填满和清空自己的缓冲区,这将是慢点。

Once it's done with that, there might be a bit more to read. 完成此操作后,可能还有更多内容需要阅读。 In our case, 65600 - 65536 == 64 , so it needs to read at least 64 more bytes. 在我们的例子中, 65600 - 65536 == 64 ,因此它至少需要再读取64个字节。 But yet it reads 4096! 但它的读数为4096! What gives? 是什么赋予了? Well, the key here is that the point of a BufferedReader is to minimize the number of kernel reads we actually have to do, as each read has significant overhead in and of itself. 好吧,这里的关键是BufferedReader的目的是最大程度地减少我们实际需要执行的内核读取次数,因为每次读取本身都有大量开销。 So it simply reads another block to fill its buffer (so 4096 bytes) and gives you the first 64 of these. 因此,它仅读取另一个块来填充其缓冲区(即4096个字节),并为您提供其中的前64个。

Hopefully, that makes sense in terms of explaining why it happens like this. 希望在解释为什么会这样发生时,这是有意义的。

As a demonstration, we could try this program: 作为演示,我们可以尝试以下程序:

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

With this, strace tells us: 这样,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

Sure enough, this follows the same pattern: as many blocks as possible, and then one more. 果然,这遵循相同的模式:越多的块,再越多的块。

dd , in a quest for high efficiency of copying lots and lots of data, would try to read up to a much larger amount at once, which is why it only uses one read. 为了提高复制大量数据的效率, dd将尝试一次读取大量数据,这就是为什么它仅使用一次读取的原因。 Try it with a larger set of data, and I suspect you may find multiple calls to read. 尝试使用更大的数据集,我怀疑您可能会发现有多个读取请求。

TL;DR: the BufferedReader reads as many full blocks as possible (64 * 4096) and then one extra block of 4096 to fill its buffer. TL; DR:BufferedReader读取尽可能多的完整块(64 * 4096),然后再读取一个额外的4096块以填充其缓冲区。

EDIT: 编辑:

The easy way to change the buffer size, as @fcatho pointed out, is to change the buffering argument on open : 如@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). 可选的buffering参数指定文件所需的缓冲区大小:0表示未缓冲,1表示行缓冲,任何其他正值表示使用(大约)该大小(以字节为单位)的缓冲区。 A negative buffering means to use the system default, which is usually line buffered for tty devices and fully buffered for other files. 负缓冲意味着使用系统默认值,通常对tty设备使用行缓冲,而对于其他文件则使用完全缓冲。 If omitted, the system default is used. 如果省略,则使用系统默认值。

This works on both Python 2 and Python 3 . 这适用于Python 2Python 3

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM