管道實現進程間通信

一、

實現進程間通信最簡單的方式就是通過管道機制來實現,管道是一種最基本的IPC機制,由pipe函數創建。pipe所創建的管道實際上是在內核中開闢一塊緩衝區,它有一個獨端和寫段,通過read和write來實現往管道里寫和讀,由於管道是通過父進程調用pipe函數所產生的管道,所以和它通信的只能是它的子進程或者和它有血緣關係的進程。後面一會具體講。先通過圖示來看看管道是怎麼來實現通信的




通信機制:

1、父進程先通過調用pipe函數創建一個管道。父進程的文件描述符讀和寫分別指向管道的兩端。

2、父進程通過調用fork函數來創建子進程,然後子進程的文件描述符的讀和寫也分別指向管道的兩端。

3、父進程關閉自己的讀端,子進程關閉自己的寫端。然後父進程往管道里寫東西,子進程從管道里讀東西,以此來實現父子進程之間的通信。


管道的特點:

1、管道只能進行單向通道。f[0]爲讀,f[1]爲寫。

2、只有具有血緣關係的兩個進程才能用管道來通信。

3、管道是面向數據流的服務。

4、管道的生命週期隨進程。

5、同步與互斥


先來看看父進程如何通過代碼來創建管道,並且和子進程連接上的。

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

int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if(ret == -1)
    { 
        perror("errno");
        return 1;
    }

    pid_t id = fork();
    if(id < 0)
    {
        perror("errno");
        return 2;
    }
    else if(id == 0)
    {//child
        close(_pipe[0]);
        int i = 0;
        char *msg = NULL;
        while(i<100)
        {
            msg = "hello pipe! i am child!\n";
            write(_pipe[1],msg,strlen(msg));
            sleep(1);
            printf("pipe count %d\n",i++);
        }
        exit(0);
    }
    else{//father
        close(_pipe[1]);
        char _msg[100];
        int i = 0;
        while(i<100){
            int ret = read(_pipe[0],_msg,sizeof(_msg));
            printf("%scode is:%d\n",_msg,ret);
        }
    }
    return 0;
}

這個管道里子進程先關閉它的讀端,再發送一串字符串,加一個計數器,表示往管道里寫了幾個消息。父進程關閉寫段,然後在管道里讀子進程寫的數據。



但使用管道時要注意以下四種情況

1、如果子進程在往管道里寫完數據並且關閉了寫段,而父進程的讀端一直沒有關閉。這時,在父進程讀完管道里的數據之後,就相當於讀到了管道的結束部分,執行完父進程程序之後直接退出。

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

int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if(ret == -1)
    { 
        perror("errno");
        return 1;
    }

    pid_t id = fork();
    if(id < 0)
    {
        perror("errno");
        return 2;
    }
    else if(id == 0)
    {//child
        close(_pipe[0]);
        int i = 0;
        char *msg = NULL;
        while(i<5)
        {
            msg = "hello pipe! i am child!";
            write(_pipe[1],msg,strlen(msg));
            sleep(1);
            printf("pipe count %d\n",i++);
        }
     close(_pipe[1]);
        exit(0);
    }
    else{//father
        close(_pipe[1]);
        char _msg[100];
        int i = 0;
        while(i<10){
            sleep(1);
            int ret = read(_pipe[0],_msg,sizeof(_msg));
            printf("%scode is:%d\n",_msg,ret);
            i++;
        }
        if(waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    }
    return 0;
}


2、如果在子進程往管道里寫數據後,沒有繼續再往管道里寫數據。這時讀端直接從管道里讀取數據,而當讀端讀完數據後,會一直等到寫段繼續給管道里寫東西。這時就會發生阻塞,直到寫段繼續往管道里寫數據讀端才能返回退出。

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

