1. Futex 介绍
需求
Futex是Fast Userspace mutexes的缩写,其设计思想是通过增加在用户态原子检查来决定是否陷入内核的等待队列。
在传统的 UNIX 系统中,System-V IPC(如信号量、消息队列和共享内存)是进程间同步的基本机制。这些机制在内核中维护着记录共享状态的对象(如信号量的计数值),并向用户暴露一个整数句柄(如 semid)作为访问接口。以信号量为例,当将其用作互斥锁时,对信号量的 P/V 操作(即加锁/解锁)必须通过 semop 等系统调用完成。这意味着每次获取或释放锁都需要陷入内核,当锁争用率较低时,系统调用的开销会成为显著的性能瓶颈。
在实际运行环境中,诸多同步操作往往不存在竞争条件。具体而言,当某进程进入临界区至退出临界区的时段内,通常并无其他进程尝试进入同一临界区或请求相同的同步变量。然而,即便在此类无竞争场景下,该进程仍需陷入内核态以访问维护共享状态的内核对象,查验是否存在竞争进程;退出临界区时,亦需再次陷入内核,检查是否有进程阻塞于同一同步变量之上,以便执行唤醒操作。此类非必要的系统调用(或内核态切换)导致了显著的性能开销。
为了解决这个问题,Futex就应运而生。Futex是一种用户态和内核态混合机制,需要两个部分合作完成。
在用户态,Futex指的是一个原子变量,用来记录某种锁状态。需要持锁时,如果锁空闲则直接改变Futex变量值,即表明持有该锁,不需要陷入内核。释放锁时,如果锁状态中没有线程等锁,则直接置锁状态为空闲,也无需陷入内核。
对于内核而言,Futex是一个系统调用。当用户态出现锁竞争,线程需要休眠等待时,通过Futex系统调用陷入内核并挂在wait queue中。当释放锁发现有线程在休眠等锁时,通过Futex系统调用陷入内核去唤醒等锁的线程。
使用
简述流程:
- Futex变量位于一段共享的内存中且操作是原子的,当进程/线程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的Futex变量。
- 如果没有竞争发生,则只修改Futex,而不用再执行系统调用。
- 当通过访问Futex变量告诉进程/线程有竞争发生时,执行系统调用去完成相应的处理(wait 将其他进程/线程阻塞或者 wake up唤醒等待进程/线程):比如,通过系统调用(如Futex_wait)将当前进程/线程挂起,让出CPU。

上层应用
Linux内核提供的futex(快速用户空间互斥锁)是构建高效用户态锁和信号量的基础原语。作为底层同步原语,可直接用于构建高层抽象:互斥锁(mutexes)、条件变量(condition variables)、读写锁(read-write locks)、屏障(barriers)、信号量(semaphores)。基于Futex系统调用实现了很多上层的库,多数开发者应通过系统库间接使用,比如:java中的隐式锁synchronize,和java.util.concurrent库实现的lock机制。C++的pthread库中实现的锁机制(NPTL的pthreads实现)。

但是本文主要还是集中于Linux内核提供的futex上,不过要注意直接操作futex需对并发编程有深入理解。
2. Futex 的数据结构
在用户态,futex是一种32位值(也称为futex字),其地址需作为参数传递给futex()系统调用。(注意:所有平台上futex的大小均为32位,包括64位系统。)所有futex操作都基于该值进行控制。Futex作为一种用户态和内核态混合的同步机制,支持进程内的线程之间和进程间的同步锁操作。
- 当用于线程同步时,因为线程共享虚拟内存空间,虚拟地址就可以唯一的标识出Futex变量,即线程用同样的虚拟地址来访问Futex变量。只需将futex字存储在全局变量中供所有线程共享即可。
- 当用于进程间时,进程有独立的虚拟内存空间,因此只有通过mmap(2)或shmat(2)等系统调用创建共享内存区域,让它们共享同一段物理地址空间来使用Futex变量。(因此,不同进程中futex字的虚拟地址可能不同,但这些地址最终都指向物理内存中的同一位置。)
只有当用户态判断需要阻塞时,才调用FUTEX_WAIT,此时内核才创建struct futex_q,把线程挂起(内核不会修改用户态的 futex 字)。在内核中通过struct futex_q结构将一个Futex变量与一个挂起的进程(线程)关联起来,其定义以及关键成员的作用如下:
struct futex_q {
struct plist_node list; // 链表节点
struct task_struct *task; // 挂起在该Futex变量关联的进程(线程)
spinlock_t *lock_ptr; // 自旋锁,控制链表访问
union futex_key key; // Futex变量地址标识
// 与优先级继承相关
struct futex_pi_state *pi_state;
struct rt_mutex_waiter *rt_waiter;
union futex_key *requeue_pi_key;
……
};

