当前位置: 首页 > news >正文

Linux 之 【进程间通信】(共享内存、ftok、shmget、shmat、shmdt、shctl、IPC相关指令)

目录

1.共享内存的通信原理

2.共享内存的创建

ftok

shmget

IPC相关命令

共享内存的生命周期

3.共享内存的(去)关联

shmat

shmdt

4.共享内存的释放

shmctl

shmctl(shmid, IPC_RMID, NULL);

5.共享内存的使用

1.共享内存的通信原理

操作系统预先分配一块物理内存(称为共享内存),多个进程通过各自的页表将这段内存映射到自身的虚拟地址空间(通常是共享区),使得不同进程的虚拟地址最终指向同一块物理内存。进程可以直接读写该内存区域进行数据交换,从而实现高效通信,但需配合同步机制保证数据一致性

由于进程具有严格的独立性,其直接分配的内存只能自己独享,无法直接用于进程间通信。为了解决这一矛盾,共享内存必须由作为全局管理者的操作系统内核来提供。进程通过系统调用请求操作系统执行以下核心步骤来间接使用共享内存:创建/获取一块共享内存区域、将其关联(映射)到自身地址空间并使用、通信完成后去关联、最终由系统销毁该区域。

  1. 创建/获取(申请内存):进程请求操作系统创建一块新的共享内存,或获取一块已存在的共享内存的标识符。操作系统在物理内存中为其分配空间。
  2. 关联:进程将这块共享内存映射到自己的进程地址空间(通常是共享区),操作系统建立进程页表到该物理内存的映射关系,并返回映射后的虚拟首地址供进程访问。
  3. 使用:进程通过得到的虚拟地址直接读写该内存区域,进行进程间通信。
  4. 去关联:通信完成后,进程断开与该共享内存的映射关系(去关联),使其从本进程的地址空间中移除。
  5. 销毁(释放):当所有使用该共享内存的进程都完成去关联后,操作系统最终释放该共享内存资源。

