Linux_管道通信

news2024/10/7 14:29:49

目录

一、匿名管道

1、介绍进程间通信

2、理解管道

3、管道通信

4、用户角度看匿名管道

5、内核角度看匿名管道 

6、代码实现匿名管道 

6.1 创建子进程

6.2 实现通信

7、匿名管道阻塞情况 

8、匿名管道的读写原子性

二、命名管道

1、命名管道 

1.1 命名管道通信

2、代码实现命名管道

结语 


前言:

        在Linux下有一种概念叫做管道,管道的作用是实现进程间通信功能,其本质是一个文件,该文件也被当成进程通信的缓冲区,即一个进程往缓冲区内写数据,另一个进程从缓冲区内读数据,这一过程称之为进程间通信。管道分为两种:匿名管道、命名管道,匿名管道只限于含亲缘关系的进程间通信,而命名管道可以让两个无亲缘关系的进程进行通信。

一、匿名管道

1、介绍进程间通信

        进程间通信在Linux下是个很重要的概念,他允许两个以上的进程进行相互传递数据,在如今的实际生活中,早已离不开进程间通信,因为日常生活中的网上冲浪就是一种进程通信的行为,只不过通信的进程在不同的主机上,但是不论是不同主机的进程通信还是同一主机的进程通信的逻辑都相差不差,即进程间要看到相同的资源。

2、理解管道

        管道就是一种在同一主机下的进程通信,他的作用就如同现实中的“管道”一样,只允许一边写数据一边读数据,因此单个管道是一种半双工的通信方式。在Linux下可以使用管道将两个以上的指令进行结合输出,操作指令如下:

        管道的作用就是将前一个指令的输出结果写进管道,让后一个指令从管道中读取内容并执行。


        管道示意图:

3、管道通信

         管道是进程通信中一种最古老的方式,因为指令就是进程,所以在上述例子中,ll进程和wc进程利用管道进行进程通信,只不过通信的过程让系统封装好了,因此在上层无法清晰的看到整个通信过程,但是能够明白管道的通信逻辑是让两个进程看到同一份资源,所以使用管道进行进程通信的具体结构图如下:

        上图中父进程创建了一个子进程,并且子进程通过拷贝父进程的PCB,也会让子进程指向管道文件,此时就满足了两个进程看到同一份资源的条件(ps:0、1、2是默认打开的3个文件描述符,他们对应上层的三个文件流stdin、stdout、stderr)。

        将带有亲缘进程间通信的管道称为匿名管道,值得注意的是:管道的原理就是在内存上开一个临时文件,而不是在磁盘上新建文件,所以当管道文件由系统自动识别并回收。

4、用户角度看匿名管道

        匿名管道只适用于亲缘进程间通信,在用户层面上只关心进程文件描述符和管道文件的关系,具体使用匿名管道通信的步骤如下:

        1、父进程在fork子进程前要先创建管道文件,并且以读、写方式打开管道文件,此时会返回两个文件描述符3、4。


         2、父进程fork出子进程,因此子进程的3、4文件描述符也会指向同一个管道文件。


        3、父进程关闭文件描述符3,子进程关闭文件描述符4(或者相反),这样做的原因是当下只有一个缓冲区存放通信的数据,若一个进程既能写又能读,则无法保证该进程读到的数据一定是对方发来的数据,因为有可能读到自己发送的数据。

         此时就可以进行父进程向子进程发送数据的通信方式了。

5、内核角度看匿名管道 

         从内核角度看匿名管道通信,父进程创建管道文件时,会以读、写两种方式打开管道文件,因此在内核中一个管道文件是被打开了两次,可以理解为创建了两个struct file的结构体来管理一个管道文件

        具体示意图如下:

        注意:上图父进程关闭文件描述符3,子进程关闭文件描述符4,所以当前管道文件的struct file的引用计数是2,只有关闭了父进程的文件描述符4和子进程的文件描述符3,系统才会回收管道文件。

6、代码实现匿名管道 

        系统已经为用户提供了实现匿名管道的接口,接口介绍如下:

#include <unistd.h>//pipe所需要的头文件

