Linux處理序間通訊之管道(pipe)、具名管道(FIFO)與訊號(Signal)

來源:互聯網
上載者:User

整理自網路

Unix IPC包括:管道(pipe)、具名管道(FIFO)與訊號(Signal)

 

管道(pipe)

管道可用於具有親緣關係進程間的通訊,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通訊;

實現機制:

管道是由核心管理的一個緩衝區,相當於我們放入記憶體中的一個紙條。管道的一端串連一個進程的輸出。這個進程會向管道中放入資訊。管道的另一端串連一個進程的輸入,這個進程取出被放入管道的資訊。一個緩衝區不需要很大,它被設計成為環形的資料結構,以便管道可以被迴圈利用。當管道中沒有資訊的話,從管道中讀取的進程會等待,直到另一端的進程放入資訊。當管道被放滿資訊的時候,嘗試放入資訊的進程會等待,直到另一端的進程取出資訊。當兩個進程都終結的時候,管道也自動消失。

從原理上,管道利用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讀寫規則

1.從FIFO中讀取資料: 約定:如果一個進程為了從FIFO中讀取資料而阻塞開啟了FIFO,那麼稱該進程內的讀操作為設定了阻塞標誌的讀操作

2.從FIFO中寫入資料: 約定:如果一個進程為了向FIFO中寫入資料而阻塞開啟FIFO,那麼稱該進程內的寫操作為設定了阻塞標誌的寫操作。

詳見:http://blog.csdn.net/MONKEY_D_MENG/article/details/5570468

 

 

訊號(Signal)

訊號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於處理序間通訊外,進程還可以發送訊號給進程本身;Linux除了支援Unix早期訊號
語義函數sigal外,還支援語義符合Posix.1標準的訊號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠訊號機制,又
能夠統一對外介面,用sigaction函數重新實現了signal函數)

訊號種類

每種訊號類型都有對應的訊號處理常式(也叫訊號的操作),就好像每個中斷都有一個插斷服務常式一樣。大多數訊號的預設操作是結束接收訊號的進程;然而,一個進程通常可以請求系統採取某些代替的操作,各種代替操作是:

  • 忽略訊號。隨著這一選項的設定,進程將忽略訊號的出現。有兩個訊號  不可以被忽略:SIGKILL,它將結束進程;SIGSTOP,它是作業控制機制的一部分,將掛起作業的執行。
  • 恢複訊號的預設操作。
  • 執行一個預先安排的訊號處理函數。進程可以登記特殊的訊號處理函數。當進程收到訊號時,訊號處理函數將像插斷服務常式一樣被調用,當從該訊號處理函數返回時,控制被返回給主程式,並且繼續正常執行。

但是,訊號和中斷有所不同。中斷的響應和處理都發生在核心空間,而訊號的響應發生在核心空間,訊號處理常式的執行卻發生在使用者空間。

那麼,什麼時候檢測和響應訊號呢?通常發生在兩種情況下:

  • 當前進程由於系統調用、中斷或異常而進入核心空間以後,從核心空間返回到使用者空間前夕;
  • 當前進程在核心中進入睡眠以後剛被喚醒的時候,由於檢測到訊號的存在而提前返回到使用者空間。

函數原型等詳見:http://www.cnblogs.com/biyeymyhjob/archive/2012/08/04/2622265.html

訊號本質

訊號是在軟體層次上對中斷機制的一種類比,在原理上,一個進程收到一個訊號與處理器收到一個插斷要求可以說是一樣的。訊號是非同步,一個進程不必通過任何操作來等待訊號的到達,事實上,進程也不知道訊號到底什麼時候到達。

訊號是處理序間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的進程有哪些事情發生了。訊號機制經過POSIX即時擴充後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。

訊號來源

訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用發送訊號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟體來源還包括一些非法運算等操作。

 

關於訊號處理機制的原理(核心角度)

核心給一個進程發送非強制中斷訊號的方法,是在進程所在的進程表項的訊號網域設定對應於該訊號的位。這裡要補充的是,如果訊號發送給一個正在睡眠的進程,那麼要 看該進程進入睡眠的優先順序,如果進程睡眠在可被中斷的優先順序上,則喚醒進程;否則僅設定進程表中訊號域相應的位,而不喚醒進程。這一點比較重要,因為進程檢查是否收到訊號的時機是:一個進程在即將從核心態返回到使用者態時;或者,在一個進程要進入或離開一個適當的低調度優先順序睡眠狀態時。    

