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

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

1、定义


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


统计一个目录中文件的数目命令: ls | wc -l,为了执行该命令,shell 创建了两个进程来分别执行ls 和wc;通常情况下,进程 ls 的输出直接通过 stdout 输出到控制台,但是为了两个进程能够进行通信,系统会建立一个管道,然后把进程 ls 发的内容输出到管道,进程 wc 从管道中读取进程 ls 输出的内容,从而实现进程间的通信。

1f7a702028bf4933bf7ba57cc3e7b0e0.png


2、特点


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

cb34969908de484885e4be1720c6e0d3.png

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


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


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


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

9a5e533826c84a3dbc62374f3c85b95c.png

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

cc5f2f41ca8e422a83ba6d2b12d6468a.png


3、匿名管道的使用


创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
     pipefd[0] 对应的是管道的读端
     pipefd[1] 对应的是管道的写端
返回值:
     成功 0
     失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
查看管道缓冲大小命令
ulimit -a
查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd, int name);



4、匿名管道通信案例


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

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    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;
}


相关文章
|
1月前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
3月前
|
存储 Linux Docker
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
关于如何在CentOS 7.6上安装Docker、介绍Docker存储引擎以及服务进程关系的实战案例。
166 3
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
|
3月前
|
算法 调度 UED
操作系统中的进程管理:原理与实践
在数字世界的心脏跳动着无数进程,它们如同细胞一般构成了操作系统的生命体。本文将深入探讨进程管理的奥秘,从进程的诞生到成长,再到最终的消亡,揭示操作系统如何协调这些看似杂乱无章却又井然有序的活动。通过浅显易懂的语言和直观的比喻,我们将一起探索进程调度的策略、同步机制的重要性以及死锁问题的解决之道。准备好跟随我们的脚步,一起走进操作系统的微观世界,解锁进程管理的秘密吧!
72 6
|
4月前
|
人工智能 PyTorch 算法框架/工具
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
【8月更文挑战第6天】Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
3月前
|
SQL 网络协议 数据库连接
已解决:连接SqlServer出现 provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程【C#连接SqlServer踩坑记录】
本文介绍了解决连接SqlServer时出现“provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程”错误的步骤,包括更改服务器验证模式、修改sa用户设置、启用TCP/IP协议,以及检查数据库连接语句中的实例名是否正确。此外,还解释了实例名mssqlserver和sqlserver之间的区别,包括它们在默认设置、功能和用途上的差异。
|
4月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
184 3
|
4月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
4月前
|
Linux API C语言
Linux源码阅读笔记02-进程原理及系统调用
Linux源码阅读笔记02-进程原理及系统调用
|
4月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
74 0