//传一个数组给到pipe,pipe调用成功时返回0,失败返回-1
//调用成功pipefd数组的第一个元素是读的下标,第二个元素是写的下标
int pipe(int pipefd[2]);

        代码验证pipe接口:

#include<iostream>
#include<unistd.h>
#include <errno.h> 

using namespace std;

int main()
{
    int pipefd[2]={0};
    int n = pipe(pipefd);
    if(n == -1)
    {
        perror("pipe");
        return -1;
    }

    cout<<pipefd[0]<<" "<<pipefd[1]<<endl;
    return 0;
}

        运行结果:

        从结果可以发现,pipe确实改变了数组pipefd元素的值,让该进程的3号文件描述符以读的方式打开管道文件,该进程的4号文件描述符以写的方式打开文件,说明底层已经完成了使用匿名管道通信的第一步,即下图:

6.1 创建子进程

        有了上述的代码基础,就可以创建子进程并建立进程间通信的整体架构了,代码如下:

#include <iostream>
#include <unistd.h>
#include <errno.h>

using namespace std;

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n == -1)
    {
        perror("pipe");
        return -1;
    }

    // 下面实现的是子进程写,父进程读
    pid_t id = fork();
    if (id < 0)
        return -2;

    // chlid
    if (id == 0)
    {
        close(pipefd[0]); // 关闭读,子进程只写
        // 子进程的通信过程
        cout << "子进程开始传输数据" << endl;
        sleep(3);
        
        close(pipefd[1]); // 通信完毕后关闭写端
        exit(0);
    }

    // father
    close(pipefd[1]); // 关闭写,父进程只读
    // 父进程的通信过程
    cout << "父进程开始读取数据" << endl;
    sleep(3);

    close(pipefd[0]); // 通信完毕后关闭读端

    return 0;
}

         运行结果:

        上述代码完成的是匿名管道通信的第二步、第三步:

6.2 实现通信

        创建子进程并建立起进程通信的框架后,就可以进行父子进程通信了,所以需要在上述代码的通信过程中实现具体的通信,代码如下: 

#include <iostream>
#include <unistd.h>
#include <errno.h>
#include <cstring>

using namespace std;
#define NUM 1024

// child
void Writer(int wfd)
{
    string s = "你好父进程,我是子进程";
    pid_t self = getpid();
    int number = 0, time = 5;

    char buffer[NUM];
    while (time--)
    {
        sleep(1);

        // 构建发送字符串
        buffer[0] = 0;
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        cout << buffer << endl;
        // 发送/写入给父进程, system call
        write(wfd, buffer, strlen(buffer)); 
    }
    cout<<"子进程数据发送完成"<<endl;
}

// father
void Reader(int rfd)
{
    char buffer[NUM];

    while (true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1); // 预留\0的位置
        if (n > 0)
        {
            buffer[n] = 0; // 手动添加\0
            cout << "父进程收到子进程的消息[" << getpid() << "]# " << buffer << endl;
        }
        else if (n == 0)
        {
            printf("father read file done!\n");
            break;
        }
        else
            break;
        cout << endl;
    }
}

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n == -1)
    {
        perror("pipe");
        return -1;
    }

    // 下面实现的是子进程写,父进程读
    pid_t id = fork();
    if (id < 0)
        return -2;

    // chlid
    if (id == 0)
    {
        close(pipefd[0]); // 关闭读,子进程只写
        // 子进程的通信过程
        cout << "子进程开始传输数据" << endl;
        Writer(pipefd[1]);
        sleep(1);
        cout << "子进程关闭写端" << endl;
        close(pipefd[1]); // 通信完毕后关闭写端
        exit(0);
    }

    // father
    close(pipefd[1]); // 关闭写,父进程只读
    // 父进程的通信过程
    cout << "父进程开始读取数据" << endl;
    Reader(pipefd[0]);

    close(pipefd[0]); // 通信完毕后关闭读端

    return 0;
}

         运行结果:

