將千兆字節的數據寫入管道時會發生什麼?
假設我有一個帶有名為 的配方的生成文件,
hour_long_recipe
顧名思義,它需要一個小時才能執行。在整個食譜中的隨機點,它會詢問是/否問題。假設它總共問了 10 個問題。一種可能的(並且經常推薦的)執行方式是:
yes | make hour_long_recipe
它用 . 回答所有問題
y
。但是,據我了解,無論是否實際使用其標準輸入中的數據,它都會以高達每秒 10.2 GiByes
的速度輸出到其標準輸出。make
即使它只有 10 MiB/s(
yes
如果相信該 reddit 執行緒的話,比任何實現都慢得多),在一個小時的過程中,它加起來會超過 35 GiB,其中只有 20 個字節將被讀取。數據去哪兒了?可以將它保存到磁碟,但這很浪費,如果磁碟填滿的速度足夠快,它甚至會導致make
失敗。大概作業系統會阻止它到達那個位置,但是怎麼做呢?限制是什麼,達到限制時會發生什麼?
tl; dr:在某些時候,
yes
如果數據沒有在另一端被讀取,將被阻止寫入。在讀取該數據或接收到信號之前,它將無法繼續執行,因此您通常無需擔心yes
寫入千兆字節和千兆字節的數據。要記住的重要一點是,管道是一種 FIFO 資料結構,而不僅僅是一個純粹的流,如果沒有立即在接收器上讀取,它會丟棄數據。也就是說,雖然在大多數情況下它可能看起來是從寫入應用程序到讀取應用程序的無縫數據流,但它確實需要中間儲存來執行此操作,並且中間儲存的大小是有限的。*
如果我們查看pipe(7) 手冊頁,我們可以閱讀以下有關該內部緩衝區大小的資訊(已添加重點):
在 2.6.11 之前的 Linux 版本中,管道的容量與系統頁面大小相同(例如,在 i386 上為 4096 字節)。從 Linux 2.6.11 開始,管道容量為 16 個頁面(即,在頁面大小為 4096 字節的系統中為 65,536 字節)。從 Linux 2.6.35 開始,預設管道容量為 16 頁,但可以使用 fcntl(2) F_GETPIPE_SZ 和 F_SETPIPE_SZ 操作查詢和設置容量。
假設您使用的是標準 x86_64 系統,那麼您很可能使用 4KiB 頁面,因此管道容量的 2^16 上限可能是正確的,除非管道的任一側在某些時候使用
fcntl(F_SETPIPE_SZ)
。無論哪種方式,原理都是:管道兩側之間的中間儲存是有限的,並且儲存在記憶體中。在抽像管道
a | b
中,此儲存用於在a
寫入某些數據和b
實際讀取數據之間的時間段內。那麼,假設您的make
呼叫(以及任何通過繼承連接到此管道的子項)實際上並未嘗試讀取標準輸入,或者只是謹慎地這樣做,當緩衝區空間耗盡時,write
系統呼叫yes
最終將不會從睡眠中喚醒yes
.yes
然後將等待被喚醒,或者當緩衝區空間再次可用,或者收到信號時。**所有這些都由核心的程序調度程序處理。您可以在 中看到它pipe_write()
,它是write()
管道的處理程序:static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from) { /* ... */ if (pipe_full(pipe->head, pipe->tail, pipe->max_usage)) wake_next_writer = false; if (wake_next_writer) wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM); /* ... */ }
當
make
一方最終終止時,yes
將SIGPIPE
作為寫入管道的結果發送,另一端沒有任何剩餘。然後,根據yes
實現,這將呼叫它自己的信號處理程序或預設的核心信號處理程序,然後它將終止。***
- 在簡單的情況下,接收方以與寫入數據大致相同的速率處理數據,此傳輸也可以是零複製,沒有中間緩衝區,通過使用虛擬記憶體映射並提供寫入過程中的物理頁面可用於接收器。但是,您所描述的情況肯定最終需要使用管道緩衝區來儲存未讀數據。
** 也有可能使用
O_NONBLOCK
文件描述符上設置的標誌完成寫入,從而啟用非阻塞模式。在這種情況下,您可能會得到一個不完整的寫入,然後寫入將返回EAGAIN
,應用程序將需要自己處理。它可能會通過暫停或執行它選擇的一些其他程式碼來處理管道已滿的情況。但是,對於我能找到的每個現代yes
版本和大多數其他應用程序,上面的描述就是發生的情況,因為它們不使用O_NONBLOCK
.*** 應用程序在收到後可以為所欲為
SIGPIPE
——它甚至可以在理論上決定不終止。然而,所有常見yes
的都使用預設SIGPIPE
處理程序,它只是終止而不執行任何更多的使用者空間指令。