System V共享内存机制详解与实践
共享内存概述
共享内存的基本概念
共享内存是进程间通信(IPC)方式中效率最高的一种。当共享内存区域被映射到参与通信的进程地址空间后,这些进程之间的数据交换将不再需要内核介入。换句话说,进程无需通过系统调用进入内核态即可直接读写数据。 可以从另一个角度理解:操作系统为通信双方分配了一块公共内存区域,双方可以直接对该区域进行读取和写入操作,整个过程完全在用户态完成。共享内存工作原理

共享内存的使用流程
使用共享内存进行进程间通信需要经历以下步骤:- 创建或获取共享内存块
- 将共享内存挂载到进程地址空间
- 通过映射的地址进行数据交换
- 卸载共享内存
- 删除共享内存资源
关键函数接口详解
1. 创建或获取共享内存:shmget函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key:用于标识共享内存块的唯一键值,通过ftok函数生成
- size:需要创建的共享内存大小(字节)
- shmflg:权限标志位,常用组合如下:
- IPC_CREAT:当共享内存不存在时创建,存在时返回已存在的内存块
- IPC_EXCL:与IPC_CREAT配合使用,不存在则创建,存在则返回错误
- 0660:权限掩码,表示所有者可读写,组用户可读写
2. 生成唯一键值:ftok函数
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数说明:
- pathname:指定一个已存在的文件路径,通常使用当前目录"."
- proj_id:用户自定义的项目标识符(通常只使用低8位),如0x66
返回值是生成的key_t类型键值,用于后续shmget调用。
3. 挂载共享内存:shmat函数
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
- shmid:shmget返回的共享内存标识符
- shmaddr:指定映射地址,传入NULL由系统自动选择合适地址
- shmflg:操作权限,0表示读写,SHM_RDONLY表示只读
4. 卸载共享内存:shmdt函数
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数为shmat返回的地址指针。成功返回0,失败返回-1。
5. 控制共享内存:shmctl函数
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用命令(cmd参数):
- IPC_RMID:删除共享内存块
- IPC_STAT:获取共享内存状态信息
- IPC_SET:设置共享内存属性
第三个参数buf是输出型参数,用于获取共享内存的相关信息(如创建者PID、权限、大小等)。
共享内存数据结构
每个共享内存块都对应一个shmid_ds结构体,该结构体记录了共享内存的各种属性信息。Linux内核通过统一的ipc_perm结构体来管理不同IPC资源的权限,这使得共享内存、消息队列和信号量可以使用相同的管理机制。
实战案例:进程间消息传递
公共头文件定义
#pragma once
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <cstring>
#include <string>
#include <iostream>
#include <cstdlib>
#include <cerrno>
using namespace std;
const char* kPathName = ".";
const int kProjId = 0x66;
const size_t kShmSize = 4096;
class SharedMemoryHelper {
public:
static int generateKey() {
int key = ftok(kPathName, kProjId);
if (key == -1) {
cerr << "ftok failed: " << errno << " " << strerror(errno) << endl;
exit(1);
}
return key;
}
static int createOrGetShm(key_t key, size_t size, int flag) {
int shmid = shmget(key, size, flag);
if (shmid == -1) {
cerr << "shmget failed: " << errno << " " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
static int getShm(key_t key) {
return createOrGetShm(key, kShmSize, IPC_CREAT | 0660);
}
static int createShm(key_t key) {
return createOrGetShm(key, kShmSize, IPC_CREAT | IPC_EXCL | 0660);
}
static void* attachShm(int shmid) {
void* addr = shmat(shmid, nullptr, 0);
if (addr == (void*)-1) {
cerr << "shmat failed: " << errno << " " << strerror(errno) << endl;
exit(3);
}
return addr;
}
static void detachShm(void* addr) {
if (shmdt(addr) == -1) {
cerr << "shmdt failed: " << errno << " " << strerror(errno) << endl;
exit(4);
}
}
static void removeShm(int shmid) {
if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
cerr << "shmctl failed: " << errno << " " << strerror(errno) << endl;
exit(5);
}
}
};
服务端进程实现
#include "shm_helper.hpp"
#include <unistd.h>
#include <cstdio>
int main() {
int key = SharedMemoryHelper::generateKey();
cout << "Generated key: " << key << endl;
int shmid = SharedMemoryHelper::createShm(key);
cout << "Shared memory ID: " << shmid <&endl;
sleep(3);
char* shmAddr = static_cast<char*>(SharedMemoryHelper::attachShm(shmid));
printf("Mapped address: %p\n", static_cast<void*>(shmAddr));
while (true) {
printf("Received: %s\n", shmAddr);
sleep(1);
}
SharedMemoryHelper::detachShm(shmAddr);
sleep(3);
SharedMemoryHelper::removeShm(shmid);
return 0;
}
客户端进程实现
#include "shm_helper.hpp"
#include <unistd.h>
#include <cstdio>
int main() {
int key = SharedMemoryHelper::generateKey();
cout << "Generated key: " << key <&endl;
int shmid = SharedMemoryHelper::getShm(key);
cout << "Shared memory ID: " << shmid <&endl;
sleep(3);
char* shmAddr = static_cast<char*>(SharedMemoryHelper::attachShm(shmid));
printf("Mapped address: %p\n", static_cast<void*>(shmAddr));
const char* msgTemplate = "Client says hello";
int counter = 1;
while (true) {
snprintf(shmAddr, kShmSize, "%s [#%d] PID=%d",
msgTemplate, counter++, getpid());
sleep(5);
}
SharedMemoryHelper::detachShm(shmAddr);
return 0;
}
运行效果与问题分析
通过上述代码可以观察到服务端持续读取共享内存中的数据,客户端定时更新数据。然而,这种实现存在一个明显的缺陷:由于缺乏同步机制,当写入速度较慢而读取速度较快时,会出现重复读取相同数据的情况。
解决这个问题的一种方式是结合管道机制。当写端写入速度慢、读端读取速度快时,管道的read操作会被阻塞,从而避免无效的重复读取。
消息队列与信号量简介
消息队列和信号量的使用方法与共享内存类似,都需要通过ftok函数生成key值,然后使用对应的get函数创建资源。
值得注意的是,这三种IPC资源在Linux内核中共享相同的管理结构。系统通过ipc_perm结构体来统一管理这些资源的权限信息,无论资源类型如何,其第一个成员都是ipc_perm结构。这意味着内核可以通过统一的接口来处理不同类型IPC资源的权限验证和资源管理。
查看系统中IPC资源可以使用以下命令:
- 查看共享内存:
ipcs -m - 查看消息队列:
ipcs -q - 查看信号量:
ipcs -s - 删除指定资源:
ipcrm -m/-q/-s [id]