UNIX中管道的理解與實現
管道是什麼
首先來看一個命令:
cat file1 file2 | sort
cat表示讀取file1、file2中的資料,然後使用管道 |
,將這些內容作為輸入,使用sort函式作為輸出,最後輸出在螢幕上。
管道做了什麼事
熟悉類UNIX系統的朋友一定經常使用管道,其實它就是用來做程序通訊的。我們很多時候需要將一個檔案中的內容作為另一個檔案的輸入,或者將一個程式執行的結果作為另一個程式的輸入,這時候管道就派上用場了。我們這裡先只考慮無名管道。
無名管道
- 只能用於具有親緣關係的程序之間,父子程序,兄弟程序之間通訊,因為只有子程序才能繼續父程序的檔案描述符。
- 半雙共通訊(同一時刻只能對管道進行一種操作(讀操作或者寫操作)),具有讀埠和寫埠。
- 管道是一種特殊的檔案,可以使用檔案I/O函式(read,write…)來操作,但不能使用lseek函式來定位操作。
- 管道是在記憶體中,不用我們主動去刪除。
- 管道是基於佇列實現的,有大小限制。
管道實現原理
相關概念與函式
檔案描述符(File Descriptor)
對於核心而言,所有開啟檔案都是由檔案描述符引用。檔案描述符(fd)是一個非負整數。
當開啟一個現存檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。fd可以理解為一個檔案的標識,系統呼叫中的open
creat
都返回fd,並將其作為引數傳給read
或write
。 通常情況,UNIX shell**使檔案描述符0與程序的標準輸入相結合,檔案描述符1與標準輸出相結合,檔案描述符2與標準出錯輸出相結合**。
因此,檔案描述符可以看成是檔案描述符表的一個下標,我們可以通過這個fd訪問檔案的資訊(
fstate
),也可以使用write
或read
對檔案進行修改,具體細節可參考這篇文章。 總之,檔案描述符是標識每一個檔案及狀態的重要標識,而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:
心得
- 在UNIX中,標準輸入輸出也是一個檔案,只是預設用檔案識別符號0,1與之對應。
- 需熟練掌握UNIX系統呼叫的使用方法,以及程序的管理。
參考資料:
- 程序通訊之無名管道
- 在linux上 重定向 管道實現
- 對stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的學習
- Operating System:Design and Implementation,Third Edition
轉自:https://blog.csdn.net/crazy_scott/article/details/79462967