为了管理系统中可能存在的多块共享内存,操作系统采用“先描述,再组织”的方式:用一个内核数据结构(如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函数必须包含这两个头文件。
参数1const char *pathname路径名指向一个已存在且可访问的文件(或目录)的路径字符串
必须存在:路径指向的文件必须存在且有访问权限。
通常用法:使用一个固定的、所有进程都能访问的文件(如/tmp/myapp,/etc/passwd, 或项目特定文件)。
原理依据:函数会使用该文件的st_dev(设备号)和st_ino(i-node号)信息。
参数2int 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)。
参数1key_t key键值,用于唯一标识系统中共享内存对象。
IPC_PRIVATE(0):总是创建的共享内存,通常用于父子进程间。
用户指定key:通过ftok()函数生成,或直接指定一个非零整数,用于无亲缘关系进程间的约定。
参数2size_t size请求的共享内存大小(字节)
创建时:指定新共享内存的最小尺寸,系统可能会向上取整到页大小(如4KB)的整数倍。
获取时:如果只是获取已存在的共享内存,size可以为0;若指定了大小,且小于已存在内存的大小,可能会报错。
参数3int shmflg创建标志与权限标志的组合。这是一个位掩码,由两部分按位或(|)组成
A. 行为控制标志:
IPC_CREAT:如果key对应的共享内存不存在,则创建它。如果存在,则直接获取其shmid
IPC_EXCL:与IPC_CREAT一同使用。如果共享内存已存在,则调用失败(返回-1errno设为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>
参数1int shmid
shmget()返回的共享内存标识符
• 必须是已存在的有效共享内存ID
• 用于指定要附加的共享内存段
参数2const void *shmaddr
• 期望的附加地址(建议设为NULL
shmaddr == NULL:系统自动选择合适地址
shmaddr != NULL:尝试在指定地址附加(需对齐)
• 通常设置为NULL让系统选择,可避免地址冲突
参数3int shmflg
• 附加标志位(按位或组合)
SHM_RDONLY:以只读方式关联
SHM_RND:当shmaddr非NULL时,将其向下取整到SHMLBA边界
0:默认读写方式
返回值成功:返回关联后的共享内存段在共享区的起始地址(void*类型)
失败:返回(void*)-1,并设置errno
权限控制• 创建时通过shmget()的权限位控制(如0666
• 关联时可通过SHM_RDONLY限制为只读
• 权限检查:进程必须有相应的访问权限
引用计数每次成功shmat()会增加shm_nattch计数
每次shmdt()会减少计数
当计数为0且已标记删除时,内存被释放
错误码EACCES:权限不足
EINVALshmid无效或shmaddr不对齐
ENOMEM:进程地址空间不足
EMFILE:进程附加的共享内存段过多
ENOSPC:超出系统限制
注意事项1. 关联后必须检查返回值是否为(void*)-1
2. 不同进程关联后在各自共享区的起始地址可能不同,不应存储绝对指针
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标志时,物理内存被释放
错误码EINVALshmaddr不是已关联的共享内存地址
**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>
参数1int shmid
• 共享内存标识符
• 由shmget()返回的有效ID
参数2int cmd
• 控制命令,指定要执行的操作
• 主要分为三类:查询、设置、控制
参数3struct 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_STAT2获取共享内存状态信息输出缓冲区,接收状态信息0读权限
IPC_SET1修改共享内存参数输入缓冲区,提供新参数0属主或root
IPC_RMID0立即/延迟删除共享内存可设为NULL0属主或root
IPC_INFO3获取系统IPC信息输出struct shminfo索引值
SHM_INFO14获取系统共享内存信息输出struct shm_info索引值
SHM_STAT13获取共享内存状态(通过索引)输出struct shmid_dsshmid
SHM_LOCK11锁定共享内存到物理RAM可设为NULL0root
SHM_UNLOCK12解锁共享内存可设为NULL0root
  • shmctl(shmid, IPC_RMID, NULL);

  1. 标记删除:立即将共享内存标记为dest状态
  2. 延迟释放:
    1. 如果nattch == 0:立即释放物理内存和内核结构
    2. 如果nattch > 0:等待所有进程shmdt()分离后自动释放
  3. 阻止新挂接:标记后,其他进程无法再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)这里使用命名管道进行保护措施(同步机制):写端先通过共享内存进行完写入操作后,再通过命名管道发送一个字符通知读端,读端只有通过命名管道读取到字符后才通过共享内存进行读取操作

http://www.jsqmd.com/news/305133/

相关文章:

  • 如何提高大数据领域数据建模的准确性和可靠性
  • CGO调用OpenCV实现多角度模板匹配性能分析
  • 基于STM32单片机烟雾温度防盗报警 物联网云平台 火灾检测系统DIY
  • Photoshop CS6 精简绿色版Photoshop CS6 精简绿色版分享
  • 基于STM32单片机物联网云平台 WIFI点滴速度液体检测 输液系统DIY
  • 【Termux】Photopea离线版部署
  • python脚本实现短剧配音
  • 洛谷 P9100 [PA 2020] Miny 题解
  • Java应用实例:简易背单词程序(更新)
  • 初识线程:带你理解程序运行的基本流程
  • 后端开发效率翻倍:IntelliJ IDEA的5个“神级插件
  • Zookeeper在大数据实时报表系统中的应用
  • 063.经典搜索,剪枝
  • 从零开始学大模型核心:向量嵌入技术完全指南
  • CF2029G Balanced Problem
  • 【技术干货】大模型记忆机制进化全攻略:从存储到经验的AI认知革命
  • 1.5万字硬核AI架构指南:从单体智能到系统智能的实战设计
  • 双非二程序员的大模型逆袭之路:RAG与Agent技术学习指南
  • 大模型应用工程师学习路线:从提示词工程到AI系统构建,年薪50w+技能全攻略_这是一份大模型应用学习路线!(附学习资料)
  • AARONIA(安诺尼)PBS 1 与 PBS 2 近场探头 —— 精准定位电磁干扰源
  • 20260126 之所思 - 人生如梦
  • mysql day2
  • YOLOv8改进 - 注意力机制 | SENetV2: 用于通道和全局表示的聚合稠密层,结合SE模块和密集层来增强特征表示
  • 21点,如何计算胜率高达75%
  • 干瞪眼游戏胜率较高的玩法分析
  • 中国船级社信息开发咨询中心 APP开发工程师职位深度解析与技术面试指南
  • 北航杭州创新研究院移动客户端/前端开发工程师岗位深度解析与面试指南
  • 量子科技长三角产业创新中心 AI软件开发工程师岗位深度解析与面试指南
  • Oracle到YashanDB适配:dbms_obfuscation_toolkit的平滑迁移
  • vue3 - 01 路由的配置和使用