【Linux】进程间通信 -> 匿名管道命名管道

进程间通信的目的

  1. 数据传输:一个进程许需要将它的数据发送给另外一个进程。
  2. 资源共享:多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的分类

  • 管道
  1. 匿名管道pipe
  2. 命名管道FIFO
  • Sytem V IPC
  1. System V消息队列
  2. System V共享内存
  3. System V信号量
  • POSIX IPC
  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

管道

我们把一个进程连接到另一个进程的一个数据流,称为管道。

进程是具有独立性的!要让进程间进行通信,“成本”一定不低。要让不同进程通信,首先要先让它们看到同一份资源。其次是通信。

这个公共的资源是谁提供的呢?其中一个进程?直接在进程内部创建资源,其他进程看不到。

所以,我们该如何理解进程间通信的本质问题呢?

  • OS需要给直接或间接给通信双方进程提供“内存空间”
  • 要通信的进程,必须看到同一份资源

所谓不同的通信种类,本质就是:上面所说的资源,是OS中的哪一个模块提供的!

未来学习的进程间通信的接口,与其说是通信的接口,不如说它是让不同的进程看到同一份资源的接口。

匿名管道

如果是一个普通文件,需要将内核缓冲区里的数据刷新到磁盘中。但是进程间通信,是一个进程的数据给另外一个进程,是内存到内存之间的。不需要将内核缓冲区里的数据刷新到磁盘,另一个进程再从磁盘中读取,因为会大大降低通信的效率。

既然不需要刷新缓冲区,那么OS就不需要在磁盘中创建打开文件,然后在内存中创建struct file对象。OS不需要访问磁盘,直接就可以在内存中创建struct file对象,创建对应的缓冲区,然后将对象的地址填入到文件描述符中,那么再fork创建子进程时,子进程会拷贝父进程的文件描述符表,通过文件描述符,进而父子进程就能看到同一个文件。父子进程双方就能基于这个内存级文件来进行通信了。

一般在文件里面标定一个文件使用的是文件名,但是这个管道文件,是一个内存级文件,没有名字所以叫匿名管道。

从文件描述符角度理解管道

  • 为什么让父进程分别以读和写的方式打开同一文件呢?

如果以只读或只写方式打开文件,那么子进程也会继承父进程的只读或只写方式,父子进程双方打开文件的方式是一样的,就完不成单向通信了。只有分别以读和写的方式打开,读和写的文件描述符才会被子进程继承,然后再选择对应的通信方向,关闭特定的文件描述符即可。

以读和写方式打开文件的本质:就是让子进程也能看到读写段端,让后续能自由的选择通信方向。

  • 必须要关闭父子进程特定的文件描述符吗?例如父进程写,关闭读端,子进程读,关闭写端。

也可以不关特定的文件描述符。但是一般都建议关掉,因为这个不用的文件描述符有可能被别人用到,进而就有可能修改管道数据,引起程序运行出问题。

再来理解管道:父进程通过调用管道特定的系统调用,以读和写的方式打开一个内存级文件,再通过fork创建子进程的方式,被子进程继承下去,再关闭对应的读写端,进而形成的一条通信信道,这一套通信信道是基于文件的,所以叫管道。

用fork来共享管道原理

从内核角度理解管道本质

看待管道,就如同看待文件一样,管道的使用和文件一致,迎合了“Linux下一切皆文件”的思想。

创建匿名管道

参数fd :文件描述符数组, 其中 fd[0] 表示读端,  fd[1] 表示写端。
返回值: 成功返回 0 ,失败返回-1。
文件描述符fd[0]、fd[1]默认从3开始,因为fd0、1、2默认被三个标准输入输出占用。
示例代码:
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;int main()
{// 第一步:创建管道文件,打开读写端int fds[2];int n = pipe(fds); // 成功返回0,失败返回-1assert(n == 0);// 第二步:创建子进程pid_t id = fork();assert(id >= 0);if (id == 0){// 子进程进行写入close(fds[0]);// 子进程的通信代码int cnt = 0;const char *s = "我是子进程,我正在给你发消息";while (true){cnt++;char buffer[1024]; // 只有子进程能看到!snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));sleep(1); // 细节:子进程每隔一秒写一次}// 子进程close(fds[1]);exit(0);}// 父进程进行读取close(fds[1]);// 父进程的通信代码while (true){char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); // 多留一个位置给\0if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;// 细节:父进程没有进行sleep}}n = waitpid(id, nullptr, 0);assert(n == id);close(fds[0]);
}

