linux的IPC進程通訊方式-匿名管道(一)

linux的IPC進程通訊-匿名管道

什麼是管道

若是你使用過Linux的命令,那麼對於管道這個名詞你必定不會感受到陌生,由於咱們一般經過符號"|"來使用管道,可是管道的真正定義是什麼呢?管道是一個進程鏈接數據流到另外一個進程的通道,它一般是用做把一個進程的輸出經過管道鏈接到另外一個進程的輸入。
舉個例子,在shell中輸入命令:ls -l | grep string,咱們知道ls命令(其實也是一個進程)會把當前目錄中的文件都列出來,可是它不會直接輸出,而是把原本要輸出到屏幕上的數據經過管道輸出到grep這個進程中,做爲grep這個進程的輸入,而後這個進程對輸入的信息進行篩選,把存在string的信息的字符串(以行爲單位)打印在屏幕上。php

使用popen函數

1.popen函數和pclose函數介紹,函數原型以下(C語言)linux

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

poen函數容許一個程序將另外一個程序做爲新進程來啓動,並能夠傳遞數據給它或者經過它接收數據。command是要運行的程序名和相應的參數。open_mode只能是"r(只讀)"和"w(只寫)"的其中之一。popen的函數返回值是FILE類型的指針,而Linux把一切都視爲文件,也就是說咱們可使用stdio I/O庫中的文件處理函數來對其進行操做。shell

若是open_mode是"r",主調用程序就可使用被調用程序的輸出,經過函數返回的FILE指針,就能夠能過stdio函數(如fread)來讀取程序的輸出;若是open_mode是"w",主調用程序就能夠向被調用程序發送數據,即經過stdio函數(如fwrite)向被調用程序寫數據,而被調用程序就能夠在本身的標準輸入中讀取這些數據。數組

pclose函數用於關閉由popen建立出的關聯文件流。pclose只在popen啓動的進程結束後才返回,若是調用pclose時被調用進程仍在運行,pclose調用將等待該進程結束。它返回關閉的文件流所在進程的退出碼。swoole

二、例子
不少時候,咱們根本就不知道輸出數據的長度,爲了不定義一個很是大的數組做爲緩衝區,咱們能夠以塊的方式來發送數據,一次讀取一個塊的數據併發送一個塊的數據,直到把全部的數據都發送完。下面的例子就是採用這種方式的數據讀取和發送方式。源文件名爲popen.c,代碼以下:併發

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