7、匿名管道阻塞情况 

        1、读写端正常,当管道为空,读端就会阻塞,比如在上述代码中让子进程停留在write函数之前,观察父进程read函数,测试结果如下: 

         发现父进程阻塞在read函数处,原因就是管道为空但读写端都未关闭,read函数读取不到任何数据会阻塞在此处。


        2、读写端正常,当管道写满时,写端就会阻塞(因为管道也是有大小的),比如在上述代码中让父进程停留在read函数之前,这样就会让子进程一直往管道里写数据,最终导致管道被写满,导致write函数阻塞住,测试结果如下:

        从结果看,子进程一直在写数据,但是父进程没有读取数据,最终导致子进程阻塞在write处。


        3、读端正常,把写端的文件描述符关闭,在读端把管道内的数据读完后,下一次读取会导致read函数直接返回0,不会导致read阻塞,将上述代码的写端先关闭,读端不关闭,观察写端关闭后read函数的返回值,测试结果如下:

        从结果看到,子进程写端关闭后,父进程的read函数就会返回0。


        4、 写端正常,把读端的文件描述符关闭,此时操作系统会直接把整个写端进程以信号的方式终止,终止信号为13号,可以通过waitpid函数来获取子进程的终止信号,测试结果如下:

8、匿名管道的读写原子性

        原子性即保证数据的完整性,比如写端写了hello world,则读端必须要等写端把整个hello world写完才能读,不能在写端写了一个hello时就去读。对于写入原子性,Linux规定当单次写入的数据小于等于PIPE_BUF(即管道的容量)则保证这一次的写入是原子性的,大于则不能保证原子性。对于读取原子性,可以理解为用户控制得好则读取就是原子性的,控制不好读取的大小则读取就不是原子性的。

二、命名管道

1、命名管道 

        上述的匿名管道的局限性在于只能作用于亲缘进程间通信,而现实生活中大多数进程间通信都不是亲缘关系,因此就引出另一个概念:命名管道,他允许两个以上的非亲缘进程进行通信,他是一种特殊的文件形式。因为管道文件是内存级别的,所以无论往里面写多少数据,管道文件的大小始终是0,原因就是管道文件的数据没有被写到磁盘中,仅仅在内存中就被消耗了。

        在Linux指令中,通常用‘|’来表示匿名管道,同样命名管道也可以直接用指令创建出来,测试如下:


        命名管道实现通信的逻辑就是让两个进程通过该“namepipe”命名管道文件进行通信,即发送端把数据写进该文件里,读取端从该文件里读数据,并且这两个进程可以是无亲缘关系的,因为他们只需要找到namepipe文件的路径即可完全通信。

1.1 命名管道通信

        这里用两个窗口模拟两个进程,一个窗口给命名管道发送数据,另一个窗口从管道中读取数据,以此完成通信,测试结果如下:

         从结果发现,依靠命名管道完成了两个进程的通信。

2、代码实现命名管道

        和实现匿名管道逻辑一样,系统为上层提供了一个接口mkfifo来创建命名管道,指令介绍如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
//pathname表示创建命名管道的路径
//mode表示命名管道的权限
//mkfifo成功返回0,失败返回-1

        有了此接口,就可以实现简单的无亲缘进程间通信了,我们可以创建两个独立的主进程(发送方和接收方),然后通过mkfifo生成的命名管道进行通信。


        发送方代码如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

int main()
{
    int fd = open("my_namepipe", O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        return -1;
    }

    string line;
    while (true)
    {
        cout<<"客户端发送的数据:";
        cin>>line;
        int n = write(fd,line.c_str(),strlen(line.c_str()));
        if(n==-1)
        {
            perror("write");
            return -1;
        }
    }
    close(fd);
    return 0;
}

        命名管道文件由接收方在通信前生成,接收方代码如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

int main()
{
    int mode = 0666;
    int n = mkfifo("./my_namepipe", mode);
    if (n == -1)
    {
        perror("mkfifo");
        return -1;
    }

    int fd = open("my_namepipe", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        return -1;
    }

    char buff[1024];
    while (true)
    {
        int n = read(fd, buff, sizeof(buff) - 1);
        if (n > 0)
        {
            buff[n] = 0;
            cout << "服务器接收到的数据:" << buff << endl;
        }
    }

    close(fd);
    if(unlink("my_namepipe")==-1)
    {
        perror("unlink");
        return -1;
    }

    return 0;
}

        运行结果:

        注意事项:读端、写端双方必须都打开了命名管道文件才会都往下执行,也就是说只要有一方没有打开命名管道文件,则另一方就会阻塞在open处。 

