管道

7.1 管道

在進程之間通信的最簡單的方法是通過一個文件,其中有一個進程寫文件,而另一個進程從文件中讀,這種方法比較簡單,其優點體現在:

只要進程對該文件具有訪問權限,那麼,兩個進程間就可以進行通信。

進程之間傳遞的數據量可以非常大。

儘管如此,使用文件進行進程間通信也有兩大缺點:

·空間的浪費。寫進程只有確保把新數據加到文件的尾部,才能使讀進程讀到數據,對長時間存在 的進程來說,這就可能使文件變得非常大。
·時間的浪費。如果讀進程讀數據比寫進程寫數據快,那麼,就可能出現讀進程不斷地讀文件尾部, 使讀進程做很多無用功。

要克服以上缺點而又使進程間的通信相對簡單,管道是一種較好的選擇。

所謂管道,是指用於連接一個讀進程和一個寫進程,以實現它們之間通信的共享文件,又稱pipe文件。向管道(共享文件)提供輸入的發送進程(即寫進程), 以字符流形式將大量的數據送入管道;而接受管道輸出的接收進程(即讀進程),可從管道中接收數據。由於發送進程和接收進程是利用管道進行通信的,故又稱管 道通信。這種方式首創於Unix系統,因它能傳送大量的數據,且很有效,故很多操作系統都引入了這種通信方式,Linux也不例外。

爲了協調雙方的通信,管道通信機制必須提供以下三方面的協調能力:
·互斥。當一個進程正在對pipe進行讀/寫操作時,另一個進程必須等待。
·同步。當寫(輸入)進程把一定數量(如4KB)數據寫入pipe後,便去睡眠等待,直到讀(輸出)進程取走數據後,再把它喚醒。當讀進程讀到一空pipe時,也應睡眠等待,直至寫進程將數據寫入管道後,纔將它喚醒。
·對方是否存在。只有確定對方已存在時,方能進行通信。

7.1.1 Linux管道的實現機制

在Linux中,管道是一種使用非常頻繁的通信機制。從本質上說,管道也是一種文件,但它又和一般的文件有所不同,管道可以克服使用文件進行通信的兩個問題,具體表現爲:

·限制管道的大小。實際上,管道是一個固定大小的緩衝區。在Linux中,該緩衝區的大小爲1頁,即4K字節,使得它的大小不象文件那樣不加檢驗地增長。 使用單個固定緩衝區也會帶來問題,比如在寫管道時可能變滿,當這種情況發生時,隨後對管道的write()調用將默認地被阻塞,等待某些數據被讀取,以便 騰出足夠的空間供write()調用寫。

·讀取進程也可能工作得比寫進程快。當所有當前進程數據已被讀取時,管道變空。當這種情況發生時,一個隨後的read()調用將默認地被阻塞,等待某些數據被寫入,這解決了read()調用返回文件結束的問題。
注意:從管道讀數據是一次性操作,數據一旦被讀,它就從管道中被拋棄,釋放空間以便寫更多的數據。

1. 管道的結構
在 Linux 中,管道的實現並沒有使用專門的數據結構,而是藉助了文件系統的file結構和VFS的索引節點
inode。通過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。如圖 7.1所示。

 圖7.1中有兩個 file 數據結構,但它們定義文件操作例程地址是不同的,其中一個是向管道中寫入數據的例 程地址,而另一個是從管道中讀出數據的例程地址。這樣,用戶程序的系統調用仍然是通常的文件操作,而內核卻利用這種抽象機制實現了管道這一特殊操作。

2.管道的讀寫
管道實現的源代碼在fs/pipe.c中,在pipe.c中有很多函數,其中有兩個函數比較重要,即管道讀函數
pipe_read()和管道寫函數pipe_wrtie()。管道寫函數通過將字節複製到 VFS 索引節點指向的物理內存而寫入數據,而管道讀函數則通過複製物理內存中的字節而讀出數據。當然,內核必須利用一定的機制同步對管道的訪問,爲此,內核使用 了鎖、等待隊列和信號。

當寫進程向管道中寫入時,它利用標準的庫函數write(),系統根據庫函數傳遞的文件描述符,可找到該文件的 file 結構。file 結構中指定了用來進行寫操作的函數(即寫入函數)地址,於是,內核調用該函數完成寫操作。寫入函數在向內存中寫入數據之前,必須首先檢查 VFS 索引節點中的信息,同時滿足如下條件時,才能進行實際的內存複製工作:

·內存中有足夠的空間可容納所有要寫入的數據;

·內存沒有被讀程序鎖定。

如果同時滿足上述條件,寫入函數首先鎖定內存,然後從寫進程的地址空間中複製數據到內存。否則,寫入進程就休眠在 VFS 索引節點的等待隊列中,接下來,內核將調用調度程序,而調度程序會選擇其他進程運行。 寫入進程實際處於可中斷的等待狀態,當內存中有足夠的空間可以容納寫入數據,或內存被解鎖時,讀取進程會喚醒寫入進程,這時,寫入進程將接收到信號。當數 據寫入內存之後,內存被解鎖,而所有休眠在索引節點的讀取進程會被喚醒。

管道的讀取過程和寫入過程類似。但是,進程可以在沒有數據或內存被鎖定時立即返回錯誤信息,而不是阻塞該進程,這依賴於文件或管道的打開模式。反之,進程 可以休眠在索引節點的等待隊列中等待寫入進程寫入數據。當所有的進程完成了管道操作之後,管道的索引節點被丟棄,而共享數據頁也被釋放。

