進程-IPC 管道 (一)

詳見:https://github.com/ZhangzheBJUT/linux/blob/master/IPC(%E4%B8%80).md

一 IPC 概述

進程間通信就是在不同進程之間傳播或交換信息,那麼不同進程之間存在着什麼雙方都可以訪問的介質呢?進程的用戶空間是互相獨立的,一般而言是不能互相訪問的,唯一的例外是共享內存區。系統空間是“公共場所”,所以內核顯然可以提供這樣的條件,如下圖所示。除此以外,那就是雙方都可以訪問的外設了,兩個進程可以通過磁盤上的普通文件交換信息,或者通過“註冊表”或其它數據庫中的某些表項和記錄交換信息。
                                              

linux下的進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟件發佈中心)。它們在進程間通信方面的側重點有所不同,前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了 system V IPC,通信進程侷限在單個計算機內;後者則跳過了該限制,形成了基於套接字 socket的進程間通信機制。

Linux 則把兩者繼承了下來,如下圖示:
                     

Unix  IPC包括: 管道、FIFO、信號;                                                                                
System V IPC包括:System V消息隊列、System V信號燈、System V共享內存區;                                            
Posix IPC包括: Posix消息隊列、Posix信號燈、Posix共享內存區。                                                       

有兩點需要簡單說明一下:                                                                                          
    1.由於Unix版本的多樣性,電子電氣工程協會(IEEE)開發了一個獨立的Unix標準,這個新的ANSI Unix標準被稱爲計算
      機環境的可移植性操作系統界面(PSOIX)。現有大部分Unix和流行版本都是遵循POSIX標準的,而Linux從一開始就遵循POSIX標準;     
    2.BSD並不是沒有涉足單機內的進程間通信(socket本身就可以用於單機內的進程間通信)。事實上,很多Unix版本的單
      機IPC留有BSD的痕跡,如4.4BSD支持的匿名內存映射、4.3+BSD對可靠信號語義的實現等等。

上圖給出了linux 所支持的各種IPC手段,爲了避免概念上的混淆,在儘可能少提及Unix的各個版本的情況下,所有問題的討論最終都會歸結到linux環境下的進程間通信上來。並且,對於linux所支持通信手段的不同實現版本(如對於共享內存來說,有Posix共享內存區以及System V共享內存區兩個實現版本)下面將主要介紹Posix API。

linux下進程間通信的幾種主要手段

  • 管道(Pipe)及命名管道(named pipe):管道可用於具有親緣關係進程間的通信;命名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信;
  • 信號(Signal):信號是比較複雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;linux 除了支持Unix早期信號語義函數signal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數);
  • 信號量(semaphore):主要作爲進程間以及同一進程不同線程之間的同步手段。
  • 共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式,是針對其它通信機制運行效率較低而設計的。它往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
  • 消息隊列:消息隊列是消息的鏈接表,包括Posix消息隊列和system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。
  • 套接字(Socket):更爲一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上,Linux和System V的變種都支持套接字。

二 管道

當從一個進程連接數據流到另一個進程時,使用術語管道(pipe)。通常是把一個進程的輸出通過管道連接到另一個進程的輸入。
對於shell命令來說,命令的連接是通過管道操作符來完成的,如下所示:

  cmd1 | cmd2    

  shell負責安排兩個命令的標準輸入和標準輸出
  cmd1的標準輸入來自終端鍵盤  
  cmd1的標準輸出傳遞給cmd2,作爲它的標準輸入  
  cmd2的標準輸出連接到終端屏幕

               
shell所做的工作實際上是對標準輸入和標準輸出流進行重新連接,使數據流從鍵盤輸入通過兩個命令最終輸出到屏幕上。

2.1.poen與pclose函數

函數原型:

#include <stdio.h>  
FILE *popen(const char*command,const char *open_mode);  
int pclose(FILE *stream_to_close);

函數描述:

popen      函數允許一個程序將另一個程序作爲新進程來啓動,並可以傳遞數據給它或者通過它來接收數據。
command    字符串是要運行的程序名和相應的參數,這個命令被送到 /bin/sh 以 -c 參數執行, 即由 shell來執行。
open_mode  必須爲"r"或者"w",二者只能選擇一個,函數的返回值FILE*文件流指針,通過常用的stdio庫函數
           (如fread)來讀取被調用程序的輸出。如果open_mode是"w",調用程序就可以用fwrite調用向被調用程序發送
           數據,而被調用程序可以在自己的標準輸入上讀取數據。

補:/bin/sh -c   
        Read commands from the command_string operand instead of from the standard input. 
        Special  parameter 0 will be  set from the command_name operand and the positional 
        parameters ($1, $2, etc.) set from the remaining argument operands.

