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

devtools/2024/12/28 2:37:40/

进程间通信的目的

  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.ppmy.cn/devtools/145990.html

相关文章

InnoDB 事务并发问题

1.并发问题发生场景 并发问题产生的原因是多个线程操作了同一条记录&#xff0c;可分为三种场景。 1.1读-读 即并发事务相继读取相同的记录。 读操作不会改变数据本身&#xff0c;因此并不会引起并发问题。 1.2写-写 即并发事务相继对相同的记录做出改动。 这种情况下会…

免费线上签字小程序,开启便捷电子签名

虽如今数字化飞速发展的时代&#xff0c;但线上签名小程序的开发制作却并非易事。需要攻克诸多技术难题&#xff0c;例如确保签名的真实性与唯一性&#xff0c;防止签名被伪造或篡改。 要精准地捕捉用户手写签名的笔迹特征&#xff0c;无论是笔画的粗细、轻重&#xff0c;还是…

android jetpack compose Model对象更新变量 UI不更新、不刷新问题

以前是搞老本行Android原生开发的&#xff0c;因为工作原因&#xff0c;一直在用vue小程序&#xff1b;因为一些工作需要&#xff0c;又需要用到Android原生开发&#xff0c;建了个项目&#xff0c;打开源码一看&#xff0c;天塌了&#xff01;&#xff01;&#xff01;我以前的…

设计模式01:创建型设计模式之单例、简单工厂的使用情景及其基础Demo

一、单例模式 1.情景 连接字符串管理 2.好处 代码简洁&#xff1a;可全局访问连接字符串。性能优化&#xff1a;一个程序一个连接实例&#xff0c;避免反复创建对象&#xff08;连接&#xff09;和销毁对象&#xff08;连接&#xff09;。线程安全&#xff1a;连接对象不会…

【设计模式】装饰器模式(Decorator Pattern)

定义 装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式。 装饰器模式通过创建一个装饰类&#xff0c;包装原始对象&#xff0c;并在保持原始对象接口不变的情况下&#xff0c;扩展其功能。 模式示例 #include <iostream> #include <strin…

详细对比JS中XMLHttpRequest和fetch的使用

在JavaScript中&#xff0c;XMLHttpRequest 和 fetch 是两种用于进行 HTTP 请求的 API。它们的主要区别在于设计理念、用法和功能支持。以下是两者的详细对比&#xff1a; 1. 语法与用法 XMLHttpRequest: 较老的 API&#xff0c;最早出现在 2000 年代。支持异步和同步请求&…

【开发问题记录】eslint9 中 eslint 和 prettier冲突

文章目录 1、引言2、问题复现3、问题修复4、注意5、eslint-plugin-prettier/recommended 与自己的默认规则&#xff0c;冲突解决 1、引言 eslint 和 prettier 这俩都是在前端工程化中不可缺少的东西&#xff0c;但这俩&#xff0c;在一块运行的时候&#xff0c;总会有点问题 Es…

TP5 动态渲染多个Layui表格并批量打印所有表格

记录&#xff1a; TP5 动态渲染多个Layui表格每个表格设置有2行表头&#xff0c;并且第一行表头在页面完成后动态渲染显示内容每个表格下面显示统计信息可点击字段排序一次打印页面上的所有表格打印页面上多个table时,让每个table单独一页 后端代码示例&#xff1a; /*** Nod…