进程间通信?
因为每一个进程都有一个虚拟地址空间, 保证了进程的独立性, 但也正是因此导致进程间无法通信. 所以需要操作系统提供进程间通信方式, 又因为通信的场景不同, 所以提供了多种不同的进程间通信方式。
进程间通信的目的
数据传输
资源共享
进程控制
通知事件
进程间通信方式:
System V 标准的进程间通信方式:
管道: 用于进程间的数据传输
共享内存: 用于进程间的数据共享
消息队列: 用于进程间的数据传输
共享内存
顾名思义, 共享内存就是进程都能访问到的一块物理内存.[多个进程将同一块物理内存映射到自己的虚拟地址空间]共享内存是System V版本的一个进程间通信方式,也是最快的进程间通信方式.
共享内存是系统为了进程间的通讯而预留的一块内存区域.如下突所示:
一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不在涉及到内核,这就是说进程不在通过执行进行内核的系统调用来传递数据.
在 /proc/sys/kernel/ 目录下, 记录着共享内存的一些参数信息, 可以根据实际需要对这些参数进行一定的修改
shmall,系统上可以使用的共享内存总量(bytes)
shmmax, 内核所允许的一个共享内存区的最大字节数(bytes)
shmmni,系统范围内最大共享内存区标识符数 (个)
共享内存特点
- 最快的进程间通信方式
其他进程间通信方式都使用了内核中的一块缓冲区, 通信时都会涉及用户态与内核态之间的两次数据拷贝, 而共享内存直接通过虚拟地址映射访问物理内存, 减少了两次用户态与内核态之间的数据拷贝,因此通信速度最快 - 生命周期随内核
在物理内存上开辟空间, 信息存储在内核中,是属于内核的一个进程间通信资源,不会随着进程的退出而释放; 内核重启会释放或者可以用户手动释放(使用 shmdt); - 不具备同步与互斥关系,操作不安全
共享内存本身是不具备同步与互斥的特性的, 需要用户自己实现(一般与信号量结合使用实现进程间的同步与互斥) - 数据写入是一种针对地址指向空间的覆盖式写入
共享内存相关API
- 创建共享内存(开辟物理内存空间)
int shmget(key_t key, int size, int shmflag) // 创建共享内存
key:共享内存的标识符, 多个进程通过相同的标识符可以打开同一块共享内存
size: 共享内存大小
shmflag: IPC_CREAT ---存在则打开| IPC_EXEL ---与IPC_CREAT同时使用,若存在则报错,不存在则创建| mode权限
返回值: 成功返回一个非负整数---共享内存的操作句柄, 失败返回 -1
// 生成一个key值
key_t ftok(const char *pathname, int proj_id);---通过inode节点号与projid合成一个key
系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
- 将共享内存映射到进程的虚拟地址空间
void *shmat(int shmid, void *shmaddr, int shmflg) // 共享内存操作句柄
shmid: 返回的共享内存操作句柄
shmaddr : 映射到虚拟地址空间的首地址, 通常置 NULL,
shmflag : 映射成功之后对共享内存可以进行的操作 SHM_RDONLY (只读) 通常置 0 (表示可读可写)
返回值: 成功返回映射的虚拟地址空间首地址, 通过这个地址对内存进行操作; 失败返回 (void*)-1
- 解除虚拟地址空间映射关系
// 解除映射关系
int shmdt(void *shmstart) // 解除映射
shmstart: 映射到虚拟地址空间的首地址
成功返回0 , 失败返回 -1
- 释放共享内存资源
// 删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmid: 共享内存操作句柄
cmd : 具体对共享内存要进行的操作 -----常用: IPC_RMID--删除共享内存
buf : 用于获取/设置共享内存信息的结构,不使用则置 NULL
成功返回 0 失败返回 -1
操作系统中进程间通信资源指令:
// 查看进程间通信资源
ipcs -[选项]
-m 常看共享内存
-s 查看信号量
-q 查看消息队列
共享内存的操作流程
- 创建共享内存
- 将共享内存映射到进程的虚拟地址空间
- 通过虚拟地址空间进行内存操作
- 解除映射关系
- 释放共享内存资源
注意: 删除共享内存时并不会立即释放共享内存资源, 因为如果立即释放共享内存资源,可能会造成正在访问该共享内存的进程崩溃. 所以删除共享内存时, 先将其映射 key 修改为0, 表示这块共享内存不在接受新的映射连接, 当这块共享内存映射链接数为0的时候, 共享内存自动被释放.
以代码为例, 加深对共享内存理解
创建共享内存,一端向共享内存写入数据, 通过另一端对该共享内存数据进行读取
读取共享内存数据:
/*
* 学习共享内存的操作流程并体会接口的基本使用*/
// 创建并读取共享内存数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define IPC_KEY 0x12345678
int main()
{
// 1. 创建共享内存
/* int shmget(key_t key, int size, int shmflag) key--标识符 size--共享内存shmglag--标志位|权限*/
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if(shm_id < 0)
{
perror("shmget error");
return -1;
}
// 2. 建立映射关系
/* void *shmat(int shmid, void *shmaddr, int shmflg)
* shmid---操作句柄
* shmaddr---映射首地址
* shmflag---操作权限*/
void *shm_start = shmat(shm_id, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
// 3. 进行内存操作
while(1)
{
sleep(1.5);
printf("[%s]\n", shm_start);
}
// 4. 解除映射关系
// shmdt(映射首地址 )
shmdt(shm_start);
// 5. 删除共享内存
// shmctl(操作句柄, 要进行的操作--IPC_RMID, 共享内存信息结构)
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
向共享内存写入数据:
/*
* 学习共享内存的操作流程并体会接口的基本使用*/
// 创建共享内存, 并写入数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define IPC_KEY 0x12345678
int main()
{
// 1. 创建共享内存
/* int shmget(key_t key, int size, int shmflag) key--标识符 size--共享内存大小 shmglag--标志位|权限*/
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if(shm_id < 0)
{
perror("shmget error");
return -1;
}
// 2. 建立映射关系
/* void *shmat(int shmid, void *shmaddr, int shmflg)
* shmid---操作句柄
* shmaddr---映射首地址
* shmflag---操作权限*/
void *shm_start = shmat(shm_id, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
// 3. 进行内存操作
int i = 0;
while(1)
{
sleep(1);
sprintf(shm_start, "%s+%d", "我好菜啊", i++);
}
// 4. 解除映射关系
// shmdt(映射首地址 )
shmdt(shm_start);
// 5. 删除共享内存
// shmctl(操作句柄, 要进行的操作--IPC_RMID, 共享内存信息结构)
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
结果:
消息队列
本质上是内核中的一个优先级队列, 多个进程通过向同一个队列中添加和获取节点实现通信,传输数据块, [其实现分为 System V标准的消息队列 和 POSIX标准的消息队列]
特点:
自带同步与互斥
生命周期随内核
数据传输自带优先级
为什么使用消息队列?
目前没有接触过必须要使用消息队列的项目, 所以简单总结下:
使用消息队列的优点:
- 通过异步处理提高系统性能(削峰、减少响应所需时间)
不使用消息队列:
使用消息队列:
如上图,在不使用消息队列的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。 - 降低系统耦合性
比如在下单系统中,
如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的耦合度就低。
如上图所示:消息队列可以利用发布-订阅模式工作,消息发送者发布消息,一个或多个消息接受者订阅消息。 从上图可以看到消息发送者和消息接受者之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响。
消息队列相关API:
头文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
- 创建消息队列
int msgget(key_t key, int msgflg);
key: 同共享内存一样, 使用 ftok()产生,表示某个消息队列
msgflg:
IPC_CREAT: 消息队列不存在则创建, 存在则打开返回
IPC_CREAT | IPC_EXCL : 两个选项同时使用--消息队列不存在则创建,存在则报错返回
IPC_EXCL: 单独使用没有意义
return: 成功返回消息队列的标识码(非负整数), 失败返回 -1
- 添加节点(将一条消息添加到消息队列中)
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
msqid: 消息队列的标识码
msgp: 是一个指向准备添加消息的指针
消息结构一般形式如下:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
其中 mtext 可以是数组(也可以是其它结构--由调用者自定义), 大小由msgsz指定
msgsz: 指向的消息的长度
msgflg: 默认为0
return: 成功返回0 ,失败返回 -1
- 从消息队列中获取节点(接受消息)
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
msqid: 消息队列的标识码
msgp: 是一个指向准备添加消息的指针
msgsz: 指向的消息的长度
msgtyp: 该参数等于0时,读取队列中第一条消息; 大于0时, 读取队列中第一条 msgtyp 类型的消息,(若在 msgflg 中指定了 MSG_EXCEPT,在这种情况下,队列中第一条类型不等于 msgtyp 的消息将被读取); 小于0时, 队列中第一个类型小于或等于 msgtyp 绝对值的消息将被读取。
msgflg: 默认为0
return: 成功返回实际放到缓冲区里的字符个数, 失败返回 -1
- 删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
msqid: 消息队列标识码
cmd: 三个选项:
IPC_STAT 将msqid_ds 结构中的数据设置为消息队列的当前关联值
IPC_SET 将消息队列的当前关联值设置为 msqid_ds 数据结构中给出的值
IPC_RMID 删除消息队列
buf: msqid_ds 数据结构如下:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
参考:
https://www.jianshu.com/p/36a7775b04ec
https://blog.csdn.net/qq_35190492/article/details/103153444