运行结果,父进程每隔一秒输出一次 。

  • 如果将子进程休眠时间改为5秒,会有什么现象呢?
//子进程
sleep(5);

运行结果,父进程每隔5秒输出一次。

  • 一开始子进程写入,父进程读取,输出。之后在子进程休眠的5秒内,父进程在干什么呢?

我们将代码改造一下:

    // 父进程的通信代码while (true){char buffer[1024];cout<<"AAAAAAAAAAAAAA"<<endl;ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); cout<<"BBBBBBBBBBBBBB"<<endl;if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}

可以看到父进程在read()这里阻塞了。

read这里就涉及了两个功能:

  1. 等缓冲区里有数据。
  2. 将数据从内核拷贝到用户层。

如果此时缓冲区里没有数据,父进程就会一直阻塞等待。OS将父进程从运行状态R改为阻塞状态S,放在等待队列中。等待的不就是文件吗(等管道文件里有数据)?所以文件里也一定存在类似等待队列这样的结构,将进程的PCB放入这个文件对应的等待队列中。当写了之后,缓冲区有数据了,OS识别到,再将进程的PCB从等待队列拿到运行队列,将进程状态由S改为R,就可以继续被调度了。

总结:如果管道中没有了数据,读端再读,默认会直接阻塞当前正在读取的进程!

  • 管道是一个固定大小的缓冲区。如果反过来,缓冲区写满了之后,写端继续写呢?

将代码改造一下:

    //子进程不休眠//...       while (true){cnt++;char buffer[1024];         snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));cout << "count: " << cnt << endl;}//...//父进程休眠1000秒//...while (true){sleep(1000);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}//...

一瞬间就写满了,不再继续写了。

可以看到,子进程一瞬间就将缓冲区写满了,不再继续写了。

总结:如果管道满了之后,写端再写,会发生阻塞等待,等待读端读取。

再将代码改造一下:

    //子进程不休眠//...       while (true){cnt++;char buffer[1024];         snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));cout << "count: " << cnt << endl;}//...//父进程休眠2秒//...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}//...

总结:父进程读取并不是一行一行读取的,而是按照指定大小读取的,也就是说缓冲区里有指定字节大小的数据,一次就会全部读完。

ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); 
  • 如果子进程写了一次之后,就将对应的写端描述符关闭呢?
        // ...// 子进程// ...while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());cout << "count: " << cnt << endl;write(fds[1], buffer, strlen(buffer));break;}close(fds[1]);cout << "子进程关闭写端" << endl;exit(0);}// ...// 父进程// ...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}else if (s == 0){cout << "read: " << s << endl;break;}}n = waitpid(id, nullptr, 0);assert(n == id);close(fds[0]);

父进程将管道数据读完之后,写端文件描述符也关闭了,那么就意味着已经完成了管道的读写,读端read()读到文件末尾,返回0。

  • 如果关闭读端,写端继续写呢?
        // ...// 子进程// ...while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());cout << "count: " << cnt << endl;write(fds[1], buffer, strlen(buffer));}close(fds[1]);cout << "子进程关闭写端" << endl;exit(0);}// ...// 父进程// ...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}else if (s == 0){cout << "read: " << s << endl;break;}break;}close(fds[0]);cout << "父进程关闭读端" << endl;int status = 0;n = waitpid(id, &status, 0);cout << "pid->" << n << " : " << (status & 0x7F) << endl;assert(n == id);

如果读端被关闭,写就没有意义了没有意义操作系统会杀掉写的子进程,是通过发送信号的方式被杀掉,也就相当于子进程异常退出了。一旦父进程关闭读端,子进程会立马退出,父进程waipid()就能获取到子进程的退出码。

OS会给子进程直接发送13号信号,来终止写进程。

读写特征

  1. 读快,写慢:读阻塞,等待写入。
  2. 读慢,写快:写阻塞,等待读取。
  3. 写关闭:读到0。
  4. 读关闭,终止写。

这四种读写特征分别对应了上述各种现象。

