<进程间通信>共享内存
目录
共享内存工作原理
共享内存数据结构
共享内存使用
代码演示
查看共享内存
进程关联共享内存
进程去共享内存关联
共享内存释放
指令释放
通过系统控制函数释放
共享内存删除机制
共享内存快的原因&
共享内存的缺点
前言
- System V 是一套独立于操作系统外的标准,是一个专门为了通信设计出的内核模块,我们称之为 System V 的 IPC 通信机制。共享内存 全称 System V 共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘。
共享内存工作原理
- 在物理内存中开辟一块公共内存区域,让两个不同的进程的虚拟地址同时对此建立映射关系,此时两个独立的进程能看到同一块空间。进程地址空间映射完毕以后返回首个虚拟地址,以便于进程之间进行通信,可以直接对此空间进【写入或读取】,这块公共区域就是 共享内存。
- 共享内存的目的也是让不同的进程看到同一份资源(同一个内存)
备注:
- 共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中
- 共享内存块的创建、进程间建立映射都是由 OS 实际执行的
共享内存数据结构
- 共享内存不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理。
- 共享内存不是只要在内存中开辟空间即可,系统也要为了管理共享内存,构建对应的描述共享内存的结构体对象! 在Linux中这个结构体是struct shmid_ds,这个结构体里面存放了共享内存的各种属性。
- 共享内存 = 共享内存的内核数据结构(struct shmid_ds) + 真正开辟的内存空间
- 共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述。
shstructmid_ds
{
struct ipc_perm shm_perm; /* 操作权限 */
int shm_segsz; /* 段的大小(字节) */
__kernel_time_t shm_atime; /* 最后附加时间 */
__kernel_time_t shm_dtime; /* 最后分离时间 */
__kernel_time_t shm_ctime; /* 最后修改时间 */
__kernel_ipc_pid_t shm_cpid; /* 创建者的进程 ID */
__kernel_ipc_pid_t shm_lpid; /* 最后操作者的进程 ID */
unsigned short shm_nattch; /* 当前附加的进程数 */
unsigned short shm_unused; /* 兼容性字段 */
void *shm_unused2; /* 兼容性字段,用于 DIPC */
void *shm_unused3; /* 未使用的字段 */
};
- struct ipc_perm 中存储了 共享内存中的基本信息
struct ipc_perm
{
__kernel_key_t key; // IPC 对象的唯一标识符
__kernel_uid_t uid; // 当前拥有者的用户 ID
__kernel_gid_t gid; // 当前拥有者的组 ID
__kernel_uid_t cuid; // 创建者的用户 ID
__kernel_gid_t cgid; // 创建者的组 ID
__kernel_mode_t mode; // 访问权限模式
unsigned short seq; // 序列号,用于区分同 key 的不同 IPC 对象
};
共享内存使用
函数 | 返回值 | 参数 | 备注 |
int shmget(key_t key,size_t size,int shmflg); | 创建成功返回共享内存的 shmid,失败返回 -1 | 参数1 key_t key 创建共享内存的唯一 key 值,通过 ftok函数获取
| 因为共享内存拥有自己的数据结构,所以返回值 int 实际就是 shmid,类似于文件系统中的 fd,用来对不同的共享内存块进行操作 参数2为创建共享内存的大小,单位是字节,一般设为 4096 字节(4kb),与一个 PAGE 页大小相同,有利于提高IO效率 参数3是位图结构,类似于 open 函数中的参数3(文件打开方式),常用的标志位有以下几个:
|
key_t ftok(const char* pathname,int proj_id); | 返回生成的标识值,等价于 int 类型 | 参数1 const char *pathname:项目路径,可使用 绝对 或 相对 路径 参数2 int proj_id:项目编号,可以根据实际情况编写 | 这个key值单独看起来是没有任何作用的,它要与其他的系统调用结合起来才会发挥作用,未来要通信的两个进程就可以在函数 ftok 输入相同的相同的参数,生成相同的key值,然后通过这个key值就能确定它们想要通信的共享内存是哪一个了 |
代码演示
comm.hpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." // 项目名
#define PROJID 0x0001 // 项目编号
const int gsize = 4096;
const mode_t mode = 0666;
// 将十进制转换成十六进制
string toHex(int x)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%x", x);
return buffer;
}
// 获取共享内存唯一标识
key_t getKey()
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
cerr << errno << ":" << strerror(errno) << endl;
exit(1);
}
return key;
}
// 共享内存助手
int createShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
if(shmid == -1)
{
cerr << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
// 创建共享内存
int createShm(key_t key, int size)
{
return createShmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}
// 获取共享内存
int getShm(key_t key, int size)
{
return createShmHelper(key, size, IPC_CREAT);
}
server.cc
#include "common.hpp"
int main()
{
// 创建key
key_t key = getKey();
cout << "server key: " << toHex(key) << endl;
// 服务端创建共享内存
int shmid = createShm(key, gsize);
cout << "server shmid: " << shmid << endl;
return 0;
}
client.cc
#include "common.hpp"
int main()
{
key_t key = getKey();
cout << "client key: " << toHex(key) << endl;
// 客户端打开内存
int shmid = getShm(key, gsize);
cout << "client shmid: " << shmid << endl;
return 0;
}
演示结果:
- 通过shmget和ftok函数可以获取唯一的key和shmid
查看共享内存
ipcs -m
备注:
- 从左到右依次为key值、 shmid、拥有者、权限、大小、挂接数、状态
- 共享内存 0 就是通过上述代码生成的
- 因为共享内存每次都是随机生成的,所以每次生成的 key 和 shmid 都不一样
再次运行上面代码:
解析:
- 服务端运行失败,原因是 shmget 创建共享内存失败,这是因为服务端创建共享内存时,传递的参数为 IPC_CREAT | IPC_EXCL,其中 IPC_EXCL 注定了当共享内存存在时,创建失败。
- 而客户端只是单纯的获取共享内存,同时也只传递了 IPC_CREAT 参数,所以运行才会成功。
- 综上所述,服务端运行失败的根本原因是 待创建的共享内存已存在,如果想要成功运行,需要先将原共享内存释放。
进程关联共享内存
函数 | 返回值 | 参数 | |
void* shmat(int shmid,const void* shmaffr,int shmflg) 一般为:shmat(shmid,NULL,0); | 返回值 void*,如同malloc一样,返回的是 void* 指针,可以根据需求进行强转 当进程与共享内存关联后,返回的就是共享内存映射至共享区的起始地址
一般通信数据为字符,所以可以将 shmat 的返回值强转为 char* | 参数1 int shmid 待关联的共享内存id
参数3 int shmflg 关联后,进程对共享内存的读写属性,一般直接设为 0,表示关联后,共享内存属性为 默认读写权限,更多选项如下所示(了解):
|
代码演示
comm.hpp添加:
// 关联共享内存
char* attachShm(int shmid)
{
char *start = (char*)shmat(shmid, nullptr, 0);
return start;
}
server.cc
#include "common.hpp"
int main()
{
// 创建key
key_t key = getKey();
cout << "server key: " << toHex(key) << endl;
// 服务端创建共享内存
int shmid = createShm(key, gsize);
cout << "server shmid: " << shmid << endl;
sleep(3);
// 将自己与共享内存关联起来
char *start = attachShm(shmid);
sleep(3);
sleep(5);
// 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
cout << "共享内存" << shmid << "已删除" << endl;
return 0;
}
client.cc
#include "common.hpp"
int main()
{
key_t key = getKey();
cout << "client key: " << toHex(key) << endl;
int shmid = getShm(key, gsize);
cout << "client shmid: " << shmid << endl;
sleep(3);
// 将自己与共享内存关联起来
char *start = attachShm(shmid);
sleep(5);
return 0;
}
演示结果:
备注:
- 共享内存信息中的
nattch
表示当前共享内存中的进程关联数 。注意: 程序运行结束后,会自动取消关联状态:- 共享内存的生命周期是随内核的,即进程退出以后如果没有删除共享内存,则共享内存不会消失!
进程去共享内存关联
函数 | 返回值 | 参数 | 备注 |
int shmdt(const void* shmaddr); | 去关联成功返回 0 失败返回 -1,并将错误码设置 | 参数:关联成功的返回共享内存起始地址 | 如同关闭 FILE*、fd、free 等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据) |
共享内存释放
指令释放
ipcrm -m shmid
通过系统控制函数释放
函数 | 返回值 | 参数 | 备注 |
int shmctl(int shmid,int op,struct shmid_ds* buf); | 成功返回 0 失败返回 -1 | 参数1 int shmid 待控制的共享内存 id
| 参数2:
buf 就是共享内存的数据结构,可以使用 IPC_STAT 获取,也可以使用 IPC_SET 设置 在 Linux 系统中,使用 1. 创建者权限 如果调用进程是共享内存段的创建者,那么它可以直接使用
2. 特权用户(如 root) 如果调用进程的用户是特权用户(如
|
server.cc
#include "common.hpp"
int main()
{
// 创建key
key_t key = getKey();
cout << "server key: " << toHex(key) << endl;
// 服务端创建共享内存
int shmid = createShm(key, gsize);
cout << "server shmid: " << shmid << endl;
int n = 5;
while (n)
{
// 运行五秒后删除共享内存
cout << n-- << endl;
sleep(1);
}
// 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
cout << "共享内存" << shmid << "已删除" << endl;
return 0;
}
当服务端运行结束时,会自动删除共享内存:
共享内存删除机制
- 当使用
shmctl(shmid, IPC_RMID, 0)
删除共享内存时,操作系统会检查当前是否有进程仍然挂接(附加)到该共享内存段。如果存在挂接的进程,共享内存段不会立即被销毁,而是被标记为“销毁”状态(dest
),并等待所有挂接的进程分离。
备注:
共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程
共享内存被提前删除后,状态
status
变为 销毁dest
状态
dest
的含义
- 在
ipcs
命令的输出中,dest
状态表示共享内存段已被标记为销毁,但仍然存在,直到所有挂接的进程完成分离。此时,共享内存段的mode
字段会被设置为SHM_DEST
,表示该段即将被销毁。为何已挂接的进程仍能通信
- 已挂接的进程仍然可以访问共享内存,因为它们的虚拟地址空间已经映射到共享内存段。即使共享内存被标记为销毁,这些进程仍然可以通过映射的地址访问内存内容,直到它们调用
shmdt()
分离共享内存。新进程无法挂接的原因
- 一旦共享内存被标记为销毁(
dest
),新的进程将无法再通过shmat()
挂接到该共享内存段。这是因为操作系统已经将其视为即将销毁的资源,不允许新的挂接操作。
共享内存快的原因
共享内存通信快的秘籍在于 减少数据拷贝(IO),IO是很慢、很影响效率的
比如在使用管道通信时,需要经过以下几个步骤:
- 从进程 A 中读取数据(IO)
- 打开管道,然后通过系统调用将数据写入管道(IO)
- 通过系统调用从管道读取数据(IO)
- 将读取到的数据输出至进程 B(IO)
但共享内存就不一样,直接访问同一块内存进行数据读写。
在使用共享内存通信时,只需要经过以下两步:
- 进程
A
直接将数据写入共享内存中 - 进程
B
直接从共享内存中读取数据
显然,使用共享内存只需要经过 2
次 IO
所以共享内存的秘籍是 减少拷贝(IO
)次数
- 得益于共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程通信中,速度最快的。
共享内存的缺点
共享内存这么快,为什么不直接只使用共享内存呢?
因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:
- 多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
- 即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了
总的来说,共享内存没有任何的保护机制,不加规则限制的共享内存是不推荐使用的。
当然可以利用其他通信方式,控制共享内存的写入与读取规则
- 比如使用命名管道,进程 A 写完数据后,才通知进程B,读取进程 B 读取后,才通知进程 A 写入
- 假如是多端写入、多端读取的场景,则可以引入生产者消费者模型,加入互斥锁和条件变量等待工具,控制内存块的读写。