一、通信方式回顾
在理解共享内存之前,先回顾三种通信方式:
| 通信方式 | 特点 | 举例 |
|---|---|---|
| 单工 | 单向,方向固定 | 广播、电视 |
| 半双工 | 双向,同一时刻只能一方发 | 对讲机、管道 |
| 全双工 | 双向,同一时刻双方可互发 | 电话、共享内存 |
共享内存是全双工通信------双方可以同时读写(需自行处理同步,通常配合信号量使用)。
常见 IPC(进程间通信)方式:管道、共享内存、消息队列、信号量、套接字。
共享内存是效率最高的 IPC 方式,因为数据不需要在内核态和用户态之间拷贝。
二、shmget - 创建/获取共享内存
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明
| 参数 | 含义 | 取值 |
|---|---|---|
key |
共享内存唯一标识 | IPC_PRIVATE(私有) 或 ftok() 生成 |
size |
共享内存大小(字节) | 创建时填正整数,获取已有填 0 |
shmflg |
标志位 + 权限 | IPC_CREAT、IPC_EXCL、权限位(如 0666) |
shmflg 标志位
| 标志 | 作用 |
|---|---|
IPC_CREAT |
如果 key 不存在则创建 |
IPC_EXCL |
与 IPC_CREAT 配合,key 已存在则报错 |
0666 |
权限(rw-rw-rw-) |
返回值
-
成功:返回共享内存标识符(正数)
-
失败:返回 -1 ,设置
errno
不同场景的 shmget 调用
| 场景 | 代码 | 说明 |
|---|---|---|
| 创建新共享内存 | `shmget(key, 1024, IPC_CREAT | 0666)` |
| 创建(独占) | `shmget(key, 1024, IPC_CREAT | IPC_EXCL |
| 获取已有 | shmget(key, 0, 0) |
size 填 0,flags 填 0 |
cpp
// 示例:创建 1024 字节的共享内存
int shmid = shmget(1234, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
三、shmat - 映射共享内存到进程地址空间
创建的共享内存需要映射到进程的地址空间才能使用。
cpp
#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
参数说明
| 参数 | 含义 | 取值 |
|---|---|---|
shm_id |
共享内存标识符 | shmget() 的返回值 |
shm_addr |
附加地址 | NULL(系统自动选择)或指定地址 |
shmflg |
附加标志 | 0(读写)、SHM_RDONLY(只读) |
shmflg 标志
| 标志 | 作用 |
|---|---|
0 |
默认,可读写 |
SHM_RDONLY |
只读,写入会段错误 |
SHM_EXEC |
允许执行代码 |
返回值
-
成功:返回映射后的地址指针
-
失败:返回
(void*)-1
cpp
// 示例:映射共享内存
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror("shmat");
exit(1);
}
// 现在可以像普通内存一样读写
strcpy(shmaddr, "Hello");
四、shmdt - 分离共享内存
分离操作不会删除共享内存,只是解除进程与共享内存的映射关系。
cpp
#include <sys/shm.h>
int shmdt(const void* shm_addr);
| 参数 | 含义 |
|---|---|
shm_addr |
shmat() 返回的地址 |
返回值
-
成功:返回
0 -
失败:返回
-1
cpp
// 示例:分离共享内存
if (shmdt(shmaddr) == -1) {
perror("shmdt");
exit(1);
}
注意:进程退出时会自动分离,但显式调用是良好习惯。
五、shmctl - 删除/控制共享内存
cpp
#include <sys/shm.h>
int shmctl(int shm_id, int cmd, struct shmid_ds* buf);
常用 cmd
| cmd | 作用 |
|---|---|
IPC_RMID |
删除共享内存 |
IPC_STAT |
获取共享内存状态 |
IPC_SET |
设置共享内存属性 |
cpp
// 示例:删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
六、完整示例:两个进程通信
需求
-
写进程 :持续向共享内存写入数据,输入
end则退出 -
读进程:持续读取共享内存的数据并打印,写进程退出后自动结束
写进程(write.c)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
int main() {
key_t key = 1234;
int shmid;
char* shmaddr;
// 1. 创建共享内存
shmid = shmget(key, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 2. 映射共享内存
shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror("shmat");
exit(1);
}
// 3. 写入数据
char buf[256];
while (1) {
printf("请输入:");
fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
strcpy(shmaddr, buf);
if (strcmp(buf, "end") == 0) {
break;
}
}
// 4. 分离共享内存
shmdt(shmaddr);
// 5. 删除共享内存(由写进程负责删除)
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
读进程(read.c)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
int main() {
key_t key = 1234;
int shmid;
char* shmaddr;
char last[256] = "";
// 1. 获取已存在的共享内存
shmid = shmget(key, 0, 0);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 2. 映射共享内存
shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror("shmat");
exit(1);
}
// 3. 持续读取数据
while (1) {
if (strcmp(shmaddr, last) != 0) {
printf("读取到:%s\n", shmaddr);
strcpy(last, shmaddr);
if (strcmp(shmaddr, "end") == 0) {
break;
}
}
usleep(100000); // 睡眠 0.1 秒,避免忙等待
}
// 4. 分离共享内存
shmdt(shmaddr);
return 0;
}
编译运行
bash
# 编译
gcc write.c -o write
gcc read.c -o read
# 终端1:运行读进程
./read
# 终端2:运行写进程
./write
# 写进程输入数据,读进程会实时显示
# 输入 end 后,写进程退出,读进程也随之退出
七、常用命令
| 命令 | 作用 |
|---|---|
ipcs -m |
查看系统中的共享内存 |
ipcrm -m <shmid> |
删除指定的共享内存 |
bash
# 查看所有共享内存
ipcs -m
# 删除共享内存(如果程序没删干净)
ipcrm -m 12345
八、共享内存 vs 管道
| 对比项 | 管道 | 共享内存 |
|---|---|---|
| 通信方向 | 半双工 | 全双工 |
| 数据拷贝 | 有(内核态↔用户态) | 无(直接访问) |
| 速度 | 慢 | 快(最快 IPC) |
| 同步机制 | 内核自动阻塞 | 需自行处理(如信号量) |
| 使用场景 | 简单数据流 | 大量数据、频繁通信 |
九、注意事项
| 注意事项 | 说明 |
|---|---|
| 同步问题 | 共享内存本身无同步机制,多进程同时读写需配合信号量 |
| 资源释放 | 写进程负责 shmctl(IPC_RMID) 删除,否则内存泄漏 |
| key 冲突 | 多个进程使用相同的 key 才能共享同一块内存 |
| 权限控制 | shmflg 中设置权限,如 0666 |
| 分离 vs 删除 | shmdt 只解除映射,shmctl(IPC_RMID) 才真正删除 |
十、总结
| 知识点 | 核心要点 |
|---|---|
| 共享内存特点 | 全双工、无数据拷贝、效率最高 |
| shmget | 创建/获取共享内存,返回 shmid |
| shmat | 映射到进程地址空间,返回指针 |
| shmdt | 分离共享内存(不删除) |
| shmctl | 删除/控制共享内存 |
| key 标识 | 多个进程用相同 key 共享同一块内存 |
| 同步问题 | 需自行处理(信号量),内核不负责 |
| 资源释放 | 必须调用 shmctl(IPC_RMID) 删除 |