Linux進程間通信——匿名管道

進程如果不是獨立進程,那麼它就需要和別的進程進行通信。在進程協作時可以採用共享一個緩衝區的方式來實現。當然,OS的IPC提供了一種機制,以允許不必通過共享地址空間來通信和同步其動作。這就不得不提Linux的的前身Unix。因爲Linux一開始就是從這兒借鑑的。加上Linux從一開始就遵守POSIX標準。

Unix最早是由AT&T的貝爾實驗室開發的,值得一提的是,在Unix操作系統發展的過程中,產生了許多副產物(POSIX標準也是副產物之一),其中最著名的應當是C語言。是的,它僅僅是個副產物。那個時候Ken Thompson 與Dennis Ritchie感到用彙編語言做移植太過於頭痛,他們想用高級語言來完成第三版。後來他們改造了B語言,就形成了今天大名鼎鼎的C語言。這個自發明到現在這個物聯網時代仍佔據編程語言榜前10的穩固位置。不得不感嘆其生命力的強大以及適應性的強大。當然,Ken Thompson 與Dennis Ritchie也是圖靈獎得主。

到了1980年,有兩個最主要的Unix的版本線,一個是UC Berkeley的BSD UNIX,另一個是AT&T的Unix。至今爲止UC Berkeley仍在維護Unix(這學校真牛逼)。

最初的Unix的IPC包括,管道,FIFO,信號。貝爾實驗室對Unix早期的進程通信進行了改進,形成了system V這個操作系統的IPC。它包括:system V消息隊列,system V信號燈,system V共享內存。當然POSIX IPC也有相應的一套。BSD Unix設計了socket(套接字)通信。這樣將進程之間的通信不僅僅限制在單機內。Linux繼承了這些。

進程間通信的目的:

  1. 數據傳輸:一個進程將數據發送給另一個進程
  2. 共享數據:多個進程操作共享數據(比如:售票系統),一個進程對共享數據進行了修改,另外一個進程應該立即看到,(否則票買完了,但是另一邊不知道,還在賣)
  3. 通知:一個進程告訴另外一個進程發生了某些事件。
  4. 資源共享
  5. 進程控制:一個進程控制另外一個進程的執行(例如debug程序)。它希望知道另一個進程的實時狀態。

Linux進程通信方式:

管道:管道(pipe)分爲無名管道和有名管道。無名管道用於具有親緣關係進程間的通信,有名管道則可以在任意的進程中間進行通信。

管道通信具有以下的特點:

  1. 管道是半雙工的。(雙向通信的,但是不能同時向雙方傳輸)
  2. 只能用於父子進程或者是兄弟進程之間(就是要具有親緣關係)
  3. 管道是一種文件(能讀寫),它只存在於內存之中。他是具有親緣關係的進程共享的。
  4. 寫入的內容每次都添加到管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀取數據。

Linux建立無名管道函數是pipe函數。它需要的頭文件是#include<unistd.h>.

函數原型:int pipe(int filedes[2]);

函數功能:pipe建立一個無名管道文件,若成功返回0,否則返回-1.錯誤原因由errno給出。管道文件的描述符由filedes數組返回。其中filedes[0]爲管道的讀取端,filedes[1]爲寫入端。

代碼測試如下:

#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
#include<linux/limits.h>        //這個頭文件中有PIPE_BUF


int main()
{
    int filedes[2];         //保存管道文件的文件描述符
    char str[30] = {"Hello World!"};
    char temp[30] = {0};

    if(0 != pipe(filedes))      //創建管道失敗
    {
        printf("errno=%d\n",errno);
        return 0;
    }
    if(0 < fork())          //父進程
    {
        close(filedes[0]);      //爲避免不必要的錯誤,關閉讀端
        write(filedes[1],str,strlen(str));
        close(filedes[1]);
        wait(NULL);         //回收子進程
        exit(0);
    }
    else
    {
        sleep(3);      //讓父進程先執行
        close(filedes[1]);      //爲避免不必要的錯誤,關閉寫端
        read(filedes[0],temp,strlen(str));
        close(filedes[0]);
        printf("%s\n",temp);
        exit(0);
    }
    
    return 0;
}

在讀寫管道文件的時候,最好是嚴格遵守文件的讀寫規則,在使用完畢後一定要關閉文件。爲了避免不必要的一些錯誤,在使用管道的文件的要先創建管道文件,然後創建新進程,這樣所有的進程才能共享這個管道文件。代碼中爲了避免向讀取端寫入和從寫入端讀取而引發的錯誤,在讀的時候關閉寫端,在寫的時候關閉讀端。

代碼中先讓父進程向管道文件中寫入了字符串“Hello World!”。然後子進程讀取管道文件中的字符串,並向屏幕打印。程序執行結果如下:

如果子進程讀取到的管道文件爲空,那麼read()函數將會使得進程阻塞,這時候父進程將會執行,然後完成對管道文件的寫入。之後wait()將父進程掛起,子進程完成讀取。同樣,管道已經滿時,進程再試圖寫管道,在其它進程從管道中移走數據之前,寫進程將一直阻塞。(典型的生產者——消費者模型)管道是存在於內存中的文件(實際上內核實現的一個隊列),他是進程的資源,會隨着進程的銷燬而銷燬。還有一點是管道中的東西在讀取後就會被刪除。管道文件有大小限制的,在我現在的內核版本下他是4KB。管道文件的大小由PIPE_BUF描述。它在#include<linux/limits.h>這個頭文件中給出。

#define PIPE_BUF        4096	/* # bytes in atomic write to a pipe */

向管道寫入數據的時候Linux不保證寫入的原子性,管道緩衝區一有空閒,寫進程就會去寫入。所以要及時讀取管道文件

同時管道還要求寫端對讀端的依賴性,示例代碼如下:

#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
#include<linux/limits.h>        //這個頭文件中有PIPE_BUF


int main()
{
    int filedes[2];         //保存管道文件的文件描述符
    char str[30] = {"Hello World!"};
    char temp[30] = {0};
    
    int num;
    if (0 < fork()) 
    {
        sleep(1);
        close(filedes[0]);      //關閉讀端
       num =  write(filedes[1],str,strlen(str));
       if(-1 == num)
       {
           printf("error!\n");
       }
       else
       {
           printf("write to pipe is %d\n",num);
       }
        close(filedes[1]);
        wait(NULL);
        //exit(0);
    }
    else
    {
        //子進程不讀,不寫,直接將管道文件兩端都關閉
        close(filedes[0]);
        close(filedes[1]);
        exit(0);
    }
    
    return 0;
}

輸出結果如下:

這個時候,在父進程中將無法寫入。所以管道這個描述還是很形象的,當你向一段水管裏面裝水的時候,需要將另一端堵上,否則裝入的水全都流走了。因此在父進程寫的時候,需要先關閉讀;在子進程讀的時候需要先關閉寫。同時,不能在沒有讀的情況下將管子兩頭堵上。

當子進程結束的時候,父進程關閉讀,調用write寫數據,這時候父進程將會收到子進程SIGPIPE信號,當前進程將會中斷,而不是阻塞。

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