线程 — Pthreads 基础与信号困局
线程 — Pthreads 基础与信号困局
这一章讲什么: 进程太重了——fork 复制整个地址空间, 进程间共享数据还得走 IPC。线程是轻量答案:同一个进程 里多条执行流,共享全部内存和文件,只有栈和寄存器是各自的。 本章教 Pthreads 的基本操作(创建/汇合/分离/取消),把第 14 章 的并发服务器改成线程版;最值钱的一节是信号与线程的纠缠—— 它会摧毁你对第 8 章的全部直觉,并逼出「专门线程 sigwait」 这个标准答案。
1. 顶层全景:进程 vs 线程
进程:资源的分装单位(地址空间、文件、PID)
线程:调度的执行单位;进程 = 「一个地址空间 + ≥1 条执行流」
│
共享(进程级):text/data/堆、全局变量、argv/envp、fd 表、
umask、CWD、信号处置、定时器、nice、资源上限
私有(线程级):线程 ID、运行栈+备用栈、栈指针+寄存器、
信号掩码、errno、调度属性、线程私有数据
│
Linux 实现:1:1 模型——每个用户线程 = 一个内核「轻量级进程」
(clone 出来的 task_struct);task_struct 里资源全是
指针:共享就指同一个结构,私有就各指一份(图 15-1)图说:fork 的「复制一切」和线程的「共享一切」是两个极端;
选哪个,看你要不要共享数据以及怕不怕竞争。 历史坐标:
1995 年 Open Group 定 Pthreads(IEEE 1003.1c);2005 年红帽的
NPTL 取代旧实现,性能大升1。编程要 -pthread(不是
-lpthread)2。
与进程原语的对照表(死记这张,API 就通了):
| 进程 | 线程 | 干什么 |
|---|---|---|
| fork() | pthread_create() | 造新执行流(传函数指针给新流跑) |
| exit() | pthread_exit() | 自己退出,留返回值 |
| waitpid() | pthread_join() | 等指定对象退出并收状态 |
| getpid() | pthread_self() | 拿自己的 ID |
| kill() | pthread_cancel() | 请求终止对方 |
2. 线程管理四件套
pthread_create(&tid, attr, start_fn, arg):start_fn 原型
void *fn(void *)——想传多个值?打一个结构体传指针(示范:
数组分段求和,每个线程一个 {tid,id,first,last,array,result}
结构,段界 (t*N)/K 保证段长差 ≤1)3。retval 不能指向
调用方的栈(线程死后栈作废),要全局/静态/堆4。
pthread_join(tid, &ret) = waitpid:等指定线程、收它的退出值 (void** 双重间接)5。四条 undefined 行为警告:两个线程同时 join 同一个、join 非 joinable 的、join 自己、永不 join → zombie 线程(攒多了系统造不出新线程)6。
退出纪律:main 里 return 或任何线程调 exit() → 杀全进程
所有线程;想让 main 先走、孩子们继续跑——main 调
pthread_exit()7。
detached(分离)线程:不需要 join 的就别 join——
pthread_attr_init + setdetachstate(PTHREAD_CREATE_DETACHED)
创建即分离;事后分离 pthread_detach(pthread_self()),不可逆8。
取消(pthread_cancel):只是请求;被请求者可以
setcancelstate(DISABLE) 拒收,或 type 设 ASYNCHRONOUS
(立即死)/ DEFERRED(默认:攒到下一个「取消点」函数再死)9。
栈大小:默认栈(常见 8MB,但「通常不超过 8192 字节」的
场景也不少)可能不够;可移植代码显式
pthread_attr_setstacksize;线程里要大缓冲区用堆,别在
栈上开大数组10。
3. 信号与线程:第 8 章直觉的坟墓
三个分离的事实,各自违反直觉11:
- 掩码是每线程的(
pthread_sigmask, 同 sigprocmask 原型);挂起信号也有每线程一份; - 处置(handler)是全进程一份的——线程 A 装的 handler, 信号落在线程 B 上,也是 A 的那个 handler 跑;
- 进程定向信号(kill/键盘 CTRL-C/定时器)发给谁? 内核随便挑一个没挡它的线程——不可预测。
pthread_signal_demo 用三个宏组合的实验确认了全部语义:
ps -L 能看到同 PID 下三个 TID;kill -2 打任意一个线程,
handler 没注册就全体阵亡(默认动作杀进程)12。
设计推论(原书引 Kerrisk 的标准处方):
多线程程序别做异步信号处理。 正解:① main 在创建任何 线程之前用 sigprocmask 挡掉所有要处理的信号(孩子们 继承这个掩码);② 造一个专门的线程调 sigwait() 同步等 这些信号——信号变成它队列里的事件,没有 handler、没有打断、 没有竞态。第 8 章那个 sigwait 在这才是真正归位。
4. 落点:并发服务器的线程版
第 14 章 upcase 服务器从 fork 改 pthread_create,三个
必须改的点13:
- msg 不能传局部变量地址——fork 复制地址空间,每个孩子 有自己的 msg 副本;线程共享内存,主循环里 msg 会被下一条 读覆盖,子线程拿到烂数据。每个请求要 malloc 一份 message, 线程用完自己 free;
- 收尸从 SIGCHLD handler 变成 pthread_join——主循环里 每 accept 一个就 join 一个(顺序服务,但不再阻塞监听? 不——join 会阻塞;真并发得 detached + 自清理);
- 信号处理按上一节处方重写:全部挡掉 + 专线程 sigwait。
5. 判断/边界/可带走
作者的判断: 「能不用全局共享就不用,传结构体指针」是 贯穿本章的纪律;信号处方直接引 Kerrisk 的权威做法1213。 边界: 调度属性(setschedparam 等)、spinlock、线程 attributes 全家、取消清理栈 pthread_cleanup_push 的细节都没 展开;线程安全的库函数清单留给第 16 章的语境。可带走的: ① 1:1 模型,每线程一个 LWP,task_struct 资源指针化; ② 共享/私有清单一组;③ create/join/detach/cancel 与 fork/wait 对照;④ main 退出用 pthread_exit;⑤ 多线程信号 = 同步专线程 sigwait,别异步。
6. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 线程历史与 NPTL | Chapter 15: Introduction to Threads | text/21-ch15-chapter-15-introduction-to-threads.txt:63(搜「Pthreads」) · :69(搜「NPTL」) |
| 共享/私有清单 | 同上 | :140(搜「Shared Resources」) · :180(搜「task_struct」) |
| 与进程原语对照表 | 同上 | :297(搜「Table 15-1」) |
| create/参数结构体 | 同上 | :346(搜「pthread_create」) · :509(搜「task_data」) |
| join 与四条警告 | 同上 | :407(搜「pthread_join」) · :432(搜「zombie thread」) |
| exit 警告 | 同上 | :389(搜「any thread」) |
| detach | 同上 | :584(搜「detached thread」) · :617(搜「pthread_detach」) |
| cancel 语义 | 同上 | :676(搜「request」) · :692(搜「pthread_setcancelstate」) |
| 栈大小 | 同上 | :716(搜「pthread_attr_setstacksize」) |
| 信号三事实 | 同上 | :820(搜「same prototype」) · :824(搜「process wide」) · :835(搜「any one of the threads」) |
| sigwait 处方 | 同上 | :970(搜「Kerrisk」) |
| 服务器线程化三坑 | 同上 | :1012(搜「corrupted data」) |
Footnotes
-
出处:「Chapter 15: Introduction to Threads」第 55–71 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:63,搜「Pthreads」) · :69(搜「NPTL」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 311–313 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:312,搜「-pthread」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 342–361 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:346,搜「pthread_create」) · :509(搜「task_data」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 377–380 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:377,搜「should not be located」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 399–423 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:407,搜「pthread_join」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 424–435 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:432,搜「zombie thread」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 389–397 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:389,搜「any thread」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 573–625 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:584,搜「detached thread」) · :617(搜「pthread_detach」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 668–705 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:676,搜「request」) · :692(搜「pthread_setcancelstate」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 707–761 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:716,搜「pthread_attr_setstacksize」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 817–843 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:820,搜「same prototype」) · :824(搜「process wide」) · :835(搜「any one of the threads」)。 ↩ -
出处:「Chapter 15: Introduction to Threads」第 844–976 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:956,搜「ps -L」) · :970(搜「Kerrisk」)。 ↩ ↩2 -
出处:「Chapter 15: Introduction to Threads」第 978–1050 段(
text/21-ch15-chapter-15-introduction-to-threads.txt:1012,搜「corrupted data」)。 ↩ ↩2