1. 程式人生 > >UNIX中管道的理解與實現

UNIX中管道的理解與實現

管道是什麼

首先來看一個命令:

cat file1 file2 | sort

cat表示讀取file1、file2中的資料,然後使用管道 |,將這些內容作為輸入,使用sort函式作為輸出,最後輸出在螢幕上。

管道做了什麼事

熟悉類UNIX系統的朋友一定經常使用管道,其實它就是用來做程序通訊的。我們很多時候需要將一個檔案中的內容作為另一個檔案的輸入,或者將一個程式執行的結果作為另一個程式的輸入,這時候管道就派上用場了。我們這裡先只考慮無名管道

無名管道

  1. 只能用於具有親緣關係的程序之間,父子程序,兄弟程序之間通訊,因為只有子程序才能繼續父程序的檔案描述符。
  2. 半雙共通訊(同一時刻只能對管道進行一種操作(讀操作或者寫操作)),具有讀埠和寫埠。
  3. 管道是一種特殊的檔案,可以使用檔案I/O函式(read,write…)來操作,但不能使用lseek函式來定位操作。
  4. 管道是在記憶體中,不用我們主動去刪除。
  5. 管道是基於佇列實現的,有大小限制。

管道實現原理

相關概念與函式

檔案描述符(File Descriptor)

對於核心而言,所有開啟檔案都是由檔案描述符引用。檔案描述符(fd)是一個非負整數。 
當開啟一個現存檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。fd可以理解為一個檔案的標識,系統呼叫中的open

creat都返回fd,並將其作為引數傳給readwrite。 
通常情況,UNIX shell**使檔案描述符0與程序的標準輸入相結合檔案描述符1與標準輸出相結合檔案描述符2與標準出錯輸出相結合**。 
因此,檔案描述符可以看成是檔案描述符表的一個下標,我們可以通過這個fd訪問檔案的資訊(fstate),也可以使用writeread對檔案進行修改,具體細節可參考這篇文章。 
總之,檔案描述符是標識每一個檔案及狀態的重要標識,而UNIX預設使用0,1,2作為標準輸入輸出以及錯誤輸出,因此,我們如果想要改變標準輸出,就需要將0,1對應的標準輸入輸出改成我們需要的檔案,這樣,就可在程式本身不知情的情況下對其進行操作。

fork()

fork()可以用來新建程序,實際上是建立一個原程序的副本,包括檔案描述符、暫存器值等,子程序和父程序互不相關,如果一個程序的變數發生變化,並不會影響另一個程序。 
由於我們是無名管道,需要知道進行傳輸的兩個程序之間的檔案描述符,因此fork()必不可少。

dup()

dup(fd)為複製檔案操作符的系統函式,可以定向目前未被使用的最小檔案操作符到fd所指的檔案(回憶檔案操作符其實只是一個下標)。 
例如,如果我們想用一個程式使用一個普通檔案作為標準輸出,怎麼做?可以先關閉檔案描述符1,再開啟一個新檔案(open系統呼叫函式返回的fd從0開始尋找未被使用的最小檔案描述符),這時候,檔案描述符1就被定向到那個我們需要進行輸出的普通檔案。但當我們完成輸出後,標準輸出已經無法恢復。因此,我們需要使用dup來將多個檔案描述符對應到標準輸出,這樣我們就可以進行恢復。 
下面是主要操作方法:

fd = dup(1)

該操作將標準輸出(1)分配一個新的檔案描述符fd,並使之對應於標準輸出檔案(螢幕)。也就是說,現在也可以使用fd進行標準輸出了,效果與預設的標準輸出一樣。 
然後,我們可以將標準輸出(1)關閉,開啟一個新檔案,這時候,新檔案的檔案描述符就為1,因此這個檔案就作為了標準輸出。 
當需要恢復原來的標準輸出時,先關閉檔案操作符1(使之空閒),然後執行:

n = dup(fd)

這時候,dup自動找到最小的空閒檔案操作符(1),並被定向到fd所指的檔案,也就是標準輸出。

pipe()

pipe(&fd[0])系統呼叫建立一個管道並返回兩個檔案描述符,一個用於寫,一個用於讀。 
一般來說,在本條語句之後會呼叫一個fork來建立一個子程序,然後父程序關掉用於讀的檔案描述符,子程序關掉用於寫的檔案描述符,這樣便可以做到一個程序向管道中寫資料,一個程序向管道中讀資料了。

execlp()

execlp()函式屬於exec()函式族,會從PATH環境變數所指的目錄中查詢符合引數file的檔名,找到後便執行該檔案,然後將第二個以後的引數當做該檔案的argv[0]、argv[1]……,最後一個引數必須用空指標(NULL)作結束。(具體用法見後面示例)

實現過程

有了以上的知識,我們就知道如何讓兩個程序進行通訊了。 
1. 使用pipe建立一個管道。 
2. 使用fork建立一個子程序,他們共同享有管道的讀和寫。 
3. 將一個程序的標準輸入改為管道的讀,另一個程序的標準輸出改為管道的寫。 
4. 使用exec()函式族執行所需要的程式。

具體示例

我們想自己實現一個管道,可以將number.txt中的數字讀取出來,並使用sort函式進行排序,最後將排序結果輸出在Shell中。 
原始number.txt中為:

99
123
892
12
1342
89
32
76

實現原始碼

int main(int argc, char *argv[], char **environ)
{
    int fd[2];

    pipe(fd);

    if (fork() != 0)
    {
        /*this is the father, need to read*/
        close(fd[1]);
        close(0);
        dup(fd[0]);
        close(fd[0]);
        // dup2(fd[0], 0);
        // close(fd[1]);
        execlp("sort", "sort", "-n", NULL);
        exit(0);
    }
    else {
        /*this is the child, need to write*/
        close(fd[0]);
        close(1);
        dup(fd[1]);
        close(fd[1]);
        // dup2(fd[1], 1);
        // close(fd[0]);
        execlp("cat", "cat", "numbers.txt", NULL);
        exit(0);
    }
    exit(0);
    return 0;
}

編譯執行

筆者使用Minix3.3進行編譯,其他類UNIX也可同樣進行。即可將排好序的數字輸出到Shell: 
p1

心得

  1. 在UNIX中,標準輸入輸出也是一個檔案,只是預設用檔案識別符號0,1與之對應。
  2. 需熟練掌握UNIX系統呼叫的使用方法,以及程序的管理。

參考資料:

  1. 程序通訊之無名管道
  2. 在linux上 重定向 管道實現
  3. 對stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的學習
  4. Operating System:Design and Implementation,Third Edition

 

 

轉自:https://blog.csdn.net/crazy_scott/article/details/79462967