type
status
date
slug
summary
tags
category
icon
password
Linux进程间通信
前言
- 进程为什么要通信?
- 进程也是需要某种协同的,所以如何协同的前提条件就是通信。
- 数据是由类别的,通知就绪的,有一些就是单纯的要传递给我数据,控制相关的信息。
事实:进程是具有独立性的。进程 = 内核数据结构 + 代码和数据。
- 进程如何通信?
- 进程间通信的本质,必须让不同的进程看到同一份“资源”。
- “资源”就是特定形式的内存空间。
- 这个资源是由操作系统来提供的,那么为什么不是我们两个进程中的一个?假设一个进程提供,这个资源属于谁,这个进程独有,破坏进程独立性,第三方空间。
- 我们进程访问这个空间,进行通信,本质就是访问操作系统!
- 进程代表的就是用户,“资源”从创建,使用(一般),释放资源我们需要使用系统调用接口。
- 从底层设计,从接口设计,都要由操作系统独立设计,一般操作系统会有一个独立的通信模块,属于文件系统 (IPC通信模块),标准(system V && posix)
- system V:三种方式:消息队列、共享内存、信号量
还有一种就是基于文件级别的通信方式—->管道
管道
- 管道是
Unix
中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道是一个基于文件系统的一个内存级的单向通信的文件,主要用来进行进程间通信(IPC)。
匿名管道
- 一个文件被打开两次
struct_file
是要被创建两个的,第二次打开同一个文件的时候,不需要再次加载文件
在创建一个子进程的时候,不会再次加载文件,因为进程要保持独立性,和文件没有关系。
为什么父子进程会向同一个显示器终端打印数据?
因为子进程会继承父进程的文件描述符表,会指向同一个文件,进程会默认打开三个标准输入输出错误:0,1,2,怎么做到的?
其实我们所有的在命令行中都是bash的子进程,bash打开了,所有的子进程默认也就打开了,我们只要做好约定即可。
为什么我们主动close(0/1/2),不影响父进程继续使用显示器文件呢?
- 其实在struct_file里面包含了一个引用计数,是一个内核级的,这也就能解释了。
进程间通信的本质,必须让不同的进程看到同一份“资源”,这个资源是由操作系统来分配的,我们看到的同一份“资源”就是内核级的文件缓冲区。

管道只允许单向通信,因为它简单,那么如何通信呢?
- 子进程想写就关闭读的文件描述符(3),父进程就关闭写的文件描述符(4),此时,父进程就可以通过3号描述符进行读,子进程就可以通过4号文件描述符进行写,双方就可以写入同一个管道文件了。
父子既然要关闭不需要的fd,为什么曾经要打开呢?可以不关吗?
- 如果只打开一个文件描述符的话,未来子进程继承的时候也就会继承一个,那么以读方式打开,继承只能继承读,一个管道不能同时存在读写,我们也不能以读写的方式打开,因为管道是单向通信的,万一失误了呢?这个方式很不好。
- 所以总的来说就是为了让子进程继承下去!
- 可以不关吗?可以!但是建议关了,万一读误写了呢?
还有就是为什么我们两个进程通信的时候,只是在内核级文件缓冲区,而不需要刷新到磁盘,所有虽然管道可以复用,但是还是要重新设计一下。
其中fd[0]–>读,fd[1]–>写。
因为这个管道是内存文件,没有文件名字,所以叫做匿名管道。

pipe
接下来我们可以使用pipe来打开管道
pipefd
是一个输出形参数
- 不需要文件路径和文件名(匿名文件/匿名管道)

- 成功返回
0
,失败返回1
,错误码被设置。