讀取外部程序的輸出:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main()
    {
        FILE *read_fp;
        char buffer[BUFSIZ+1];
        int chars_read;

        memset(buffer,'\0',sizeof(buffer));
        read_fp = popen("uname -a","r");
        if (read_fp !=NULL)
        {
            chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
            if (chars_read>0)
            {
                printf("output was:-\n%s\n",buffer);
            }

            pclose(read_fp);
            exit(EXIT_SUCCESS);
        }
        exit(EXIT_FAILURE);
    }

將輸出發送到外部程序:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main()
    {
        FILE *write_fp;
        char buffer[BUFSIZ+1];

        sprintf(buffer,"Once upon a time ,thera was ...\n");
        write_fp = popen("od -c","w");

        if (write_fp != NULL)   
        {
            fwrite(buffer,sizeof(char),strlen(buffer),write_fp);
            pclose(write_fp);
            exit(EXIT_SUCCESS);
        }
        exit(EXIT_FAILURE);
    }

注意:popen()函數的返回值是一個普通的標準I/O流,且它只能用pclose()函數來關閉,而不是fclose()。

popen函數運行一個程序時,它首先啓動shell,即系統中的sh命令,然後將command字符串作爲一個參數傳遞給它,由shell來負責分析命令字符串,它允許我們通過popen啓動非常複雜的shell命令。使用shell的一個不太好的影響是,針對每個popen的調用,不僅要啓動一個被請求的程序,還要啓動一個shell,即每個popen調用將多啓動兩個進程,從節省資源的角度來看,popen函數的調用成本略高,而且對目標命令的調用比正常方式要慢一些。 pclose調用只在popen啓動的進程結束後才返回。如果調用pclose時它仍在運行,pclose調用將等待該進程的結束。

2.2.pipe函數

底層pipe函數,通過這個函數在兩個程序之間傳遞數據不需要啓動一個shell來解釋請求命令,它同時還提供了對讀寫數據的更多控制。

函數原型:

    #include <unistd.h>  
    int pipe(int file_descriptor[2]);

函數描述:

pipe 函數參數是一個由兩個整數類型的文件描述符組成的數組的指針。該函數在數組中填上兩個新的文件描述符後返回0,如果
失敗則返回-1,並設置error來表明失敗的原因。  
 常見的錯誤:
      EMFILE:進程使用的文件描述符過多
      ENFILE:系統的文件表已滿
      EFAULT:文件描述符無效
兩個返回的文件描述符以一種特殊的方式連接起來,寫到file_descriptor[1]的所有數據都可以從file_descriptor[0]讀出來。
數據基於先進先出的原則進行處理,意味着如果你把1,2,3寫到file_descriptor[1],從file_descriptior[0]讀取到的數據也
是1,2,3。

注意:調用pipe函數時在內核中開闢一塊緩衝區(稱爲管道)用於通信,它有一個讀端一個寫端,然後通過file_descriptor 參數傳出給用戶程序兩個文件描述符,file_descriptor[0]指向管道的讀端,file_descriptor[1]指向管道的寫端。在用戶程序看起來管道就像一個打開的文件,通過read(file_descriptor[0])或者write(file_descriptor[1])來向這個文件讀寫數據,其實是在讀寫內核緩衝區。兩個文件描述符被強制規定file_descriptor[0] 只能指向管道的讀端,如果進行寫操作就會出現錯誤;同理 file_descriptor[1]只能指向管道的寫端,如果進行讀操作就會出現錯誤。pipe使用的是文件描述符而不是文件流,所以必須使用底層的read和write調用來訪問數據,而不是用文件流函數fread和fwrite。

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>

    int main()
    {
        int data_processed;
        int file_pipes[2];
        const char some_data[] = "1233";
        char buffer[BUFSIZ+1];

        memset(buffer,'\0',sizeof(buffer));

        if (pipe(file_pipes)==0)
        {
            data_processed = write(file_pipes[1],some_data,strlen(some_data));
            printf("Wrote %d bytes \n",data_processed);

            data_processed = read(file_pipes[0],buffer,BUFSIZ);
            printf("Read %d bytes \n",data_processed);
            exit(EXIT_SUCCESS);
        }
        exit(EXIT_FAILURE);
    }

程序說明: 這個程序用數組 file_pipes 中的兩個文描述符創建一個管道,然後它用文件描述符 file_pipes[1] 向管道中寫數據 ,再從 file_pipes[0] 讀回數據。