在内核中通过一个全局哈希表来维护所有挂起阻塞在Futex变量上的进程(线程),不同的Futex变量会根据其地址标识计算出一个hash key并定位到一个bucket上,因此挂起阻塞在同一个Futex变量的所有进程(线程)会对应到同一个bucket上,数据结构如下:
// bucket
struct futex_hash_bucket {
……
// 自旋锁,用于控制chain的访问,
// struct futex_q中lock_ptr,就是引用其所在的bucket的自旋锁!
spinlock_t lock;
// Futex使用优先级链表来实现等待队列,实现优先级继承,解决优先级翻转问题
struct plist_head chain;
};
// 全局哈希表
static struct {
struct futex_hash_bucket *queues;
unsigned long hashsize;
} __futex_data;
#define futex_queues (__futex_data.queues)
#define futex_hashsize (__futex_data.hashsize)
3. Futex 的系统调用
原型和系统调用号:
#include <linux/futex.h> /* Definition of FUTEX_* constants */
#include <sys/syscall.h> /* Definition of SYS_* constants */
#include <sys/time.h>
int futex (int *uaddr, int op, int val, const struct timespec *timeout,int *uaddr2, int val3);
#define __NR_futex 240
uaddr就是用户态下共享内存的地址,也就是用户态的Futex变量地址。op存放着操作类型(宏定义的操作码),Futex系统调用进入内核后都会先走到do_futex,这里不同操作码会调用不同函数,最常用的是futex_wait和futex_wake。
3.1 无竞争时的快速路径
当进程尝试获取锁(如互斥锁)时:检查futex变量的值(用户态,锁状态(已获取/未获取)可通过共享内存中的原子访问标志位表示)。如果锁未被占用(例如值为0),直接通过原子操作修改futex(用户态)。全程无需内核介入,性能极高。
当进程释放锁时:修改futex变量(用户态)。如果发现没有其他进程在等待(通过futex值判断),直接返回。同样无需内核介入。
futex虽然是地址,但是也具有值的属性,修改后在内核作为值去判断,而不关心地址在哪里。
3.2 竞争失败被阻塞
内核仅在futex字的值与调用线程提供的预期值(作为futex()调用的参数之一)匹配时才会实施阻塞。当进程发现锁已被占用(futex值非预期)时:需要内核介入,通过系统调用(如futex_wait)将当前线程挂起(内核态),让出CPU。内核会将线程加入与futex变量关联的等待队列。此时线程进入阻塞状态,依赖内核调度。该过程包含三个不可分割的原子化阶段:
- 载入futex字当前值
- 与预期值比对
- 执行线程阻塞
这些操作会与其它线程对同一futex字的并发操作形成严格的全序关系(totally ordered)。因此,futex字成为连接用户态同步与内核阻塞实现的枢纽。
3.3 唤醒阻塞
当锁持有者释放锁时:如果发现有其他进程在等待(通过futex值或内核状态),需通过系统调用(如futex_wake)唤醒等待队列中的线程。内核负责调度被唤醒的线程。
内核态高效唤醒机制:futex内核实现使用哈希链表管理等待队列,通过FUTEX_WAKE只唤醒指定数量的等待线程,避免广播唤醒带来的开销。最新内核优化中,还引入了direct-thread-switch机制,使唤醒线程直接复用即将睡眠线程的时间片,进一步减少调度延迟。
VIP线程插队机制(优先级优化):在内核态等待队列中,futex支持VIP线程插队唤醒,即高优先级线程即使后入队也能优先被唤醒,降低关键线程的等待时延。同时支持优先级继承——当VIP线程等待普通线程持有的锁时,临时提升持锁线程的优先级,避免优先级反转。
4. Futex 系统调用追踪范围
这里讨论加入等待队列和被唤醒的时机,对应获取失败/释放锁这两个场景。
1. 阻塞等待类调用(Blocking Waits)
用于捕获线程阻塞事件(锁竞争、条件变量等待);识别优先级继承(PI)场景(如实时任务调度)。
| 函数原型 | 作用 | 关键参数 |
|---|---|---|
__futex_wait(u32 *uaddr, flags, val, hrtimer_sleeper *to, bitset) |
基础等待操作 | uaddr: Futex地址val: 预期值bitset: 唤醒掩码 |
futex_wait_multiple(futex_vector *vs, count, hrtimer_sleeper *to) |
多Futex等待 | vs: Futex数组count: 数量 |
futex_wait_requeue_pi(u32 *uaddr, flags, val, abs_time, bitset, u32 *uaddr2) |
带优先级继承的等待迁移 | uaddr2: 目标Futex地址 |
2. 唤醒类调用(Wake-ups)
检测锁释放或信号量通知事件。
| 函数原型 | 作用 | 关键参数 |
|---|---|---|
futex_wake(u32 *uaddr, flags, nr_wake, bitset) |
基础唤醒操作 | nr_wake: 唤醒数量 |
futex_wake_op(u32 *uaddr1, flags, u32 *uaddr2, nr_wake, nr_wake2, op) |
原子操作+唤醒 | op: 自定义原子操作 |
3. 优先级继承类调用(Priority Inheritance)
监控实时任务的优先级反转处理。
| 函数原型 | 作用 | 关键参数 |
|---|---|---|
futex_lock_pi(u32 *uaddr, flags, time, trylock) |
PI锁获取 | trylock: 非阻塞模式 |
futex_unlock_pi(u32 *uaddr, flags) |
PI锁释放 |
5. Futex 用户态同步原语分析核心挑战
5.1 用户态优化导致的追踪盲区
高性能实现(如NPTL)可能跳过 futex_wait()/futex_wake() 系统调用,直接通过用户态原子操作完成同步。仅追踪内核侧调用会导致锁状态不完整(无法感知无竞争的锁操作)。
5.2 虚假唤醒与唤醒缺失干扰
task在单次锁获取中多次调用
futex_wait(),难以区分有效等待和重试,举例如下:时间线 → T1: 线程A检查锁状态 → 发现被占用 → 准备wait T2: 线程A调用 futex_wait() → 进入内核阻塞 ↓ [此时锁持有者释放锁,调用 futex_wake()] ↓ T3: 线程A被唤醒 → 返回用户态 T4: 线程A重新检查锁状态 → 发现锁仍被占用! // 虚假唤醒或新竞争者抢到了锁 T5: 线程A再次调用 futex_wait() // 这就是"多次wait"唤醒调用缺失。可能线程已经通过超时或其他机制醒来,没有在等待队列中了。也可能是长临界区,追踪记录这件事的时间太短了。
时间线 → T1: 线程A调用 futex_wait() // 可以被记录追踪 T2: 锁持有者释放锁 T3: 锁持有者调用 futex_wake() 唤醒线程A没有发生 // 唤醒缺失
参考
5.Futex机制的内核优化 写了很多Futex内核级别优化