int main()
{
    FILE *read_fp = NULL;
    FILE *write_fp = NULL;
    char buffer[BUFSIZ + 1];
    int chars_read = 0;
    
    //初始化緩衝區
    memset(buffer, '\0', sizeof(buffer));
    //打開ls和grep進程
    read_fp = popen("ls -l", "r");
    write_fp = popen("grep rwxrwxr-x", "w");
    //兩個進程都打開成功
    if(read_fp && write_fp)
    {
        //讀取一個數據塊
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        while(chars_read > 0)
        {
            buffer[chars_read] = '\0';
            //把數據寫入grep進程
            fwrite(buffer, sizeof(char), chars_read, write_fp);
            //還有數據可讀,循環讀取數據,直到讀完全部數據
            chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        }
        //關閉文件流
        pclose(read_fp);
        pclose(write_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

執行編譯composer

gcc -o popen popen.c

運行結果以下函數

[root@test test]# ./popen              
-rw-r--r--  1 admin admin    73231 Mar 24 15:52 composer.lock
-rw-r--r--  1 root  root       745 Mar 29 09:59 c_read.c
-rw-r--r--  1 root  root       773 Mar 29 09:58 c_write.c
-rw-r--r--  1 admin admin      294 Mar 24 18:46 fork2.php
-rw-r--r--  1 admin admin      225 Mar 24 17:48 fork3.php
-rw-r--r--  1 root  root      1028 Mar 29 11:39 popen.c
-rw-r--r--  1 root  root    566294 Mar 16 15:41 swoole-1.9.8.tgz

從運行結果來看,達到了信息篩選的目的。程序在進程ls中讀取數據,再把數據發送到進程grep中進行篩選處理,至關於在shell中直接輸入命令:ls -l | grep rwxrwxr-x。學習

三、popen的實現方式及優缺點
當請求popen調用運行一個程序時,它首先啓動shell,即系統中的sh命令,而後將command字符串做爲一個參數傳遞給它。ui

這樣就帶來了一個優勢和一個缺點。優勢是:在Linux中全部的參數擴展都是由shell來完成的。因此在啓動程序(command中的命令程序)以前先啓動shell來分析命令字符串,也就可使各類shell擴展(如通配符)在程序啓動以前就所有完成,這樣咱們就能夠經過popen啓動很是複雜的shell命令。

而它的缺點就是:對於每一個popen調用,不只要啓動一個被請求的程序,還要啓動一個shell,即每個popen調用將啓動兩個進程,從效率和資源的角度看,popen函數的調用比正常方式要慢一些。

pipe調用

若是說popen是一個高級的函數,pipe則是一個底層的調用。與popen函數不一樣的是,它在兩個進程之間傳遞數據不須要啓動一個shell來解釋請求命令,同時它還提供對讀寫數據的更多的控制。

pipe函數的原型以下:

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

咱們能夠看到pipe函數的定義很是特別,該函數在數組中牆上兩個新的文件描述符後返回0,若是返回返回-1,並設置errno來講明失敗緣由。

數組中的兩個文件描述符以一種特殊的方式鏈接起來,數據基於先進先出的原則,寫到file_descriptor[1]的全部數據均可以從file_descriptor[0]讀回來。因爲數據基於先進先出的原則,因此讀取的數據和寫入的數據是一致的。

特別提醒:
一、從函數的原型咱們能夠看到,它跟popen函數的一個重大區別是,popen函數是基於文件流(FILE)工做的,而pipe是基於文件描述符工做的,因此在使用pipe後,數據必需要用底層的read和write調用來讀取和發送。

二、不要用file_descriptor[0]寫數據,也不要用file_descriptor[1]讀數據,其行爲未定義的,但在有些系統上可能會返回-1表示調用失敗。數據只能從file_descriptor[0]中讀取,數據也只能寫入到file_descriptor[1],不能倒過來。

例子:
首先,咱們在原先的進程中建立一個管道,而後再調用fork建立一個新的進程,最後經過管道在兩個進程之間傳遞數據。源文件名爲pipe.c,代碼以下:

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

int main()
{
    int data_processed = 0;
    int filedes[2];
    const char data[] = "Hello pipe!";
    char buffer[BUFSIZ + 1];
    pid_t pid;
    //清空緩衝區
    memset(buffer, '\0', sizeof(buffer));

    if(pipe(filedes) == 0)
    {
        //建立管道成功
        //經過調用fork建立子進程
        pid = fork();
        if(pid == -1)
        {
            fprintf(stderr, "Fork failure");
            exit(EXIT_FAILURE);
        }
        if(pid == 0)
        {
            //子進程中
            //讀取數據
            data_processed = read(filedes[0], buffer, BUFSIZ);
            printf("Read %d bytes: %s\n", data_processed, buffer);
            exit(EXIT_SUCCESS);
        }
        else
        {
            //父進程中
            //寫數據
            data_processed = write(filedes[1], data, strlen(data));
            printf("Wrote %d bytes: %s\n", data_processed, data);
            //休眠2秒,主要是爲了等子進程先結束,這樣作也只是純粹爲了輸出好看而已
            //父進程其實沒有必要等等子進程結束
            sleep(2);
            exit(EXIT_SUCCESS);
        }
    }
    exit(EXIT_FAILURE);
}

運行結果

[root@test test]# gcc -o pipe pipe.c   
[root@test test]# ./pipe                 
Wrote 11 bytes: Hello pipe!
Read 11 bytes: Hello pipe!

可見,子進程讀取了父進程寫到filedes[1]中的數據,若是在父進程中沒有sleep語句,父進程可能在子進程結束前結束,這樣你可能將看到兩個輸入之間有一個命令提示符分隔。

把管道用做標準輸入和標準輸出

下面來介紹一種用管道來鏈接兩個進程的更簡潔方法,咱們能夠把文件描述符設置爲一個已知值,通常是標準輸入0或標準輸出1。這樣作最大的好處是能夠調用標準程序,即那些不須要以文件描述符爲參數的程序。
爲了完成這個工做,咱們還須要兩個函數的輔助,它們分別是dup函數或dup2函數,它們的原型以下

#include <unistd.h>  
int dup(int file_descriptor);  
int dup2(int file_descriptor_one, int file_descriptor_two);

dup調用建立一個新的文件描述符與做爲它的參數的那個已有文件描述符指向同一個文件或管道。對於dup函數而言,新的文件描述老是取最小的可用值。而dup2所建立的新文件描述符或者與int file_descriptor_two相同,或者是第一個大於該參數的可用值。因此當咱們首先關閉文件描述符0後調用dup,那麼新的文件描述符將是數字0.

例子
在下面的例子中,首先打開管道,而後fork一個子進程,而後在子進程中,使標準輸入指向讀管道,而後關閉子進程中的讀管道和寫管道,只留下標準輸入,最後調用execlp函數來啓動一個新的進程od,可是od並不知道它的數據來源是管道仍是終端。父進程則相對簡單,它首先關閉讀管道,而後在寫管道中寫入數據,再關閉寫管道就完成了它的任務。源文件爲pipe2.c,代碼以下:

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

int main()
{
    int data_processed = 0;
    int pipes[2];
    const char data[] = "123";
    pid_t pid;

    if(pipe(pipes) == 0)
    {
        pid = fork();
        if(pid == -1)
        {
            fprintf(stderr, "Fork failure!\n");
            exit(EXIT_FAILURE);
        }
        if(pid == 0)
        {
            //子進程中
            //使標準輸入指向fildes[0]
            close(0);
            dup(pipes[0]);
            //關閉pipes[0]和pipes[1],只剩下標準輸入
            close(pipes[0]);
            close(pipes[1]);
            //啓動新進程od
            execlp("od", "od", "-c", 0);
            exit(EXIT_FAILURE);
        }
        else
        {
            //關閉pipes[0],由於父進程不用讀取數據
            close(pipes[0]);
            data_processed = write(pipes[1], data, strlen(data));
            //寫完數據後,關閉pipes[1]
            close(pipes[1]);
            printf("%d - Wrote %d bytes\n", getpid(), data_processed);
        }
    }
    exit(EXIT_SUCCESS);
}
[root@cp01-mawmd-rd03 test]# ./pipe2 
16546 - Wrote 3 bytes
[root@cp01-mawmd-rd03 test]# 0000000   1   2   3
0000003

從運行結果中能夠看出od進程正確地完成了它的任務,與在shell中直接輸入od -c和123的效果同樣。

關於管道關閉後的讀操做的討論

如今有這樣一個問題,假如父進程向管道file_pipe[1]寫數據,而子進程在管道file_pipe[0]中讀取數據,當父進程沒有向file_pipe[1]寫數據時,子進程則沒有數據可讀,則子進程會發生什麼呢?再者父進程把file_pipe[1]關閉了,子進程又會有什麼反應呢?

當寫數據的管道沒有關閉,而又沒有數據可讀時,read調用一般會阻塞,可是當寫數據的管道關閉時,read調用將會返回0而不是阻塞。注意,這與讀取一個無效的文件描述符不一樣,read一個無效的文件描述符返回-1。

6、匿名管道的缺陷

看了這麼多相信你們也知道它的一個缺點,就是通訊的進程,它們的關係必定是父子進程的關係,這就使得它的使用受到了一點的限制,可是咱們可使用命名管道來解決這個問題。命名管道將在下一篇文章:Linux進程間通訊——使用命名管道中介紹。

引用: 本文轉載自http://blog.csdn.net/ljianhui/article/details/10168031 僅供學習使用記錄

相關文章
相關標籤/搜索