介绍
进程间通信目的
首先我们要了解进程间通信的目的,一共有4点
1:数据传输:一个进程需要将它的数据传输给另一个进程
2:资源共享:多个进程之间共享同样的资源
2:通知事件:一个进程给另一个进程发送信息,通知发生了某种事件
4:进程控制:某些进程希望完全控制另一个进程的执行,此时进程希望另一个进程能进行拦截另一个进程的一场,并能够及时知道它的状态改变
进程间通信发展
管道,SystemV进程间通信,POSIX进程间通信
其中这三个都有各自的分支
管道:匿名管道pipe,命名管道
SystemV IPC:System V 消息队列/共享内存/信号量
POSIX IPC:消息队列,共享内存,信号量,互斥量,条件变量,读写锁
管道
什么是管道:管道是UNIX系统最古老的进程间通信方式
一个进程链接另一个进程的另一个数据流成为一个管道
匿名管道
这里是示例代码
cpp
#include<unistd.h>
//功能是创建无名管道
int pipe(int fd[2]);
//参数解释:fd为文件描述符数组,fd[0]是读端,fd[1]为写端
用fork来共享管道原理

站在文件描述符角度-深度理解管道

站在内核角度

测试管道读写代码示例
cpp
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m)
do {
perror(m);
exit(EXIT_FAILURE);
} while (0)
int main(int argc, char* argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
{
ERR_EXIT("fork error");
}
if (pid == 0)
{
close(pipefd[0]);
write(pipefd[1], "hello", 9);
close(pipefd[1]);
exit(EXIT_SUCCESS);
}
close(pipefd[1]);
char buf[10] = { 0 };
read(pipefd[0], buf, 10);
printf("buf=%s\n", buf);
return 0;
}
管道读写规则
当没有数据可读时
O_NONBLOCK disable:read 调用阻塞,进程暂停执行,一直等到数据来到为止
O_NONBLOCK enable :read调用返回-1,errno值为EAGAIN
当管道满的时候
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道读写对应文件描述符被关闭,errno为EAGAIN
如果所有管道读端量不大于PIPE_BUF时,linux将保证写入的原子性
当要写⼊的数据量⼤于PIPE_BUF时,linux将不再保证写⼊的原⼦性。
管道特点
只能用于具有共同祖先的进程之间进行通信,通常一个管道由一个进程创建,然后进程调用fork,此后父子进程之间可用该管道
管道提供流式服务
一般而言,进程退出管道释放
所以管道的生命周期随进程变化而变化
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立两个管道
3-8验证管道通信的4种情况
• 读正常&&写满
• 写正常&&读空
• 写关闭&&读正常
• 读关闭&&写正常
命名管道
管道应用的一个限制就是只能在具有共同祖先的进程间通信
如果我们在不相关的进程间进行通信,可以用FIFO文件进行此工作,它通常被命名为管道
命名管道是一种特殊类型文件
创建命名管道
bash
$ mkfifo filename
匿名管道和命名管道的区别
匿名管道是pipe函数创建并打开
命名管道由mkfifo函数创建,打开用open
fifo和pipe之间唯一的区别就是他们创建和打开的方式不同,一旦这些工作完成之后,他们具有相同的语义
命名管道的打开规则
打开操作/未读/打开fifo
O_NONBLOCKdisable:阻塞直到有相应进程为写⽽打开该FIF
O_NONBLOCKenable:⽴刻返回成功
如果当前打开操作是为写⽽打开FIFO时
◦ O_NONBLOCK disable:阻塞直到有相应进程为读⽽打开该FIFO
◦ O_NONBLOCK enable:⽴刻返回失败,错误码为ENXIO
System V 共享内存
共享内存是最快的IPC形式,一旦这样的内存映射搭配共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不在通过执行进入内核的操作系统调用来传递彼此的数据
5-3 共享内存相关函数
一、shmget ():创建 / 获取共享内存
功能
创建一块新共享内存段,或获取已存在的共享内存段标识。
函数原型
bash
int shmget(key_t key, size_t size, int shmflg);
参数说明
key:共享内存键值,标识共享内存段的唯一名字,不同进程通过相同 key 访问同一块共享内存。
size:共享内存段字节大小,创建时指定,获取已有段时填 0 即可。
shmflg:权限 + 控制标志,用法同文件 open 的 mode 权限,常用组合:
IPC_CREAT:不存在则创建;已存在则直接获取该段。
IPC_CREAT | IPC_EXCL:不存在则创建;已存在函数报错返回 - 1,保证新建。
返回值
成功:返回非负整数 shmid(共享内存操作标识)
失败:返回 -1
二、shmat ():挂载共享内存到进程虚拟地址空间
功能
将指定共享内存段映射、连接到当前进程的地址空间,挂载后进程可直接读写这块内存。
函数原型
bash
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明
shmid:shmget 返回的共享内存标识符。
shmaddr:手动指定挂载地址
传 NULL:由操作系统内核自动分配合适地址(最常用);
非 NULL:用户手动指定挂载起始地址。
shmflg:挂载属性标志,两个常用取值:
SHM_RDONLY:只读挂载,进程只能读共享内存,不可写;
SHM_RND:地址对齐标志。
地址对齐规则说明
shmaddr == NULL:内核自动分配地址,无需处理对齐;
shmaddr != NULL 且无 SHM_RND:直接使用 shmaddr 作为挂载地址;
shmaddr != NULL 且带 SHM_RND:地址向下对齐到 SHMLBA 整数倍,计算公式:
bash
实际挂载地址 = shmaddr - (shmaddr % SHMLBA)
返回值
成功:返回共享内存段首地址指针;
失败:返回 (void *)-1
三、shmdt ():卸载共享内存(脱离映射)
功能
断开当前进程与共享内存段的地址映射,进程无法再访问该共享内存。
注意:仅脱离进程映射,不会删除操作系统中的共享内存实体。
函数原型
bash
int shmdt(const void *shmaddr);
参数说明
shmaddr:shmat 成功返回的共享内存首地址指针。
返回值
成功:返回 0
失败:返回 -1
四、shmctl ():共享内存控制(删除 / 修改属性 / 获取状态)
功能
对共享内存执行控制操作:获取状态、修改权限、销毁共享内存段。
函数原型
bash
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明
shmid:shmget 返回的共享内存标识;
cmd:控制动作指令(三种核心取值);
buf:struct shmid_ds 结构体指针,存储共享内存权限、状态信息。
返回值
成功:返回 0
失败:返回 -1
system V 消息队列
消息队列提供一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不用的类型值
特性方面
IPC资源必须删除,否则不会自动删除,除非自启,所以System V ipc资源的生命周期随内核变化而变化
system V 信号量
多个执行流程,能看到的同一份访问资源,共享资源
被保护起来的共享资源:临界资源
保护的方式常见为互斥和同步
任何时刻只允许一个执行流访问资源,叫做互斥
多个执行流,访问临界资源的时候,具有一定顺序性,叫做同步
系统中某些资源一次只允许一个进程实用,这样的称谓临界资源或互斥资源
在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问
临界资源的代码(⾮临界区)
•所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护
信号量
ipc资源必须删除,否则不会自动清除,除非重启,所以system v ipc 资源的生命周期随内核变化而变化
信号量是一个计数器
作用方面:临界保护区
本质方面:资源的预定机制
操作⽅⾯
• 申请资源,计数器--,P操作
• 释放资源,计数器++,V操作
内核是怎么组织管理IPC资源的
