跳转至

futex

是什么 / 解决什么问题

futex (Fast Userspace muTEX) 是 Linux 线程同步的基石。它将无竞争的快速路径保持在用户态(纯原子操作),只在有竞争时才陷入内核等待。pthread_mutex、pthread_cond、Go 的 sync.Mutex 底层都依赖 futex。

核心思想

无竞争(快速路径):
  用户态 atomic_compare_exchange → 成功获取锁
  不进内核!一条 CPU 指令完成。

有竞争(慢路径):
  atomic CAS 失败 → futex(FUTEX_WAIT) → 内核挂起线程
  释放锁时 → futex(FUTEX_WAKE) → 内核唤醒等待者

使用模式

简化的 mutex 实现

#include <linux/futex.h>
#include <sys/syscall.h>
#include <stdatomic.h>

// futex 值约定: 0=未锁, 1=已锁无等待者, 2=已锁有等待者
atomic_int lock = 0;

void mutex_lock(atomic_int *lock) {
    int expected = 0;
    // 快速路径:无竞争,CAS 0→1
    if (atomic_compare_exchange_strong(lock, &expected, 1)) {
        return;  // 获取成功,没有进内核
    }
    // 慢路径:有竞争
    while (1) {
        // 标记为"有等待者"
        if (atomic_exchange(lock, 2) == 0) {
            return;  // 在设置过程中获取了锁
        }
        // 内核等待:只有 *lock == 2 时才睡眠
        syscall(SYS_futex, lock, FUTEX_WAIT, 2, NULL, NULL, 0);
    }
}

void mutex_unlock(atomic_int *lock) {
    if (atomic_fetch_sub(lock, 1) == 1) {
        return;  // 值从 1→0,没有等待者,不需要唤醒
    }
    // 有等待者(值从 2→1→设为 0)
    atomic_store(lock, 0);
    syscall(SYS_futex, lock, FUTEX_WAKE, 1, NULL, NULL, 0);
}

futex 系统调用

#include <linux/futex.h>

// 等待: 如果 *uaddr == val 则睡眠
long futex(int *uaddr, int op, int val,
           const struct timespec *timeout,
           int *uaddr2, int val3);

// FUTEX_WAIT: 原子检查 *uaddr == val && sleep
syscall(SYS_futex, &futex_word, FUTEX_WAIT, expected_val,
        timeout, NULL, 0);
// 返回 0: 被 WAKE 唤醒
// 返回 -1, EAGAIN: *uaddr != val(值已改变,不睡眠)
// 返回 -1, ETIMEDOUT: 超时
// 返回 -1, EINTR: 被信号打断

// FUTEX_WAKE: 唤醒最多 n 个等待者
syscall(SYS_futex, &futex_word, FUTEX_WAKE, n, NULL, NULL, 0);
// 返回实际唤醒的线程数

条件变量的 futex 实现(简化)

// pthread_cond_wait 底层:
// 1. 释放关联的 mutex
// 2. futex(FUTEX_WAIT, &cond_seq, current_seq, ...)
// 3. 被唤醒后重新获取 mutex

// pthread_cond_signal 底层:
// 1. 增加序列号
// 2. futex(FUTEX_WAKE, &cond_seq, 1, ...)

行为与语义

为什么 FUTEX_WAIT 需要检查值

// 防止 lost wake-up 问题:
// 线程A准备 WAIT,但还没进入内核
// 线程B此时 WAKE
// 线程A进入 WAIT → 永远睡下去(错过了 WAKE)

// futex 的解决方案:
// WAIT 在内核中原子地检查 *uaddr == val
// 如果值已改变(说明有 WAKE 发生),立即返回 EAGAIN

FUTEX_PRIVATE_FLAG

// 如果 futex 只在同一进程的线程间使用(最常见情况):
syscall(SYS_futex, &lock, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, val, ...);

// PRIVATE 跳过了共享内存查找,性能更好
// glibc 的 pthread_mutex 默认用 PRIVATE

PI futex (Priority Inheritance)

// 优先级继承:解决优先级反转问题
// 低优先级线程持有锁 → 高优先级线程等锁 → 中优先级线程抢占低优先级
// 结果:高优先级被间接阻塞

// PI futex:内核临时提升持锁线程的优先级
FUTEX_LOCK_PI / FUTEX_UNLOCK_PI

// 对应 pthread:
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

性能考量

futex vs 自旋锁

futex spinlock
无竞争 1次 atomic(纳秒级) 1次 atomic(纳秒级)
有竞争 syscall + 上下文切换(微秒级) 忙等(浪费 CPU)
适用 竞争可能持续较长时间 临界区极短 + 少竞争

adaptive mutex(混合策略)

glibc 的 PTHREAD_MUTEX_ADAPTIVE_NP:
1. 先自旋一小段时间(期望锁很快释放)
2. 自旋未获取 → 退化为 futex WAIT(避免浪费 CPU)

适合:锁持有时间通常很短但偶尔较长的场景

futex 的内核开销

FUTEX_WAIT: 加入哈希表等待队列 + 调度出去 ≈ 几微秒
FUTEX_WAKE: 从哈希表找到等待者 + 唤醒 ≈ 几微秒

哈希表桶冲突(多个 futex 映射到同一桶)会导致性能退化
内核用 256 个桶的哈希表管理所有 futex

常见陷阱与 FAQ

1. futex 地址必须对齐

// futex 操作的地址必须是 4 字节对齐的 int
// 否则返回 EINVAL
int futex_word __attribute__((aligned(4)));

2. 虚假唤醒 (spurious wakeup)

// futex WAIT 可能在没有对应 WAKE 的情况下返回
// (内核实现原因或信号中断)
// 必须在循环中检查条件

while (atomic_load(&lock) == 2) {
    syscall(SYS_futex, &lock, FUTEX_WAIT, 2, NULL, NULL, 0);
    // 唤醒后重新检查条件
}

3. 进程间共享 futex

// 跨进程的 futex 必须在共享内存上
// 且不能用 FUTEX_PRIVATE_FLAG
void *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED | MAP_ANONYMOUS, -1, 0);
int *futex_word = (int *)shared;

// 子进程和父进程可以通过这个 futex 同步

4. pthread_mutex 的类型选择

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);

// NORMAL: 不检查重复加锁(死锁不报错)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

// ERRORCHECK: 检查重复加锁(返回 EDEADLK)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);

// RECURSIVE: 允许同一线程多次加锁(引用计数)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

观测与调试

# 查看进程是否阻塞在 futex 上
cat /proc/<pid>/wchan
# 输出 "futex_wait_queue" 说明在等锁

# strace 看 futex 调用
strace -e trace=futex -p <pid>
# futex(0x7f..., FUTEX_WAIT_PRIVATE, 0, NULL) = 0

# 统计 futex 等待时间
bpftrace -e '
tracepoint:syscalls:sys_enter_futex /args->op & 0xf == 0/ {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_futex /@start[tid]/ {
    @wait_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 检测锁竞争热点
perf lock record ./app
perf lock report

延伸阅读

  • man 2 futex — 系统调用详解
  • man 7 futex — 概念说明
  • Ulrich Drepper «Futexes are Tricky» — 经典实现参考
  • glibc nptl/ 源码 — pthread 的实际 futex 使用