歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> 進程間通信之-管道--linux內核剖析(八)

進程間通信之-管道--linux內核剖析(八)

日期:2017/3/1 12:15:28   编辑:Linux內核

管道


管道是一種兩個進程間進行單向通信的機制。

因為管道傳遞數據的單向性,管道又稱為半雙工管道。

管道的這一特點決定了器使用的局限性。管道是Linux支持的最初Unix IPC形式之一,具有以下特點:

數據只能由一個進程流向另一個進程(其中一個讀管道,一個寫管道);如果要進行雙工通信,需要建 立兩個管道。

管道只能用於父子進程或者兄弟進程間通信。,也就是說管道只能用於具有親緣關系的進程間通信。

除了以上局限性,管道還有其他一些不足,如管道沒有名字(匿名管道),管道的緩沖區大小是受限制的。管道所傳輸的是無格式的字節流。這就需要管道輸入方和輸出方事先約定好數據格式。雖然有那麼多不足,但對於一些簡單的進程間通信,管道還是完全可以勝任的。

信號和消息的區別


我們知道,進程間的信號通信機制在傳遞信息時是以信號為載體的,但管道通信機制的信息載體是消息。那麼信號和消息之間的區別在哪裡呢? 

首先,在數據內容方面,信號只是一些預定義的代碼,用於表示系統發生的某一狀況;消息則為一組連續語句或符號,不過量也不會太大。在作用方面,信號擔任進程間少量信息的傳送,一般為內核程序用來通知用戶進程一些異常情況的發生;消息則用於進程間交換彼此的數據。

在發送時機方面,信號可以在任何時候發送;信息則不可以在任何時刻發送。在發送者方面,信號不能確定發送者是誰;信息則知道發送者是誰。在發送對象方面,信號是發給某個進程;消息則是發給消息隊列。在處理方式上,信號可以不予理會;消息則是必須處理的。在數據傳輸效率方面,信號不適合進大量的信息傳輸,因為它的效率不高;消息雖然不適合大量的數據傳送,但它的效率比信號強,因此適於中等數量的數據傳送。

管道-流管道-命名管道的區別


我們知道,命名管道和管道都可以在進程間傳送消息,但它們也是有區別的。

管道這種通訊方式有兩種限制,

一是半雙工的通信,數據只能單向流動

二是只能在具有親緣關系的進程間使用。

進程的親緣關系通常是指父子進程關系。

流管道s_pipe去除了第一種限制,可以雙向傳輸。

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

管道技術只能用於連接具有共同祖先的進程,例如父子進程間的通信,它無法實現不同用戶的進程間的信息共享。再者,管道不能常設,當訪問管道的進程終止時,管道也就撤銷。這些限制給它的使用帶來不少限制,但是命名管道卻克服了這些限制。

命名管道也稱為FIFO,是一種永久性的機構。FIFO文件也具有文件名、文件長度、訪問許可權等屬性,它也能像其它Linux文件那樣被打開、關閉和刪除,所以任何進程都能找到它。換句話說,即使是不同祖先的進程,也可以利用命名管道進行通信。

如果想要全雙工通信,那最好使用Sockets API(linux並不支持s_pipe流管道)。下面我們分別介紹通過管道命令解析管道的技術模型,然後詳細說明用來進行管道編程的編程接口和系統級命令。

管道技術模型


管道技術是Linux操作系統中歷來已久的一種進程間通信機制。

所有的管道技術,無論是半雙工的匿名管道,還是命名管道,它們都是利用FIFO排隊模型來指揮進程間的通信。

對於管道,我們可以形象地把它們當作是連接兩個實體的一個單向連接器。

使用管道進行通信時,兩端的進程向管道讀寫數據是通過創建管道時,系統設置的文件描述符進行的。從本質上說,管道也是一種文件,但它又和一般的文件有所不同,可以克服使用文件進行通信的兩個問題,這個文件只存在內存中。

通過管道通信的兩個進程,一個進程向管道寫數據,另外一個從中讀數據。寫入的數據每次都添加到管道緩沖區的末尾,讀數據的時候都是從緩沖區的頭部讀出數據的。

管道命令詳解


參見

linux shell 管道命令(pipe)使用及與shell重定向區別

例如,請看下面的命令

管道符號,是unix功能強大的一個地方,符號是一條豎線:”|”,

用法: command 1 | command 2

他的功能是把第一個命令command 1執行的結果作為command 2的輸入傳給command 2

注意:

管道命令只處理前一個命令正確輸出,不處理錯誤輸出

管道命令右邊命令,必須能夠接收標准輸入流命令才行。

例如:
ls -l | more

該命令列出當前目錄中的任何文檔,並把輸出送給more命令作為輸入,more命令分頁顯示文件列表。