核心處理一個進程收到的訊號的時機是在一個進程從核心態返回使用者態時。所以,當一個進程在核心態下運行時,非強制中斷訊號並不立即起作用,要等到將返回使用者態時才處理。進程只有處理完訊號才會返回使用者態(上面的例子程式中,在步驟5中,解除阻塞後,先列印caught SIGQUIT,再列印SIGQUIT unblocked,即在sigprocmask返回前,訊號處理常式先執行),進程在使用者態下不會有未處理完的訊號。    

核心處理一個進程收到的非強制中斷訊號是在該進程的上下文中,因此,進程必須處於運行狀態。如果進程收到一個要捕捉的訊號,那麼進程從核心態返回使用者態時執行使用者定義的函數。而且執行使用者定義的函數的方法很巧妙,核心是在使用者棧上建立一個新的層,該層中將返回地址的值設定成使用者定義的處理函數的地址,這樣進程從核心返回彈出棧頂時就返回到使用者定義的函數處,從函數返回再彈出棧頂時,才返回原先進入核心的地方,接著原來的地方繼續運行。這樣做的原因是使用者定義的處理函數不能且不允許在核心態下執行(如果使用者定義的函數在核心態下啟動並執行話,使用者就可以獲得任何許可權)。

在訊號的處理方法中有幾點特別要引起注意。    

第一,在一些系統中,當一個進程處理完中斷訊號返回使用者態之前,核心清除使用者區中設定的對該訊號的處理常式的地址,即下一次進程對該訊號的處理方法又改為預設值,除非在下一次訊號到來之前再次使用signal系統調用。這可能會使得進程在調用signal之前又得 到該訊號而導致退出。在BSD中,核心不再清除該地址。但不清除該地址可能使得進程因為過多過快的得到某個訊號而導致堆疊溢位。為了避免出現上述情況。在 BSD系統中,核心類比了對硬體中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。    

第二個要引起注意的是,如果要捕捉的訊號發生於進程正在一個系統調用中時,並且該進程睡眠在可中斷的優先順序上(若系統調用未睡眠而是在運行,根據上面的分 析,等該系統調用運行完畢後再處理訊號),這時該訊號引起進程作一次longjmp,跳出睡眠狀態,返回使用者態並執行訊號處理常式。當從訊號處理常式返回 時,進程就象從系統調用返回一樣,但返回了一個錯誤如-1,並將errno設定為EINTR,指出該次系統調用曾經被中斷。這要注意的是,BSD系統中內 核可以自動地重新開始系統調用,或者手如上面所述手動設定重啟。    

第三個要注意的地方:若進程睡眠在可中斷的優先順序上,則當它收到一個要忽略的訊號時,該進程被喚醒,但不做longjmp,一般是繼續睡眠。但使用者感覺不 到進程曾經被喚醒,而是象沒有發生過該訊號一樣。所以能夠使pause、sleep等函數從掛起態返回的訊號必須要有訊號處理函數,如果沒有什麼動作,可 以將處理函數設為空白。    

第四個要注意的地方:核心對子進程終止(SIGCLD)訊號的處理方法與其他訊號有所區別。當進程正常或異常終止時,核心都向其父進程發一個SIGCLD 訊號,預設情況下,父進程忽略該訊號,就象沒有收到該訊號似的,如果父進程希望獲得子進程終止的狀態,則應該事先用signal函數為SIGCLD訊號設 置訊號處理常式,在訊號處理常式中調用wait。

SIGCLD訊號的作用是喚醒一個睡眠在可被中斷優先順序上的進程。如果該進程捕捉了這個訊號,就象普通訊號處理一樣轉到處理常式。如果進程忽略該訊號,則 什麼也不做。其實wait不一定放在訊號處理函數中,但這樣的話因為不知道子進程何時終止,在子進程終止前,wait將使父進程掛起休眠。

訊號生命週期

參考資料:

http://www.cnblogs.com/vamei/archive/2012/10/10/2715398.html

http://bbs.chinaunix.net/thread-1947211-1-1.html

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.