歡迎光臨
每天分享高質量文章

聊聊 Linux 的匿名管道

作者:Lin_R 

連結:https://my.oschina.net/LinBigR/blog/857974

相信很多在linux平臺工作的童鞋, 都很熟悉管道符 ‘|’, 透過它, 我們能夠很靈活的將幾種不同的命令協同起來完成一件任務.就好像下麵的命令:

 

echo 123 | awk '{print $0+123}'       # 輸出246

不過這次咱們不來說這些用法, 而是來探討一些更加有意思的, 那就是 管道兩邊的資料流”實時性” 和 管道使用的小提示.

 

其實我們在利用管道的時候, 可能會不經意的去想, 我前一個命令的輸出, 是全部處理完再透過管道傳給第二個命令, 還是一邊處理一邊輸出呢? 可能在大家是試驗中或者工作經驗中, 應該是左邊的命令全部處理完再一次性交給右邊的命令進行處理, 不光是大家, 我在最初接觸管道時, 也曾有這麼一個誤會, 因為我們透過現象看到的就是這樣.

 

但其實只要有簡單瞭解過管道這工具, 應該都不難得出解釋:

管道是兩邊是同時進行, 也就是說, 左邊的命令輸出到管道, 管道的右邊將馬上進行處理.

管道的定義

管道是由核心管理的一個緩衝區,相當於我們放入記憶體中的一個紙條。管道的一端連線一個行程的輸出。這個行程會向管道中放入資訊。管道的另一端連線一個行程的輸入,這個行程取出被放入管道的資訊。一個緩衝區不需要很大,它被設計成為環形的資料結構,以便管道可以被迴圈利用。當管道中沒有資訊的話,從管道中讀取的行程會等待,直到另一端的行程放入資訊。當管道被放滿資訊的時候,嘗試放入資訊的行程會堵塞,直到另一端的行程取出資訊。當兩個行程都終結的時候,管道也自動消失。

管道工作流程圖

 

透過上面的解釋可以看到, 假設 COMMAND1 | COMMAND2, 那麼COMMAND1的標準輸出, 將會被系結到管道的寫端, 而COMMAND2的標準輸入將會系結到管道的讀端,  所以當COMMAND1一有輸出, 將會馬上透過管道傳給COMMAND2, 我們先來做個實驗驗證下:

 

# 1.py
import time
import sys
while 1:
    print '1111'
    time.sleep(3)
    print '2222'
    time.sleep(3)

[root@iZ23pynfq19Z ~]# python 1 | cat 