如果我想要双向通信呢?
那就使用两个管道
为什么单向通信?
- 因为简单,只让它进行单向通信,符合这样的特点所以就叫管道。
测试管道接口 –> 代码验证
管道的4种情况
- 如果管道内部是空的 && write fd没有关闭,读取条件不具备,读进程会被阻塞(wait–>读取条件具备<–写入数据)。
- 管道被写满 && read fd不读取且没有关闭,管道被写满,写进程会被阻塞(管道被写满—>条件不具备) —> wait 写条件具备就读数据。
- 管道一直在读 && 写端关闭了wfd,读端read返回值会读到0,表示读到了文件末尾。
- 读端rfd直接关闭,但是写端wfd一直在进行写入,写端进程会被操作系统直接使用13信号关掉,相当于进程出现了异常。
管道的5种特征
- 匿名管道,只是用来进行具有血缘关系的进程之间进行通信,具有明显的顺序性。
- 管道内部,自带进程之间的同步机制(同步机制就是多执行流执行代码的时候,具有明显顺序性)。
- 管道文件的生命周期是随进程的。
- 管道文件在通信的时候,是面向字节流的,write的次数和读取的次数不是一一匹配的。
- 管道的通信模式,是一种特殊的半双工模式。
使用上面的代码验证:
ubuntu20.04管道的大小是4096Byte(4kb)我们平时在命令行中使用的|
就是匿名管道
单次向管道里面写入,写如的字节数小于
PIPE_BUF
(这是一个宏),写入操作就是原子的,而原子可以这样理解:要么不做,要做就做完,没有第三状态!
进程池案例

- 管道里没有数据,worker进程就在阻塞等待,等待任务的到来。
- master向哪一个管道进行写入,就是唤醒哪一个子进程来处理任务。
- 父进程要进行后端任务划分的负载均衡。
ProcessPool.hpp
Task.hpp
main.cc
注意:
如果直接这样写的话,运行程序会被阻塞住

原理如图:


在
WaitSubProcesses
等待子进程的时候,需要注意:方法一:先全部关闭管道写,然后进行等待子进程
方法二:倒着关闭
方法三:就顺序关闭,但是需要在创建子进程的时候关闭多余的写端
子进程除了要关闭自己的写端,同时也要关闭自己从父进程继承下来的w端
- _channels本身是被子进程继承下去的。
- 子进程不要担心,父进程会影响自己的
_channels
。
- fork之后,当前进程,只会看到所有的历史进程的wfd,并不受后续父进程emplace_backd的影响。
检测脚本
makefile

命名管道
- 管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信。
- 匿名管道只能用于具有亲缘关系的进程间通信,命名管道可用于同一主机上的任意进程间通信管道的通信本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区。
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

文件名+路径就可以看到同一份资源
代码演示
NamedPipe.hpp
server.cc
client.cc
makefile
- 首先启动程序,对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开。
- 关闭写端,读端会读到0,程序会结束。

- 这次我们先关闭读端,这个时候写端不会立即结束程序,当我们再次输入的时候程序才会退出

共享内存
共享内存原理

- 上面操作都是OS做的
- OS提供上面的
1,2
步骤的系统调用,供用户进程A,B进行调用 (系统调用)。
- AB,CD,EF,XY—–>共享内存在系统中存在多份,供不同个数,不同对进程同时通信。
- OS注定了要对共享内存进行管理(先描述,再组织),共享内存不是简单的一段内存空间,也要有描述并管理共享内存的数据结构和匹配的算法。
- 共享内存 = 内存空间(数据) + 共享内存的属性。
- 共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)。
- 共享内存的本质就是开辟一块物理内存,让多个进程映射同一块物理内存到自己的地址空间进行访问,实现数据共享的。
- 共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱。
- 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除。
共享内存数据结构:
补充指令集(IPC的指令)
shmget
创建共享内存
- 第一个参数是key(后面介绍)
- 第二个参数创建共享内存的大小(单位是字节)
- 建议是4096的整数倍
- 第三个参数就是位图(下面介绍)

- 成功返回共享内存的标识符,失败返回
1
,错误码被设置。

- IPC_CREAT:如果申请的共享内存不存在就创建,存在就获取共享内存并返回
- IPC_EXCL:单独使用没有意义,只有和IPC_CREAT组合才有意义
- IPC_CREAT | IPC_EXCL:如果要创建的共享内存不存在就创建,如果存在就出错返回,如果返回成功就意味着这个shm是全新的

