当前位置: 首页 > news >正文

Linux 进程间通信

1. 为什么需要通信?

进程之间需要协同工作完成某个任务,好比在学校中,校长想要拿到同学的基本信息,叫每个院的管理人去统计,每个院的管理人叫班主任去统计,班主任叫班长统计,班长叫每个人填好表,再依次上交

这个过程中,需要很多人参与完成一件事,一个人叫另一个人完成某件事的前提是:他得先告诉别人,也就是说,协同的前提条件是通信

通信就意味着数据的传递,而数据是有类别的,通知就绪的、单纯传递数据的、控制信息的…

2. 如何通信?

进程具有独立性,A,B两个进程,彼此之间一定不能访问对方的数据,但它们之间又需要通信,该怎么办呢?这种时候,就必须有第三者出现,也就是 OS,必须由 OS 给两个进程开辟一个共享的资源,同时它还要提供访问这片共享资源的系统调用,而共享资源的不同,提供的系统调用就不同,说明进程通信是有很多种类的

因此,进程通信的前提是:让不同的进程看到同一块"资源"(操作系统开辟的内存)

  1. 某个进程需要通信,让 OS 开辟一块内存
  2. OS 要提供系统调用,两个进程通过系统调用进行通信

3. 通信的方式

3.1 管道

管道的设计并没有采用新的技术,而是复用 Linux 内核的代码,完成进程间通信

它的通信是单向的,只能一端读,另一端写,因为它的这种特性,于是称它为管道

匿名管道

根据我们之前的知识,一个文件被打开,OS 就会创建一系列的内核数据结构

进程以 w 和 r 的方式分别打开同一个文件,在 OS 内部,该进程一定会有两个 struct file,因为打开文件的方式不一样,结构体内容也就不同,但是,文件的内容是相同的,因此,两个 struct file 都指向同一块内核缓冲区

在这里插入图片描述

然后,父进程创建子进程,子进程会继承父进程的数据,包括文件描述符,相当于将父进程的内核结构体拷贝了一份给子进程,但 struct file 等一系列文件相关的数据需要拷贝一份吗?答案是不需要,因为这些东西是文件的,跟进程没有关系,因此,子进程的文件描述符表与父进程指向相同的 struct file

在这里插入图片描述

此时进程间通信的前提就具备了,父子进程看到了同一份内核缓冲区,也就是同一份资源

这次,写入到内核缓冲区的数据未来不是刷新到磁盘,而是交给另一个进程,因此,系统调用就不要加上将内核缓冲区的数据刷新到磁盘这一步了,我们把上述的一系列结构叫做匿名管道

在这里插入图片描述

之后,我们规定父进程读,子进程写,或者父进程写,子进程读,然后将各自不需要使用的 fd 关闭,就能基于管道完成单向通信了

理解了上述内容,我们也能解释这样的现象:

  1. Linux 命令行中,我们启动的进程默认打开了0,1,2文件描述符,是谁打开的?

    bash 默认打开了0,1,2,所有的子进程继承了 bash 的文件描述符,也就默认打开了

  2. 为什么父子进程会向同一个显示器打印数据?

    子进程继承父进程的文件描述符表

  3. 为什么子进程 close(0/1/2) 不影响父进程向显示器打印数据

    子进程关闭,父进程没有关闭对应的 fd

将匿名管理的原理进行简化:

在这里插入图片描述

  1. 父子进程既然要关闭不需要的fd,为什么要打开不需要的fd?

    这是为了让子进程继承与父进程不同功能的 fd,如果父进程只打开w/r,子进程就只能继承w/r,也就不能完成一个进程写,另一个进程读的要求了

  2. 打开后能不关闭不需要的fd吗?

    答案是可以的,但是建议关闭,因为如果你是读端,但仍能对管道写,有可能会误写

  3. 为什么管道只能单向通信?不能双向吗?

    如果想要双向通信,可以使用两个管道,管道在设计时,就尽量避免复杂的情况,追求简单的模式,如果实现成双向,管道的内部必然要区分数据分别是谁写的等复杂情况,因此,就只让它进行单向通信,这也就是它为什么叫管道

  4. 子进程能继承父进程的数据,这算是通信吗?

    这是不算的,虽然子进程能看到父进程的数据,但如果要写时,会发生写时拷贝,双方是看不到彼此写入的数据的

了解了匿名管道的原理,接下来介绍匿名管道的使用

