linux进程间通信-----system V共享内存

📅 2026/7/12 23:24:04
linux进程间通信-----system V共享内存
1.为什么要进程间通信在现代os中每个进程都运行在受保护独立的虚拟地址空间中若不主动建立通道进程间是绝对隔离的进程间通信的诉求可以划分为以下几点1.数据传输一个进程需要将它的数据发给另一个进程2.资源共享多个进程之间共享同样的资源3.通知事件一个进程需要向另一个或一组进程发送消息通知发生了什么事如进程终止时要通知父进程4.进程控制有些进程需要完全控制另一个进程的执行如debug进程此时控制进程需要拦截另一个进程的所有陷入和异常并能及时知道它的状态改变。管道、消息队列这类传统IPC本质是内核维护的中转缓冲区数据传输要走「用户态 → 内核态 → 用户态」两次拷贝而共享内存走的是完全不同的路径内核通过页表映射让多个进程的虚拟地址直接指向同一块物理内存数据读写全程在用户态完成不需要内核中转数据也是linux 下唯一真正实现零拷贝的IPC机制2.system V共享内存共享内存是linux系统中大数据量情景下速度最快的IPC机制内核通过页表将同一块物理内存映射到多个进程的虚拟地址空间实现零拷贝数据交换。正因为共享内存跳过了内核的中转和调度它也是linux流IPC中少数用户需要自行实现同步机制的一种当两个进程同时发生读/写时由于CPU的调度随机性会导致非原子的读写操作被打断进而引发严重的竞态条件。这时通常需要配合同步原语来保护临界区保证同一时刻仅有一个进程访问临界资源如可以使用互斥锁信号量等。2.1全局key_t与ftok共享内存的创建依赖一个key_t类型的键值通常通过ftok(pathname,proj_id)由一个已存在的文件路径和项目ID计算得到。ftok十分依赖于文件系统的inode 编号和设备号。如果在运行期间该物理路径被删除并重建inode编号发生改变ftok生成的key也随之改变导致后续启动的进程算出不同的key无法挂载原共享内存。在网络文件系统NFS或虚拟磁盘上iNode编号某些唯一性保证较弱容易出现不同文件相同密钥的情况。进程可能意外的挂载到不属于自己的共享内存段中了。生产环境中一般使用的是固定的可控的文件路径项目ID或直接IPC_PRIVATE结合其他机制来规避冲突。2.2内存泄漏共享内存的生命周期是随内核运行期的不随创建它的进程退出而自动释放只有系统重启才会全部清空或者显式删除。但是如果进程因为段错误或信号异常终止而没有显式调用shmctl删除共享内存段就会一直存在即便无进程挂载当服务器反复此操作时就会使物理内存空间越来越少导致服务器最终挂掉应对措施1.这时可以注册信号捕捉器进行清理但是无法应对SIGKILL(9号信号)因为该信号无法被捕获或者阻塞。2.亦或者用shm_nattch通过挂载在该共享内存段的进程数来判断清理时机例如设计一个独立的“最后一个用户负责清理”的协议在某进程shmdt之后检查shm_nattch是否降为0若是则由其负责IPC_RMID注意标记为IPC_RMID并不会立刻销毁共享内存只是将其标记为待销毁状态3.人工用命令行进行查看并清理共享内存段ipcs -m//查看当前系统所有共享内存段状态 ipcrm -m shmid//删除指定标识号的共享内存段3.共享内存的数据结构在内核中每一个共享内存段都由一个shmid_ds结构体来描述。struct shmid_ds { struct ipc_perm shm_perm; //操作权限与所有权 size_t shm_segsz; //共享内存段的大小字节 pid_t shm_cpid; //创建者的进程PID pid_t shm_lpid; //最后操作者挂载/卸载的进程PID unsigned short shm_nattch; //当前挂载该段的进程数引用计数 time_t shm_atime; //最后一次挂载attach的时间 time_t shm_dtime; //最后一次卸载detach的时间 time_t shm_ctime; //最后一次修改属性如权限的时间 };我们可以通过shmctl的IPC_STAT命令读取这些信息用于调试、监控和生命周期判断。4.共享内存函数ftok函数key_t ftok(const char* pathname,int proj_id);pathname一个已存在的文件路径ftok只用它的inode编号与文件的内容和权限无关在生产环境中需使用绝对路径proj_id项目id只有低8位才会实际使用高位会被忽略所以传0x6666和0x66效果一样成功返回一个非0的key_t值失败会返回-1并设置errnoshmget函数用来创建共享内存的int shmget(key_t key,size_t size,int shmflag);key是这个共享内存段标识键值一般由ftok生成也可以传递IPC_PRIVATE值为0size是这个共享内存段的字节大小内核会按页大小通常为4KB向上取整shmflag的低9位是权限位与文件的rwxrwxrwx权限位意义一致高位可以按位或上标志位IPC_CREAT共享内存不存在时创建并返回存在时获取并返回IPC_CREAT|IPC_EXCL不存在时创建并返回存在时报错返回EEXIST成功则返回一个非负整数作为该共享内存段的标识码失败返回-1并设置errnoshmat函数将共享内存段连接到进程地址空间void* shmat(int shmid,const void* shmaddr,int shmflag);shimd指共享内存标识shmaddr指定共享内存段连接的地址shmfalg的两个可能取值为SHM_RND和SHM_RDONLYshmaddr为NULL时核心自动选择一个地址shmaddr不为NULL时且shmflag无SHM_RND标记时则以shmaddr为连接地址shmaddr不为NULL时且shmflag设置了SHM_RND标记时则连接的地址会自动向下调整为SHMLBA的整数倍。当shmflag设置了SHM_RDONLY标记时表示连接操作用来只读共享内存shmflag可以为0表示默认以可读可写方式挂载成功返回一个指针(void*)指向共享内存的起始地址失败返回(void*)-1shmdt函数将共享内存段与当前进程脱离int shmdt(const void* shmaddr);shmaddr是由shmat所返回的指针成功则返回0失败则返回-1注意将共享内存段与当前进程脱离并不等于删除共享内存段。shmctl函数用于控制共享内存int shmctl(int shmid,int cmd,struct shmid_ds* buf);shmid是由shmget返回的共享内存标识码cmd是将要采取的动作有三个可取值buf指向着一个保存的共享内存的模式状态和访问权限的数据结构成功返回0失败返回-1cmd的三个取值IPC_STAT获取共享内存段的当前状态写入 buf 指向的shmid_ds结构体中IPC_SET在进程有足够权限前提下把共享内存的当前关联值更新为shmid_ds数据结构中给出的值IPC_RMID删除共享内存段仅当最后一个进程卸载共享内存段后才真正释放buf保存共享内存模式状态和访问权限的数据结构指针5.共享内存实现通信下面实现一个基础的共享内存通信示例服务端创建共享内存读取客户端写入的数据客户端挂载共享内存逐字节写入字母注意这是基础Demo用sleep粗略控制时序仅用于演示共享内存的基本用法没有同步机制在生产环境中严禁这么写comm.h#pragma once #includestdio.h #includesys/types.h #includesys/ipc.h #includesys/shm.h #includeunistd.h #define PATHNAME . #define PROJ_ID 0x6666 int createshm(int size); int destroyshm(int shmid); int getshm(int size);comm.c#include comm.h static int commshm(int size,int shmflag) { key_t keyftok(PATHNAME,PROJ_ID); if(key0) { perror(ftok); return -1; } int shmid0; if((shmidshmget(key,size,shmflag))0) { perror(shmget); return -2; } return shmid; } int destroyshm(int shmid) { if(shmctl(shmid,IPC_RMID,NULL)0) { perror(shmctl); return -1; } return 0; } int createshm(int size) { return commshm(size,IPC_CREAT|IPC_EXCL|0666); } int getshm(int size) { return commshm(size,0666); }server.c#includecomm.h int main() { int shmidcreateshm(4096); char* addshmat(shmid,NULL,0); sleep(2); int i0; while(i26) { printf(client says# %s\n,add); sleep(1); } shmdt(add); sleep(2); destroyshm(shmid); return 0; }client.c#includecomm.h int main() { int shmidgetshm(4096); sleep(1); char* addshmat(shmid,NULL,0); int i0; while(i26) { add[i]ai; sleep(1); i; add[i]\0; } shmdt(add); sleep(2); return 0; }这就是运行结果图6.总结共享内存作为性能最高的IPC方式核心优势在于零拷贝的数据读写但代价是需要开发者自行处理同步、生命周期、数据一致性等所有问题坑点远多于管道、消息队列等其他IPC。其中最容易触发致命故障的红线就是裸指针问题每个进程的虚拟地址空间是独立的A 进程里的有效指针放到B进程的地址空间里就是非法地址直接访问必然触发段错误。共享内存里只能存偏移量、数组、完整的结构体数据绝对不能存指向进程自身栈、堆的指针。至于标准选型本文讲解的system V共享内存出现早但接口设计老旧生命周期管理不灵活key的全局命名空间也容易出现冲突。当前工业界更推荐POSIX共享内存shm_openmmap接口更统一和文件操作逻辑一致生命周期管理更灵活还可以通过文件名做权限控制。如果不是维护老项目新项目优先考虑POSIX方案。