源码:

- 那么如何保证让不同的进程看到同一个共享内存?
- 怎么保证这个共享内存是存在还是不存在呢?
就是通过 第一个参数key
谈谈key
- key是一个数字,这个数字是几,不重要。关键在于必须在内核中具有唯一性,能够让不同的进程进行唯一标识。
- 第一个进程key通过key创建共享内存,第二个之后的进程, 只要拿着同一个
key
,就可以和第一个进程看到同一个共享内存了。
- 对于一个已经创建好的共享内存,key在哪? —-> key在共享内存的描述对象中。
- 第一次创建的时候,必须有一个key,怎么有?(一会谈)
- key 类似之前的路径,都是唯一的。
ftok
- 形成key就使用下面的接口
- 第一个参数是路径名字符串
- 第二个参数是项目id
这两个参数由用户自由指定

那么这个key值能不能由操作系统自动生成,为什么要用户去设置,主要原因是因为操作系统形成了一个key,另一个进程要用这个key,但是我们不知道,所以是由用户约定的,必须由用户层下达到操作系统。
key:操作系统内标定的唯一性。
shmid:只在你的进程内,用来表示资源的唯一性。
ipcs
- 共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一直存在,除非内核重启或者用户关闭。
查看共享内存:
关闭共享内存:
- 这里的shmid是要关闭的共享内存,而不是key,在用户层只能使用shmid,内核层用的key
- 共享内存的大小一般建议是4096的整数倍,如果设置了不是整数倍,在内核层面上会进行4k对齐。

shmat
- 将共享内存挂接到程序地址空间当中
- 第一个参数是shmid
- 第二个参数是要挂接到哪个地址上,一般是nullptr
- 第三个参数是挂接内存的访问权限,默认我们设置为0
返回值:失败返回nullptr,成功返回共享内存的起始地址

shmdt
- 从进程的地址空间中分离一个共享内存段

shmctl
- 删除共享内存&&获取共享内存的属性….
参数说明:
shmid:共享内存段的标识符,通常由 shmget 函数返回。
cmd:要执行的操作的命令,可以是以下值之一:
IPC_STAT
:获取共享内存段的状态,并将结果写入buf
所指向的shmid_ds
结构。
IPC_SET
:设置共享内存段的shmid_ds
结构中的shm_perm
字段,通常用于更改权限。
IPC_RMID
:立即删除共享内存段。注意,只有当共享内存段的引用计数(即附加到它的进程数)为0时,该命令才会成功。
buf
:一个指向shmid_ds
结构的指针,该结构用于传递或接收关于共享内存段的信息。
- 对于
IPC_STAT
命令,该结构用于接收信息;对于IPC_SET
命令,该结构包含要设置的权限信息。
返回值:
- 如果成功,返回0。
- 如果失败,返回-1,并设置
errno
以指示错误原因。

- 共享内存不提供对共享内存的任何保护机制,这会导致数据不一致
- 共享内存是所有进程IPC,速度最快的,因为共享内存大大减少了数据的拷贝次数!
代码验证(使用共享内存的相关接口)
Shm.hpp
client.cc
server.cc
makefile
检测脚本

共享内存是有属性的
在server中添加一个接口:

特征总结:
- 生命周期随内核的
- 共享内存是IPC中速度最快的,这是因为减少了数据拷贝的次数,并且不需要系统调用!
系统调用其实是有成本的,例如在c++之前学的stl,空间配置器,内存池,就可以减少系统调用
- 共享内存,没有同步,互斥机制,来对多个进程的访问进行协同
system V消息队列
- 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以
system V IPC
资源的生命周期随内核- 消息队列是由操作系统来提供的

消息队列也要被操作系统进行管理,要管理就要:先描述,再组织!!!
消息队列接口:
获取:

创建密钥

控制:

内核结构

通过传入数据块



查看消息队列:

并发编程
多个执行流(进程), 能看到的同一份公共资源:共享资源
被保护起来的资源叫做临界资源
保护的方式常见:互斥与同步
任何时刻,只允许一个执行流访问资源,叫做互斥
多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问
临界资源的代码(非临界区)
所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护
system V信号量
5个概念:
- 多个执行流(进程),能看到的一份资源:共享资源
- 被保护起来的资源 —>临界资源,同步和互斥,用互斥的方式保护共享资源,临界资源
- 互斥:任何时刻只能有一个进程在访问公共资源
- 资源:要被程序员访问,资源被访问也就是通过代码来访问(代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区))
- 所谓的对共享资源进行保护(临界资源)本质是对共享资源的代码进行保护
这里的信号量也就相当于是一个计数器
- 申请计数器成功,就表示具有访问资源的权限了
- 申请了计数器资源,我当前访问我要的资源了吗?没有,申请了计数器资源是对资源的预定机制
- 计数器可以有效保证进入共享资源的执行流的数量
- 所以每一个执行流,想访问共享资源中的一部分资源,不是直接访问,而是先申请计数器!
程序员把这个计数器,叫做信号量。
操作方面:
申请资源,计数器–,P操作
释放资源,计数器++,V操作
信号量也是IPC范畴!
接口:






由于是属于IPC的,所以也可以使用
ipcs
来查看:
所有的systemV资源,声明周期,全部都随内核。
所有的systemV资源,都要被OS管理起来。


进程互斥
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到互斥资源的程序段叫临界区
特性方面:
- IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
内核是如何组织管理IPC资源的
C语言实现多态来进行管理

内核中管理
IPC资源
,用的是数组管理!之前学的shmid也就是这个数组的下标!
mmap文件映射

- 允许用户空间程序将文件或设备的内容直接映射到进程的虚拟地址空间中。通过mmap ,程序可以高效地访问文件数据,而无需通过传统的
read
或write
系统调用进行数据的复制。
- mmap 还可以用于实现共享内存,允许不同进程间共享数据。


参数介绍
void *addr
:一个提示地址,表示希望映射区域开始的地址。然而,这个地址可能会被内核忽略,特别是当我们没有足够的权限来请求特定的地址时。如果addr是NULL ,则系统会自动选择一个合适的地址
size_t length
:要映射到进程地址空间中的字节数。这个长度必须是系统页面大小的整数倍(通常是4KB ,但可能因系统而异)。如果指定的length不是页面大小的整数倍,系统可能会向上舍入到最近的页面大小边界(系统内存页大小为4KB(即4096字节),而请求的内存大小为3500字节,则按照向上舍入的原则,应分配4096字节的内存)
int prot
:指定了映射区域的内存保护属性。可以是以下值的组合(使用按位或运算符):PROT_READ
:映射区域可读。PROT_WRITE
:映射区域可写。PROT_EXEC
:映射区域可执行。
int flags
: 指定了映射的类型和其他选项MAP_PRIVATE
:创建一个私有映射。对映射区域的修改不会反映到底层文件中。MAP_SHARED
:创建一个共享映射。对映射区域的修改会反映到底层文件中(前提是文件是以写方式打开的,并且文件系统支持这种操作)。- 其他选项(如
MAP_ANONYMOUS
、MAP_ANONYMOUS_SHARED
等)可能也存在于某些系统上,用于创建不与文件关联的匿名映射。
int fd
: 一个有效的文件描述符,指向要映射的文件或设备。对于匿名映射,这个参数可以是1
(在某些系统上,也可以使用MAP_ANONYMOUS
或MAP_ANON
标志来指定匿名映射,此时fd
参数会被忽略)
off_t offset
:文件中的起始偏移量,即映射区域的开始位置。offset
和length
一起定义了映射区域在文件中的位置和大小。
返回值
成功返回0,失败返回-1
写入映射
- 默认文件大小是0,无法和mmap进行正确映射,这里需要调整文件大小,用0值填充


读取映射
获取文件真实大小



模拟实现malloc