结语 

         以上就是关于管道的讲解,管道分两种:匿名管道、命名管道,两者都可进行进程间通信,只不过匿名管道适用于亲缘进程,而命名管道可以使用非亲缘进程,并且系统提供这两个管道的接口给予用户使用,用户也可以在指令层面上使用他们,他们通信的本质都是让两个进程看到同一份资源,对该资源的读写就是通信的过程。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1889452.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VoiceCraft—— 业界最高水平的自然语音合成语言模型

VoiceCraft: 实现语音编辑和合成的 SOTA 论文地址&#xff1a;https://arxiv.org/html/2403.16973v1 源码地址&#xff1a;https://github.com/jasonppy/voicecraft 本文介绍VoiceCraft 的开发情况&#xff0c;它在语音编辑和零点语音合成 (TTS) 方面都实现了 SOTA。在本文中…

如何压缩jpg图片的大小?关于缩小jpg图片的四种方法

如何压缩jpg图片的大小&#xff1f;压缩JPG图片大小是一项常见的技术&#xff0c;用来优化图片以适应不同的应用需求。无论是为了在网页上提高加载速度、减少存储空间占用&#xff0c;还是为了便于通过电子邮件或社交媒体分享&#xff0c;压缩jpg图片都是必不可少的步骤。这种技…

AIoTedge:智能边缘计算平台

随着物联网(IoT)和人工智能(AI)技术的飞速发展&#xff0c;AIoT&#xff08;人工智能物联网&#xff09;已成为推动智能化转型的关键力量。AIoT Edge作为这一领域的创新平台&#xff0c;通过边缘计算技术&#xff0c;为企业提供了一个高效、灵活且安全的解决方案。 边云协同架构…

Java 7新特性深度解析:提升效率与功能

文章目录 Java 7新特性深度解析&#xff1a;提升效率与功能一、Switch中添加对String类型的支持二、数字字面量的改进三、异常处理&#xff08;捕获多个异常&#xff09;四、增强泛型推断五、NIO2.0&#xff08;AIO&#xff09;新IO的支持六、SR292与InvokeDynamic七、Path接口…

Xilinx FPGA:vivado实现乒乓缓存

一、项目要求 1、用两个伪双端口的RAM实现缓存 2、先写buffer1&#xff0c;再写buffer2 &#xff0c;在读buffer1的同时写buffer2&#xff0c;在读buffer2的同时写buffer1。 3、写端口50M时钟&#xff0c;写入16个8bit 的数据&#xff0c;读出时钟25M&#xff0c;读出8个16…

前端进阶全栈计划:Spring扫盲

Spring扫盲 spring 和 springboot的关系? 类比前端&#xff1a;vue.js和nuxt.js的关系 Spring Boot 是基于 Spring 框架的快速开发工具&#xff0c;简化了 Spring 应用的配置和部署。 spring核心特性 1. 依赖注入 依赖注入是Spring框架的核心功能之一。它允许你通过配置将对…

Golang | Leetcode Golang题解之第206题反转链表

题目&#xff1a; 题解&#xff1a; func reverseList(head *ListNode) *ListNode {if head nil || head.Next nil {return head}newHead : reverseList(head.Next)head.Next.Next headhead.Next nilreturn newHead }

什么是Web3D交互展示?有什么优势?

在智能互联网蓬勃发展的时代&#xff0c;传统的图片、文字及视频等展示手段因缺乏互动性&#xff0c;正逐渐在吸引用户注意力和提升宣传效果上显得力不从心。而Web3D交互展示技术的横空出世&#xff0c;则为众多品牌与企业开启了一扇全新的展示之门&#xff0c;让线上产品体验从…

学校卫星电子怎么自动校准时间呢

在学校的教室里&#xff0c;卫星电子钟精准地为师生们提供着时间服务&#xff0c;而其自动校准时间的功能令人称奇。那么&#xff0c;学校卫星电子钟是如何实现自动校准时间的呢&#xff1f; 学校卫星电子钟自动校准时间的原理基于卫星导航系统。常见的如北斗卫星导航系统或 GP…

小程序-<web-view>嵌套H5页面支付功能

