Linux進程通信(一)管道

管道:

管道是進程間通信的主要手段之一。一個管道實際上就是個只存在於內存中的文件,對這個文件的操作要通過兩個已經打開文件進行,它們分別代表管道的兩端。管道是一種特殊的文件,它不屬於某一種文件系統,而是一種獨立的文件系統,有其自己的數據結構。根據管道的適用範圍將其分爲:無名管道和命名管道。

     無名管道

主要用於父進程與子進程之間,或者兩個兄弟進程之間。在linux系統中可以通過系統調用建立起一個單向的通信管道,且這種關係只能由父進程來建立。因此,每個管道都是單向的,當需要雙向通信時就需要建立起兩個管道。管道兩端的進程均將該管道看做一個文件,一個進程負責往管道中寫內容,而另一個從管道中讀取。這種傳輸遵循“先入先出”(FIFO)的規則。

   命名管道

命名管道是爲了解決無名管道只能用於近親進程之間通信的缺陷而設計的。命名管道是建立在實際的磁盤介質或文件系統(而不是隻存在於內存中)上有自己名字的文件,任何進程可以在任何時間通過文件名或路徑名與該文件建立聯繫。爲了實現命名管道,引入了一種新的文件類型——FIFO文件(遵循先進先出的原則)。實現一個命名管道實際上就是實現一個FIFO文件。命名管道一旦建立,之後它的讀、寫以及關閉操作都與普通管道完全相同。雖然FIFO文件的inode節點在磁盤上,但是僅是一個節點而已,文件的數據還是存在於內存緩衝頁面中,和普通管道相同。

實現機制:

管道是由內核管理的一個緩衝區,相當於我們放入內存中的一個紙條。管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。一個緩衝區不需要很大一般爲4K大小,它被設計成爲環形的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。

從原理上,管道利用fork機制建立,從而讓兩個進程可以連接到同一個PIPE上。最開始的時候,上面的兩個箭頭都連接在同一個進程Process 1上(連接在Process 1上的兩個箭頭)。當fork複製進程的時候,會將這兩個連接也複製到新的進程(Process 2)。隨後,每個進程關閉自己不需要的一個連接 (兩個黑色的箭頭被關閉; Process 1關閉從PIPE來的輸入連接,Process 2關閉輸出到PIPE的連接),這樣,剩下的紅色連接就構成了如上圖的PIPE。

詳細的管道創建過程如下圖:


實現細節:

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

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

 

關於管道的讀寫

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

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

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

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

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

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

Linux函數原型

#include <unistd.h>

int pipe(int filedes[2]);

filedes[0]用於讀出數據,讀取時必須關閉寫入端,即close(filedes[1]);

filedes[1]用於寫入數據,寫入時必須關閉讀取端,即close(filedes[0])。

程序實例:

複製代碼
int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];
   
    if(pipe(fd)  0){                 /* 先建立管道得到一對文件描述符 */
        exit(0);
    }

    if((pid = fork())  0)            /* 父進程把文件描述符複製給子進程 */
        exit(1);
    else if(pid > 0){                /* 父進程寫 */
        close(fd[0]);                /* 關閉讀描述符 */
        write(fd[1], "\nhello world\n", 14);
    }
    else{                            /* 子進程讀 */
        close(fd[1]);                /* 關閉寫端 */
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}
複製代碼

 

命名管道(named PIPE)

由於基於fork機制,所以管道只能用於父進程和子進程之間,或者擁有相同祖先的兩個子進程之間 (有親緣關係的進程之間)。爲了解決這一問題,Linux提供了FIFO方式連接進程。FIFO又叫做命名管道(named PIPE)。

FIFO (First in, First out)爲一種特殊的文件類型,它在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那麼內核就會在這兩個進程之間建立管道,所以FIFO實際上也由內核管理,不與硬盤打交道。之所以叫FIFO,是因爲管道本質上是一個先進先出的隊列數據結構,最早放入的數據被最先讀出來,從而保證信息交流的順序。FIFO只是借用了文件系統(file system,命名管道是一種特殊類型的文件,因爲Linux中所有事物都是文件,它在文件系統中以文件名的形式存在。)來爲管道命名。寫模式的進程向FIFO文件中寫入,而讀模式的進程從FIFO文件中讀出。當刪除FIFO文件時,管道連接也隨之消失。FIFO的好處在於我們可以通過文件的路徑來識別管道,從而讓沒有親緣關係的進程之間建立連接

函數原型:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );

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

 程序實例:

複製代碼
#include <stdio.h>  
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
      
int main()  
{  
    int res = mkfifo("/tmp/my_fifo", 0777);  
    if (res == 0)  
    {  
        printf("FIFO created/n");  
    }  
     exit(EXIT_SUCCESS);  
}  
複製代碼

編譯這個程序:

gcc –o fifo1.c fifo

 運行這個程序:

$ ./fifo1

 用ls命令查看所創建的管道

$ ls -lF /tmp/my_fifo

prwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|

注意:ls命令的輸出結果中的第一個字符爲p,表示這是一個管道。最後的|符號是由ls命令的-F選項添加的,它也表示是這是一個管道。

FIFO讀寫規則

從FIFO中讀取數據:

約定:如果一個進程爲了從FIFO中讀取數據而阻塞打開FIFO,那麼稱該進程內的讀操作爲設置了阻塞標誌的讀操作。

  • 如果有進程寫打開FIFO,且當前FIFO內沒有數據,則對於設置了阻塞標誌的讀操作來說,將一直阻塞。對於沒有設置阻塞標誌讀操作來說則返回-1,當前errno值爲EAGAIN,提醒以後再試。
  • 對於設置了阻塞標誌的讀操作說,造成阻塞的原因有兩種:當前FIFO內有數據,但有其它進程在讀這些數據;另外就是FIFO內沒有數據。解阻塞的原因則是FIFO中有新的數據寫入,不論信寫入數據量的大小,也不論讀操作請求多少數據量。
  • 讀打開的阻塞標誌只對本進程第一個讀操作施加作用,如果本進程內有多個讀操作序列,則在第一個讀操作被喚醒並完成讀操作後,其它將要執行的讀操作將不再阻塞,即使在執行讀操作時,FIFO中沒有數據也一樣(此時,讀操作返回0)。
  • 如果沒有進程寫打開FIFO,則設置了阻塞標誌的讀操作會阻塞。

注:如果FIFO中有數據,則設置了阻塞標誌的讀操作不會因爲FIFO中的字節數小於請求讀的字節數而阻塞,此時,讀操作會返回FIFO中現有的數據量。

向FIFO中寫入數據:

約定:如果一個進程爲了向FIFO中寫入數據而阻塞打開FIFO,那麼稱該進程內的寫操作爲設置了阻塞標誌的寫操作。

對於設置了阻塞標誌的寫操作:

  • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果此時管道空閒緩衝區不足以容納要寫入的字節數,則進入睡眠,直到當緩衝區中能夠容納要寫入的字節數時,纔開始進行一次性寫操作。
  • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。FIFO緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據,寫操作在寫完所有請求寫的數據後返回。

對於沒有設置阻塞標誌的寫操作:

  • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。在寫滿所有FIFO空閒緩衝區後,寫操作返回。
  • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果當前FIFO空閒緩衝區能夠容納請求寫入的字節數,寫完後成功返回;如果當前FIFO空閒緩衝區不能夠容納請求寫入的字節數,則返回EAGAIN錯誤,提醒以後再寫;

參考資料:

http://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html

https://www.ibm.com/developerworks/cn/linux/l-ipc/part1/



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