一、管道的概念
管道是一種隊列類型的數據結構,它的數據從一端輸入,另一端輸出。管道最常見的應用是連接兩個進程的輸入輸出,即把一個進程的輸出編程另一個進程的輸入。shell中存在專門的管道運算符"|",例如shell命令:
ps -ef |grep init
命令"ps -ef"分析當前運行的全部進程,並將結果打印到屏幕上。進程"grep init"從輸入的字符串中查找包含字符"init"的子串,並打印結果。這兩個領命通過管道符連接起來後就成了一個新的應用:查找正在應用的、命名中包含字符"init"的進程。
二、無名管道
無名管道通暢直接稱之爲管道,它佔用兩個文件描述符,不能被非血緣關係的進程共享,一般應用在父子進程中。
1.無名管道的建立
UNIX中一切皆爲文件,管道也是文件的一種,成爲管道文件。當系統創建一個管道時,它返回兩個文件描述符:一個文件以只寫打開,作爲管道的輸入端;另一個文件以只讀打開,作爲管道的輸出端。
在UNIX中,採用函數pipe創建無名管道,其原型爲:
#include<unistd.h>
int pipe(int fildes[2]);/*其中fildes[0]爲讀而開,fildes[1]爲寫而開,fildes[1]的輸出是fildes[0]的輸入*/
函數pipe在內核中創建一個管道,並分配兩個文件描述符標識管道的兩端,這兩個文件描述符存儲於fildes[0]和fildes[1]中。一般約定fildes[0]爲輸入端,進程向此文件描述符寫入數據,fildes[1]描述管道的輸出端,進程向此文件描述符中讀取數據。函數pipe調用成功時返回0,否則返回-1。
2.單向管道流模型
管道的兩端(輸入和輸出端)被一個進程控制沒有太大的意義,如果管道的兩端分別控制在不通的進程中,這兩個進程之間就能夠進行通信。擁有管道輸入端的進程,可以向管道發送信息,擁有管道輸出端的進程,可以從管道中接收一個進程發送來的信息。
1)從父進程流向子進程的管道
在父進程創建無名管道併產生子進程後,父子進程均擁有管道兩端的訪問權。此時關閉父進程的管道輸出端、關閉子進程的管道輸入端,就形成一個從父進程到子進程的管道流,數據由父進程寫入、從子進程讀出。創建從父進程流向子進程的管道過程如下:
1st:創建管道,返回無名管道的兩個文件描述符fildes[0]和fildes[1]。
int fildes[2];
pipe(fildes);
2nd:創建子進程,子進程繼續無名管道文件描述符。
3rd:父進程關閉管道的輸出端,即關閉只讀文件描述符fildes[0]。
close(fildes[0]);
4th:子進程關閉管道的輸入端,即關閉只寫文件描述符fildes[1]。
close(fildes[1]);
2)從子進程流行父進程的管道
在父進程創建無名管道併產生子進程後,父子進程均擁有管道兩端的訪問權。此時關閉父進程的管道輸入端、關閉子進程的管道輸出端,就形成一個從子進程到父進程的管道流,數據由子進程寫入,從父進程讀出。創立從子進程流向父進程的管道過程如下:
1st:創建管道,返回無名管道的兩個文件描述符fildes[0]和fildes[1];
2nd:創建子進程,子進程中繼續無名管道文件描述符。
3rd:父進程關閉管道的輸入端,即關閉只讀文件描述符fildes[1];
4th:子進程關閉管道的輸出端,即關閉只寫文件描述符fildes[0];
ex:一個管道的例子:父進程向管道寫入一行字符,子進程讀取數據並打印到屏幕上。
程序中父進程分別向管道寫入字符串"Hello!"和"World!",子進程一次性從管道中讀出並打印這些數據。
【實踐經驗】在進程的通信中,我們無法判斷每次通信中報文的字節數,即無法對數據流進行自動拆分,從而發生了上例中字進程一次性讀取父進程兩次通信的報文情況。爲了能正常拆分發送報文,我們常常採用以下幾種方法:
1)固定長度:發送進程每次寫入固定字節的數據,接受進程每次讀取固定字節的內容,報文中多餘部分填充空格或填充0,根據填充0,根據填充的位置,本方法又可以分爲左對齊和右對齊兩種。
2)顯式長度:每條報文由"長度域"和"數據域"組成,"長度域"大小固定,存儲了"數據域"的長度,分爲字符串型和整型兩種,"數據域"是傳輸的實際報文數據。接受進程先獲取"長度域"數據,轉換爲"數據域"的長度,再讀取相應長度的信息即爲"數據域"內容。
3)短連接。每當進程間需要通信時,創建一個通信線路,發送一條報文後立即廢棄這條通信路線。這種方式爲Socket通信中很常用。
3.雙向管道模型
管道是進程之間的一種單向交流方法,要實現進程間的雙向交流,就必須通過兩個管道來完成。創立雙向管道的過程如下:
1st:創建管道,返回兩個無名管道文件描述符fildes1,fildes2。爲了簡化書寫,我們稱fildes1爲管道1,fildes2爲管道2。
int fildes1[2],int fildes2[2];
pipe(fildes1);
pipe(fildes2);
2nd:創建子進程,子進程中繼承管道I和管道II。
3rd:父進程關閉管道I的輸出端,即關閉只讀文件描述符fildes[0];
close(fildes1[0]);
4th:子進程關閉管道I的輸入端,即關閉只寫文件描述符fildes2[1];
close(fildes[1]);
5th:父進程關閉管道II的輸入端,即關閉只讀文件描述符fildes2[1];
close(fildes[0]);
6th:子進程關閉管道II的輸出端,即關閉只寫文件描述fildes2[0];
close(fildes[1]);
ex:一個父子通信進程間雙向管道通信的實例,父進程首先向子進程傳送兩次數據,再接受進程傳送過來的兩次數據。爲了能夠正確拆分數據流,從父進程流向子進程的管道I採用"固定長度"方法傳送數據,從子進程流向父進程的管道II採用"顯示長度"方法傳回數據。
固定長度:管道輸入時固定寫入長度len個字符,管道輸出時也固定讀取len個字符,採用了左對齊方式,多餘部分跳蟲ASCII碼0,“固定長度”數據的管道操作方法如下:
顯示長度:顯示長度報文的"長度域"可分爲整形和字符串類型兩種,以4字節“長度域”傳輸數據"Hello!" 爲例,整型長度域爲:
0x06 ,0x00, 0x00,0x00,"Hello!"
出於兼容性考慮,一般採用網絡字節順序的整型。
字符串長度域報文爲:
"0006Hello!"
本例採用"4字節字符串"+"數據"的格式傳送報文,輸入時寫入數據長度再寫數據內容,如下:
管道的輸出操作可分爲以下操作:
1st:讀入4個字節,轉化爲整形長度。如將字符串"0006"轉爲爲整型6。
2nd:讀入數據,字節數爲步驟1st中獲取的整型。
主程序:父子進程雙向管道通信實例的主函數如下:
4.連接標準I/O的管道模型
管道在shell中最常見的應用是連接不同進程的輸入輸出,比如使用A進程的輸出變成B進程的輸入等。考察shell命令"cat pipe3.c | more",進程"more"使用了進程"cat pipe3.c"的輸出。
ex1.分別重定向標準輸入、標準輸出、標準錯誤輸出到文件描述符fd1,fd2,fd3;
---複製文件描述fd1到文件描述符0即可重定向標準輸入:
dup2(fd1,0);
dup2(fd2,1);
dup2(fd3,2);
當執行dup2(fd2,0)後,文件描述符0就對應到了fd1鎖對應的文件中,而一些標準輸出函數,如printf、puts等仍然想描述符0寫入內容,從而達到了重定向的效果。
1)模型
使用管道將父進程標準輸入連接到子進程標準輸入的方法如下:
1st:創建管道,返回無名管道的兩個文件描述符fildes[0]和fildes[1]
2nd:創建子進程,子進程中繼承無名管道文件描述符
3rd:父進程關閉管道的輸出端,即關閉只讀文件描述符fildes[0]
4th:父進程將標準輸入(stdout,文件描述符1)重定向爲文件描述符fildes[1]。
5th:子進程關閉管道的輸入端,即關閉只寫文件描述符fildes[1].
6th:子進程將標準輸入(stdin文件描述符爲0)重定向爲文件描述符fildes[0]。
2)實例:一個將父進程標準輸入流連接到子進程標準輸入流的管道,父進程向stdout輸出的'Hello!'直接轉移到子進程的stdin,由子進程"gets(buf)"語句所獲取。
5.popen模型
創建連接標準I/O的管道需要多個步驟,需要使用大量的代碼,型號UNIX提供了一組函數簡化這個複雜的過程,其原型如下:
#include<stdio.h>
FILE *popen(const char *command,char *type);
int pclose(FILE *stream);
函數popen函數類似於函數system,它首先fork一個子進程,然後調用exec執行參數command中給定的shell命令。不同的是,函數popen自動在父進程和exec創建的子進程之間建立了一個管道,這個管道可以連接子進程的標準輸入,也可以連接子進程的標準輸出,參與type決定了一個管道I/O類型,其取值與含義如下:
r 創建與子進程的標準輸出連接的管道(管道數據由子進程流向父進程)
w 創建於子進程的標準輸入連接的管道(管道數據由父進程流向子進程)
函數popen調用成功時返回一個標準I/O的FILE文件流,它的讀寫屬性由參數type決定,調用失敗時返回NULL。
函數pclose關閉由popen打開的文件流,它調用時返回exec進程退出時的狀態。否則返回-1。
ex:模擬shell命令"ps -ef |grep init"的例子,它的流程如下:
1st:調用popen創建子進程,執行命令"grep init",並創建一個寫管道out連接到該子進程的標準輸入,此時執行命令grep init 所分析的文本內容需要從管道out中讀出。
2nd:調用popen創建子進程執行"ps -ef",並創建一個度管道in連接該子進程的標準輸出,此時執行命令ps -ef 的結果將寫入到管道in中。
3rd:從管道in中讀取數據,並將該數據寫入管道out中,即把執行命令 ps-ef打印的結果作爲輸入提交給命令grep init執行。
三、有名管道FIFO
管道如果無名,只能在共同血緣進程中使用;管道如果有名,就可以在整個系統中使用。FIFO管道,有名的管道,它以一種特殊的文件類型存儲於文件系統中,以供血緣關係進程訪問。
1.有名管道的建立:
shelle命令和C程序都可以創建有名管道,其中創建有名管道的shell命令如下:
1)命令mknod創建管道
可以創建特殊類型的文件,其實用方式如下
/etc/mknod name [b|c ] major minor/*創建塊設備或字符設備文件*/
/etc/mknod name p /*創建管道文件*/
/etc/mknod name s /*創建信號量*/
/etc/mknod name m /*創建共享內存*/
參數name爲創建愛你的文件名稱,參數major和minor分別代表主、次設備
ex1:創建有名管道k1
$ mknod k1 p
$ls -l k1
prw-r--r-- 1 root sys 0 [date]
2)命令mkfifo創建管道
專門創建有名管道文件,它的語法如下:
mkfifo [-m Mode] File ...
其中參數Mode是管道文件創建後的訪問權限,File是管道文件創建後的名稱
ex1:創建一個用戶本身可讀寫,其它任何用戶都只讀的管道文件k2
mkfifo -m 644 k2
3)函數mkfifo創建管道
UNIX中的C語言,也提供了創建有名管道的函數,其原型如下:
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(char *path,mode_t mode);
其中path-->管道文件的路徑和名稱
mode-->管道文件的權限,類似open函數的第三個參數,並且自帶了O_CREAT 和O_EXCL選項,因此本函數只能創建一個不存在的管道文件,或者返回"文件已存在"錯誤。如果只是希望打開而不創建文件,請使用open或fopen。
成功調用mkfifo返回0,錯誤飯後-1。
2.有名管道的應用:
管道本身就是文件,因此對普通文件的操作也適合於管道文件,可以按照以下步驟應用管道。
1st:chuangjian guandao wenjian (mknod或mkfifo或者函數mkfifo)
2nd:讀進程
1)只讀打開管道文件(用open或fopen)
2)讀管道(應用read或fread等)
3rd:寫進程
1)只寫打開管道文件(open或fopen)
2)些管道(write或fwrite)
4th:關閉管道文件(close或fclose)
低級文件編程庫和標準文件編程庫都可以操作管道,在打開管道文件務必請先確認該管道是否存在和是否具備訪問權限。
管道在執行讀寫操作前,兩端必須同時打開,否則執行打開管道某端操作的進程將一直阻塞知道某個進程以相反方向打開管道位置。
一個雙進程讀寫管道的例子,寫進程創建FIFO文件,再打開其寫端口,然後讀取鍵盤輸入並將此輸入信息發送給管道中,當鍵盤輸入"exit"或"quit"時程序退出,如:
讀進程打開管道文件的讀端口,然後從管道中讀取信息,並將此信息打印到屏幕上,當從管道讀取到"exit"或"quit"時程序退出。
3.管道的模型:
1)1-1模型
2)n-1模型
3)n-n模型