管道命令與重定向區別


區別是:

左邊的命令應該有標准輸出 | 右邊的命令應該接受標准輸入

左邊的命令應該有標准輸出 > 右邊只能是文件

左邊的命令應該需要標准輸入 < 右邊只能是文件

管道觸發兩個子進程執行”|”兩邊的程序;而重定向是在一個進程內執行

重定向與管道在使用時候很多時候可以通用

其實,在shell裡面,經常是條條大路通羅馬的。

一般如果是命令間傳遞參數,還是管道的好,如果處理輸出結果需要重定向到文件,還是用重定向輸出比較好。

前面的例子實際上就是在兩個命令之間建立了一根管道(有時我們也將之稱為命令的流水線操作)。

第一個命令ls執行後產生的輸出作為了第二個命令more的輸入。

這是一個半雙工通信,因為通信是單向的。兩個命令之間的連接的具體工作,是由內核來完成的。

當然內核也為我們提供了一套接口(系統調用),除了命令之外,應用程序也可以使用管道進行連接。

管道編程技術

管道的接口


無名管道pipe


創建管道pipe

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

pipe()會建立管道,並將文件描述詞由參數 filedes 數組返回。

filedes[0]為管道裡的讀取端,所以pipe用read調用的。

filedes[1]則為管道的寫入端。使用write進行寫入操作。

返回值

若成功則返回零,否則返回-1,錯誤原因存於 errno 中。

錯誤代碼

EMFILE 進程已用完文件描述詞最大量

ENFILE 系統已無文件描述詞可用。

EFAULT 參數 filedes 數組地址不合法。

當調用成功時,函數pipe返回值為0,否則返回值為-1。成功返回時,數組fds被填入兩個有效的文件描述符。數組的第一個元素中的文件描述符供應用程序讀取之用,數組的第二個元素中的文件描述符可以用來供應用程序寫入。

關閉管道close

關閉管道只是將兩個文件描述符關閉即可,可以使用普通的close函數逐個關閉。

如果管道的寫入端關閉,但是還有進程嘗試從管道讀取的話,將被返回0,用來指出管道已不可用,並且應當關閉它。如果管道的讀出端關閉,但是還有進程嘗試向管道寫入的話,試圖寫入的進程將收到一個SIGPIPE信號,至於信號的具體處理則要視其信號處理程序而定了。

dup函數和dup2函數


dup和dup2也是兩個非常有用的調用,它們的作用都是用來復制一個文件的描述符。

它們經常用來重定向進程的stdin、stdout和stderr。

這兩個函數的原型如下所示:

#include 

int dup( int oldfd );

int dup2( int oldfd, int targetfd )

dup函數

利用函數dup,我們可以復制一個描述符。傳給該函數一個既有的描述符,它就會返回一個新的描述符,這個新的描述符是傳給它的描述符的拷貝。這意味著,這兩個描述符共享同一個數據結構。

例如,如果我們對一個文件描述符執行lseek操作,得到的第一個文件的位置和第二個是一樣的。下面是用來說明dup函數使用方法的代碼片段:

int fd1, fd2;
fd2 = dup( fd1 );

需要注意的是,我們可以在調用fork之前建立一個描述符,這與調用dup建立描述符的效果是一樣的,子進程也同樣會收到一個復制出來的描述符。

dup2函數

dup2函數跟dup函數相似,但dup2函數允許調用者規定一個有效描述符和目標描述符的id。

dup2函數成功返回時,目標描述符(dup2函數的第二個參數)將變成源描述符(dup2函數的第一個參數)的復制品,換句話說,兩個文件描述符現在都指向同一個文件,並且是函數第一個參數指向的文件。

下面我們用一段代碼加以說明:


int oldfd;
oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
dup2( oldfd, 1 );
close( oldfd );

我們打開了一個新文件,稱為“app_log”,並收到一個文件描述符,該描述符叫做fd1。我們調用dup2函數,參數為oldfd和1,這會導致用我們新打開的文件描述符替換掉由1代表的文件描述符(即stdout,因為標准輸出文件的id為1)。任何寫到stdout的東西,現在都將改為寫入名為“app_log”的文件中。需要注意的是,dup2函數在復制了oldfd之後,會立即將其關閉,但不會關掉新近打開的文件描述符,因為文件描述符1現在也指向它。

命名管道mkfifo


mkfifo函數的作用是在文件系統中創建一個文件,該文件用於提供FIFO功能,即命名管道。前邊講的那些管道都沒有名字,因此它們被稱為匿名管道,或簡稱管道。對文件系統來說,匿名管道是不可見的,它的作用僅限於在父進程和子進程兩個進程間進行通信。而命名管道是一個可見的文件,因此,它可以用於任何兩個進程之間的通信,不管這兩個進程是不是父子進程,也不管這兩個進程之間有沒有關系。Mkfifo函數的原型如下所示:

#include 
#include 
int mkfifo( const char *pathname, mode_t mode );

mkfifo函數需要兩個參數,第一個參數(pathname)是將要在文件系統中創建的一個專用文件。第二個參數(mode)用來規定FIFO的讀寫權限。Mkfifo函數如果調用成功的話,返回值為0;如果調用失敗返回值為-1。下面我們以一個實例來說明如何使用mkfifo函數建一個fifo,具體代碼如下所示:

    int ret;

    ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
    if (ret == 0)
    {
      // 成功建立命名管道
    }
    else
    {
      // 創建命名管道失敗
    }

在這個例子中,利用/tmp目錄中的cmd_pipe文件建立了一個命名管道(即fifo)。之後,就可以打開這個文件進行讀寫操作,並以此進行通信了。命名管道一旦打開,就可以利用典型的輸入輸出函數從中讀取內容。舉例來說,下面的代碼段向我們展示了如何通過fgets函數來從管道中讀取內容:

    pfp = fopen( "/tmp/cmd_pipe", "r" );
    ret = fgets( buffer, MAX_LINE, pfp );

我們還能向管道中寫入內容,下面的代碼段向我們展示了利用fprintf函數向管道寫入的具體方法:

    pfp = fopen( "/tmp/cmd_pipe", "w+ );
    ret = fprintf( pfp, "Here’s a test string!\n" );

對命名管道來說,除非寫入方主動打開管道的讀取端,否則讀取方是無法打開命名管道的。Open調用執行後,讀取方將被鎖住,直到寫入方出現為止。盡管命名管道有這樣的局限性,但它仍不失為一種有效的進程間通信工具。

無名管道


無名管道為建立管道的進程及其子孫提供一條以比特流方式傳送消息的通信管道。

該管道再邏輯上被看作管道文件,在物理上則由文件系統的高速緩沖區構成,而很少啟動外設。

發送進程利用文件系統的系統調用write(fd[1],buf,size),把buf 中的長度為size字符的消息送入管道入口fd[1]

接收進程則使用系統調用read(fd[0],buf,size)從管道出口fd[0]出口讀出size字符的消息置入buf中。

這裡,管道按FIFO(先進先出)方式傳送消息,且只能單向傳送消息(如圖)。

這裡寫圖片描述

無名管道pipe讀寫


管道用於不同進程間通信。通常先創建一個管道,再通過fork函數創建一個子進程,該子進程會繼承父進程創建的管道。注意事項:必須在系統調用fork()前調用pipe(),否則子進程將不會繼承文件描述符。否則,會創建兩個管道,因為父子進程共享同一段代碼段,都會各自調用pipe(),即建立兩個管道,出現異常錯誤。

無名管道讀寫過程如圖所示

這裡寫圖片描述

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_DATA_LEN 256
#define DELAY_TIME 1

int main(void)
{
    pid_t       pid;
    char        buf[MAX_DATA_LEN];
    const char  *data="Pipe Test program";
    int         real_read, real_write;
    int         pipe_fd[2];

    memset((void*)buf, 0, sizeof(buf));

    if(pipe(pipe_fd) < 0)
    {
        perror("Pipe create error...\n");
        exit(1);
    }
    else
    {
        printf("Pipe create success...\n");
    }

    if ((pid = fork()) < 0)
    {
        perror("Fork error!\n");

        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am the child process, PID = %d, PPID = %d", getpid(), getppid());

        close(pipe_fd[1]);
        sleep(DELAY_TIME * 3);

        if ((real_read=read(pipe_fd[0],buf, MAX_DATA_LEN)) > 0)
        {
            printf("Child receive %d bytes from pipe: '%s'.\n", real_read, buf);
        }

        close(pipe_fd[0]);

        exit(0);
    }
    else
    {
        printf("I am the parent process, PID = %d, PPID = %d", getpid(), getppid());

        close(pipe_fd[0]);
        sleep(DELAY_TIME);

        if ((real_write = write(pipe_fd[1], data, strlen(data))) > 0)
        {
            printf("Parent write %d bytes into pipe: '%s'.\n", real_write, data);
        }

        close(pipe_fd[1]);
        waitpid(pid,NULL,0);

        exit(0);
    }

    return EXIT_SUCCESS;
}

多進程管道讀寫


建立一個管道。同時,父進程生成子進程P1,P2,這兩個進程分別向管道中寫入各自的字符串,父進程讀出它們(如圖)。

#include < stdio.h>  
main( )  
{  
  int I,r,p1,p2,fd[2];  
  char buf[50],s[50];  
  pipe(fd); /*父進程建立管道*/  
  while((p1=fork()) = = -1);  
  if(p1 = = 0 )  
  {  
     lockf(fd[1],1,0); /*加鎖鎖定寫入端*/  
     sprinrf(buf, ”child process P1 is sending messages! \n”);  
     printf(“child process P1! \n”);  
     write(fd[1],buf, 50); /*把buf中的50個字符寫入管道*/  
     sleep(5);  
     lockf(fd[1],0,0); /*釋放管道寫入端*/  
     exit(0); /*關閉P1*/  
  }  
  else /*從父進程返回,執行父進程*/  
{  
    while((p2=fork()) = = -1); /*創建子進程P2,失敗時循環*/  
    if(p2 = = 0) /*從子進程P2返回,執行P2*/  
    {  
       lockf(fd[1],1,0); / *鎖定寫入端*/  
       sprintf(buf, ”child process P2 is sending messages \n”);  
       printf(“child process P2 ! \n”);  
       write(fd[1],buf,50); /*把buf中字符寫入管道*/  
       sleep(5); /* 睡眠等待*/  
       lockf (fd[1],0,0); /*釋放管道寫入端*/  
       exit(0); /*關閉P2*/  
     }  
    wait(0);  
    if (r = read(fd[0],s 50) = = -1)  
      printf(“can’t read pipe \n”);  
    else printf(“%s\n”,s);  
    wait(0);  
    if(r = read(fd[0],s,50)= = -1)  
      printf(“can’t read pipe \n”);  
    else printf((“%s\n”,s);  
    exit(0);  
}  
}  

使用dup函數實現指令流水


我們的子進程把它的輸出重定向的管道的輸入,然後,父進程將它的輸入重定向到管道的輸出。這在實際的應用程序開發中是非常有用的一種技術。

#include 
#include 
#include 

int main()
{
    int pfds[2];
    if ( pipe(pfds) == 0 )
    {

        if ( fork() == 0 )
        {
            close(1);
            dup2( pfds[1], 1 );
            close( pfds[0] );
            execlp( "ls", "ls", "-1", NULL );

        }
        else
        {
            close(0);
            dup2( pfds[0], 0 );
            close( pfds[1] );
            execlp( "wc", "wc", "-l", NULL );
        }
    }

    return 0;
}

命名管道


write端

#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#define FIFO        "myfifo"
#define BUFF_SIZE   1024

int main(int argc,char* argv[])
{
    char    buff[BUFF_SIZE];
    int     real_write;
    int     fd;

    if(argc <= 1)
    {
        printf("Usage: %s string\n", argv[0]);

        exit(1);
    }
    else
    {
        printf("%s at PID = %d\n", argv[0], getpid());
    }

    sscanf(argv[1], "%s", buff);

    // 測試FIFO是否存在,若不存在,mkfifo一個FIFO
    if(access(FIFO, F_OK) == -1)
    {
        if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Can NOT create fifo file!\n");

            exit(1);
        }
    }

    //  調用open以只寫方式打開FIFO,返回文件描述符fd
    if((fd = open(FIFO, O_WRONLY)) == -1)
    {
        printf("Open fifo error!\n");

        exit(1);
    }

    //  調用write將buff寫到文件描述符fd指向的FIFO中
    if ((real_write = write(fd, buff, BUFF_SIZE)) > 0)
    {
        printf("Write into pipe: '%s'.\n", buff);
        exit(1);
    }

    close(fd);
    exit(0);

}

read端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define FIFO "myfifo"
#define BUFF_SIZE 1024

int main(int argc, char *argv[])
{
    char    buff[BUFF_SIZE];
    int     real_read;
    int     fd;

    printf("%s at PID = %d  ", argv[0], getpid());

    //  access確定文件或文件夾的訪問權限。即,檢查某個文件的存取方式
    //  如果指定的存取方式有效,則函數返回0,否則函數返回-1
    //  若不存在FIFO,則創建一個
    if(access(FIFO, F_OK) == -1)
    {
        if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Can NOT create fifo file!\n");
            exit(1);
        }
    }

    //  以只讀方式打開FIFO,返回文件描述符fd
    if((fd = open(FIFO, O_RDONLY)) == -1)
    {
        printf("Open fifo error!\n");
        exit(1);
    }

    //  調用read將fd指向的FIFO的內容,讀到buff中,並打印
    while(1)
    {
        memset(buff, 0, BUFF_SIZE);

        if ((real_read = read(fd, buff, BUFF_SIZE)) > 0)
        {
            printf("Read from pipe: '%s'.\n",buff);
        }
    }

    close(fd);
    exit(0);
}
Copyright © Linux教程網 All Rights Reserved