管道的特征

  • 管道的生命周期随进程,进程退出,管道释放。
  • 只能用于具有共同祖先(具有血缘关系的进程)之间进行通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后,父子进程之间就可以应用管道。常用于父子通信。
  • 管道是面向字节流的。
  • 内核会对管道操作进行同步与互斥,对共享资源进行保护的方案。
  • 管道是半双工的,数据只能向一个方向流动,需要双方进行通信时,需要建立两个管道。

命名管道

匿名管道应用的一个限制就是只能在具有血缘关系的进程之间进行通信,如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来完成,被称为命名管道,命名管道是一种特殊类型的文件。

命名管道可以从命令行上创建:

$mkfifo filename

  •  命名管道是如何让不同的进程看到同一份资源的呢?

命名管道也可以通过函数创建:

  • 创建

成功返回0,失败返回-1。

  • 删除

成功返回0,失败返回-1。

命名管道与匿名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfififo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。

匿名管道是通过文件描述符来让具有血缘关系的进程进行通信的。

命名管道是通过文件名来让不同的进程使用同一个管道通信的。

client&server通信

示例代码:

Makefile:

.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11 -gclient:client.ccg++ -o $@ $^ -std=c++11 -g.PHONY:clean
clean:rm -f server client

comm.hpp 

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>#define NAMED_PIPE "/tmp/mypipe"bool createFifo(const std::string &path)
{umask(0);int n = mkfifo(path.c_str(), 0600);if (n == 0)return true;else{std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;return false;}
}void removeFifo(const std::string &path)
{int n = unlink(path.c_str());assert(n == 0);//assert只在Debug下有效,在release下就没有了。(void)n;
}

 server.cc 

#include "comm.hpp"int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;//removeFifo(NAMED_PIPE);return 0;
}

此时就在tmp路径下创建了名为“mypipe”的管道。

下面来进行server和client的通信。通信的过程就是对文件的读写读写操作。

client.cc

int main()
{int wfd = open(NAMED_PIPE,O_WRONLY);if(wfd<0) exit(1);char buffer[1024];while(true){std::cout<<"Please say# ";fgets(buffer,sizeof buffer,stdin);ssize_t s = write(wfd,buffer,strlen(buffer));assert(s==strlen(buffer));(void)s;}close(wfd);return 0;
}

server.cc

int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;std::cout<<"server begin"<<std::endl;int rfd = open(NAMED_PIPE, O_RDONLY);std::cout<<"server end"<<std::endl;if(rfd < 0) exit(1);//readchar buffer[1024];while(true){ssize_t s = read(rfd, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;std::cout << "client->server# " << buffer << std::endl;}//如果读端关闭,写端读到0else if(s == 0){std::cout << "client quit, me too!" << std::endl;break;}//读取错误else{std::cout << "err string: " << strerror(errno) << std::endl;break;}}close(rfd);// sleep(10);removeFifo(NAMED_PIPE);return 0;
}

这样就完成了客户端client和服务端server两个进程间的通信。

  • 为什么服务端读取的时候会多一行空行呢?

原因是我们从键盘输入的时候会摁回车键,例如:输入“hello world”。实际fgets读取到的为:“hello world \n”。“\n”也会被读取到,所以会被多打印一行空行。

再对代码做一下优化。

client.cc:

int main()
{std::cout<<"client begin"<<std::endl;int wfd = open(NAMED_PIPE,O_WRONLY);std::cout<<"client end"<<std::endl;if(wfd<0) exit(1);char buffer[1024];while(true){std::cout<<"Please say# ";fgets(buffer,sizeof buffer,stdin);if(strlen(buffer)>0) buffer[strlen(buffer)-1] = 0;ssize_t s = write(wfd,buffer,strlen(buffer));assert(s==strlen(buffer));(void)s;}close(wfd);return 0;
}

 server.cc:

int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;std::cout<<"server begin"<<std::endl;int rfd = open(NAMED_PIPE, O_RDONLY);std::cout<<"server end"<<std::endl;if(rfd < 0) exit(1);//readchar buffer[1024];while(true){ssize_t s = read(rfd, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;std::cout << "client->server# " << buffer << std::endl;}//如果读端关闭,写端读到0else if(s == 0){std::cout << "client quit, me too!" << std::endl;break;}//读取错误else{std::cout << "err string: " << strerror(errno) << std::endl;break;}}close(rfd);// sleep(10);removeFifo(NAMED_PIPE);return 0;
}

