进程间通信(上)— 共享内存、信号量、消息队列
进程间通信(上)— 共享内存、信号量、消息队列
这一章讲什么: 进程之间不能互看内存(fork 给的也只是副本), 那它们怎么交换数据?答案分两大流派:共享内存——造一块大家 都能映射进来的内存,快,但「别打起来」归你管;数据传输—— 走内核管理的通道(消息队列、管道),慢,但通道自带排队和互斥。 本章讲 POSIX 版的三大件:共享内存、信号量、消息队列,并用 「生产者-消费者」这个经典问题把三者串成一个完整方案。 第 13 章接棒讲管道和 FIFO。
1. 顶层全景:一张图定终身
共享内存流派:造一块共享内存 → 两个进程各自 mmap 进来
│ 数据传输 = 普通的读写指令,内核不参与 → 最快
│ 代价:谁都可以随时读写 → 竞争条件 → 必须配同步(信号量)
▼
数据传输流派:消息队列(mq_send/mq_receive 整块收发、按优先级)
│ 内核当邮差 → 自带互斥、自带阻塞唤醒 → 不会打起来
│ 代价:每次传递都进出内核 → 比共享内存慢
▼
判据:传得频繁、量大、要极限性能 → 共享内存 + 信号量;
要省心、消息有边界、要优先级 → 消息队列。图说:两条路线的根本权衡是「快但要自己管秩序」vs「慢但秩序 内核管」。 另外每个设施都有两套 API:System V 版(1983 年 AT&T,接口老、通用但繁)和 POSIX 版(接口和文件 I/O 同构, 自洽好学)——本书教 POSIX,理由明说:更好设计1。
三件套一句话版:消息队列=信箱(投递/取走,取走即销毁, 整封取出);信号量=同步工具不是数据通道(一个不许为负的 整数,两个原子操作);共享内存=一块大家都能映射的内存2。
2. 共享内存:五步法与一个铁律
POSIX 的共享内存对象活在内存 tmpfs 里、名字出现在 /dev/shm/
(真能 ls、cat!),但「打开」用的竟是个文件描述符——
API 和文件同构,这是 POSIX IPC 的设计口味3。创建者五步走4:
① shm_open("/名字", O_CREAT|O_EXCL|O_RDWR, 0600) → fd(名字必须以 / 开头)
② ftruncate(fd, 字节数) ← 新对象长 0,先量尺寸
③ mmap(NULL, 字节数, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
← 把物理页映射进地址空间,返回首地址
④ 用!*(char*)shmp + offset 读写
⑤ munmap → close → shm_unlink(名字);映射全撤了才真正销毁加入者更省:shm_open 同名打开(不调 ftruncate)+ mmap
即用5。MAP_SHARED = 所有进程看到同一份更新;
MAP_PRIVATE = 谁写内核给谁单独拷一份(做内存映射文件时的
copy-on-write 语义)6。
铁律:共享内存里不许放指针
两个 demo 演示这条铁律的代价。demo1 用偏移(0x64、0xC8) 在固定位置写 "Hello"/"World",user 程序按同一偏移读——完美。 但注意各自打印的映射地址:0x7f75cd792000 vs 0x7fa439bb9000, 每次运行还都变(ASLR)——内核不保证两个进程把共享区映射到 同一个虚拟地址7。
demo2 故意违反铁律:往共享结构里塞了两个指针(ptr1 存 malloc 堆地址、ptr2 存结构内数组的地址),user 程序一解引用 直接段错误——ptr1 指向的是对方进程的堆(在这边是无效地址), ptr2 存的是对方地址空间里的地址8。原书加框总结:
共享内存里一律用偏移量(整数),不用地址。要指向区内的 数据就存「从区首到数据的字节数」,用的时候
(char*)shmp + offset现算。
3. 竞争条件:为什么需要信号量
共享内存里放 counter,counter = counter + 1 看着是一条
C 语句,编译出来是三条机器指令(取到寄存器 → 加一 → 存回)。
两个进程交错执行,从 5 出发各加一次,可能得到 69:
t0: P1 取 counter → 寄存器1=5
t1: P1 加一 → 寄存器1=6
t2: P2 取 counter → 寄存器2=5 ← P1 还没存回!
t3: P2 加一 → 寄存器2=6
t4: P1 存回 → counter=6
t5: P2 存回 → counter=6 ← 丢了 7同步方法的任务:保证「读-改-写」整段不可分。信号量 (Dijkstra 1963 年发明,源自荷兰语 probeer/increase—— P/V 操作)就是一个不许为负的整数 + 两个原子操作10:
sem_wait():S>0 就减一并通过;S==0 就阻塞排队;sem_post():加一;若有进程在排队就唤醒一个。
初值 1 = 二元信号量(互斥锁):只有一人能过,过了就把临界区 独占,出来 post 放人。初值 N = 计数信号量:N 人可同时过, 管「池子里还有几个资源」11。
主线例子:unnamedsem_demo——一个数从 1 改成 2,天塌了
程序:共享内存里放 { sem_t sem; size_t count; },父子各循环
1,000,000 次——子 count++、父 count--,每次前后夹
sem_wait/sem_post。信号量初值 1 时:
$ ./unnamedsem_demo
The final value of count, which should be 0, is 0. ← 永远正确把初值改成 2 再跑:
The final value of count, which should be 0, is -169841.
The final value of count, which should be 0, is 17422. ← 每次错得不一样图说:初值 2 = 两个人同时进临界区 = 信号量形同虚设,竞争
条件回来了——错值的随机性正是「谁撞上交错」的指纹。 无名
信号量要跨进程用,sem_init(&sem, 1, 初值) 的第二个参数
pshared 必须是 1,且信号量本身得放在共享内存里12。
命名信号量(sem_open,同 shm_open 的命名规则)只在一个场景
真有用:不相干、又没有共享内存的进程要同步(如协作访问
共享文件)——否则无名版够用13。namedsem_demo 用它治
父子 fprintf 交错:初值 1 时 grep -c 'ba|ab' = 0(行不混),
改 2 后 = 993(几乎行行混)14。
System V 信号量:为什么没人爱
System V 版没有现成的 wait/post——操作的是信号量集合、 增减量可以 >1、创建和初始化分两步(两步之间就有竞争!), 还得先学 IPC key 的生成。功能更通用,复杂度翻倍,POSIX 版 是「简单且自洽」的替代15。
4. 三信号量方案:生产者-消费者的完整解
问题:共享环形缓冲区,生产者往 rear 写、消费者从 front 读, 不能覆盖没读的、不能读没写的,还不能忙等(不能 while 轮询 计数器——白烧 CPU)。解 = 三个信号量16:
typedef struct {
sem_t filledbuf_count; /* 已填槽数,初值 0 ——「有货吗」 */
sem_t emptybuf_count; /* 空槽数,初值 BUF_SIZE ——「有空位吗」 */
sem_t mutex; /* 二元,初值 1 —— 保护缓冲区本体 */
int front, rear;
int buf[BUF_SIZE];
} sharedbuf;
生产者 add_next():
WAIT(emptybuf_count); ← 没空位就睡着,消费者 post 唤醒
WAIT(mutex); ← 独占缓冲区
buf[rear] = data; rear = (rear+1) % BUF_SIZE;
POST(mutex);
POST(filledbuf_count); ← 通知消费者:多了一件货
消费者 get_next(): 完全镜像(先 filled,后 mutex,结尾 post empty)图说:mutex 管「别同时碰数组」,filled/empty 管「有没有货/
有没有位」——计数信号量把「忙等轮询」变成了「睡-醒」,这是
信号量设计里最漂亮的一笔。 消费者创建共享区+三信号量并常
驻,生产者从 stdin 读数写入;seq 1 10000 | shm_producer +
重定向的 consumer,wc 验货恰 10,000 行17。
5. POSIX 消息队列:带优先级的信箱
和管道(第 13 章)的本质区别18:
- 整块收发:N 字节的消息取走时必取整封,不能只取 M<N;
- 按优先级出队(0..MQ_PRIO_MAX-1):最高优先级先出, 同级先来的先出——不是 FIFO,是优先队列;
- 描述符/打开描述对,与文件 fd/OFD 同构(第 4 章那张表换个 词就是它的结构)。
API:mq_open(同 shm_open 风格)、mq_send(des, ptr, len, prio)、mq_receive(...,&prio)(缓冲区 ≥ mq_msgsize)、
mq_getattr/mq_setattr(只能改 mq_maxmsg/mq_msgsize)、
mq_notify(sigevent 异步通知——队列来消息时给进程发信号,
handler 里再 mq_receive,不用死等)、mq_close/mq_unlink
(内核持久:不 unlink 重启前都在)19。
演示:发送方把字符串长度当优先级(mq_send(..., length)),
接收方先睡 20 秒让队列堆满,再逐个取——输出严格按长度从大到小,
优先级机制一目了然20。
6. 作者的判断与证据
有明证的:
- 两类方法的内核参与差异、共享内存的速度优势,是机制层面 的事实1。
- 指针陷阱的两个 demo(地址打印 0x7f75... vs 0x7fa4...;demo2 段错误)可复现,铁律总结在边框里78。
- 竞争条件的指令交错时序(t0–t5)与 counter 5→6 是教科书 级推演9。
- 信号量初值 1 vs 2 的对照运行(0 vs -169841)12。
作者的判断:
- 「教 POSIX 不教 System V,System V 信号量只给对比」—— 明说空间不够、POSIX 更易学115。
- 「命名信号量用处不多,共享文件该用文件锁」——点到为止的 工程判断(文件锁本书不展开)13。
- 生产者-消费者用「整数而非字符串」简化——教学纯度优先, 数据细节不干扰结构17。
7. 边界与局限
- System V IPC 只给了导览(shmget/semget/msgget 一套没教);
- 文件锁(fcntl 记录锁)多次提到但不展开,治「多进程写同一 文件」的官方手段缺席13;
- mq_notify 只讲了机制,信号 handler 里收消息的完整程序留给 读者19;
- 共享内存的同步原语选择不止信号量——mutex(第 16 章线程 里讲)放共享内存也能用,本书选信号量因为「进程间」语境更顺;
- 消息队列的内核持久既是特性也是坑:崩掉的程序留下的队列 要手动清理19。
8. 可带走的
主线例子,一行复述: 共享内存里父子各 100 万次 count±, 信号量初值 1 → 恒 0;初值 2 → 随机错(-169841/17422)—— 同步工具的值由初值决定,形同虚设比不用更糟。
- IPC 两大流派:共享内存(快、同步归你)vs 数据传输(内核管 秩序、慢);POSIX API 与文件 I/O 同构,比 System V 好学好用。
- 共享内存五步法:shm_open → ftruncate → mmap(MAP_SHARED) → 用 → munmap/close/shm_unlink;对象真身活在 /dev/shm。
- 共享内存里禁止指针,只许偏移!——各进程映射地址不同且 随机(ASLR),存进去的地址到那边是无效地址。
counter++是三条机器指令,交错执行就丢更新——竞争条件 的微观面目。- 信号量 = 非负整数 + 原子 wait(减,0 则睡)/post(加,唤 醒一个);初值 1 = 互斥,初值 N = N 个资源。
- 无名信号量跨进程:sem_init(pshared=1) 且信号量在共享内存 里;-lpthread。
- 三信号量生产者-消费者:filled(0)/empty(BUF_SIZE)/mutex(1) ——计数信号量把忙等变成睡-醒。
- 消息队列:整块收发、按优先级出队、mq_notify 异步通知、 内核持久要 mq_unlink。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 两大类方法与 POSIX/SysV 之分 | Chapter 12: Introduction to Interprocess Communication | text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:59(搜「Shared Memory Methods」) · :138(搜「System V is the name」) · :208(搜「shmget(2)」) |
| 三件套总览 | 同上 | :159(搜「Message queues」) · :177(搜「integer variable」) · :198(搜「memory segments」) |
| 共享内存五步法 | 同上 | :270(搜「first step」) · :276(搜「ftruncate」) · :288(搜「page table」) |
| shm_open/mmap 细节 | 同上 | :337(搜「int shm_open」) · :404(搜「void *mmap」) · :421(搜「MAP_SHARED」) |
| demo1 与 /dev/shm | 同上 | :483(搜「strcpy(shmp + offset1」) · :534(搜「8192」) |
| 指针陷阱两个 demo | 同上 | :561(搜「0x7f75cd792000」) · :633(搜「malloc(20)」) · :680(搜「segmentation fault」) · :696(搜「do not use pointers」) |
| 竞争条件时序 | 同上 | :722(搜「mov register1」) · :745(搜「final value」) |
| 信号量 P/V 与二元/计数 | 同上 | :761(搜「Dijkstra」) · :778(搜「Block calling」) · :802(搜「binary semaphore」) |
| System V 信号量对比 | 同上 | :826(搜「semctl」) · :836(搜「creation of a semaphore」) |
| named/unnamed 信号量 | 同上 | :856(搜「Named semaphore」) · :904(搜「sem_init」) |
| 主线例子初值对照 | 同上 | :983(搜「is 0」) · :989(搜「-169841」) |
| sem_open 语义表 | 同上 | :1013(搜「Table 12-1」) |
| namedsem_demo 对照 | 同上 | :1128(搜「2000 2000 16384000」) · :1140(搜「993」) |
| 三信号量方案 | 同上 | :1231(搜「filledbuf_count」) · :1248(搜「add_next」) · :1392(搜「seq 1 10000」) |
| 消息队列三特性 | 同上 | :1421(搜「stream of bytes」) · :1429(搜「priority queue」) · :1438(搜「queue descriptor」) |
| mq API 全家 | 同上 | :1493(搜「mq_open」) · :1526(搜「mq_send」) · :1456(搜「mq_notify」) · :1467(搜「kernel persistence」) |
| 优先级演示 | 同上 | :1562(搜「priority 11」) · :1597(搜「sleep(20)」) |
Footnotes
-
出处:「Chapter 12: Introduction to Interprocess Communication」第 55–110 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:59,搜「Shared Memory Methods」)与第 138–153 段的 POSIX/System V 对比(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:145,搜「System V IPC is the name」)。 ↩ ↩2 ↩3 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 159–224 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:166,搜「mailboxes」) · :177(搜「integer variable」) · :198(搜「memory segments」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 237–247 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:246,搜「tmpfs」) · :251(搜「shm_open()」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 268–297 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:270,搜「first step」) · :276(搜「ftruncate」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 317–325 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:322,搜「ftruncate」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 418–431 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:421,搜「MAP_SHARED」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 548–597 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:561,搜「0x7f75cd792000」) · :568(搜「different starting address」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 598–708 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:633,搜「malloc(20)」) · :680(搜「segmentation fault」) · :696(搜「do not use pointers」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 710–758 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:722,搜「mov register1」) · :745(搜「final value」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 760–804 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:761,搜「Dijkstra」) · :778(搜「Block calling」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 799–820 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:802,搜「binary semaphore」) · :807(搜「counting semaphore」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 890–995 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:951,搜「sem_init」) · :983(搜「is 0」) · :989(搜「-169841」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1050–1064 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1059,搜「unrelated processes」) · :1064(搜「file locks」)。 ↩ ↩2 ↩3 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1065–1144 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1128,搜「2000 2000 16384000」) · :1140(搜「993」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 821–850 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:832,搜「more complex」) · :836(搜「creation of a semaphore」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1145–1246 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1215,搜「counting semaphore」) · :1231(搜「filledbuf_count」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1276–1414 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1392,搜「seq 1 10000」) · :1318(搜「INITSEM」)。 ↩ ↩2 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1416–1468 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1421,搜「stream of bytes」) · :1429(搜「priority queue」)。 ↩ -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1443–1484 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1456,搜「mq_notify」) · :1467(搜「kernel persistence」)。 ↩ ↩2 ↩3 -
出处:「Chapter 12: Introduction to Interprocess Communication」第 1559–1674 段(
text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1562,搜「priority 11」) · :1597(搜「sleep(20)」)。 ↩