编程语言研究书库

进程间通信(上)— 共享内存、信号量、消息队列

进程间通信(上)— 共享内存、信号量、消息队列

这一章讲什么: 进程之间不能互看内存(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:

  1. 整块收发:N 字节的消息取走时必取整封,不能只取 M<N;
  2. 按优先级出队(0..MQ_PRIO_MAX-1):最高优先级先出, 同级先来的先出——不是 FIFO,是优先队列;
  3. 描述符/打开描述对,与文件 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)—— 同步工具的值由初值决定,形同虚设比不用更糟。

  1. IPC 两大流派:共享内存(快、同步归你)vs 数据传输(内核管 秩序、慢);POSIX API 与文件 I/O 同构,比 System V 好学好用。
  2. 共享内存五步法:shm_open → ftruncate → mmap(MAP_SHARED) → 用 → munmap/close/shm_unlink;对象真身活在 /dev/shm。
  3. 共享内存里禁止指针,只许偏移!——各进程映射地址不同且 随机(ASLR),存进去的地址到那边是无效地址。
  4. counter++ 是三条机器指令,交错执行就丢更新——竞争条件 的微观面目。
  5. 信号量 = 非负整数 + 原子 wait(减,0 则睡)/post(加,唤 醒一个);初值 1 = 互斥,初值 N = N 个资源。
  6. 无名信号量跨进程:sem_init(pshared=1) 且信号量在共享内存 里;-lpthread。
  7. 三信号量生产者-消费者:filled(0)/empty(BUF_SIZE)/mutex(1) ——计数信号量把忙等变成睡-醒。
  8. 消息队列:整块收发、按优先级出队、mq_notify 异步通知、 内核持久要 mq_unlink。

9. 原文地图

主题原书章原文位置
两大类方法与 POSIX/SysV 之分Chapter 12: Introduction to Interprocess Communicationtext/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

  1. 出处:「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

  2. 出处:「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」)。 ↩

  3. 出处:「Chapter 12: Introduction to Interprocess Communication」第 237–247 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:246,搜「tmpfs」) · :251(搜「shm_open()」)。 ↩

  4. 出处:「Chapter 12: Introduction to Interprocess Communication」第 268–297 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:270,搜「first step」) · :276(搜「ftruncate」)。 ↩

  5. 出处:「Chapter 12: Introduction to Interprocess Communication」第 317–325 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:322,搜「ftruncate」)。 ↩

  6. 出处:「Chapter 12: Introduction to Interprocess Communication」第 418–431 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:421,搜「MAP_SHARED」)。 ↩

  7. 出处:「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

  8. 出处:「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

  9. 出处:「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

  10. 出处:「Chapter 12: Introduction to Interprocess Communication」第 760–804 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:761,搜「Dijkstra」) · :778(搜「Block calling」)。 ↩

  11. 出处:「Chapter 12: Introduction to Interprocess Communication」第 799–820 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:802,搜「binary semaphore」) · :807(搜「counting semaphore」)。 ↩

  12. 出处:「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

  13. 出处:「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

  14. 出处:「Chapter 12: Introduction to Interprocess Communication」第 1065–1144 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1128,搜「2000 2000 16384000」) · :1140(搜「993」)。 ↩

  15. 出处:「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

  16. 出处:「Chapter 12: Introduction to Interprocess Communication」第 1145–1246 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1215,搜「counting semaphore」) · :1231(搜「filledbuf_count」)。 ↩

  17. 出处:「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

  18. 出处:「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」)。 ↩

  19. 出处:「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

  20. 出处:「Chapter 12: Introduction to Interprocess Communication」第 1559–1674 段(text/18-ch12-chapter-12-introduction-to-interprocess-communic.txt:1562,搜「priority 11」) · :1597(搜「sleep(20)」)。 ↩

On this page