当读端先执行时,会在open()阻塞。

当写端再执行时,读端才继续调度执行。

总结:只有当两个进程同时打开文件程序才能继续向后运行。

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

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

相关文章

Pytorch注意力机制应用到具体网络方法(闭眼都会版)

文章目录 以YoloV4-tiny为例要加入的注意力机制代码模型中插入注意力机制 以YoloV4-tiny为例 解释一下各个部分&#xff1a; 最左边这部分为主干提取网络&#xff0c;功能为特征提取中间这边部分为FPN&#xff0c;功能是加强特征提取最后一部分为yolo head&#xff0c;功能为获…

修改el-select下拉框高度;更新:支持动态修改

文章目录 效果动态修改&#xff1a;效果代码固定高度版本动态修改高度版本&#xff08;2024-12-25 更新&#xff1a; 支持动态修改下拉框高度&#xff09; 效果 动态修改&#xff1a;效果 代码 固定高度版本 注意点&#xff1a; popper-class 尽量独一无二&#xff0c;防止影…

如何完全剔除对Eureka的依赖,报错Cannot execute request on any known server

【现象】 程序运行报错如下&#xff1a; com.netflix.discovery.shared.transport.TransportException报错Cannot execute request on any known server 【解决方案】 &#xff08;1&#xff09;在Maven工程中的pom去掉Eureka相关的引用&#xff08;注释以下部分&#xff0…

vscode写python,遇到问题:ModuleNotFoundError: No module named ‘pillow‘(已解决 避坑)

1 问题&#xff1a; ModuleNotFoundError: No module named pillow 2 原因&#xff1a; 原因1&#xff1a;安装Pillow的pip命令所处的python版本与vscode调用的python解释器版本不同。 如&#xff1a; 原因2&#xff1a;虽然用的是pillow&#xff0c;但是写代码的时候只能用…

Ashy的考研游记

文章目录 摘要12.1112.2012.21 DAY1&#xff08;政治/英语&#xff09;政治英语 12.22 DAY2&#xff08;数学/专业课&#xff09;数学专业课 结束估分 摘要 在24年的12月里&#xff0c;Ashy完成了他的考研冲刺&#xff0c;顺利的结束了他本年度的考研之旅。 在十二月里&#…

AIGC实践|AI/AR助力文旅沉浸式互动体验探索

前言&#xff1a; 本篇文章的创作灵感来源于近期热门话题——让文物“动起来”&#xff0c;各大博物馆成为新进潮流打卡地。结合之前创作的AI文旅宣传片良好的流量和反馈&#xff0c;外加最近比较感兴趣的AR互动探索&#xff0c;想尝试看看自己能不能把这些零碎的内容整合起来…

Kubernetes(k8s)离线部署DolphinScheduler3.2.2

1.环境准备 1.1 集群规划 本次安装环境为&#xff1a;3台k8s现有的postgreSql数据库zookeeper服务 1.2 下载及介绍 DolphinScheduler-3.2.2官网&#xff1a;https://dolphinscheduler.apache.org/zh-cn/docs/3.2.2 官网安装文档&#xff1a;https://dolphinscheduler.apach…

C++的侵入式链表

非侵入式链表 非侵入式链表是一种链表数据结构&#xff0c;其中每个元素&#xff08;节点&#xff09;并不需要自己包含指向前后节点的指针。链表的结构和节点的存储是分开的&#xff0c;链表容器会单独管理这些指针。 常见的非侵入式链表节点可以由以下所示&#xff0c;即&a…

在vscode的ESP-IDF中使用自定义组件

以hello-world为例&#xff0c;演示步骤和注意事项 1、新建ESP-IDF项目 选择模板 从hello-world模板创建 2、打开项目 3、编译结果没错 正在执行任务: /home/azhu/.espressif/python_env/idf5.1_py3.10_env/bin/python /home/azhu/esp/v5.1/esp-idf/tools/idf_size.py /home…

2025差旅平台怎么选?一体化、全流程降本案例解析