因爲管道的實現涉及很多文件的操作,因此,當讀者學完有關文件系統的內容後來讀pipe.c中的代碼,你會覺得並不難理解。

7.1.2 管道的應用

管道是利用pipe()系統調用而不是利用open()系統調用建立的。pipe()調用的原型是:

 

int pipe(int fd[2])


我們看到,有兩個文件描述符與管道結合在一起,一個文件描述符用於管道的read()端,一個文件描述符用於管道的write()端。由於一個函數調用不能返回兩個值,pipe()的參數是指向兩個元素的整型數組的指針,它將由調用兩個所要求的文件描述符填入。

fd[0]元素將含有管道read()端的文件描述符,而fd[1]含有管道write()端的文件描述符。系統可根據fd[0]和
fd[1]分別找到對應的file 結構。在第8章我們會描述pipe()系統調用的實現機制。

注意,在pipe的參數中,沒有路徑名,這表明,創建管道並不象創建文件一樣,要爲它創建一個目錄連接。

這樣做的好處是,其它現存的進程無法得到該管道的文件描述符,從而不能訪問它。那麼,兩個進程如何使用一個管道來通信呢?

我們知道,fork()和exec()系統調用可以保證文件描述符的複製品既可供雙親進程使用,也可供它的子女進程使用。也就是說,一個進程用 pipe()系統調用創建管道,然後用fork()調用創建一個或多個進程,那麼,管道的文件描述符將可供所有這些進程使用。pipe()系統調用的具體 實現將在下一章介紹。

這裏更明確的含義是:一個普通的管道僅可供具有共同祖先的兩個進程之間共享,並且這個祖先必須已經建立了供它們使用的管道。

注意:在管道中的數據始終以和寫數據相同的次序來進行讀,這表示lseek()系統調用對管道不起作用。

下面給出在兩個進程之間設置和使用管道的簡單程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, world!n";
char readbuffer[80];
pipe(fd);
if((childpid = fork()) == -1)

{
printf("Error:fork");

exit(1);
}
if(childpid == 0) /* 子進程是管道的寫進程 */

{
close(fd[0]); /*關閉管道的讀端 */
write(fd[1], string, strlen(string));
exit(0);
}

else /* 父進程是管道的讀進程 */
{

close(fd[1]); /*關閉管道的寫端 */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
printf("Received string: %s", readbuffer);
}
return(0);
}


注意,在這個例子中,爲什麼這兩個進程都關閉它所不需的管道端呢?這是因爲寫進程完全關閉管道端時,文件結束的條件被正確地傳遞給讀進程。而讀進程完全關閉管道端時,寫進程無須等待繼續寫數據。



阻塞讀和寫分別成爲對空和滿管道的默認操作,這些默認操作也可以改變,這就需要調用fcntl()系統調用, 對管道文件描述符設置O_NONBLOCK標誌可以忽略默認操作:



# include <fcntl.h>

fcntl(fd,F_SETFL,O_NONBlOCK);



7.1.3 命名管道(FIFO)

Linux 還支持另外一種管道形式,稱爲命名管道,或 FIFO,這是因爲這種管道的操作方式基於“先進先出”原理。上面講述的管道類型也被稱爲“匿名管道”。命名管道中,首先寫入管道的數據是首先被讀出的數據。


匿名管道是臨時對象,而 FIFO 則是文件系統的真正實體,如果進程有足夠的權限就可以使用 FIFO。FIFO 和匿名管道的數據結構以及操作極其類似,二者的主要區別在於,FIFO 在使用之前就已經存在,用戶可打開或關閉 FIFO;而匿名管道只在操作時存在,因而是臨時對象。



爲了創建先進先出文件,可以從shell提示符使用mknod命令或可以在程序中使用mknod()系統調用。



mknod()系統調用的原型爲:

#include <sys/type.h>
#inlcude <sys/state.h>
#include <fcntl.h>
#include <unistd.h>
int mknod(char *pathname,node_t mode, dev_t dev);


其中pathname是被創建的文件名稱,mode表示將在該文件上設置的權限位和將被創建的文件類型(在此情況下爲S_IFIFO),dev是當創建設備特殊文件時使用的一個值。因此,對於先進先出文件它的值爲0。



一旦先進先出文件已經被創建,它可以由任何具有適當權限的進程利用標準的open()系統調用加以訪問。當用open()調用打開時,一個先進先出文件和 一個匿名管道具有同樣的基本功能。即當管道是空的時候,read()調用被阻塞。當管道是滿的時候,write()等待被阻塞,並且當用fcntl()設 置O_NONBLOCK標誌時,將引起read() 調用和write()調用立即返回。在它們已被阻塞的情況下,帶有一個EAGAIN錯誤信息。



由於命名管道可以被很多無關係的進程同時訪問,那麼,在有多個讀進程和/或多個寫進程的應用中使用FIFO 是非常有用的。



多個進程寫一個管道會出現這樣的問題,即多個進程所寫的數據混在一起怎麼辦?幸好系統有這樣的規則:一個write()調用可以寫管道能容納(Linux 爲4K字節)的任意個字節,系統將保證這些數據是分開的。這表示多個寫操作的數據在FIFO文件中並不混合而將被維持分離的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章