#include <unistd.h>int pipe(int pipefd[2]);
#include <cstdlib>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>void ChildWrite(int fd)
{while(true){static int cnt = 0;std::string msg = "Hello from child process-" + std::to_string(++ cnt);write(fd, msg.c_str(), msg.size());sleep(1);}
}void ParentRead(int fd)
{while(true){char buf[1024] = { 0 };ssize_t n = read(fd, buf, sizeof(buf) - 1);if(n > 0){buf[n] = '\0';std::cout << "Parent read: " << buf << std::endl;}}
}int main()
{int pipefd[2];int n = pipe(pipefd);if(n < 0){std::cerr << "pipe error" << std::endl;return -1;}std::cout << "pipefd[0] = " << pipefd[0] << std::endl;std::cout << "pipefd[1] = " << pipefd[1] << std::endl;pid_t id = fork();if(id == 0){// childclose(pipefd[0]); // 子进程关闭读端ChildWrite(pipefd[1]);close(pipefd[1]);exit(1);}// parentclose(pipefd[1]); // 父进程关闭写端ParentRead(pipefd[0]);close(pipefd[0]);pid_t wid = waitpid(id, nullptr, 0);if(wid > 0){std::cout << "child process " << id << " exit" << std::endl;}return 0;
}

在这里插入图片描述

管道的 4 种情况:

  1. 如果管道为空,写端一直不写且没有关闭,则读端进入阻塞状态(等待读取条件具备)
  2. 如果管道满了,读端一直不读且没有关闭,则写端进入阻塞状态(等待写入条件具备)
  3. 读端在读时,写端关闭了,此时读端 read 会读到 0,表示读到文件末尾
  4. 写端在写时,读端关闭了,写端会被 OS 使用 13 号信号 SIGPIPE 杀掉,相当于进程出现了异常(OS不会浪费时间和空间做没有意义的事)

匿名管道的 5 种特征:

  1. 匿名管道只适用于具有血缘关系的进程之间通信,常用于父子进程
  2. 管道内部,自带进程之间同步(多执行流执行代码时,具有明显的顺序性)的机制,管道内部有数据,读端会读取数据,管道内部没有数据,读端会阻塞等待
  3. 管道文件的生命周期是随进程的(当进程被释放,它指向所有的文件也会被释放)
  4. 管道文件在通信时,是面向字节流的,读的次数和写的次数不是一一对应的
  5. 管道的通信模式,是一种特殊的半双工模式

可以使用匿名管道实现进程池

代码:processpool

命名管道

匿名管道只适用于具有血缘关系的进程之间进行通信,那如果两个毫不相干的进程也想通信呢?

通信的前提是看到同一份资源,如果两个进程打开同一个文件,由于不同进程对同一份文件可能进行不同的操作,因此它们都要有一份 struct file 结构,但文件的内容肯定一样,因此共享同一份内核缓冲区

此时,两个进程就看到了同一份资源,只不过未来刷新到内核缓冲区里的数据不需要刷新到磁盘,我们把这个特殊的文件叫做管道文件

如何保证两个进程看到了同一份资源,也就是打开了同一个文件?文件路径能标识文件的唯一性

在这里插入图片描述

代码:NamedPipe

3.2 System V IPC

管道技术复用 Linux 内核代码,也有基于 System V 标准专门为进程间通信单独设计出来的方案,只不过它们只适用于本地通信

共享内存

在这里插入图片描述

当进程 A 需要和进程 B 通信时,让 OS 创建共享内存,挂接到自己的地址空间,进程 B 也要找到相同的共享内存,但进程 A 和进程 B 是两个毫不相干的进程,进程 B 如何得知进程 A 创建的共享内存呢?共享内存一定要有唯一性标识,如果让系统自动生成唯一性标识,进程 B 也就无法得知,因此共享内存的唯一性标识需要用户自己设置,是两个进程都可知的

#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
/*
key: 用户设置的共享内存唯一性标识,使用ftok()函数生成
size: 共享内存的大小,OS会根据4096的倍数申请
shmflg: 
1. IPC_CREAT: 共享内存不存在则创建,存在则获取
2. IPC_EXCL: 共享内存不存在则创建,存在则报错,一定会获取一个新的共享内存,配合IPC_CREAT使用
shmget()函数调用成功则返回shmid
*/int shmctl(int shmid, int cmd, struct shmid_ds *buf);
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id)
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

key 和 shmid:

  • key:用户生成,内核使用,内核用来区分共享内存的唯一性的,用户不能通过 key 管理共享内存
  • shmid:内核返回给用户,用户通过 shmid 管理共享内存

共享内存的缺点:

  • 共享内存不提供保护机制,意味着可能会导致读写数据不一致的问题,可以利用管道解决

共享内存的优点:

  • 共享内存是所有 IPC 方案中效率最高的,因为减少了数据的拷贝次数

代码:Shm

http://www.xdnf.cn/news/1139563.html

相关文章:

  • 计算机网络概述
  • 深入解析Hadoop:机架感知算法与数据放置策略
  • redis中间件
  • 《计算机“十万个为什么”》之什么是 feed 流
  • c++学习之---哈希表的实现(开放定址法和链地址法)
  • Ubuntu FTP服务搭建与配置
  • LVS实验
  • 尚庭公寓----------分页查询
  • 基于MATLAB的极限学习机ELM的数据回归预测方法应用
  • 快速了解网络爬虫
  • 区块链之Casper协议背景、演变发展、运作机制和潜在风险
  • 切比雪夫不等式的理解以及推导【超详细笔记】
  • Python获取网页乱码问题终极解决方案 | Python爬虫编码处理指南
  • VSCode 配置 C# 开发环境完整教程(附效果截图)
  • 透过结构看时间——若思考清洗则表达有力
  • Qt初阶开发:QMediaPlayer的介绍和使用
  • 适配器模式 (Adapter Pattern)
  • 基于MATLAB的极限学习机ELM的数据分类预测方法应用
  • 闲庭信步使用图像验证平台加速FPGA的开发:第二十一课——高斯下采样后图像还原的FPGA实现
  • 嵌入式时钟系统
  • 产品经理如何绘制流程图
  • vue中的this.$set
  • Python元组(Tuple)指南
  • FastAdmin系统框架通用操作平滑迁移到新服务器的详细步骤-优雅草卓伊凡
  • 爬虫小知识
  • 【Lua】题目小练2
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘django’问题
  • vue-cli 模式下安装 uni-ui
  • JAVA面试宝典 -《Kafka 高吞吐量架构实战:原理解析与性能优化全攻略》
  • 图片上传实现