进程间通信——匿名管道原理及详解(附有案例代码)

1、定义

       管道也叫无名(匿名)管道,它是是UNIX系统IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制。

       统计一个目录中文件的数目命令: ls | wc -l,为了执行该命令,shell 创建了两个进程来分别执行ls 和wc;通常情况下,进程 ls 的输出直接通过 stdout 输出到控制台,但是为了两个进程能够进行通信,系统会建立一个管道,然后把进程 ls 发的内容输出到管道,进程 wc 从管道中读取进程 ls 输出的内容,从而实现进程间的通信。
进程间通信——匿名管道原理及详解(附有案例代码)_第1张图片

2、特点

A.管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

进程间通信——匿名管道原理及详解(附有案例代码)_第2张图片

B.管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

C.一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。

D.在管道中数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的(半双工可以理解为独木桥,可以双向传递但是同一时刻只能单向传递)从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法随机的访问数据。

E.匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。原因:fork出来的子进程中文件描述符和父进程中的文件描述符一样,即读写描述符一一对应。

进程间通信——匿名管道原理及详解(附有案例代码)_第3张图片

 F.管道数据秉承先进先出原则,可以理解为一个环形队列,之所以是环形,因为队头数据读出后,还可以写入新数据,不至于内存浪费。 

进程间通信——匿名管道原理及详解(附有案例代码)_第4张图片

3、匿名管道的使用 

创建匿名管道
#include 
int pipe(int pipefd[2]);

功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
     pipefd[0] 对应的是管道的读端
     pipefd[1] 对应的是管道的写端
返回值:
     成功 0
     失败 -1

管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

查看管道缓冲大小命令
ulimit -a

查看管道缓冲大小函数
#include 
long fpathconf(int fd, int name);

4、匿名管道通信案例 

说明:由于匿名管道是半双工的,所以同一时刻读端和写端只能有一个开启,另一个需要手动关闭; 

// 子进程发送数据给父进程,父进程读取到数据输出
#include 
#include 
#include 
#include 
#include 

int main() {
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端,同一时刻写端、读端只能有一个开启
        close(pipefd[1]);        
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            
            // 向管道中写入数据
            //char * str = "hello,i am parent";
            //write(pipefd[1], str, strlen(str));
            //sleep(1);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());

        // 关闭读端,同一时刻写端、读端只能有一个开启
        close(pipefd[0]);
        char buf[1024] = {0};

        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));

            // int len = read(pipefd[0], buf, sizeof(buf));
            // printf("child recv : %s, pid : %d\n", buf, getpid());
            // bzero(buf, 1024);
        }
    }
    return 0;
}

5、注意事项

1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端
读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

6、总结

    读管道:
        管道中有数据,read返回实际读到的字节数。
        管道中无数据:
            写端被全部关闭,read返回0(相当于读到文件的末尾)
            写端没有完全关闭,read阻塞等待

    写管道:
        管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
        管道读端没有全部关闭:
            管道已满,write阻塞
            管道没有满,write将数据写入,并返回实际写入的字节数

认为设置管道非阻塞

int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
flags |= O_NONBLOCK;            // 修改flag的值
fcntl(fd[0], F_SETFL, flags);   // 设置新的flag

设置非阻塞案例:

#include 
#include 
#include 
#include 
#include 
#include 
/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
*/
int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};

        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        close(pipefd[0]);
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
        
    }
    return 0;
}

你可能感兴趣的:(Linux,操作系统,linux,c++,匿名管道)