嵌入式C/C++进阶-进程/线程通信《共享内存,一文搞定》
目录
1 简介
1.1 本质
共享内存是一种进程间通信(IPC)机制,允许多个进程直接访问同一块内存区域,从而实现高速数据交换。
✅ 一句话本质: 让不同进程“共享同一片内存”,从而跳过内核缓冲区,数据不需要多次拷贝。
1.2 原理
-
操作系统在内核中分配一块共享内存段。
-
进程通过系统调用将这块内存映射(mmap/attach)到自己的地址空间。
-
多个进程可同时读写这块区域。
-
因为没有内核中转,所以速度最快。
-
但必须使用**同步机制(如信号量、互斥锁)**来防止读写冲突。
1.3 实现方式
两种主流标准👇
| 标准 | 接口 | 特点 |
|---|---|---|
| System V | shmget, shmat, shmdt, shmctl |
老标准,兼容性好,常用于Linux内核传统接口 |
| POSIX | shm_open, mmap, shm_unlink |
新标准,文件描述符式操作,更现代、可与文件系统结合 |
1.4 特点总结
-
⚡ 速度最快 的进程间通信方式(零拷贝)
-
🚫 无内核缓冲区(直接读写)
-
🧩 需要同步机制 保证一致性
-
📦 可配合信号量、互斥锁等实现安全通信
1.5 同步机制
共享内存只是提供一块可以被多个进程同时访问的内存区域。多个进程同时读写可能导致数据竞争,因此需要同步机制。
| 同步方式 | 说明 | 适用场景 |
|---|---|---|
| System V 信号量(semget / semop / semctl) | 共享内存 + 信号量 是最经典组合,用信号量实现互斥锁或同步信号。 | 精细控制访问顺序,如“生产者-消费者”模型。 |
| POSIX 信号量(sem_open / sem_wait / sem_post) | 跨进程、接口更现代、更方便。 | 新项目建议使用。 |
| 文件锁(flock / fcntl) | 利用文件描述符锁来保护共享区数据访问。 | 简单粗暴、但性能略低。 |
| 自旋锁 / 互斥变量(pthread_mutex_t) | 若共享内存用于多线程(同进程或共享 pthread mutex 的跨进程模式)。 | 多线程场景或轻量同步。 |
2 C语言实现
2.1 System V程序实现
2.1.1 接口讲解
1️⃣ key_t ftok(const char *pathname, int proj_id)
用途:
生成唯一的 IPC 键(key_t),供 shmget / msgget / semget 等使用。
参数:
-
pathname:必须指向已存在的文件(一般是/tmp目录下的文件)。 -
proj_id:1–255 的小整数,用来区分不同 IPC 对象。
返回值:
-
成功 →
key_t(非负整数) -
失败 →
(key_t)-1,并设置errno
常见错误:
| errno | 说明 |
|---|---|
ENOENT |
指定的 pathname 不存在。 |
EACCES |
没有访问 pathname 的权限。 |
2️⃣ int shmget(key_t key, size_t size, int shmflg)
用途:
创建或获取一个共享内存段,返回标识符 shmid。
参数:
-
key:由ftok或IPC_PRIVATE获得,也可以直接指定key,在很多嵌入式系统中只要双方(创建端与访问端)约定同一个 key 值即可共享同一段内存。 -
size:共享内存段大小(字节)。首次创建时有效。 -
shmflg:权限和标志的组合。-
权限位:
-
0600 —— 只有创建者(owner)能读写
0660 —— 创建者和同组用户可读写
0666 —— 所有人都能读写
-
标志:
-
IPC_CREAT→ 如果指定 key 对应的段不存在,则创建一个新段;否则返回已有段的shmid。 -
IPC_EXCL→ 若已存在则失败(与IPC_CREAT组合使用)。
-
返回值:
-
成功 → 非负整数
shmid,是段标识后续可以使用。 -
失败 →
-1并设置errno。
常见错误:
| errno | 原因与说明 |
|---|---|
EACCES |
指定 key 对应的段存在,但调用进程没有足够权限。 |
EEXIST |
当同时使用 IPC_CREAT | IPC_EXCL 且对象已存在时发生 |
EINVAL |
传入的 size 太小或太大(超过系统上限 SHMMAX),或段已存在且小于当前的传入size。 |
ENOENT |
指定 key 的段不存在,且未指定 IPC_CREAT。 |
ENOMEM |
系统共享内存资源不足,无法分配新的段。 |
3️⃣ void *shmat(int shmid, const void *shmaddr, int shmflg)
用途:
将共享内存段映射到当前进程地址空间。
参数:
-
shmid:由shmget返回的段标识符。 -
shmaddr:希望映射到的地址,一般传NULL让内核自动选择。 -
shmflg:映射模式:-
0→ 可读写; -
SHM_RDONLY→ 只读; -
可附加
SHM_RND(地址向下取整至 SHMLBA 边界)。
-
返回值:
-
成功 → 有效指针。
-
失败 →
(void*)-1并设置errno。
常见错误:
| errno | 说明 |
|---|---|
EACCES |
没有对应的访问权限(例如只读进程尝试写)。 |
EINVAL |
无效的 shmid 或 shmaddr 不满足对齐要求。 |
EIDRM |
对应段已被标记删除(IPC_RMID)。 |
ENOMEM |
内核无法在进程地址空间中分配映射。 |
4️⃣ int shmdt(const void *shmaddr)
用途:
解除当前进程对共享内存段的映射(detach)。
参数:
-
shmaddr:由shmat返回的地址。
返回值:
-
成功 → 0
-
失败 →
-1并设置errno
常见错误:
| errno | 说明 |
|---|---|
EINVAL |
提供的地址不是任何已映射的共享内存起始地址。 |
5️⃣ int shmctl(int shmid, int cmd, struct shmid_ds *buf)
用途:
控制或查询共享内存段的属性,或删除它。
常见 cmd 值:
| cmd | 用途 |
|---|---|
IPC_STAT |
读取共享内存的属性(放入 buf)。 |
IPC_SET |
修改共享内存属性(从 buf 读取)。需有权限。 |
IPC_RMID |
标记删除。最后一个 detach 后真正销毁。 |
返回值:
-
成功 → 0
-
失败 →
-1并设置errno
常见错误:
| errno | 说明 |
|---|---|
EACCES |
调用进程无权限执行所请求的操作。 |
EINVAL |
无效的 shmid 或 cmd 值。 |
EIDRM |
段已被标记删除。 |
EPERM |
非段所有者或管理员尝试 IPC_RMID / IPC_SET。 |
2.1.2 使用示例
sv_writer.c
// sv_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_PATH "/tmp" // ftok 要求存在的路径
#define SHM_PROJ_ID 'S'
#define SHM_SIZE 4096
#define SEM_NAME "/sv_sem"
struct shm_area {
int ready; // 0 = not ready, 1 = ready
char msg[SHM_SIZE - sizeof(int)];
};
int main(void){
key_t key = ftok(SHM_PATH, SHM_PROJ_ID);
if (key == (key_t)-1){ perror("ftok"); return 1; }
int shmid = shmget(key, sizeof(struct shm_area), IPC_CREAT | 0666);
if (shmid == -1){ perror("shmget"); return 1; }
struct shm_area *p = (struct shm_area *) shmat(shmid, NULL, 0);
if (p == (void *)-1){ perror("shmat"); return 1; }
// 创建/打开命名信号量,初始值 0(等待 reader)
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
if (sem == SEM_FAILED){ perror("sem_open"); shmdt(p); return 1; }
// 写数据
p->ready = 0; // 标记未就绪
const char *text = "Hello from System V shared memory!";
strncpy(p->msg, text, sizeof(p->msg)-1);
p->msg[sizeof(p->msg)-1] = '\0';
// 内存写完后设置 ready 并通知 reader
__sync_synchronize(); // memory barrier
p->ready = 1;
if (sem_post(sem) == -1) perror("sem_post");
printf("writer: wrote message and posted semaphore\n");
// 等一会儿再清理(让 reader 有时间读)——生产环境请使用更健壮的同步/协议
sleep(2);
// detach 并请求删除共享内存(IPC_RMID)
if (shmdt(p) == -1) perror("shmdt");
if (shmctl(shmid, IPC_RMID, NULL) == -1) perror("shmctl(IPC_RMID)");
// 关闭并 unlink 信号量
if (sem_close(sem) == -1) perror("sem_close");
if (sem_unlink(SEM_NAME) == -1) perror("sem_unlink");
return 0;
}
sv_reader.c
// sv_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_PATH "/tmp"
#define SHM_PROJ_ID 'S'
#define SHM_SIZE 4096
#define SEM_NAME "/sv_sem"
struct shm_area {
int ready;
char msg[SHM_SIZE - sizeof(int)];
};
int main(void){
key_t key = ftok(SHM_PATH, SHM_PROJ_ID);
if (key == (key_t)-1){ perror("ftok"); return 1; }
int shmid = shmget(key, sizeof(struct shm_area), 0666);
if (shmid == -1){ perror("shmget"); return 1; }
struct shm_area *p = (struct shm_area *) shmat(shmid, NULL, SHM_RDONLY);
if (p == (void *)-1){ perror("shmat"); return 1; }
sem_t *sem = sem_open(SEM_NAME, 0);
if (sem == SEM_FAILED){ perror("sem_open"); shmdt((void*)p); return 1; }
// 等待 writer 发信号
if (sem_wait(sem) == -1){ perror("sem_wait"); }
// 确认 ready
while (!p->ready) { /* small spin */ usleep(1000); }
printf("reader: got message: \"%s\"\n", p->msg);
if (shmdt((void*)p) == -1) perror("shmdt");
if (sem_close(sem) == -1) perror("sem_close");
return 0;
}
运行顺序:先启动 sv_reader(它会阻塞等待信号量),再 sv_writer(写并 sem_post)。也可以先 writer,reader 也能打开后等信号。
2.2 POSIX 程序实现
2.2.1接口说明
-
int shm_open(const char *name, int oflag, mode_t mode);-
用途:创建或打开一个 POSIX 共享内存对象,返回文件描述符(像
open)。 -
参数:
-
name:以'/'开头的名字(例如"/my_shm"),在命名空间中全局可见。 -
oflag:O_CREAT、O_EXCL、O_RDWR、O_RDONLY等。 -
mode:权限位(如0666),仅在O_CREAT时生效。
-
-
返回:fd >= 0 成功;失败返回 -1(
errno)。
-
-
int ftruncate(int fd, off_t length);-
用途:设置共享内存对象的大小(创建后必须设置大小才能
mmap)。 -
返回:0 成功;-1 失败。
-
-
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);-
用途:把共享内存对象映射到进程地址空间。
-
参数:
-
prot:PROT_READ/PROT_WRITE。 -
flags:MAP_SHARED(必须)等。
-
-
返回:指针;失败返回
MAP_FAILED(并设置errno)。
-
-
int munmap(void *addr, size_t length);-
解除映射,返回 0 成功,-1 失败。
-
-
int close(int fd);-
关闭文件描述符(不删除对象)。
-
-
int shm_unlink(const char *name);-
删除 POSIX 共享内存对象(类似
unlink),返回 0 成功,-1 失败。删除后仍可被已经打开的 fd / mmap 使用,直到全部 close/unmap 为止。
-
错误 errno:EEXIST, ENOENT, ENOMEM, EINVAL 等。
2.2.2 使用示例
posix_writer.c
// posix_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // O_CREAT...
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>
#include <errno.h>
#define SHM_NAME "/my_posix_shm"
#define SEM_NAME "/posix_sem"
#define SHM_SIZE 4096
struct shm_area {
int ready;
char msg[SHM_SIZE - sizeof(int)];
};
int main(void){
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd == -1){ perror("shm_open"); return 1; }
if (ftruncate(fd, sizeof(struct shm_area)) == -1){ perror("ftruncate"); close(fd); return 1; }
struct shm_area *p = mmap(NULL, sizeof(struct shm_area), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED){ perror("mmap"); close(fd); return 1; }
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
if (sem == SEM_FAILED){ perror("sem_open"); munmap(p, sizeof(*p)); close(fd); return 1; }
// 写入
p->ready = 0;
const char *text = "Hello from POSIX shared memory!";
strncpy(p->msg, text, sizeof(p->msg)-1);
p->msg[sizeof(p->msg)-1] = '\0';
__sync_synchronize();
p->ready = 1;
if (sem_post(sem) == -1) perror("sem_post");
printf("posix writer: wrote and posted\n");
sleep(2);
// 清理
if (munmap(p, sizeof(*p)) == -1) perror("munmap");
close(fd);
if (shm_unlink(SHM_NAME) == -1) perror("shm_unlink"); // 若想保留以供后续使用,可注释
if (sem_close(sem) == -1) perror("sem_close");
if (sem_unlink(SEM_NAME) == -1) perror("sem_unlink");
return 0;
}
posix_reader.c
// posix_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>
#define SHM_NAME "/my_posix_shm"
#define SEM_NAME "/posix_sem"
#define SHM_SIZE 4096
struct shm_area {
int ready;
char msg[SHM_SIZE - sizeof(int)];
};
int main(void){
int fd = shm_open(SHM_NAME, O_RDONLY, 0666);
if (fd == -1){ perror("shm_open"); return 1; }
struct shm_area *p = mmap(NULL, sizeof(struct shm_area), PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED){ perror("mmap"); close(fd); return 1; }
sem_t *sem = sem_open(SEM_NAME, 0);
if (sem == SEM_FAILED){ perror("sem_open"); munmap((void*)p, sizeof(*p)); close(fd); return 1; }
if (sem_wait(sem) == -1) perror("sem_wait");
while (!p->ready) usleep(1000);
printf("posix reader: got message: \"%s\"\n", p->msg);
munmap((void*)p, sizeof(*p));
close(fd);
sem_close(sem);
return 0;
}
运行顺序同上:通常先运行 reader(阻塞),再 writer。也可先 writer,reader 启动后会立即读取(取决于是否 shm_unlink 被调用等)。
3 面试常见问题
1. 什么是共享内存?它和其他进程通信方式(管道、消息队列、socket)有什么区别?
共享内存是一种由操作系统内核维护的进程间通信机制,允许多个进程直接访问同一块物理内存区域。
与管道、消息队列、socket 的区别是:共享内存无需数据拷贝,是所有 IPC 中速度最快的方式;但它本身不提供同步,需要程序员自己控制访问顺序。
2. System V 共享内存的创建和使用流程是什么?涉及哪些系统调用?
典型流程如下:
-
用
ftok()生成 key(或自定义 key、IPC_PRIVATE)。 -
调用
shmget()创建或打开共享内存段。 -
使用
shmat()将该段映射到本进程地址空间。 -
读写共享内存内容。
-
用
shmdt()解除映射。 -
最后用
shmctl(shmid, IPC_RMID, NULL)删除共享内存段。
3. shmget()、shmat()、shmdt()、shmctl() 的作用和参数说明。
-
shmget(key, size, shmflg):创建或打开共享内存段。key 为标识,size 为大小,shmflg 为权限和标志(如IPC_CREAT|0666)。返回 shmid。 -
shmat(shmid, addr, flag):将共享内存映射到当前进程地址空间。addr 一般传 NULL,flag 可为 0 或 SHM_RDONLY。返回映射指针。 -
shmdt(addr):解除映射。成功返回 0。 -
shmctl(shmid, cmd, buf):控制命令,如 IPC_RMID 删除、IPC_STAT 获取信息。
4. key_t key 是什么?IPC_PRIVATE 的作用是什么?
key 是共享内存的唯一标识,同一 key 表示同一段共享内存。IPC_PRIVATE 是一个特殊常量(通常值为 0),表示“私有段”,仅能在父子进程间共享,系统不会为其匹配已存在的段。
5. 如何通过 ftok() 生成共享内存的 key?如果路径或 proj_id 不同会怎样?key_t ftok(const char *pathname, int proj_id) 根据文件 inode 和 proj_id 组合生成 key。
如果路径或 proj_id 任意一个不同,生成的 key 通常也不同。路径文件必须存在。
6. 共享内存创建后,如果再次调用 shmget() 指定相同 key 但不同 size,会返回什么?
如果共享内存已经存在,shmget() 会忽略传入的 size,只要权限允许,仍返回原来的 shmid。
但若新传入的 size 小于已存在段的大小,或大于系统上限,会返回 -1,并设置 errno=EINVAL。
7. 如何判断共享内存是否已经存在?
调用 shmget(key, size, 0666),若返回 -1 且 errno=ENOENT 表示不存在;
若成功返回 shmid,则表示已存在。也可加上 IPC_CREAT|IPC_EXCL,若已存在会返回 -1 并置 errno=EEXIST。
8. shmat() 返回的地址是怎么确定的?能否指定固定地址?
默认传入 NULL,让系统自动选择合适的虚拟地址;
也可以传入一个固定地址(不推荐),系统会尝试在该处映射,但若该地址被占用,会失败。
9. 为什么共享内存是最快的进程间通信方式?
因为通信双方访问的是同一块物理内存,没有数据在内核与用户空间之间复制。
其他 IPC(管道、消息队列)都要在内核中转,产生额外的复制与上下文切换。
10. 为什么共享内存需要同步机制?
因为多个进程可能同时修改同一内存区域,若不控制访问顺序,会产生数据竞争、写覆盖或读取中间态数据。
11. System V 共享内存是否提供同步机制?如果没有,如何实现同步?
System V 共享内存本身不带同步功能。
可用以下方式实现同步:
-
System V 信号量(semget、semop)。
-
POSIX 信号量(sem_init、sem_wait、sem_post)。
-
互斥锁放在共享内存中并设置属性
PTHREAD_PROCESS_SHARED。
12. 在多进程读写共享内存时,如何避免竞争条件?
可通过信号量、互斥锁或原子操作控制访问顺序。
例如:写前加锁、写后解锁,读前也加锁保证不会读取到未完成的数据。
13. 一个进程写、多个进程读的场景是否一定需要加锁?为什么?
若写入是原子操作(如单个 4 字节整数更新),读方可不加锁。
但若写入结构体或连续字段,更新不是原子的,读者可能看到部分更新的数据,因此仍需同步或使用“版本号两次读一致”方案。
14. 写入共享内存时可能出现“中间态”吗?
会。如果一次写入包含多个成员字段,CPU 可能分多次写入内存,读进程可能读到部分旧数据、部分新数据,这就是中间态。
15. 对于结构体写入共享内存时,哪些操作是原子的?
单次写入等于或小于 CPU 原子宽度的基本类型(通常 ≤8 字节的整型或指针)是原子的。
结构体整体写入或浮点、字符串等多字段更新不是原子的。
16. 如何保证跨进程的数据一致性?
常见方法:
-
加锁保护写入。
-
使用版本号机制:写前自增序号为奇数,写完改为偶数,读方前后两次序号相等即认为数据一致。
-
使用原子变量或内存屏障控制读写顺序。
17. 如果进程异常退出,共享内存段会怎样?
共享内存仍然存在,因为它由内核维护。
只有调用 shmctl(shmid, IPC_RMID, NULL) 才会标记删除,最后一个 detach 的进程退出后才真正释放。
18. 如何删除共享内存段?什么时候应该删除?
用 shmctl(shmid, IPC_RMID, NULL) 删除。
一般在通信双方结束时删除;若可能异常退出,可在启动时检测并清理“孤儿”段。
19. 如何查看系统当前的共享内存段(命令行工具)?
使用 ipcs -m 查看,ipcrm -m <shmid> 删除指定共享内存段。
20. System V 与 POSIX 共享内存的区别?(API、命名、生命周期等)
System V:通过 key 标识,用 shmget/shmat/shmdt/shmctl;
POSIX:通过名字标识(如 /shm_name),用 shm_open/mmap/munmap/shm_unlink;
System V 段存在于内核直到删除,POSIX 段通常与文件描述符关联,控制更方便。
21. System V 共享内存的权限控制是怎样的?
权限位与文件类似:
-
0400所有者可读 -
0200所有者可写 -
0040组可读 -
0020组可写 -
0004其他可读 -
0002其他可写
一般使用0666或0600。
22. 共享内存的最大大小受哪些系统参数限制?
主要受以下内核参数影响(可通过 cat /proc/sys/kernel/... 查看):
-
kernel.shmmax:单段最大字节数。 -
kernel.shmall:系统允许分配的总页数。 -
kernel.shmmni:系统可创建的段数量。
23. 在嵌入式系统中使用共享内存时有哪些注意事项?
内存资源有限,应控制段大小;
避免频繁创建/销毁;
在 ARM 等弱内存序平台需显式内存屏障;
同步可用轻量信号量或简易锁;
若无 MMU,要确保共享区映射在同一物理地址上。
24. 如何在多线程环境中使用共享内存?线程间和进程间共享的区别是什么?
线程间共享同一地址空间,不需要共享内存;
进程间地址空间独立,必须通过共享内存实现。
多线程访问共享内存同样需要锁,只是锁可以是普通线程锁,不需要 PTHREAD_PROCESS_SHARED。
25. shmctl(…, IPC_RMID, …) 删除共享内存后,已有进程还能访问吗?
能。删除标记只是延迟生效。
只要进程仍然 attach,该段不会立即释放;最后一个 detach 或进程退出后,内核才真正销毁。
26. 如何实现共享内存的环形缓冲区或生产者–消费者模型?
在共享内存中建立循环队列结构,包含写指针、读指针、缓冲区。
写进程更新写指针,读进程更新读指针;
使用信号量或互斥锁保证指针更新的原子性。
27. 共享内存是否可以在不同用户或不同权限进程间使用?
可以,只要权限(mode bits)允许或属主、属组匹配。
系统管理员也可通过 ipcrm 管理不同用户的段。
28. 如何调试共享内存中的数据内容?
可使用 ipcs -m 查看段号;
用调试器(gdb)或小程序 shmat() 附加到段,直接打印结构体;
或在 /dev/shm/(POSIX)下直接用 hexdump/od 查看内容。
29. 当程序崩溃或异常退出时,如何防止遗留“孤儿”共享内存段?
在程序启动时扫描 ipcs -m 并删除不再使用的段;
也可以注册 atexit() 或信号处理函数,在进程结束时自动调用 shmctl(…, IPC_RMID, NULL)。
30. 如果两个进程使用不同的结构体版本(字段不一致),会有什么后果?
因为共享内存只是原始字节区,结构体布局不同会导致读写错位,造成数据错误或崩溃。
必须保持两端结构体定义、字节对齐、字节序完全一致;推荐在结构体中添加版本号字段以检测兼容性。
更多推荐
所有评论(0)