目录
1.共享内存的通信原理
2.共享内存的创建
ftok
shmget
IPC相关命令
共享内存的生命周期
3.共享内存的(去)关联
shmat
shmdt
4.共享内存的释放
shmctl
shmctl(shmid, IPC_RMID, NULL);
5.共享内存的使用
1.共享内存的通信原理
操作系统预先分配一块物理内存(称为共享内存),多个进程通过各自的页表将这段内存映射到自身的虚拟地址空间(通常是共享区),使得不同进程的虚拟地址最终指向同一块物理内存。进程可以直接读写该内存区域进行数据交换,从而实现高效通信,但需配合同步机制保证数据一致性
由于进程具有严格的独立性,其直接分配的内存只能自己独享,无法直接用于进程间通信。为了解决这一矛盾,共享内存必须由作为全局管理者的操作系统内核来提供。进程通过系统调用请求操作系统执行以下核心步骤来间接使用共享内存:创建/获取一块共享内存区域、将其关联(映射)到自身地址空间并使用、通信完成后去关联、最终由系统销毁该区域。
- 创建/获取(申请内存):进程请求操作系统创建一块新的共享内存,或获取一块已存在的共享内存的标识符。操作系统在物理内存中为其分配空间。
- 关联:进程将这块共享内存映射到自己的进程地址空间(通常是共享区),操作系统建立进程页表到该物理内存的映射关系,并返回映射后的虚拟首地址供进程访问。
- 使用:进程通过得到的虚拟地址直接读写该内存区域,进行进程间通信。
- 去关联:通信完成后,进程断开与该共享内存的映射关系(去关联),使其从本进程的地址空间中移除。
- 销毁(释放):当所有使用该共享内存的进程都完成去关联后,操作系统最终释放该共享内存资源。
为了管理系统中可能存在的多块共享内存,操作系统采用“先描述,再组织”的方式:用一个内核数据结构(如struct shmid_ds)来描述每一块共享内存的信息(大小、权限、关联进程数等),再通过链表等数据结构将这些描述对象组织起来,实现统一高效的管理
2.共享内存的创建
ftok
| 组成部分 | 名称与类型 | 详细说明与作用 |
|---|---|---|
| 函数原型 | key_t ftok(const char *pathname, int proj_id); | 将文件路径和项目ID转换为System V IPC机制(消息队列、共享内存、信号量)使用的键值(key)。 |
| 函数功能 | 生成IPC键值 | 根据给定的路径名和项目ID,生成一个唯一的key_t类型的键值,用于不同进程通过相同参数获取同一个IPC对象。 |
| 包含头文件 | #include <sys/ipc.h>#include <sys/types.h> | 使用ftok函数必须包含这两个头文件。 |
| 参数1 | const char *pathname | 路径名:指向一个已存在且可访问的文件(或目录)的路径字符串。 •必须存在:路径指向的文件必须存在且有访问权限。 •通常用法:使用一个固定的、所有进程都能访问的文件(如 /tmp/myapp,/etc/passwd, 或项目特定文件)。•原理依据:函数会使用该文件的 st_dev(设备号)和st_ino(i-node号)信息。 |
| 参数2 | int proj_id | 项目ID:用户指定的单字节整数值(0-255)。 •范围限制:只有低8位有效(0-255),超出范围会被截断取低8位。 •用途:允许在同一文件上生成多个不同的key值。 •常见值:通常用ASCII字符表示,如 'a'、'b'或十六进制0x01等。 |
| 返回值 | key_t key | 成功:返回生成的key_t类型的键值(通常是32位整数)。失败:返回 (key_t)-1,并设置errno指示错误原因:• ENOENT:路径名指向的文件不存在• EACCES:对路径名指向的文件无搜索权限• ESTALE:文件系统已卸载 |
| 关键特性 | 1.确定性:相同(pathname, proj_id)组合在任何进程、任何时间调用都产生相同的key值。2.跨进程性:不同进程使用相同参数即可获得相同key,从而访问同一个IPC对象。 3.非唯一性风险:如果文件被删除重建,i-node号可能变化,导致key值改变。 | |
| 常见错误 | 1.文件不存在:最常见错误,确保路径文件存在且稳定。 2.权限不足:进程对路径文件无访问权限。 3.项目ID溢出:超出255的值会被截断,可能导致意外冲突。 4.文件被删除重建:i-node号改变,新旧进程可能获得不同key。 |
共享内存的键值(key)是用户进程预先约定的一个标识符,它的核心作用是让不同进程能够通过相同的key值找到或创建同一个共享内存对象。key并非由操作系统保证全局唯一,而是由应用程序自身确保其唯一性(通常通过ftok()函数基于文件路径生成)。第一个进程使用shmget(key, size, IPC_CREAT)创建共享内存时,系统会将此key与它关联起来;后续其他进程只需使用相同的key调用shmget()即可获取该共享内存的访问句柄。因此,key在整个共享内存通信机制中扮演的是进程间“约定地址”的角色,类似于文件系统中的路径名,其唯一性和一致性由使用它的各进程共同维护
操作系统无法预知哪些进程需要通信、何时通信。若由系统自动分配key,则进程仍需通过其他渠道(如文件、环境变量或硬编码)交换这个key,这反而增加了耦合度和复杂性。用户约定key(通常通过
ftok或预定义常量)使得通信双方能以松耦合的方式独立连接到同一资源,共享内存因此成为一种独立、通用、不依赖特定进程关系的通信机制
shmget
| 组成部分 | 名称与类型 | 详细说明与作用 |
|---|---|---|
| 功能 | 获取共享内存标识符 | 根据key创建或获取一块共享内存,返回一个内核标识符(shmid),用于后续操作(shmat,shmdt,shmctl)。 |
| 参数1 | key_t key | 键值,用于唯一标识系统中共享内存对象。 • IPC_PRIVATE(0):总是创建新的共享内存,通常用于父子进程间。•用户指定 key:通过ftok()函数生成,或直接指定一个非零整数,用于无亲缘关系进程间的约定。 |
| 参数2 | size_t size | 请求的共享内存大小(字节)。 •创建时:指定新共享内存的最小尺寸,系统可能会向上取整到页大小(如4KB)的整数倍。 •获取时:如果只是获取已存在的共享内存, size可以为0;若指定了大小,且小于已存在内存的大小,可能会报错。 |
| 参数3 | int shmflg | 创建标志与权限标志的组合。这是一个位掩码,由两部分按位或(|)组成:A. 行为控制标志: • IPC_CREAT:如果key对应的共享内存不存在,则创建它。如果存在,则直接获取其shmid。• IPC_EXCL:与IPC_CREAT一同使用。如果共享内存已存在,则调用失败(返回-1,errno设为EEXIST)。这用于确保创建者拿到的是全新的内存。B. 权限标志(八进制): • 例如 0666:表示所有用户(属主、组员、其他人)都可读写。• 例如 0644:属主可读写,组员和其他人只读。• 权限位实际受系统 umask值影响(最终权限 =shmflg & ~umask)。 |
| 返回值 | int shmid | 成功:返回一个非负整数,即共享内存标识符。这是后续所有操作(挂接、控制、去关联)的句柄。 失败:返回 -1,并设置全局变量errno指示错误原因(如ENOENT-未找到,EACCES-权限不足,EEXIST-已存在,EINVAL-参数无效等)。 |
申请共享内存时,操作系统会直接按内存页(通常4KB)的整数倍进行物理内存分配,然后让用户使用申请的那一部分(这样设计较为简单)。所以建议申请4kb的整数倍,减少浪费
在共享内存通信机制中,IPC键值(key)是用户层(进程间)约定的查找标识符,它由应用程序通过预定义或ftok函数生成,用于在不同进程中指向同一个共享内存对象
共享内存标识符(shmid)是操作系统内核分配和管理共享内存的唯一标识,它在系统范围内唯一,进程通过shmget系统调用获得shmid后,所有后续操作(映射、控制、分离)都基于此shmid进行。
shmid与“一切皆文件”的理念兼容性较差,因为它是一个独立的整数句柄,而不是通过文件系统路径访问;key像是一个逻辑路径名,但其实现机制独立于文件系统。
IPC相关命令
# 查看所有IPC资源 ipcs # 查看特定类型的资源 ipcs -m # 共享内存 ipcs -q # 消息队列 ipcs -s # 信号量 # 详细查看 ipcs -a # 所有详细信息(默认) ipcs -l # 显示系统限制 ipcs -p # 显示PID信息 ipcs -t # 显示时间信息 ipcs -c # 显示创建者/拥有者 ipcs -u # 显示摘要使用情况# 删除共享内存 ipcrm -m <shmid> # 按shmid删除 ipcrm -M <key> # 按key删除 # 删除消息队列 ipcrm -q <msqid> # 按msqid删除 ipcrm -Q <key> # 按key删除 # 删除信号量 ipcrm -s <semid> # 按semid删除 ipcrm -S <key> # 按key删除 # 删除所有用户拥有的IPC资源(危险!) ipcrm -a # 删除当前用户的所有IPC ipcrm -A # 删除所有用户的IPC(需要root)共享内存的生命周期
共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一直存在,除非内核重启(用户释放)
3.共享内存的(去)关联
shmat
| 项目 | 详细说明 |
|---|---|
| 函数原型 | void *shmat(int shmid, const void *shmaddr, int shmflg) |
| 功能描述 | 将共享内存段关联到调用进程的地址空间,使进程能够访问共享内存 |
| 包含头文件 | #include <sys/types.h>#include <sys/shm.h> |
| 参数1 | int shmid•由 shmget()返回的共享内存标识符• 必须是已存在的有效共享内存ID • 用于指定要附加的共享内存段 |
| 参数2 | const void *shmaddr• 期望的附加地址(建议设为 NULL)• shmaddr == NULL:系统自动选择合适地址• shmaddr != NULL:尝试在指定地址附加(需对齐)• 通常设置为 NULL让系统选择,可避免地址冲突 |
| 参数3 | int shmflg• 附加标志位(按位或组合) • SHM_RDONLY:以只读方式关联• SHM_RND:当shmaddr非NULL时,将其向下取整到SHMLBA边界• 0:默认读写方式 |
| 返回值 | 成功:返回关联后的共享内存段在共享区的起始地址(void*类型)失败:返回 (void*)-1,并设置errno |
| 权限控制 | • 创建时通过shmget()的权限位控制(如0666)• 关联时可通过 SHM_RDONLY限制为只读• 权限检查:进程必须有相应的访问权限 |
| 引用计数 | 每次成功shmat()会增加shm_nattch计数每次 shmdt()会减少计数当计数为0且已标记删除时,内存被释放 |
| 错误码 | EACCES:权限不足EINVAL:shmid无效或shmaddr不对齐ENOMEM:进程地址空间不足EMFILE:进程附加的共享内存段过多ENOSPC:超出系统限制 |
| 注意事项 | 1. 关联后必须检查返回值是否为(void*)-12. 不同进程关联后在各自共享区的起始地址可能不同,不应存储绝对指针 3. 使用完毕后必须调用 shmdt()分离4. 确保同步机制(信号量等)已就绪 5. 考虑使用 SHM_RDONLY提高安全性 |
shmdt
| 项目 | 详细说明 |
|---|---|
| 函数原型 | int shmdt(const void *shmaddr) |
| 功能描述 | 将共享内存段从调用进程的地址空间中分离(解除关联) |
| 包含头文件 | #include <sys/types.h>#include <sys/shm.h> |
| 参数 | const void *shmaddr• 由 shmat()返回的共享内存起始地址• 必须是有效的、已关联的共享内存地址 • 不能是NULL或无效地址 |
| 返回值 | 成功:返回0 失败:返回-1,并设置 errno |
| 引用计数变化 | shm_nattch--(减少关联进程计数)当计数为0且 shm_perm.mode包含IPC_RMID标志时,物理内存被释放 |
| 错误码 | EINVAL:shmaddr不是已关联的共享内存地址**EFAULT **:shmaddr`指向无效地址(罕见) | |
| 与shmat关系 | 配对操作:shmat- 关联(建立映射)shmdt- 分离(解除映射) |
| 注意事项 | 1. 分离后不能再使用该指针访问共享内存 2. 分离不释放物理内存,除非是最后一个进程且已标记删除 3. 可以多次调用 shmdt(对同一地址多次调用返回EINVAL)4. 指针变量本身不会被修改,但指向的内存已无效 |
| 进程退出 | 进程正常或异常退出时,系统自动执行shmdt操作但显式调用 shmdt是良好编程习惯 |
| 分离后的指针 | 指针变量仍保留原值,但指向无效内存 建议分离后立即设为NULL: shmdt(ptr); ptr = NULL; |
4.共享内存的释放
shmctl
| 项目 | 详细说明 |
|---|---|
| 函数原型 | int shmctl(int shmid, int cmd, struct shmid_ds *buf) |
| 功能描述 | 对共享内存段进行控制操作,包括查询状态、修改属性、删除等 |
| 包含头文件 | #include <sys/ipc.h>#include <sys/shm.h> |
| 参数1 | int shmid• 共享内存标识符 • 由 shmget()返回的有效ID |
| 参数2 | int cmd• 控制命令,指定要执行的操作 • 主要分为三类:查询、设置、控制 |
| 参数3 | struct shmid_ds *buf• 数据缓冲区指针 • 根据cmd不同,可能是输入或输出 • 某些cmd可设为NULL |
| 返回值 | 成功:根据cmd不同返回不同值 失败:返回-1,并设置 errno |
| 命令分类 | 查询命令:IPC_STAT, IPC_INFO, SHM_INFO, SHM_STAT 设置命令:IPC_SET 控制命令:IPC_RMID, SHM_LOCK, SHM_UNLOCK |
| 命令 | 值 | 功能 | buf参数用途 | 返回值 | 权限要求 |
|---|---|---|---|---|---|
| IPC_STAT | 2 | 获取共享内存状态信息 | 输出缓冲区,接收状态信息 | 0 | 读权限 |
| IPC_SET | 1 | 修改共享内存参数 | 输入缓冲区,提供新参数 | 0 | 属主或root |
| IPC_RMID | 0 | 立即/延迟删除共享内存 | 可设为NULL | 0 | 属主或root |
| IPC_INFO | 3 | 获取系统IPC信息 | 输出struct shminfo | 索引值 | 无 |
| SHM_INFO | 14 | 获取系统共享内存信息 | 输出struct shm_info | 索引值 | 无 |
| SHM_STAT | 13 | 获取共享内存状态(通过索引) | 输出struct shmid_ds | shmid | 无 |
| SHM_LOCK | 11 | 锁定共享内存到物理RAM | 可设为NULL | 0 | root |
| SHM_UNLOCK | 12 | 解锁共享内存 | 可设为NULL | 0 | root |
shmctl(shmid, IPC_RMID, NULL);
- 标记删除:立即将共享内存标记为dest状态
- 延迟释放:
- 如果nattch == 0:立即释放物理内存和内核结构
- 如果nattch > 0:等待所有进程shmdt()分离后自动释放
- 阻止新挂接:标记后,其他进程无法再shmat()挂接该内存
// IPC_RMID 的典型使用模式 int main() { int shmid = shmget(key, size, IPC_CREAT|0666); // 立即标记删除(推荐做法) shmctl(shmid, IPC_RMID, NULL); // 此时: // 1. 已关联的进程可继续使用 // 2. 新进程无法关联 // 3. 所有进程分离后自动释放 void *ptr = shmat(shmid, NULL, 0); // ... 使用 ... shmdt(ptr); // 最后一个进程分离后,内存释放 return 0; }5.共享内存的使用
- processa.cc
#include "comm.hpp" //读取 int main() { //创建与关联 int shmid = CreateShm(); char* shmaddr = (char*)shmat(shmid, nullptr, 0); //创建管道 Init init; // 打开管道 int fd = open(FIFO_FILE, O_RDONLY); if (fd < 0) { logObj(Fatal, "error string: %s, error code: %d", strerror(errno), errno); exit(FIFO_OPEN_ERR); } //开始通信 struct shmid_ds shmds; char tmp; while(true) { int n = read(fd, &tmp, 1);//看看是否被通知 if(n <= 0) break; cout << "client say@" << shmaddr << endl; sleep(1); shmctl(shmid, IPC_STAT, &shmds); cout << "shm nattch: " << shmds.shm_nattch << endl; cout << "shm size: " << shmds.shm_segsz << endl; printf("shm key: 0x%x\n", shmds.shm_perm.__key); cout << "shm mode: " << shmds.shm_perm.mode << endl; } //去关联与删除 shmdt(shmaddr); shmctl(shmid, IPC_RMID, nullptr); close(fd); return 0; }- processb.cc
#include "comm.hpp" //写入 int main() { //获取与关联 int shmid = GetShm(); char* shmaddr = (char*)shmat(shmid, nullptr, 0); // 打开管道 int fd = open(FIFO_FILE, O_WRONLY); if (fd < 0) { logObj(Fatal, "error string: %s, error code: %d", strerror(errno), errno); exit(FIFO_OPEN_ERR); } //开始通信 while(true) { cout << "Please Enter@"; fgets(shmaddr, 4096, stdin); write(fd, "c", 1);//通知对方 } //去关联 shmdt(shmaddr); return 0; }(1)processa是读端,processb是写端
(2)processa创建并关联共享内存后进行读取操作,processb获取并关联共享内存后进行写入操作
(3)进程一旦拥有并关联共享内存后,进程就能够直接将共享内存作为自己的内存空间来使用(不需要通过系统调用)。所以一旦processb进行写入操作,processa立马就能执行读取操作
(4)显然,使用共享内存进行通信是所有进程间通信方式中最快的,因为它拷贝少
通信方式 数据拷贝次数 内核/用户切换 具体过程 共享内存 1次(或0次) 1次(建立映射) 1. 数据写入共享内存(1次)
2. 其他进程直接读取(0次拷贝)管道 4次 2次(读写各1次) 1. 用户缓冲区→内核管道缓冲区(1次)
2. 内核→用户缓冲区(1次)
总计:2次拷贝/方向,双向4次(5)当然,共享内存的数据要由用户自己维护,这是因为共享内存没有同步(读端不会阻塞等待写端写入)和互斥(写端未完整写入完数据就被读端读取,导致数据不一致问题)机制
(6)这里使用命名管道进行保护措施(同步机制):写端先通过共享内存进行完写入操作后,再通过命名管道发送一个字符通知读端,读端只有通过命名管道读取到字符后才通过共享内存进行读取操作