第 33 章 线程:更多细节 (Threads: Further Details)
核心结论
-
线程栈大小默认 2 MB (x86-32),主线程除外;可调小节省 VA 空间(创建 ~1500 个线程)或调大避免栈溢出;最小值 sysconf(_SC_THREAD_STACK_MIN) = 16384 (NPTL)。
-
信号与线程交互复杂:信号 disposition 进程级;signal mask 线程级;signal action 进程级(未处理 signal 终止/停止所有线程);thread-directed 信号(硬件异常/SIGPIPE/pthread_kill)vs process-directed 信号(kill/sigqueue/终端)。
-
pthread_sigmask() 替代 sigprocmask():多线程程序不能用 sigprocmask——SUSv3 未规定;pthread_sigmask 用法同 sigprocmask。
-
pthread_kill/pthread_sigqueue 发信号给线程:pthread_kill = tgkill(tgid, tid, sig);pthread_sigqueue 带数据(rt_tgsigqueueinfo 系统调用,2.6.31+)。
-
async 信号推荐模式:所有线程 block async 信号 + 单个专用线程用 sigwait/sigwaitinfo/sigtimedwait 同步接收;handler 内可调非 async-signal-safe 函数(含 Pthreads)。
-
fork/exec/exit 与线程:fork 仅复制调用线程(其他线程消失、TSD destructor 与 cleanup 不执行);fork 后立即 exec 是推荐模式;exec 终止所有非 exec 调用线程;exit()/main return 终止所有线程且不执行 destructor/cleanup。
-
LinuxThreads vs NPTL:LinuxThreads 不用 CLONE_THREAD(每线程独立 PID)——大量 SUSv3 不一致;NPTL 用 CLONE_THREAD(同 TGID)——更符合 SUSv3;现代 Linux 用 NPTL。
|
本章主旨
本章是「线程」四章的最后一章——深入线程与 UNIX 传统 API(信号、fork/exit)的交互,以及 Linux 两种 Pthreads 实现(LinuxThreads vs NPTL)的对比。读者应掌握:线程栈大小与 RLIMIT_STACK 关系;信号如何映射到线程模型(thread-directed vs process-directed);pthread_sigmask vs sigprocmask;推荐的 async 信号处理模式(专用线程 + sigwait);fork/exec/exit 与线程的交互(特别是 pthread_atfork);线程实现模型(M:1/1:1/M:N);LinuxThreads 大量 SUSv3 不一致 vs NPTL 的修复;如何查询当前线程实现。理解「线程与信号天生不兼容」是设计多线程程序的核心原则——尽量避免在多线程程序中使用信号。 |
一、核心概念
本章围绕 6 个核心概念展开:从线程栈与 VA 空间限制,到信号与线程的复杂映射、pthread_sigmask/pthread_kill、async 信号处理模式、fork/exec/exit 与线程交互、线程实现模型。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
线程栈大小与 RLIMIT_STACK |
Linux/x86-32 默认非主线程栈 2 MB;最小 sysconf(_SC_THREAD_STACK_MIN) = 16384 (NPTL);NPTL 用 RLIMIT_STACK 作为默认栈大小(运行前 ulimit –s 设置;setrlimit 太晚——NPTL 在 main 前初始化时确定)。 |
§33.1;pthread_attr_setstacksize 显式设置;x86-32 用户 VA 3 GB → 默认 2 MB 栈可创建 ~1500 线程;需大量线程时调小栈。 |
信号 → 线程映射 |
signal action 进程级;signal disposition 进程级(共享);signal mask 线程级;signal pending 既有 process-wide 也有 per-thread;硬件异常/SIGPIPE/pthread_kill 是 thread-directed;kill/sigqueue/终端信号是 process-directed。 |
§33.2.1;process-directed 信号由内核任意选一个不 block 该信号的线程处理;pthread_mutex_lock 被信号 handler 中断时自动重启;pthread_cond_wait 自动重启或返回 0(spurious wake-up);sigaltstack 线程级且不继承。 |
pthread_sigmask / pthread_kill / pthread_sigqueue |
pthread_sigmask = 线程级 sigprocmask(多线程程序唯一合法方式);pthread_kill(target, sig) = tgkill(tgid, tid, sig) 发信号给特定线程;pthread_sigqueue 带 sigqueue 数据(glibc 2.11+ + rt_tgsigqueueinfo 系统调用,Linux 2.6.31+)。 |
§33.2.2-§33.2.3;sigwait 等 sigwaitinfo 同步接收信号;多线程等 sigwait 同信号时只一个能接收。 |
async 信号推荐模式 |
所有线程 block async 信号 + 专用线程用 sigwait/sigwaitinfo 同步接收;主线程先 block 后创建线程(继承 mask);专用线程可安全调非 async-signal-safe 函数(含 Pthreads)+ 修改共享变量(需 mutex)+ signal cond。 |
§33.2.4;新线程继承创建者 mask;这是「避免在多线程程序用 signal handler」的最佳实践。 |
fork/exec/exit 与线程 |
fork:仅复制调用线程,其他线程消失,TSD destructor + cleanup handler 不执行,mutex/cond 状态保留(导致死锁/不一致);exec:所有线程终止,不执行 destructor/cleanup;exit() 或 main return:所有线程立即消失,不执行 destructor/cleanup。 |
§33.3;推荐:fork 后立即 exec;pthread_atfork(prepare, parent, child) 注册 fork handler;vfork 行为差异(NPTL 不调 fork handler,LinuxThreads 调)。 |
线程实现模型 (M:1/1:1/M:N) 与 Linux 选型 |
M:1 用户级线程(fast 但 blocking sys call 阻塞所有);1:1 内核线程(slow per-op 但真并行);M:N 两级调度(复杂);Linux 选 1:1(NPTL);LinuxThreads 用 clone(VM|FILES|FS|SIGHAND)——无 CLONE_THREAD(每线程独立 PID);NPTL 加 CLONE_THREAD|SETTLS|PARENT_SETTID|CHILD_CLEARTID|SYSVSEM。 |
§33.4-§33.5;getconf GNU_LIBPTHREAD_VERSION 查询;LD_ASSUME_KERNEL=2.2.5 强制用 LinuxThreads。 |
二、详细笔记
33.1 线程栈
What:每个线程有固定大小栈(创建时确定);Linux/x86-32 默认 2 MB(非主线程),最小 16384 字节。
Why:栈大小决定线程数上限(VA 空间限制);栈太小 → 栈溢出;栈太大 → VA 浪费。
How:
栈大小控制(§33.1):
-
pthread_attr_setstacksize(attr, size):设置线程属性栈大小。
-
pthread_attr_setstack(attr, addr, size):同时控制栈地址(牺牲可移植性)。
-
sysconf(_SC_THREAD_STACK_MIN):查询架构最小栈大小。
NPTL 与 RLIMIT_STACK(§33.1):
-
NPTL 在运行时初始化时(main 之前)确定默认栈大小。
-
若 RLIMIT_STACK ≠ unlimited,则用 RLIMIT_STACK 作为默认栈大小。
-
必须在运行前用
ulimit –s设置;setrlimit 太晚。
线程数估算(§33.1):
-
x86-32 用户 VA = 3 GB。
-
默认栈 2 MB → 最多 ~1500 线程(还要扣 text/data/shared libs/mmap)。
-
减小栈大小(如 256 KB)→ 线程数翻 8 倍。
-
增大栈大小(如 16 MB)→ 线程数减 8 倍(但每线程栈深度更大)。
When:写大量线程的程序(thread pool、高并发服务器)——pthread_attr_setstacksize 显式设置;栈深度需求大(递归、深嵌套)——增大栈。
Example:
// 摘自《The Linux Programming Interface》第 33 章
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 1024 * 1024); /* 1 MB 栈 */
pthread_create(&thr, &attr, func, arg);
pthread_attr_destroy(&attr);
33.2 信号与线程
What:UNIX 信号模型早于 Pthreads 几十年;线程与信号交互复杂——某些 signal 概念是进程级,另一些是线程级。
Why:理解信号在多线程程序中的行为是写正确多线程信号处理程序的前提;推荐在多线程程序中尽量避免信号。
How:
进程级 vs 线程级(§33.2.1):
-
signal action 进程级:未处理的 stop/terminate 信号 → 整个进程停止/终止。
-
signal disposition 进程级:所有线程共享;A 线程设 SIGINT handler → B 线程收到 SIGINT 也调 handler。
-
signal mask 线程级:每线程独立 mask;用 pthread_sigmask 设置;无进程级 mask。
-
signal pending:内核维护 process-wide set + 每线程 set;sigpending 返回 union。
-
thread-directed 信号:硬件异常(SIGBUS/SIGFPE/SIGILL/SIGSEGV)、SIGPIPE、pthread_kill/pthread_sigqueue。
-
process-directed 信号:kill/sigqueue、终端信号(SIGINT/SIGTSTP/SIGWINCH)、计时器(SIGALRM)。
-
进程级信号到达时:内核任意选一个不 block 的线程处理(保持传统信号语义)。
-
mutex_lock 被 handler 中断 → 自动重启;cond_wait 被中断 → 自动重启或返回 0(spurious wake-up)。
pthread_sigmask(§33.2.2):
// 摘自《The Linux Programming Interface》第 33 章
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
-
同 sigprocmask 用法;多线程程序唯一合法方式。
-
多线程程序中 sigprocmask 行为 SUSv3 未规定。
-
Linux 上两者实现相同。
pthread_kill / pthread_sigqueue(§33.2.3):
// 摘自《The Linux Programming Interface》第 33 章
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
-
pthread_kill 发信号给同进程指定线程;实现为 tgkill(tgid, tid, sig)。
-
pthread_sigqueue 带 sigqueue 数据;需 glibc 2.11+ + Linux 2.6.31+(rt_tgsigqueueinfo)。
async 信号推荐模式(§33.2.4):
// 摘自《The Linux Programming Interface》第 33 章
/* 主线程先 block 所有 async 信号 */
sigset_t mask;
sigfillset(&mask);
pthread_sigmask(SIG_BLOCK, &mask, NULL); /* 主线程 block */
/* 创建线程(继承 mask) */
pthread_create(&t, NULL, signal_thread, NULL);
/* signal_thread 用 sigwait 同步接收 */
void *signal_thread(void *arg) {
sigset_t mask;
int sig;
sigfillset(&mask);
pthread_sigmask(SIG_BLOCK, &mask, NULL); /* 确保 block */
while (1) {
sigwait(&mask, &sig); /* 同步接收 */
/* 处理信号——可调非 async-signal-safe 函数 + Pthreads */
}
}
要点:
-
所有线程 block → 信号只能被专用 sigwait 线程接收。
-
主线程先 block 再创建线程——避免「创建前信号被其他线程接收」。
-
专用线程 sigwait 同步接收——可调任意函数(mutex/cond/printf/malloc)。
-
多线程等 sigwait 同信号时仅一个接收(不确定哪个)。
When:多线程程序处理 async 信号(SIGINT/SIGTERM/SIGALRM)——用此模式;避免在多线程中用 signal handler。
Example:见上述 signal_thread。
33.3 线程与进程控制
What:fork/exec/exit 与线程的交互;多线程程序 fork 必须谨慎。
Why:fork 在多线程程序中是常见 bug 来源——mutex 死锁、TSD 内存泄漏;理解 pthread_atfork 是解决之道。
How:
线程 + exec(§33.3):
-
任何线程调 exec → 程序完全替换;除 exec 调用线程外所有线程消失。
-
不执行 TSD destructor 或 cleanup handler。
-
mutex/cond 等 Pthreads 对象消失。
-
exec 后线程 ID 未规定。
线程 + fork(§33.3):
-
仅复制调用 fork 的线程;其他线程消失。
-
不执行 TSD destructor 或 cleanup handler → 内存泄漏。
-
全局变量状态保留(含 mutex/cond)→ mutex 死锁风险(lock 状态继承但 owner 消失)。
-
TSD 块可能不可访问(指针丢失)。
推荐模式(§33.3):
-
fork 后立即 exec——exec 销毁所有 Pthreads 对象。
-
必须 fork 不 exec → pthread_atfork。
pthread_atfork(§33.3):
// 摘自《The Linux Programming Interface》第 33 章
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
-
prepare:在 fork 前调(注册顺序的逆序)——锁 mutex/获取锁。
-
parent:在 fork 后父进程中调(注册顺序)——释放 prepare 锁。
-
child:在 fork 后子进程中调(注册顺序)——重置状态/解锁。
-
子进程继承 fork handler;exec 后不保留。
-
vfork 在 NPTL 下不调 fork handler;LinuxThreads 调。
线程 + exit(§33.3):
-
任何线程调 exit() 或 main return → 所有线程立即消失。
-
不执行 TSD destructor 或 cleanup handler。
When:多线程程序尽量用 fork+exec 模式(exec 销毁所有 Pthreads 对象);不能用 fork+exec 时用 pthread_atfork。
Example:
// 摘自《The Linux Programming Interface》第 33 章
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static void prepare(void) { pthread_mutex_lock(&mtx); }
static void parent(void) { pthread_mutex_unlock(&mtx); }
static void child(void) { pthread_mutex_unlock(&mtx); /* reset */ }
pthread_atfork(prepare, parent, child);
pid_t pid = fork();
if (pid == 0) {
/* 子进程 */
execve(...);
}
33.4 线程实现模型
What:线程映射到内核调度实体(KSE)有 3 种方式——M:1、1:1、M:N。
Why:理解不同模型的取舍是明白 Linux 为何选 1:1(NPTL)的关键。
How:
模型对比(§33.4):
| 模型 | 实现 | 优点 | 缺点 |
|---|---|---|---|
M:1 (用户级) |
用户态线程库 |
thread op 快(无 sys call);易移植 |
blocking sys call 阻塞所有;多核无法调度;无法按线程设优先级 |
1:1 (内核级) |
每线程 = KSE |
blocking 不阻塞其他;多核并行;可设线程优先级 |
thread op 慢(sys call);大量线程 → kernel 调度开销 |
M:N (两级) |
多线程映射到多 KSE |
兼具 M:1 快 + 1:1 并行 |
复杂(线程调度分散在内核 + 用户);信号处理难 |
Linux 选型(§33.4-§33.5):
-
NPTL 选 1:1——Linux scheduler 已能高效调度大量 KSE;NPTL 测试可创建 10 万线程。
-
M:N 复杂度不值得——NGPT(IBM 的 M:N)性能反不如 NPTL。
-
LinuxThreads 用 M:1 风格但每线程用独立 KSE(接近 1:1 但无 CLONE_THREAD)。
When:理解 NPTL 高性能背后的设计选择;评估自定义线程调度时的取舍。
Example:无——理解概念即可。
33.5 LinuxThreads vs NPTL
What:Linux 两个 Pthreads 实现——LinuxThreads(旧,1996-2002)和 NPTL(新,2003+,现代 Linux 默认)。
Why:LinuxThreads 大量 SUSv3 不一致;NPTL 修复并提升性能(10 万线程测试);写新代码无需关心(默认 NPTL)。
How:
LinuxThreads 实现(§33.5.1):
-
用
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND创建线程。 -
无 CLONE_THREAD → 每线程独立 PID(不符合 SUSv3)。
-
额外「manager」线程处理线程创建/终止。
-
用信号实现内部操作(前 3 个 realtime 信号或 SIGUSR1/SIGUSR2)。
LinuxThreads SUSv3 不一致(§33.5.1):
-
getpid() 每线程返回不同值;getppid() 非主线程返回 manager 线程 PID。
-
fork 后只有 fork 调用线程能 wait 子进程。
-
非主线程 exec → 新进程 PID 是 exec 线程而非主线程的。
-
线程不共享 credentials;set-UID 程序中 pthread_kill 可能失败。
-
线程不共享进程级 pending 信号;信号可能所有线程都处理(不符合 SUSv3)。
-
sigaltstack per-thread 但新线程继承(不符合 SUSv3)。
-
不共享 session ID/进程组 ID;setsid/setpgid 行为异常。
-
fcntl record locks 不共享;resource limits 不共享;times()/getrusage() per-thread;setpriority/nice 不共享;setitimer 不共享;SysV semaphore undo 不共享。
-
其他:manager 线程被杀需手动清理;core dump 可能不全;TIOCNOTTY 只主线程能用。
NPTL 实现(§33.5.2):
-
用
CLONE_VM|CLONE_FILES|CLONE_FS|CLONE_SIGHAND|CLONE_THREAD|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID|CLONE_SYSVSEM。 -
有 CLONE_THREAD → 同 TGID(符合 SUSv3);ps 显示单行,ps -L 显示所有线程。
-
用前 2 个 realtime 信号(cancel + credentials 同步)。
-
无 manager 线程。
-
SUSv3 一致性大幅提升;剩余不一致:nice 不共享。
-
早期 2.6.x 不一致:sigaltstack 继承(2.6.16 前)、setsid/setpgid 仅主线程(2.6.16 前)、setitimer 不共享(2.6.12 前)、resource limit 不共享(2.6.10 前)、times/getrusage per-thread(2.6.9 前)。
NPTL 内核变更(§33.5.2):
-
thread groups 完善;futex 新同步原语;get_thread_area/set_thread_area 系统调用;threaded core dumps;信号管理修改;exit_group 系统调用(_exit/exit → exit_group;pthread_exit → 真正 _exit);scheduler 重写支持大量 KSE;进程终止性能改进;clone() 扩展。
ABI 兼容(§33.5.2):
-
NPTL 设计为 ABI 兼容 LinuxThreads——链接 LinuxThreads 的程序无需重链接即可用 NPTL。
-
但行为可能变化(更符合 SUSv3)。
查询当前实现(§33.5.3):
// 摘自《The Linux Programming Interface》第 33 章
$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.3.4
$ $(ldd /bin/ls | grep libc.so | awk '{print $3}') | egrep -i 'threads|nptl'
Native POSIX Threads Library by Ulrich Drepper et al
选择实现:
-
LD_ASSUME_KERNEL=2.2.5 ./prog强制用 LinuxThreads。 -
现代 Linux 默认 NPTL。
When:新代码无需关心(默认 NPTL);调试老程序(可能依赖 LinuxThreads 行为)——查询当前实现。
Example:见上述 shell 命令。
33.6 高级 Pthreads 特性
What:Pthreads API 的高级特性——实时调度、进程共享 mutex/cond、barriers、读写锁、自旋锁。
Why:了解高级特性是选择正确同步原语的前提。
How:
-
实时调度:pthread_attr_setschedpolicy/param 设置 SCHED_FIFO/RR/DEADLINE(见第 35 章)。
-
进程共享 mutex/cond:PTHREAD_PROCESS_SHARED 属性 + 共享内存区域;NPTL 支持。
-
barriers:pthread_barrier_wait 等所有线程到达才继续。
-
读写锁:pthread_rwlock 允许多读单写。
-
自旋锁:pthread_spin_lock 短等待时避免 mutex context switch 开销。
When:高并发读 → 读写锁;线程组阶段同步 → barrier;锁持有时间极短 → 自旋锁;多进程线程同步 → 进程共享 mutex。
Example:略——详见 [Butenhof, 1996]。
三、关键图表
|
信号概念在多线程中的归属
|
|
fork/exec/exit 与线程交互
推荐:多线程 fork 后立即 exec。 |
|
LinuxThreads vs NPTL 关键差异
|
四、思维导图
mindmap
root((第 33 章 线程细节))
线程栈
默认 2 MB x86 32
sysconf STACK MIN 16384
pthread attr setstacksize
RLIMIT STACK 默认
ulimit s 设置时机
VA 空间 1500 线程
信号线程映射
action 进程级
disposition 进程级
mask 线程级
pending 进程 线程
thread directed 异常 SIGPIPE
process directed kill 终端
mutex lock 自动重启
cond wait 重启或 spurious
pthread sigmask kill
sigmask 替代 sigprocmask
pthread kill tgkill
pthread sigqueue 带数据
2.6.31 rt tgsigqueueinfo
async 信号模式
所有线程 block
主线程先 block
专用线程 sigwait
可调非 async safe
可 mutex cond
fork exec exit
fork 仅调用线程
exec 全部消失
exit 全消失
TSD cleanup 不执行
mutex 死锁风险
pthread atfork
prepare parent child
vfork NPTL 不调 handler
线程实现模型
M 1 用户级 快
1 1 内核级 Linux 选
M N 两级 复杂
Linux 选 1 1
10 万线程 NPTL
LinuxThreads vs NPTL
LinuxThreads 旧 不一致
NPTL 现代 默认
CLONE THREAD 关键差异
getpid 行为差异
getconf 查询
LD ASSUME KERNEL 切换
ABI 兼容
高级特性
实时调度 SCHED FIFO
进程共享 mutex
barrier 读写锁
自旋锁
五、重点与易错点
-
Linux/x86-32 默认栈 2 MB——主线程除外;减小栈可显著增加线程数上限(VA 空间限制)。
-
RLIMIT_STACK 在 NPTL 运行时初始化时确定——main 之前的 setrlimit 太晚;必须用
ulimit –s在运行前设置。 -
signal disposition 是进程级——所有线程共享;一个线程设 handler,另一个线程收到信号也调该 handler。
-
signal mask 是线程级——每线程独立;用 pthread_sigmask 设置;多线程程序不能用 sigprocmask。
-
thread-directed 信号 vs process-directed:硬件异常/SIGPIPE/pthread_kill 是 thread-directed;kill/sigqueue/终端信号是 process-directed。
-
pthread_cond_wait 被 handler 中断——自动重启或返回 0(spurious wake-up);SUSv3 要求此行为。
-
sigaltstack 线程级且不继承——新线程需自己调 sigaltstack;LinuxThreads 错误继承导致 chaos。
-
多线程 fork 危险——仅调用 fork 线程存在,其他线程消失;mutex 状态保留(lock 状态继承但 owner 消失 → 死锁);TSD destructor/cleanup 不执行 → 内存泄漏。
-
多线程 fork 推荐模式:fork 后立即 exec;不能用此模式时用 pthread_atfork 注册 prepare/parent/child handler。
-
exec 销毁所有 Pthreads 对象——除 exec 调用线程外所有线程消失;不执行 destructor/cleanup。
-
exit()/main return 终止所有线程——不执行 destructor/cleanup;区别于 pthread_exit 仅终止调用线程。
-
async 信号推荐模式:所有线程 block + 专用线程 sigwait;主线程先 block 再创建线程(继承 mask);专用线程可调非 async-signal-safe 函数(含 Pthreads)。
-
pthread_mutex_lock 被 handler 中断——自动重启;这是 SUSv3 要求;pthread_join 同样。
-
LinuxThreads 大量 SUSv3 不一致——每线程独立 PID、不共享 credentials/limits/timers、signal pending 不共享等;现代 Linux 用 NPTL。
-
NPTL 仍不一致——nice 值不共享;早期 2.6.x 还有 sigaltstack 继承、setsid/setpgid 限制、setitimer/limits 不共享等。
-
查询线程实现:
getconf GNU_LIBPTHREAD_VERSION输出「NPTL 2.3.4」等;老系统可用 ldd 查 libc 路径 + 执行。 -
NPTL ABI 兼容 LinuxThreads——链接 LinuxThreads 的程序无需重链接即可用 NPTL;但行为可能变化(更符合 SUSv3)。
-
pthread_atfork 在 vfork 下行为差异——NPTL 不调 fork handler,LinuxThreads 调。
-
线程与信号天生不兼容——多线程程序应尽量避免用信号;必须用时用专用线程 sigwait 模式。
-
跨章衔接:第 29-32 章 Pthreads 基础与同步 → 第 33 章线程与信号、fork 交互、Linux 实现差异;第 34 章进程组与会话 → 与线程组交互;第 21 章信号处理 → async-signal-safe;第 35 章调度 → 实时线程 SCHED_FIFO/RR。
-