前言:为什么需要共享内存?
在进程间通信(IPC)的世界里,共享内存以其无可比拟的速度优势 脱颖而出。想象两个进程需要频繁交换大量数据------如果每次通信都要经过内核缓冲区的拷贝,效率将大打折扣。共享内存的核心理念是:让不同进程直接读写同一块物理内存,
一、共享内存的本质
1.1 工作原理
共享内存通过将同一块物理内存映射到不同进程的地址空间中实现通信。这个过程类似动态库的加载:
就像在同一个进程中操作内存一样自然。
cpp
进程A虚拟空间 物理内存 进程B虚拟空间
共享区 ────────────> 共享内存段 <─────────── 共享区
│ │ │
└───────────────────┴────────────────────┘
通过页表建立映射关系
1.2 生命周期特性
与文件描述符不同,共享内存的生命周期随内核而非进程:
-
进程退出 → 共享内存仍然存在
-
必须显式删除或重启系统才会释放
-
这既是优点(持久化),也是隐患(资源泄漏)
二、核心概念:key 与 shmid
2.1 关键区分
初学者最容易混淆的两个概念:
| 概念 | 类比 | 作用域 | 用途 |
|---|---|---|---|
| key(键值) | 文件的 inode | 内核内部 | 唯一标识共享内存 |
| shmid | 文件描述符 fd | 用户空间 | 操作共享内存的句柄 |
2.2 为什么需要 key?
这是共享内存设计的精妙之处:如何在通信前让双方约定同一资源?
解决方案:通过 ftok() 函数生成唯一的 key:
cpp
key_t ftok(const char *pathname, int proj_id);
-
pathname:存在的文件路径(如 "/tmp") -
proj_id:项目ID(任意非零整数) -
原理:结合文件 inode 和 proj_id 生成唯一 key
三、五个关键系统调用
3.1 创建/获取:shmget()
cpp
int shmget(key_t key, size_t size, int shmflg);
参数解析:
-
key:由 ftok() 生成或用户指定(0 表示私有) -
size:共享内存大小(实际分配为4096的倍数) -
shmflg:标志位组合-
IPC_CREAT:不存在则创建 -
IPC_EXCL:存在则失败(与 IPC_CREAT 联用确保新建) -
权限位:如 0666(八进制,类似文件权限)
-
3.2 关联:shmat()
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
要点:
-
返回映射后的虚拟地址起始指针
-
shmaddr通常设为 NULL(由系统选择地址) -
关联后即可像普通内存一样读写
3.3 解除关联:shmdt()
注意: 解除关联 ≠ 删除共享内存,只是断开当前进程的映射。
cpp
int shmdt(const void *shmaddr);
3.4 控制:shmctl()
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用命令:
-
IPC_RMID:标记删除(引用计数为0时实际删除) -
IPC_STAT:获取共享内存状态信息
3.5 生成 key:ftok()
如前所述,这是进程间"秘密握手"的方式。
四、实践代码示例
4.1 封装类设计
一个良好的封装应该隐藏细节:
cpp
class Shm {
public:
void Create(); // 创建新共享内存
void Get(); // 获取现有共享内存
void Attach(); // 关联到进程空间
void Detach(); // 解除关联
void Delete(); // 删除共享内存
private:
int _shmid; // 共享内存ID
void* _start_addr; // 映射地址
int _size; // 内存大小
};
4.2 服务端代码(读取)
cpp
// 服务端:持续读取共享内存内容
Shm myshm;
myshm.Create(); // 创建共享内存
myshm.Attach(); // 关联到进程
char* buffer = (char*)myshm.Addr();
while (true) {
// 直接读取内存,无需系统调用!
std::cout << "Received: " << buffer << std::endl;
sleep(1);
}
4.3 客户端代码(写入)
cpp
// 客户端:向共享内存写入数据
Shm myshm;
myshm.Get(); // 获取共享内存
myshm.Attach(); // 关联到进程
char* buffer = (char*)myshm.Addr();
while (true) {
std::cout << "Input: ";
std::cin >> buffer; // 直接写入内存,立即对端可见!
}
五、关键细节与陷阱
5.1 权限问题
常见错误: 忘记设置权限导致无法挂接
cpp
// 错误:缺少权限位
shmget(key, size, IPC_CREAT | IPC_EXCL);
// 正确:添加权限
shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
5.2 大小对齐
操作系统实际分配的内存是4096字节的倍数:
cpp
// 请求4097字节
shmget(key, 4097, ...);
// 实际分配8192字节(4096×2)
// 但用户只能安全访问前4097字节
5.3 残留共享内存处理
程序崩溃可能导致共享内存残留,需要清理:
cpp
// 启动时清理可能存在的旧共享内存
int old_shmid = shmget(key, 0, 0);
if (old_shmid >= 0) {
shmctl(old_shmid, IPC_RMID, nullptr);
}
六、优势与局限
6.1 优势
-
极速通信:直接内存访问,零拷贝
-
自然编程模型:像操作普通内存一样使用
-
大容量支持:适合传输大量数据
6.2 局限与解决方案
-
无同步机制 → 需要配合信号量/互斥锁
-
生命周期管理复杂 → 设计明确的清理机制
-
安全问题 → 设置适当的权限控制
七、实际应用场景
7.1 高性能数据处理
-
图像/视频处理流水线
-
科学计算中间结果交换
-
实时数据采集系统
7.2 进程池通信
-
工作进程共享任务队列
-
缓存数据共享
-
配置信息同步
八、最佳实践建议
-
总是检查返回值:系统调用可能失败
-
明确所有权:哪个进程负责创建和删除
-
添加同步机制:即使是简单应用也建议使用信号量
-
设计优雅的关闭流程:确保资源正确释放
-
考虑容错性:处理进程异常退出的情况
结语
共享内存是IPC工具箱中的"高性能武器"。它打破了进程间的壁垒,让数据可以在不同进程间自由流动。然而,正如课程中所强调的:能力越大,责任越大。共享内存的高效性伴随着更复杂的资源管理和同步需求。
掌握共享内存不仅是学习一个技术,更是理解操作系统如何平衡隔离与共享这一永恒主题。从匿名管道到命名管道,再到共享内存,我们看到了操作系统设计者如何在不同需求间寻找平衡点。
记住课程中的金句:"当我们解决问题时,往往不可避免地带来新的问题"。在共享内存的世界里,这个新问题就是同步与并发控制------这将是下一章(信号量)要解决的核心挑战。
*第一次运行服务端时,可能汇报文件存在的报错,这时候手动删除一下残留的共享内存就可以在正常运行了
bash
ipcs -m
ipcrm -m shmid(上面那个命令可以查看到)
共享内存:
cpp
#pragma once
#include <iostream>
#include <sys/shm.h>
#include <cstdio>
#include <cstdlib>
#include <sys/ipc.h>
#include <sys/types.h>
const int gsize = 128;
// 用户给予的两个数据
#define PATHNAME "/tmp"
#define PROJ_ID 0x66
class Shm
{
public:
Shm() : _shmid(-1), _size(gsize) {}
void Delete()
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
(void)n;
}
void Attach()
{
_start_addr = shmat(_shmid, nullptr, 0);
if ((long long int)_start_addr == -1)
{
exit(3);
}
}
void Detach()
{
int n = shmdt(_start_addr);
(void)n;
}
void CleanExistShm()
{
key_t key = GetKey();
int ExistShmId = shmget(key, _size, IPC_CREAT | 0666);
if (ExistShmId >= 0)
{
int ret = shmctl(ExistShmId, IPC_RMID, nullptr);
if (ret == 0)
{
std::cout << "Clean success, exist shmid: " << ExistShmId << std::endl;
}
else
{
perror("Clean exist shm failed");
}
}
}
void Get()
{
// CleanExistShm();
GetHelper(IPC_CREAT);
}
void Create()
{
CleanExistShm();
GetHelper(IPC_CREAT | IPC_EXCL | 0666);
}
void *Addr()
{
return _start_addr;
}
int Size()
{
return _size;
}
~Shm() {};
private:
key_t GetKey()
{
return ftok(PATHNAME, PROJ_ID);
}
void GetHelper(int shmflg)
{
// 获得key
key_t key = GetKey();
if (key < 0)
{
std::cerr << "GetKey Error" << std::endl;
exit(1);
}
_shmid = shmget(key, _size, shmflg);
if (_shmid < 0)
{
perror("shmget");
exit(2);
}
printf("key = 0x%x,key = %d\n", key, key);
}
private:
int _shmid;
int _size;
void *_start_addr;
};
服务端:
cpp
#include "shm.hpp"
#include <iostream>
#include <unistd.h>
int main()
{
Shm myshm;
myshm.Create();
// sleep(3);
myshm.Attach();
// sleep(3);
char *shm_start = (char *)myshm.Addr();
int size = myshm.Size();
while (true)
{
// 本质就是读取共享内存!
for (int i = 0; i < size; i++)
{
std::cout << shm_start[i] << ' ';
}
std::cout << std::endl;
sleep(1);
}
myshm.Detach();
myshm.Delete();
return 0;
}
客户端:
cpp
#include "shm.hpp"
#include <iostream>
#include <unistd.h>
int main()
{
Shm myshm;
myshm.Get();
// sleep(3);
myshm.Attach();
// sleep(3);
char *shm_start = (char*)myshm.Addr();
int size = myshm.Size();
int index = 0;
while(true)
{
std::cout << "Please Enter@ ";
std::cin >> *shm_start;
// *(shm_start + index) = ch;
// shm_start[index++] = ch;
shm_start++;
index %= size;
// sleep(1);
}
myshm.Detach();
return 0;
}