int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if(ret == -1)
    { 
        perror("errno");
        return 1;
    }

    pid_t id = fork();
    if(id < 0)
    {
        perror("errno");
        return 2;
    }
    else if(id == 0)
    {//child
        close(_pipe[0]);
        int i = 0;
        char *msg = NULL;
        while(i<10)
        {
            msg = "hello pipe! i am child!";
            write(_pipe[1],msg,strlen(msg));
            sleep(1);
        }
     //close(_pipe[1]);
        exit(0);
    }
    else{//father
        close(_pipe[1]);
        char _msg[100];
        int i = 0;
        while(i<5){
            int ret = read(_pipe[0],_msg,sizeof(_msg));
            printf("%scode is:%d\n",_msg,ret);
            i++;
        }
        if(waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    return 0;
}



3、如果讀端的文件描述符全部關閉了,這時再往寫段寫數據。進程就會收到一個信號SIGPIPE,通常會導致進程異常終止退出。

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

int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if(ret == -1)
    { 
        perror("errno");
        return 1;
    }

    pid_t id = fork();
    if(id < 0)
    {
        perror("errno");
        return 2;
    }
    else if(id == 0)
    {//child
        close(_pipe[0]);
        int i = 0;
        char *msg = NULL;
        while(i<10)
        {
            if(i<5){
            msg = "hello pipe! i am child!";
            write(_pipe[1],msg,strlen(msg));
            sleep(1);
            i++;
            }
        }
     //close(_pipe[1]);
        exit(0);
    }
    else{//father
        close(_pipe[1]);
        char _msg[100];
        int i = 0;
        while(i<3){
            int ret = read(_pipe[0],_msg,sizeof(_msg));
            printf("%s:code is:%d\n",_msg,ret);
            i++;
        }
         close(_pipe[0]);
         sleep(2);
        if(waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    return 0;
}


4、在讀端沒有繼續讀取數據並且沒有關閉自己的文件描述符,而這時寫段也沒有停止往管道里寫數據。那麼就會發生寫段把管道寫滿的情況,這時會造成阻塞,直到管道里有空位置時,進程才能返回退出!

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

int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if(ret == -1)
    { 
        perror("errno");
        return 1;
    }

    pid_t id = fork();
    if(id < 0)
    {
        perror("errno");
        return 2;
    }
    else if(id == 0)
    {//child
        close(_pipe[0]);
        int i = 0;
        char *msg = NULL;
        while(1)
        {
            msg = "hello pipe! i am child!";
            write(_pipe[1],msg,strlen(msg));
            printf("%d\n",i++);
        }
     //close(_pipe[1]);
        exit(0);
    }
    else{//father
        close(_pipe[1]);
        char _msg[100];
        int i = 0;
        while(i<3){
            int ret = read(_pipe[0],_msg,sizeof(_msg));
            printf("%s:code is:%d\n",_msg,ret);
            i++;
        }
        if(waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    }
    return 0;
}


這裏的2850是管道被寫滿的情況,間接說明這裏的管道容量是這麼大。



二、命名管道 

匿名管道的不足之處就是沒有名字,因此只能實現具有血緣關係的進程間通信。因此就要提出一種新的管道技術來實現任意進程間的通信,即命名管道(FIFO)。命名管道是以路徑名來命名的,只要任意兩個進程訪問相同的路徑,就可以達到通信的效果。值得注意的是,命名管道是按照先進先出的原則,即最先被寫入的數據先被讀出來。


Linux下有兩種兩式創建命名管道。一是在Shell下交互地建立一個命名管道,二是在程序中使用系統函數建立命名管道。Shell方式下可使用mknod或mkfifo命令,下列命令使用mknod創建了一個命名管道: mknod namedpipe 

這兩個函數都能創建個FIFO件,注意是創建個真實存在於件系統中的件,filename指定了件名,mode則指定了件的讀寫權限。mknod是的函數,使mkfifo函數更加簡單和規範,所以建議在可能的情況下,儘量使mkfifo不是mknod。

mkfifo函數的原型:int mkfifo(const char *filename, mode_t mode); 

第一個參數是文件名,第二個參數是文件的權限

先來看看代碼實現


write端

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

#define _PATH_ "../fifo/my_fifo"
#define _SIZE_ 100

int main()
{
    umask(0);
    int ret = mkfifo(_PATH_,0666|S_IFIFO);
    if(ret == -1){
        printf("mkfifo error!\n");
        return 1;
    }
    int fd = open(_PATH_,O_RDWR);
    if(fd < 0){
        printf("open error!\n");
    }
    char buf[_SIZE_];
    memset(buf,'\0',sizeof(buf));
    while(1){
    printf("Please Enter# ");
    fflush(stdout);
    int ret = read(0,buf,sizeof(buf)-1);
    if(ret < 0){
        printf("write error!\n");
        break;
    }
        if(ret > 0){
            buf[ret] = 0;
            write(fd,buf,strlen(buf));
        }
    if(strncmp(buf,"quit",4) == 0){
        break;
    }
    }
    close(fd);
    return 0;
}


read端

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

#define _PATH_ "../fifo/my_fifo"
#define _SIZE_ 100

int main()
{
    int fd = open(_PATH_,O_RDWR);
    if(fd <= 0){
        printf("open file error!\n");
        return 1;
    }
    char buf[_SIZE_];
    while(1){
        int ret = read(fd,buf,sizeof(buf));
        if(ret <= 0){
            printf("read file to end!\n");
            break;
        }
        if(ret > 0)
        {
            buf[ret] = 0;
            printf("client say# %s",buf);
        }
        if(strncmp(buf,"quit",4) == 0){
            break;
        }
    }
    close(fd);
    return 0;
}


在這裏面注意的就是在創建命名管道時,注意文件的權限,可以通過修改umask來改變它,這裏將管道文件權限設置爲666。而文件是通過調用FIFE*來讀寫的,所以在寫端的讀寫方式上採用讀寫(O_RWONLY),否則會造成阻塞。



可以看到,在通過FIFO實現兩個進程通信之後,管道的權限變爲了666並且標誌位爲P,表明它是管道。那麼如果再次向通過管道寫數據的話還能不能往裏寫呢?答案是不能,因爲創建的管道已經存在了。這時就應該先銷燬上面創建的管道,然後在重新創建一個管道進行通信。



FIFO還可以直接在shell下創建一個管道,然後通過echo重定向到管道里。

這裏我寫一段簡單的腳本來證明一下


可以看到,左邊的窗口沒隔1秒往FIFO裏寫句hello bit,右邊窗口直接去管道里讀。而且值得注意的是在shell裏創建的管道也存在同步與互斥,可以再寫段腳本看看。先讓寫段每隔一秒往管道里寫句數據,然後讀端每隔兩秒在管道里讀取數據,結果只要是寫段往裏寫的速度和讀端的一直就會保證他的原子性。


這裏看的不是很清楚,自己可以試試看是不是和預想的結果一樣!



上面爲了讓兩個進程實現通信用了兩種方式來實現,分別是匿名管道(PIPE)和命名管道(FIFO)。那麼就來總結一下兩個管道都有什麼區別:

1、FIFO可以實現任意進程之間的通信,而PIPE則只支持具有血緣關係進程的通信。

2、FIFO是以讀寫文件的方式來實現的,所以可以雙向通信,而PIPE只支持單方面的通信。

3、FIFO可以支持多終端的通信,可以有多個讀端。而PIPE只支持一個讀和寫端。

4、FIFO是通過文件的形式創建的管道,而PIPE是在內核中開闢一塊空間通過文件描述符來實現的通信。

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