背景&#xff1a;小程序未发布前&#xff0c;公司使用vue框架搭建了管理系统&#xff0c;为了减少开发成本&#xff0c;微信提供了web-view来帮助已有系统能在小程序上发布&#xff0c;详见web-view | 微信开放文档。因公司一直未打通嵌套H5小程序的支付功能&#xff0c;导致用…

湖北建筑安全员A证跨省调出审核不通过?可能是这些原因

湖北建筑安全员A证跨省调出审核不通过&#xff1f;可能是这些原因 湖北建筑安全员A证跨省调出审核不通过怎么办&#xff1f; 湖北建筑安全员ABC正常情况下都是可以跨省调出的&#xff0c;现在建筑三类人员安全员ABC在全国工程质量安全监管信息平台都是可以查询的&#xff0c;在…

offer150-16:数值的整数次方

题目描述:实现函数double Power(double base,int exponent),求base 的exponent次方。不得使用库函数&#xff0c;同时不需要考虑大数问题。 分析&#xff0c;题目要求实现库函数pow(),由于不需要考虑大数问题&#xff0c;不必担心溢出&#xff0c;那么就需要对输入的各种情况进…

LeetCode热题100刷题3:3. 无重复字符的最长子串、438. 找到字符串中所有字母异位词、560. 和为 K 的子数组

3. 无重复字符的最长子串 滑动窗口、双指针 class Solution { public:int lengthOfLongestSubstring(string s) {//滑动窗口试一下//英文字母、数字、符号、空格,ascii 一共包含128个字符vector<int> pos(128,-1);int ans 0;for(int i0,j0 ; i<s.size();i) {//s[i]…

【Python网络通信】基于Bypy调用百度网盘api实现自动上传和下载网盘文件

网盘对于大家的生活工作可以说是息息相关&#xff0c;但是如果每天都重复去上传下载文件就会很浪费时间&#xff0c;所以有没有什么办法可以解放双手&#xff1f;那就是网盘接口&#xff0c;本文通过Bypy库实现百度网盘的自动上传和下载文件。 原创作者&#xff1a;RS迷途小书童…

java 面试题 - 索引

上脑图&#xff0c;大家要记住&#xff01;&#xff01; 看不清&#xff0c;上大图&#xff01; 这几总结就够用&#xff01;&#xff01;

SpringBoot苏桦旅游管理系统-计算机毕业设计源码02123

摘要 旅游业在全球范围内不断发展&#xff0c;为了提供高效的旅游管理和服务&#xff0c;开发一个旅游管理系统具有重要意义。本文旨在设计和实现该旅游管理系统&#xff0c;以满足用户和管理员的需求。该系统采用Spring Boot作为后端框架&#xff0c;利用其简化的开发流程和强…

六、【源码】应用上下文

源码地址&#xff1a;https://github.com/spring-projects/spring-framework 仓库地址&#xff1a;https://gitcode.net/qq_42665745/spring/-/tree/06-application-context 应用上下文 实现应用上下文的核心类&#xff1a; 1.ApplicationContext&#xff1a;Spring 应用上…

java设计模式(五)建造者模式(Builder Pattern)

1、模式介绍&#xff1a; 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它允许你创建一个复杂对象的表示&#xff0c;同时隐藏了创建细节。该模式将对象的构建过程和表示分离&#xff0c;使得相同的构建过程可以创建不同的表示。 2、应…

如何选择适合自己的巴比达内网穿透方案

选择适合自己的巴比达内网穿透方案&#xff0c;需要考虑几个关键因素&#xff0c;包括您的具体需求、安全性要求、技术水平以及预算。以下是一些选择巴比达内网穿透方案的建议步骤&#xff1a; 1. 确定需求和用途 首先&#xff0c;需要明确您希望通过内网穿透实现的具体目标和…

项目实战-MySQL极佳优化方案---前缀索引

一、应用背景 由于公司项目数据量较大&#xff0c;开发小程序和APP过程中&#xff0c;SQL查询超过1.5s以上的等待时间&#xff0c;因此需要对SQL或者数据表结构设计进行优化。就不讲SQL语句优化啦&#xff0c;而是记录一下数据表结构设计优化的前缀索引实现。 二、MySQL索引 …