在上面的命令, 我們可以猜測下輸出結果: 究竟是 睡眠6秒之後, 輸出”1111222″, 還是輸出 “1111” 睡眠3秒, 再輸出 “2222”, 然後再睡眠3秒, 再輸出”1111″ 呢? 答案就是: 都不是! what! 這不可能, 大家可以嘗試下, 我們會看到終端沒反應了,  為什麼呢? 這就要涉及到檔案IO的緩衝方式了,關於檔案IO, 可以參考我的另一篇文章: 淺談檔案描述符1和2, 在最下麵的地方提到檔案IO的三種緩衝方式:

 

  • 全緩衝: 直到緩衝區被填滿,才呼叫系統I/O函式, (一般是針對檔案)

  • 行緩衝: 遇到換行符就輸出(標準輸出)

  • 無緩衝: 沒有緩衝區,資料會立即讀入或者輸出到外存檔案和裝置上(標準錯誤

 

因為python是預設採用帶緩衝的fputs(參考py27原始碼: fileobject.c: PyFile_WriteString函式), 又因為標準輸出被改寫到管道, 所以將會採取全緩衝的方式(shell 命令具體要看實現, 因為有些是用不帶緩衝write實現,如果不帶緩衝區,會直接寫入管道), 所以將會採取全緩衝的方式, 也就是說, 直到緩衝區被填滿, 或者手動顯示呼叫flush刷入,才能看到輸出.那我們可以將程式碼改寫成下麵兩種方式吧

 

# 方式1: 填滿緩衝區, 我這邊大小是4096位元組, 你們也可以試下這個值, 估計都一樣
import time
import sys
while 1:
    print '1111' * 4096
    time.sleep(3)
    print '2222' * 4096
    time.sleep(3)

# 方式2: 手動刷入寫佇列
import time
import sys
while 1:
    print '1111'
    sys.stdout.flush()    // 因為是標準輸出, 所以直接透過sys的介面去flush
    time.sleep(3)
    print '2222' 
    sys.stdout.flush()
    time.sleep(3

輸出結果:

# 第一種方式:
[root@iZ23pynfq19Z ~]# python 1 | cat 
1111.....(超多1, 刷屏了..)
睡眠3秒..
2222.....(超多2, 刷屏了..)

# 第二種方式:
[root@iZ23pynfq19Z ~]# python 1 | cat 
1111
睡眠3秒..
2222
睡眠3秒..
1111
....

在這裡我們已經能夠得出結果, 如果像我們以前所想的那樣, 要等到COMMAND1全部執行完才一次性輸出給COMMAND2, 那麼結果應該是無限堵塞..因為我的程式一直沒有執行完..這樣應該是不符合老前輩們設計初衷的, 因為這樣可能會導致管道越來越大..然而管道也是有大小的~ 具體可以去看posix標準, 所以我們得出結論是: 只要COMMAND1的輸出寫入管道的寫端(不管是緩衝區滿還是手動flush), COMMAND2都將立刻得到資料並且馬上處理.

 

那麼 管道兩邊的資料流”實時性” 討論到就先暫告一段落, 接下來將在這個基礎上繼續討論: 管道使用的小提示.

 

在開始討論前, 我想先引入一個專業術語, 也是我們偶爾會遇到的, 那就是: SIGPIPE 

或者是一個更加具體的描述: broken pipe (管道破裂)

 

上面的專業術語都是跟管道讀寫規則息息相關的, 那咱們來看下 管道的讀寫規則吧:

 

  • 當沒有資料可讀時

    • O_NONBLOCK (未設定):read呼叫阻塞,即行程暫停執行,一直等到有資料來到為止。

    • O_NONBLOCK  ( 設定 ) :read呼叫傳回-1,errno值為EAGAIN。

  • 當管道滿的時候

    • O_NONBLOCK (未設定):write呼叫阻塞,直到有行程讀走資料

    • O_NONBLOCK ( 設定 ):呼叫傳回-1,errno值為EAGAIN

  • 如果所有管道寫端對應的檔案描述符被關閉,則read傳回0

  • 如果所有管道讀端對應的檔案描述符被關閉,則write操作會產生訊號SIGPIPE

  • 當要寫入的資料量不大於PIPE_BUF時,linux將保證寫入的原子性。

  • 當要寫入的資料量大於PIPE_BUF時,linux將不再保證寫入的原子性。

 

在上面我們可以看到, 如果我們收到SIGPIPE訊號, 那麼一般情況就是讀端被關閉, 但是寫端卻依舊嘗試寫入

 

咱們來重現下 SIGPIPE

 

#!/usr/bin/python
import time
import sys
while 1:
    time.sleep(10)   # 手速不夠快的童鞋可以將睡眠時間設定長點
    print '1111'
    sys.stdout.flush()

這次執行命令需要考驗手速了, 因為我們要趕在py醒過來之前, 將讀端行程殺掉

 python 1 | cat 

------------------------
# 另一個終端
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'cat|python'
root     10775  4074  0 00:05 pts/2    00:00:00 python 1
root     10776  4074  0 00:05 pts/2    00:00:00 cat        # 讀端行程
root     10833 32581  0 00:06 pts/0    00:00:00 grep -P cat|python

[root@iZ23pynfq19Z ~]# kill 10776  

輸出結果

[root@iZ23pynfq19Z ~]# python 1 | cat 
Traceback (most recent call last):
  File "1", line 6in <module>
    sys.stdout.flush()
IOError: [Errno 32] Broken pipe
Terminated

從上圖我們可以驗證兩個點: 

 

  • 當我們殺掉讀端時, 寫端會收到SIGPIPE而預設退出, 管道結束

  • 當我們殺掉讀端時, 寫端的程式並不會馬上收到SIGPIPE, 相反的, 只有真正寫入管道寫端時才會觸發這個錯誤

 

如果寫入一個 讀端已經關閉的管道, 將會收到一個 SIGPIPE, 那讀一個寫端已經關閉的管道又會這樣呢?

 

import time
import sys

# 這次我們不需要死迴圈, 因為我們想要寫端快點關閉退出
time.sleep(5)   
print '1111'
sys.stdout.flush()

 

# 因為我們想要 讀端 等到足夠長的時間, 讓寫端關閉, 所以我們需要利用awk先睡眠10秒
[root@iZ23pynfq19Z ~]# python 1.py | awk '{system("sleep 10");print 123}' 

------------------------
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'awk|python'
root     11717  4074  0 00:20 pts/2    00:00:00 python 1.py
root     11718  4074  0 00:20 pts/2    00:00:00 awk {system("sleep 10");print 123}
root     11721 32581  0 00:20 pts/0    00:00:00 grep -P awk|python

# 5秒過後
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'awk|python'
root     11685  4074  0 00:20 pts/2    00:00:00 awk {system("sleep 10");print 123}
root     11698 32581  0 00:20 pts/0    00:00:00 grep -P awk|python

# 10秒過後
[root@iZ23pynfq19Z ~]# python 1 | awk '{system("sleep 10");print 123}' 
123

 

在上面也已經證明瞭上文提到的讀寫規則: 如果所有管道寫端對應的檔案描述符被關閉,將產生EOF結束標誌,read傳回0, 程式退出。

總結

透過上面的理論和實驗, 我們知道在使用管道時, 兩邊命令的資料傳輸過程, 以及對管道讀寫規則有了初步的認識, 希望我們以後在工作時, 再接觸管道時, 能夠更加有把握的去利用這一強大的工具。

贊(0)

分享創造快樂