差旅支出在企业中一直是一项重要但容易被忽视的成本开支&#xff0c;尤其是在项目驱动型企业中&#xff0c;因频繁的差旅需求&#xff0c;支出规模往往持续增长。以差旅平台分贝通签约伙伴——某智能制造业的业务模式为例&#xff0c;该模式要求员工定期前往不同的工厂、供应商…

【linux】NFS实验

NFS NFS服务 nfs,最早是Sun这家公司所发展出来的,它最大的功能就是可以透过网络,让不同的机器,不同的操作系统,进行实现文档的共享。所以你可以简单的将他看做是文件服务器。 实验准备 ①先准备一个服务器端的操作系统和客户端的操作系统(Red Hat)。 ②选择NAT模式,…

智源研究院与安谋科技达成战略合作,共建开源AI“芯”生态

12月25日&#xff0c;智源研究院与安谋科技&#xff08;中国&#xff09;有限公司&#xff08;以下简称“安谋科技”&#xff09;与正式签署战略合作协议&#xff0c;双方将面向多元AI芯片领域开展算子库优化与适配、编译器与工具链支持、生态系统建设与推广等一系列深入合作&a…

ROG NUC:强大内核激发创意,AI赋能学子科技探索

有这么一款能够激发无限创意、助力科技探索的迷你主机&#xff0c;它以其卓越的性能和迷你的身材成为了成为了ProArt百校行活动中的明星产品&#xff0c;助力广大学子勇敢探索未知&#xff0c;追逐属于自己的科技梦想。它就是ROG NUC 2024&#xff01; 强大性能&#xff0c;创意…

从零玩转CanMV-K230(8)-多线程例程

文章目录 前言一、_thread模块API二、使用示例创建并启动线程停止线程_thread.exit() 总结 前言 K230上不支持threading&#xff0c;只能支持_thread&#xff0c;该模块实现了相应 CPython 模块的子集&#xff0c;CPython 是 Python 编程的参考实现 语言&#xff0c;也是最著名…

yii2 手动添加 phpoffice\phpexcel

1.下载地址&#xff1a;https://github.com/PHPOffice/PHPExcel 2.解压并修改文件名为phpexcel 在yii项目的vendor目录下创建一个文件夹命名为phpoffice 把phpexcel目录放到phpoffic文件夹下 查看vendor\phpoffice\phpexcel目录下会看到这些文件 3.到vendor\composer目录下…

数据库-用户管理

一、创建用户 create user xy104192..168.42.24 identified by 123456;xy104&#xff1a;用户名 localhost&#xff1b;这个权限最高的root用户 %&#xff1a;任务ip地址 192.168.42.24&#xff1a;登录的IP地址 identified by ‘123456’&#xff1a;指定该用户的密码 mysql…

数据库安全-redisCouchdb

1.redis未授权访问 默认端口:6379 1.1 Redis沙盒逃逸漏洞RCE-CVE-2022-0543 介绍&#xff1a;Redis 是一套开源的使用 ANSI C编写、支持网络、可基于内存亦可持久化的日志型、键值存储数据库&#xff0c;并提供多种语言的API。Redis 如果在没有开启认证的情况下&#xff0c;…

springboot集成websokcet+uniapp开发聊天原型验证(一)

1. 整体思路 群组聊天功能实现思路 需要为每个群组维护一个对应的集合&#xff08;可以是 Set 等数据结构&#xff09;&#xff0c;用来存放该群组内所有在线用户的 WebSocketSession。当有消息发送到群组时&#xff0c;遍历该群组对应的集合&#xff0c;向其中的每个在线用户…

Linux -- 线程的优点、pthread 线程库

目录 线程的优点 pthread 线程库 前言 认识线程库 简单验证线程的独立栈空间 线程的优点 与进程之间的切换相比&#xff0c;线程之间的切换需要操作系统做的工作要少得多。 调度进程时&#xff0c;CPU 中有一个 cache&#xff08;缓存&#xff0c;提高运行效率&#xff0…

【magic-dash】01:magic-dash创建单页面应用及二次开发

文章目录 一、magic-dash是什么1.1 安装1.2 使用1.2.1 查看内置项目模板1.2.2 生成指定项目模板1.2.3 查看当前magic-dash版本1.2.4 查看命令说明1.2.5 内置模板列表二、创建虚拟环境并安装magic-dash三、magic-dash单页工具应用开发3.1 创建单页面项目3.1.1 使用命令行创建单页…