pthread_mutex_lock
在没有竞争时不需要进内核——一条 CAS 就够。竞争时才用
futex(Fast Userspace
muTEX)进入内核等待。
一、先看图
flowchart TD
LOCK[pthread_mutex_lock] --> CAS{CAS 成功?}
CAS -- 是 --> HOLD[持有锁<br/>零 syscall]
CAS -- 否 --> FUTEX[futex(FUTEX_WAIT)<br/>进入内核等待]
FUTEX --> HASH[futex hash table<br/>按地址查找]
HASH --> SLEEP[加入等待队列<br/>schedule]
UNLOCK[pthread_mutex_unlock] --> CAS2{有等待者?}
CAS2 -- 否 --> DONE[直接返回]
CAS2 -- 是 --> WAKE[futex(FUTEX_WAKE)<br/>唤醒一个]
classDef fast fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef slow fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class HOLD,DONE fast
class FUTEX,HASH,SLEEP,WAKE slow
class LOCK,UNLOCK,CAS,CAS2 fast
二、核心 API
// 用户态变量
int futex_word = 0;
// 等待:如果 *uaddr == val 则睡眠
futex(uaddr, FUTEX_WAIT, val, timeout, NULL, 0);
// 唤醒:唤醒最多 n 个等待者
futex(uaddr, FUTEX_WAKE, n, NULL, NULL, 0);关键:内核不管锁语义——只做”比较并等待”和”唤醒”。锁协议完全在用户态实现。
三、内核实现
3.1 Hash Table
// kernel/futex/core.c
static struct futex_hash_bucket futex_queues[1 << FUTEX_HASHBITS];用户地址 → hash → bucket → 等待队列。
3.2 等待路径
futex_wait()→ hash 查找 bucket → 锁 bucket- 检查
*uaddr == val(防止 lost wakeup) - 加入等待队列 →
schedule()
3.3 唤醒路径
futex_wake()→ hash 查找 bucket- 唤醒队列中 n 个等待者
四、PI futex(优先级继承)
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);低优先级线程持锁 → 高优先级线程等待 → 低优先级线程临时提升到高优先级。
内核用 rt_mutex 实现 PI futex →
复用优先级继承链。
五、Robust futex
进程持锁时崩溃 → 锁永远不会释放 → 其他线程永远等待。
pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);内核维护 robust list → 进程退出时自动标记锁为
FUTEX_OWNER_DIED → 下一个 waiter 收到
EOWNERDEAD → 可以修复状态。
六、FUTEX_REQUEUE
场景:pthread_cond_broadcast()
唤醒所有等待者 → 它们全部竞争 mutex → 惊群。
FUTEX_CMP_REQUEUE:原子地把 condvar
等待队列的线程 转移 到 mutex 等待队列 →
只唤醒一个。
七、futex2(5.16+)
futex_waitv:同时等待多个 futex 地址。
struct futex_waitv waitv[] = {
{ .uaddr = addr1, .val = expected1, .flags = FUTEX_32 },
{ .uaddr = addr2, .val = expected2, .flags = FUTEX_32 },
};
futex_waitv(waitv, 2, 0, timeout);用途:Wine/Proton 模拟 Windows WaitForMultipleObjects。
八、安全问题
8.1 CVE-2021-3347
PI futex 的 requeue 路径存在 use-after-free → 内核提权。
根因:futex 状态机复杂 + 用户态可构造任意地址。
8.2 futex 是攻击面
- 用户态提供地址 → 内核信任并操作
- 状态机复杂 → 边界情况多
- 多年来多个 CVE
九、观察
# 查看 futex 等待
cat /proc/<pid>/syscall | grep futex
strace -e futex -p <pid>
# 统计
bpftrace -e 'tracepoint:syscalls:sys_enter_futex { @op[arg1 & 0x7f] = count(); }'
# op: 0=WAIT, 1=WAKE, 6=CMP_REQUEUE, 9=WAIT_BITSET十、小结
- futex = 用户态快路径(CAS)+ 内核慢路径(等待/唤醒)
- glibc pthread_mutex、条件变量、读写锁都基于 futex
- PI futex 解决优先级反转
- robust futex 处理持锁进程崩溃
- futex2 支持多地址等待
参考文献
kernel/futex/- Ulrich Drepper, “Futexes Are Tricky.” 2011
man 2 futexman 7 futex- Collabora, “futex_waitv: nested wait for gaming.” 2021
工具
strace -e futexbpftraceperf trace -e futex
上一篇:percpu_refcount 下一篇:优先级反转
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】优先级反转与继承
火星探路者号因优先级反转重启——这是实时系统最经典的故事。本文讲优先级反转现象、PIP 优先级继承协议、PCP 优先级天花板、rt_mutex、PREEMPT_RT 的 spinlock 转换、DEADLINE 任务反转。
【操作系统百科】SCHED_FIFO/RR 与 PREEMPT_RT
Linux 里的实时:SCHED_FIFO/RR 提供优先级调度,但原版内核仍有不可抢占点。PREEMPT_RT 补丁集 20 年后(6.12)合入主线,把几乎所有 spinlock 变 rt_mutex、IRQ 变线程。本文讲 RT 调度语义、RT-throttling、cyclictest 基线、优先级继承、以及 RT 部署的陷阱。
【操作系统百科】线程模型:1:1、N:1、M:N 与虚拟线程
同样一个 'concurrent' 的需求,OS 级 1:1 线程、用户态 N:1 协程、调度器混合 M:N、运行时虚拟线程各给出了不同答案。本文梳理 LinuxThreads → NPTL 的演进、Solaris LWP 的失败、Go GPM / Java Loom / Erlang BEAM / async-await 的选择,以及 M:N 模型的阿克琉斯之踵:阻塞 syscall。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。