管道的真正優勢體現在:兩個進程之間傳遞數據。 當程序用fork調用創建新進程時,原先打開的文件描述符仍將保持打開狀態。如果在原先的進程中創建一個管道,然後再調用fork創建新進程,即可以通過管道在兩個進程之間傳遞數據,如下圖所示:

         

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>


int main()
{
    int data_processed;
    int file_pipes[2];

    const char some_data[] = "1233";
    char buffer[BUFSIZ+1];
    pid_t fork_result;


    memset(buffer,'\0',sizeof(buffer));

    if (pipe(file_pipes)==0)
    {
        fork_result  = fork();

        if (fork_result ==-1)
        {
            fprintf(stderr,"Fork failed");
            exit(EXIT_FAILURE);
        }

        if (fork_result ==0)
        {
            close(file_pipes[1]);
            sleep(2);
            data_processed = read(file_pipes[0],buffer,BUFSIZ);
            printf("Read %d bytes :%s \n",data_processed,buffer);


        }
        else
        {
            close(file_pipes[0]);
            data_processed = write(file_pipes[1],some_data,strlen(some_data));
            printf("Wrote %d bytes \n",data_processed);
        }

        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

程序說明: 程序首先用pipe函數創建一個管道,接着用fork調用創建一個新進程。如果fork調用成功,父進程首先關閉讀操作符,然後寫數據到管道中,而子進程首先關閉寫操作符,然後從管道中讀取數據。父子進程都在只調用了一次write或read之後就退出。其原理如下圖所示:
    

系統維護一個文件的文件描述符表計數,父子進程都各自有指向相同文件的文件描述符,當關閉一個文件描述符時,相應計數減1,當這個計數減到0時,文件就被關閉,因此雖然父進程關閉了其文件描述符 file_pipes[0],但是這個文件的文件描述符計數還沒等於0,所以子進程還可以讀取。也可以這麼理解,父進程和子進程都有各自的文件描述符,雖然在父進程中關閉了file_pipes[0],但是對子進程中的 file_pipes[0] 沒有影響。

注:1.文件表中的每一項都會維護一個引用計數,標識該表項被多少個文件描述符(fd)引用,在引用計數爲0的時候,表項纔會被刪除。所以調用close(fd)關閉子進程的文件描述符,只會減少引用計數,但是不會使文件表項被清除,所以父進程依舊可以訪問。
2.當沒有數據可讀時,read調用通常會阻塞,即它將暫停進程來等待直到有數據到達爲止。如果管道的另一端已被關閉,也就是說沒有進程打開這個管道並向它寫數據了,此時read調用將會被阻塞。注意,這與讀取一個無效的文件描述符不同,read把無效的文件描述符看做一個錯誤並返回-1.

在pipe使用中,也可以在子進程中運行一個與其父進程完全不同的另一個程序,而不是僅僅運行一個相同的程序。這個可由exec調用來實現。在上面的例子中,因爲子進程本身有 file_pipes 數據的一個副本,所以這並不成爲問題。但經過exec調用後,原來的進程已經被新的子進程替換了。爲解決這個問題,可以將文件描述符(實際上是一個數字)作爲一個參數傳遞給exec啓動程序。詳細實現見:pipe3.c 和 pipe4.c

2.3.命名管道FIFO

無名管道只能用在父子進程之間,這些程序由一個共同的祖先進程啓動。但如果想在不同進程之間交換數據,這就不太方便。而這可以使用FIFO文件來完成在不相關的進程之間交換數據,它通常叫做命名管道(named pipe)。
命名管道是一種特殊類型的文件,它在文件系統中以文件名的形式存在,但它的行爲卻和已經看到過的沒有名字的管道類似。

使用下面兩個函數可以創建一個FIFO文件:

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

命名管道的一個非常有用特點是:由於它們出現在文件系統中,所以它們可以像平常的文件名一樣在命令中使用,使用FIFO只是爲了單向傳遞數據。

使用方法如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    int res = mk  fifo("/tmp/my_fifo",0777);
    if (res == 0)
    {
        printf("FIFO created.\n");
    }
    exit(EXIT_SUCCESS);
}

查看運行結果: 
注:輸出結果中的第一個字符爲p,表示這是一個管道文件。不同的用戶掩碼最後得到的文件訪問權限是不同的(我的用戶掩碼爲0002)。

與通過pipe調用創建管道不同,FIFO是以命名文件的形式存在,而不是打開的文件描述符,所以對它進行讀寫操作必須先打開它。FIFO也用open和close函數打開和關閉。 對FIFO文件來說,傳遞給open調用的是FIFO的路徑名,而不是一個正常的文件。

打開一個FIFO文件的方法如下:

open(const char*path,O_RDONLY);
    在這種情況下,open調用將阻塞,除非有一個進程以寫的方式打開同一個FIFO,否則它不會返回。
open(const char* path,O_RDONLY|O_NONBLOCK)
    在這種情況下,即使沒有其它進程以寫方式打開FIFO,open調用也將成功並立刻返回。
open(const char *path,O_WRONLY)
    在這種情況下,open調用將會阻塞,直到有一個進程以讀方式打開一個FIFO爲止。
open(const char*path,O_WRONLY|O_NONBLOCK)
    這個函數調用總是立刻返回,但如果沒有一個進程以讀方式打開FIFO文件,open調用將返回一個錯誤並且FIFO也不會被打開。
    如果確實有一個進程以讀方式打開FIFO文件,那麼我們就可以通過它返回的文件描述符對這個FIFO文件進行讀寫操作。

注意:

  • 使用open打開FIFO文件程序不能以O_RDWR模式打開FIFO文件進行讀寫操作。這樣做的後果是未明確定義。如果確實需要在程序之間雙向傳遞數據,最好使用一對FIFO。
  • O_NONBLOCK 分別搭配O_RDONLY 和 O_WRONLY在效果上是不同的,如果沒有進程以讀方式打開管道,非阻塞寫方式的open調用將失敗,但非阻塞讀方式的open調用總是成功。close調用的行爲並不受 O_NONBLOCK標誌的影響。

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    
    #define FIFO_NAME  "/tmp/my_fifo"
    
    int main(int argc,char* argv[])
    {
        int res;
        int open_mode = 0;
        int i;
    
        if (argc <2)
        {
            fprintf(stderr, "Usage:%s <some combination of \
                O_RDONLY O_WRONLY O_NONBLOCK>\n",*argv );
            exit(EXIT_FAILURE);
        }
    
        for (i = 1; i < argc; ++i)
        {   
            if (strncmp(*++argv,"O_RDONLY",8) == 0)
            {   
                open_mode |= O_RDONLY;
            }
            if (strncmp(*argv,"O_WRONLY",8) == 0    )
            {
                open_mode |= O_WRONLY;
            }
            if (strncmp(*argv,"O_NONBLOCK",10) == 0)
            {
                open_mode |= O_NONBLOCK;
            }
        }
    
        if (access(FIFO_NAME,F_OK) == -1)
        {
            res = mkfifo(FIFO_NAME,0777);
    
            if (res !=0)
            {
                fprintf(stderr,"Could not create fifo %s\n",FIFO_NAME);
                exit(EXIT_FAILURE);
            }
        }
    
    
        printf("Process %d opening FIFO\n",getpid());
        res = open(FIFO_NAME,open_mode);
        printf("Process %d result %d\n",getpid(),res);
        sleep(5);
        if (res != -1)
        {
            (void)close(res);
        }
        printf("Process %d finished\n",getpid());
        exit(EXIT_SUCCESS);
    }
    

使用 O_NONBLOCK模式會影響到對FIFO的read和write調用。

小結:
1. 從FIFO中讀取數據

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

2. 從FIFO中寫入數據
FIFO的長度是需要考慮的一個很重要因素。
系統對任一時刻在一個FIFO中可以存在的數據長度是有限制的。它由#define PIPE_BUF定義,在頭文件limits.h 中。在Linux和其它類UNIX系統中,它的值通常是4096字節,Red Hat Fedora9 下是4096,但在某些系統中它可能會小到512字節。
雖然對於只有一個FIFO寫進程和一個FIFO讀進程而言,這個限制並不重要,但只使用一個FIFO並允許多個不同進程向一個FIFO讀進程發送寫請求的情況是很常見的。如果幾個不同的程序嘗試同時向FIFO寫數據,能否保證來自不同程序的數據塊不相互交錯就非常關鍵了。也就是說,必須保證每個寫操作“原子化”。

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

    • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果此時管道空閒緩衝區不足以容納要寫入的字節數,則進入睡眠,直到當緩衝區中能夠容納要寫入的字節數時,纔開始進行一次性寫操作。即寫入的數據長度小於等於PIPE_BUF時,那麼或者寫入全部字節,或者一個字節都不寫入,它屬於一個一次性行爲,具體要看FIFO中是否有足夠的緩衝區。
    • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。FIFO緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據,寫操作在寫完所有請求的數據後返回。
  • 對於沒有設置阻塞標誌的寫操作:

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

  • 信號量(semaphore):主要作爲進程間以及同一進程不同線程之間的同步手段。
發佈了111 篇原創文章 · 獲贊 27 · 訪問量 55萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章