簡體   English   中英

為什么Python將讀取功能拆分為多個系統調用?

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

我測試了這個:

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內存管理有關嗎?

為什么會這樣?

我對確切為什么會做一些研究。

注意:我使用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字節,因為它是小於或等於65600的4096字節的最大倍數。這樣做,它可以將數據直接讀到輸出中,而避免填滿和清空自己的緩沖區,這將是慢點。

完成此操作后,可能還有更多內容需要閱讀。 在我們的例子中, 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所指出的,更改緩沖區大小的簡單方法是在open上更改buffering參數:

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

(...)

可選的buffering參數指定文件所需的緩沖區大小:0表示未緩沖,1表示行緩沖,任何其他正值表示使用(大約)該大小(以字節為單位)的緩沖區。 負緩沖意味着使用系統默認值,通常對tty設備使用行緩沖,而對於其他文件則使用完全緩沖。 如果省略,則使用系統默認值。

這適用於Python 2Python 3

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM