進程通信方法之一--管道(的容量與實現)

一、進程通信

1、進程間通信的作用

        進程間需要數據傳輸、資源共享和事件通知。

2、進程間通信的方式

進程間通信主要包括管道,系統ipc(包括消息隊列,信號量,共享存儲),socket。

        管道通信(無名管道和命名管道)

        信號通信

        內存資源共享

        消息隊列

       信號量

集合上述兩種從物理和內容方式的劃分,可以這樣理解上圖:
(1)同主機進程間數據交互機制:無名管道(PIPE)、有名管道(FIFO)、消息隊列(Message Queue)和共享內存(Shared Memory)。
(2)同主機進程間同步通信機制:信號量(Semaphore)。
(3)同主機進程間異步通信機制:信號(Signal)。
(4)不同主機間進程數據交互機制:套接字(Socket)、遠程調用RPC(Remote Procedure Call)。

進程通信方式如下圖:


二、管道

       管道是單向的、先進先出的,它把一個進程的輸出和另一個進程的輸入連接在一起。一個進程(寫進程)在管道的尾部寫入數據,另一個進程(讀進程)在管道的頭部讀出數據。數據被一個進程讀出後,將被從管道中刪除,其它讀進程將再不能讀到這些數據。管道提供了簡單的流控制機制,進程試圖讀空管道時,進程將阻塞。同樣,管道已經滿時,進程再試圖向管道寫入數據,進程將阻塞。

1、管道的實現原理

(1 )Linux管道的實現機制

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

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

·      讀取進程也可能工作得比寫進程快。當所有當前進程數據已被讀取時,管道變空。當這種情況發生時,一個隨後的read()調用將默認地被阻塞,等待某些數據被寫入,這解決了read()調用返回文件結束的問題。

注意:從管道讀數據是一次性操作,數據一旦被讀,它就從管道中被拋棄,釋放空間以便寫更多的數據。

2)管道的結構

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

 

 

 

 

 

 

 

 

 

 

 

 

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

(3)管道的讀寫

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

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

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

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

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

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

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

<1>管道創建

       管道包括無名管道有名管道兩種,前者用於父進程和子進程之間的通信,後者可用於運行同一系統中任意兩個進程間的通信。

       無名管道由pipe()函數創建:

       int pipe(int pipefiledis[2])

       當一個管道創建時,它會建立兩個文件描述符:

 filedis[0]用於讀管道,filedis[1]用於寫管道。

<2>管道關閉

        管道關閉只需將這兩個文件描述符關閉即可,可以使用普通的close函數進行關閉。
<3>管道的讀寫

 1.無名管道(用於父、子進程間的通信)

       管道用於不同進程間的通信。通常先創建一個管道,再通過fork函數創建一個子進程,該子進程會繼承父進程所創建的管道。

注:必須在系統調用fork()前調用pipe(),否則子進程將不會繼承文件描述符。

2.命名管道(用於任意兩個進程間的通信)

2.1 創建

#include<sys/types.h>

#include<sys/stat.h>

int mkfifo(const char *pathname,mode_t mode)

pathname:FIFO文件名

mode:屬性

一旦創建了FIFO就可以用open打開它,一般的訪問函數(close、write、read等)都可以用於FIFO。

實質上來講命名管道就是一個文件

2.2 操作

     當FIFO打開時,非阻塞標誌(O_NONBLOCK)

     將對以後的讀寫產生如下的影響:

     1.沒有使用O_NONBLOCK:訪問要求無法滿足時進程將阻塞。如試圖讀取空的FIFO,將導致進程阻塞;

     2.使用O_NONBLOCK:訪問要求無法滿足時不阻塞,立刻出錯返回,errno是ENXIO。

管道只是在內核內存中保持的一個緩衝區。這個緩衝區有一個最大值容量。一旦一個管道是充分的,進一步寫入到管塊,直到讀者刪除來自管道的一些數據。

2、管道容量(pipe capacity)

下面我們用代碼進行管道容量的探究

當管道滿時,

O_NONBLOCK discable: write調用阻塞,直到有程序讀走數據

O_NONBLOCK enable:調用返回-1,errno值爲EAGAIN

管道是一塊內存緩衝區,可寫下面程序測試管道的容量pipe capacity:

  1 #include <stdio.h>                                                                                                       
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <unistd.h>
  5 #include <stdlib.h>
  6 #include <fcntl.h>
  7 #include <error.h>
  8 #include <string.h>
  9 #include <signal.h>
 10 #include <errno.h>
 11 #define ERR_EXIT(m)\
 12     do{\
 13         perror(m);\
 14         exit(EXIT_FAILURE);\
 15     }while(0) 
 16 int main()
 17 {
 18     int pipefd[2];
 19     if(pipe(pipefd)==-1)
 20     {
 21         ERR_EXIT("pipe error");
 22     }
 23     int ret;
 24     int count=0;
 25     int flags=fcntl(pipefd[1],F_GETFL);
 26     fcntl(pipefd[1],F_SETFL,flags|O_NONBLOCK);//feizuse
 27     while(1)
 28     {
 29         ret=write(pipefd[1],"A",1);
 30         if(ret==-1)
 31         {
 32             printf("err=%s\n",strerror(errno));
 33             break;
 34         }
 35 
 36         count++;
 37     }
 38     printf("count=%d\n",count);//guandaorongliang
 39     return 0;
 40 }   

程序中將寫端文件描述符設置爲非阻塞,當管道被寫滿時不會等待其他進程讀取數據,而是直接返回-1並置errno,結果如下:


如此可見,管道的容量及pipe capacity爲65536。APUE中說PIPE_BUF定義了內核管道緩衝區的大小,linux中爲4096 byte,而TLPI中說,管道是維持在內核內存中的一個緩衝區,自Linux 2.6.11開始,該緩衝區大小爲65536 byte

注意:

在次區別pipe_buf和pipe capacity:

pipe_buf指原子操作的最大值,而pipe capacity指管道的最大值,即容量;管道的大小,也就是確定pipe_buf的值,這個值由內核設定,




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