前两篇我已经把互斥锁和条件变量在用户态(libstdc++/glibc)怎么“用 futex”讲清楚了。现在我想把最后一块拼图补上:Linux 内核里的 futex 到底怎么实现 WAIT/WAKE?为什么不会丢唤醒?等待队列怎么组织?唤醒为什么要先 mark 再 wake?
这篇我还是按同一套路:建立模型 → 用关键源码验证 → 最后自己总结一遍。
(所有代码片段都是源码节选)
1)我先把概念纠正:futex 在内核里不是“一个对象”,而是一套“按 key 排队”的机制
用户态说“我在 uaddr 上 wait”,内核不会给你创建一个“futex对象”。内核做的是:
- 把用户态地址
uaddr映射成一个 futex_key - 用 key 做哈希,找到一个 hash bucket
- 把当前线程对应的一个 futex_q 节点挂进 bucket 的等待链表
- wake 的时候:同样把 uaddr→key→bucket,然后在链表里筛选匹配 key 的 futex_q,逐个唤醒
1.1 核心结构:bucket 与 futex_q(每个等待线程一个节点)
// kernel/futex/futex.h(节选)
struct futex_hash_bucket {
atomic_t waiters;
spinlock_t lock;
struct plist_head chain;
} ____cacheline_aligned_in_smp;
struct futex_q {
struct plist_node list;
struct task_struct *task;
spinlock_t *lock_ptr;
union futex_key key;
u32 bitset;
...
};
我的理解:
hb->chain是“这个桶里所有 futex 等待者的链表”(链表上会混多个 key)- 一个等待线程对应一个
futex_q - 匹配关系靠
q->key
1.2 key 的意义:private/shared 决定“uaddr 是否能跨进程匹配”
内核在 core.c 的注释里把 key 的规则写得很清楚:
/* kernel/futex/core.c(节选)
* For shared mappings: (inode->i_sequence, page->index, offset_within_page)
* For private mappings: (current->mm, address, 0)
*/
int get_futex_key(u32 __user *uaddr, bool fshared, union futex_key *key, ...)
所以“一个 uaddr 对应一个 key”要加前提:
- 在同一进程里,private futex:uaddr→key(key 里包含 mm + address)
- shared futex:uaddr→key(key 里包含 inode + page + offset,可以跨进程匹配)
1.3 哈希桶怎么找?
// kernel/futex/core.c(节选)
struct futex_hash_bucket *futex_hash(union futex_key *key)
{
u32 hash = jhash2((u32 *)key, ..., key->both.offset);
return &futex_queues[hash & (futex_hashsize - 1)];
}
总结:
全局有很多个 bucket(
futex_queues[]),每个 bucket 一条chain。不同 key 会落到不同 bucket;哈希冲突会让不同 key 共用同一个 bucket 的 chain,但最终用futex_match(key)精确筛选。
2)我最想搞懂的问题:为什么 futex 不会丢唤醒?
我在 waitwake.c 文件头看到一段“READ this before hacking futexes!” 的注释,它直接把“丢唤醒反例”写出来了,并给出正确序列化方式。
核心思想一句话:
- WAIT 侧:算桶/加锁后再读一次用户态值确认没变,没变才入队睡眠
- WAKE 侧:修改用户态值后再 wake,通过 bucket 里的 waiters 计数 + 内存屏障,避免 waker 误判“没人等”而直接返回
这段注释建议你认真读一遍,它基本就是 futex 正确性的教科书。
3)FUTEX_WAIT 的内核路径:setup(二次检查)→ queue(入队)→ schedule(睡)→ unqueue(醒/超时/信号)
3.1 futex_wait() 主流程(最核心的函数)
// kernel/futex/waitwake.c(节选)
int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val,
ktime_t *abs_time, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q q = futex_q_init;
int ret;
if (!bitset)
return -EINVAL;
q.bitset = bitset;
retry:
ret = futex_wait_setup(uaddr, val, flags, &q, &hb);
if (ret)
goto out;
futex_wait_queue(hb, &q, to);
ret = 0;
if (!futex_unqueue(&q))
goto out;
ret = -ETIMEDOUT;
if (to && !to->task)
goto out;
if (!signal_pending(current))
goto retry;
ret = -ERESTARTSYS;
...
out:
...
return ret;
}
我把它拆成四句话:
futex_wait_setup:准备等待(算 key/桶,加锁,二次读用户态值校验 expected)futex_wait_queue:把 q 入队并进入调度睡眠- 醒来后:
futex_unqueue(&q)检查自己是否已被 waker 从链表摘掉(摘掉则成功返回) - 否则根据超时/信号/虚假唤醒决定重试或返回
3.2 “二次检查用户态值(expected 语义)”在哪里?
我关心的就是:内核怎么保证 “只有 *uaddr == val 才会睡”。
关键逻辑在 setup 阶段(你在同文件能看到的这段就是核心):
// kernel/futex/waitwake.c(节选)
ret = futex_get_value_locked(&uval, uaddr);
...
if (uval != val) {
futex_q_unlock(*hb);
ret = -EWOULDBLOCK;
}
这就是 futex 的灵魂语义:
expected 不匹配就绝不入队睡眠,直接返回 -EWOULDBLOCK(用户态通常把它当作 EAGAIN)。
这也解释了你前面学 mutex/condvar 时反复看到的模式:
- 用户态先读值,带 expected 调用 WAIT
- 内核在加锁后再读一次,如果值变了就不睡(避免睡死)
3.3 真正“入队 + 睡眠”在哪?
在 futex_wait_queue:
// kernel/futex/waitwake.c(节选)
void futex_wait_queue(struct futex_hash_bucket *hb, struct futex_q *q,
struct hrtimer_sleeper *timeout)
{
set_current_state(TASK_INTERRUPTIBLE|TASK_FREEZABLE);
futex_queue(q, hb); // 挂到 hb->chain,然后释放 hb->lock
if (timeout)
hrtimer_sleeper_start_expires(timeout, HRTIMER_MODE_ABS);
if (likely(!plist_node_empty(&q->list))) {
if (!timeout || timeout->task)
schedule();
}
__set_current_state(TASK_RUNNING);
}
我的理解:
set_current_state先把自己标成可睡,再入队并放锁,避免“waker 已经 wake 了我但我还没睡”这类竞态futex_queue负责把 q 挂进hb->chain(一个桶的一条链)- 如果 q 已经被移除(别人已经标记唤醒),就跳过 schedule
4)FUTEX_WAKE 的内核路径:uaddr→key→bucket→遍历 chain→match→mark→wake_up_q
4.1 futex_wake():筛选匹配 key 的等待者
// kernel/futex/waitwake.c(节选)
int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q *this, *next;
union futex_key key = FUTEX_KEY_INIT;
int ret;
DEFINE_WAKE_Q(wake_q);
if (!bitset)
return -EINVAL;
ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &key, FUTEX_READ);
if (unlikely(ret != 0))
return ret;
hb = futex_hash(&key);
if (!futex_hb_waiters_pending(hb))
return ret;
spin_lock(&hb->lock);
plist_for_each_entry_safe(this, next, &hb->chain, list) {
if (futex_match(&this->key, &key)) {
if (!(this->bitset & bitset))
continue;
futex_wake_mark(&wake_q, this);
if (++ret >= nr_wake)
break;
}
}
spin_unlock(&hb->lock);
wake_up_q(&wake_q);
return ret;
}
我抓三点:
- 同桶不同 key 混在一条链上,靠
futex_match(&this->key, &key)过滤 nr_wake表示最多唤醒几个bitset是 WAIT_BITSET/WAKE_BITSET 的过滤器(this->bitset & bitset)
4.2 bitset 是什么?
一句话:按位过滤唤醒。waiter 带 q->bitset,waker 带 bitset,按位与不为 0 才唤醒。
它不是 glibc condvar 的 G1/G2 分组机制(glibc condvar 主要靠两个不同的 uaddr:__g_signals[0/1] 分开),bitset 是内核提供的额外过滤能力。
5)我最关键的细节理解:为什么要先 mark 再 wake_up_q?
5.1 futex_wake_mark() 做了什么?(不是立即唤醒)
// kernel/futex/waitwake.c(节选)
void futex_wake_mark(struct wake_q_head *wake_q, struct futex_q *q)
{
struct task_struct *p = q->task;
get_task_struct(p);
__futex_unqueue(q);
smp_store_release(&q->lock_ptr, NULL);
wake_q_add_safe(wake_q, p);
}
我对这段的理解是:
- 它“拆的是 futex_q 节点”,不是“拆 addr”
__futex_unqueue(q):把这个等待者从hb->chain摘掉q->lock_ptr = NULL(release):发布一个可靠的“我已经被 waker 真正出队/标记唤醒”状态
这不是为了禁止虚假唤醒,而是为了让 waiter 能安全判断/释放自己的 futex_q,避免并发生命周期问题wake_q_add_safe:把任务加入本次待唤醒队列(只是收集)
5.2 真正唤醒发生在哪里?
真正让线程 runnable 的动作发生在 wake_up_q(&wake_q),而且是在释放 hb->lock 之后:
spin_unlock(&hb->lock);
wake_up_q(&wake_q);
我的理解:
wake_q就像一个“本次需要唤醒的任务列表”- 先在持锁区把要唤醒的任务收集起来(避免在 spinlock 内做重活)
- 放锁后统一 wake(性能和锁竞争更好)
6)把内核 futex 和用户态 mutex/condvar 拼起来
现在我脑子里能把三层拼成一个闭环:
- 用户态协议(glibc/libstdc++)负责“什么时候 wait / 什么时候 wake / 状态字怎么变 / signal 资格怎么判定”
- 内核 futex 负责“按 key 排队、按 key 唤醒”,并用 二次读 expected + waiters 计数与屏障 保证不丢唤醒
- 所以 mutex/condvar 的所有模式最终都归结为一句话:
- *WAIT:只有当 uaddr == expected 才会入队睡
- WAKE:按 key 在桶链表里找到匹配等待者,摘掉 futex_q,再统一 wake
7)我自己的最后总结
- futex 在内核里不是一个对象,而是 “uaddr→key→bucket→等待链表” 的机制;每个等待线程对应一个
futex_q节点。 FUTEX_WAIT的正确性来自两件事:
1)加锁后二次读取用户态值并对比 expected,不匹配就不睡(返回 -EWOULDBLOCK);
2)入队与睡眠顺序正确(设置 task state、入队、释放锁、schedule),配合 waiters 计数与屏障避免丢唤醒。FUTEX_WAKE做的是:uaddr→key→桶→遍历链表筛选 key 匹配的 futex_q;对每个目标 q 做 mark(出队 + 发布 lock_ptr=NULL + 加入 wake_q),释放桶锁后再wake_up_q真正唤醒。- bitset 是内核提供的“按位过滤唤醒”能力,用于 WAIT_BITSET/WAKE_BITSET;glibc condvar 的 G1/G2 分组主要不是靠 bitset,而是靠两个不同 uaddr(
__g_signals[0/1])。 - 最后:用户态的 mutex/condvar/semaphore 协议之所以可靠,是因为内核 futex 提供了“expected 等待 + 按 key 唤醒”的基础语义。