Futex
Futex
(Fast Userspace Mutex) 是一种用于实现高效同步的机制,允许在用户态完成大部分锁操作,仅在必要时才进入内核态。Futex 最常见的使用场景是互斥锁、条件变量和读写锁。
1. Futex 的用户态使用方式
1.1 基本使用流程
futex()
是 Futex 操作的核心系统调用。用户态程序通过共享内存中的整数变量进行 Futex 操作。Futex 的基本流程如下:
- 检查条件:首先在用户态检查共享变量的值。如果共享变量表示锁可用,直接修改变量以获取锁;如果表示锁不可用,则进入 Futex 等待状态。
- 调用
futex()
:如果锁不可用,用户态程序调用futex()
系统调用进入内核态并阻塞自己,直到锁可用。 - 解锁和唤醒:持有锁的线程在释放锁时检查是否有等待线程,如果有,调用
futex()
系统调用唤醒等待的线程。
1.2 Futex 系统调用
futex()
系统调用的原型如下:
int futex(int *uaddr, int futex_op, int val,
const struct timespec *timeout, int *uaddr2, int val3);
uaddr
:指向用户态的整型变量,该变量被 Futex 用作锁或条件变量。futex_op
:操作码,决定执行的 Futex 操作,常用操作包括:FUTEX_WAIT
:等待uaddr
等于val
。FUTEX_WAKE
:唤醒等待在uaddr
上的一个或多个进程。FUTEX_REQUEUE
:将等待的进程从uaddr
转移到uaddr2
。
val
:FUTEX_WAIT
操作时,uaddr
的预期值。timeout
:可选的超时时间,用于FUTEX_WAIT
。uaddr2
:用于FUTEX_REQUEUE
操作,作为目标 Futex 变量地址。val3
:额外参数,用于某些特定的 Futex 操作。
1.3 Futex 使用示例
以下是一个简单的 Futex 互斥锁实现示例:
#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int futex_lock = 0; // Futex 锁,初始值为 0 表示锁未被持有
void futex_wait(int *futex_addr, int expected_val) {
syscall(SYS_futex, futex_addr, FUTEX_WAIT, expected_val, NULL, NULL, 0);
}
void futex_wake(int *futex_addr) {
syscall(SYS_futex, futex_addr, FUTEX_WAKE, 1, NULL, NULL, 0);
}
void lock() {
while (__sync_val_compare_and_swap(&futex_lock, 0, 1) != 0) {
futex_wait(&futex_lock, 1);
}
}
void unlock() {
futex_lock = 0;
futex_wake(&futex_lock);
}
void *thread_func(void *arg) {
lock();
printf("Thread %ld acquired the lock
", (long)arg);
unlock();
return NULL;
}
int main() {
pthread_t threads[5];
for (long i = 0; i < 5; ++i) {
pthread_create(&threads[i], NULL, thread_func, (void *)i);
}
for (int i = 0; i < 5; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
在这个示例中,futex_lock
是一个 Futex 锁。lock()
函数尝试获取锁,如果失败则调用 futex_wait()
等待。unlock()
函数释放锁并调用 futex_wake()
唤醒等待的线程。
2. Futex 的内核态实现方式
Futex 的内核实现涉及 Linux 内核的几个模块,主要包括对 Futex 系统调用的处理、Futex 队列的管理、以及与进程调度的集成。以下是 Futex 内核实现的主要部分:
2.1 Futex 的内核数据结构
内核中的 Futex 实现主要依赖于 futex_q
和 futex_hash_bucket
这两个数据结构:
futex_q
:每个等待 Futex 的进程在内核中对应一个futex_q
结构体,它代表 Futex 队列中的一个等待节点。futex_hash_bucket
:内核使用哈希表将不同 Futex 的地址映射到 Futex 队列。哈希表的每个槽对应一个futex_hash_bucket
结构体,里面存储了等待在同一个 Futex 地址上的futex_q
队列。
2.2 Futex 系统调用的执行流程
futex()
系统调用的内核实现分为两个主要操作:FUTEX_WAIT
和 FUTEX_WAKE
。
-
FUTEX_WAIT:
- 检查用户态的 Futex 变量是否等于预期值(
val
)。如果不等,直接返回错误。 - 将当前进程添加到 Futex 等待队列中。
- 调用调度器将当前进程挂起,等待 Futex 被唤醒。
- 检查用户态的 Futex 变量是否等于预期值(
-
FUTEX_WAKE:
- 找到对应的 Futex 等待队列。
- 将一个或多个等待在 Futex 队列中的进程从队列中移除。
- 调用调度器将被唤醒的进程标记为可运行。
2.3 内核代码实现示例
Futex 的实现代码主要位于 kernel/futex.c
文件中。以下是 Futex 等待操作的内核代码片段:
int futex_wait(u32 __user *uaddr, u32 val, unsigned int flags,
ktime_t *abs_time, u32 bitset)
{
struct hrtimer_sleeper timeout, *to = NULL;
struct restart_block *restart;
struct futex_hash_bucket *hb;
struct futex_q q;
int ret;
if (abs_time) {
to = &timeout;
hrtimer_init_sleeper(to, current);
}
retry:
ret = get_futex_value_locked(&curval, uaddr);
if (ret)
return ret;
if (curval != val)
return -EAGAIN;
q.key = futex_key(uaddr);
hb = futex_hash_bucket(&q.key);
queue_futex(&q, hb);
if (abs_time)
hrtimer_start_expires(&to->timer, HRTIMER_MODE_ABS);
ret = futex_wait_queue_me(hb, &q, flags, to);
if (ret == -EINTR)
goto retry;
return ret;
}
在这个代码片段中,futex_wait()
函数处理 FUTEX_WAIT
操作。首先检查用户态的 Futex 变量的值,然后将当前进程排入 Futex 等待队列,并根据情况启动超时定时器。最后,将当前进程挂起,直到被唤醒或超时。
3. Futex 的性能优势
Futex 的关键优势在于其混合了用户态和内核态操作。大多数锁操作发生在用户态,避免了进入内核态的开销。只有在发生锁竞争时才进入内核态,这大大提高了性能,尤其是在多核处理器上。
通过 Futex,Linux 能够提供高效的进程和线程同步机制,使得程序在处理高并发场景时能充分利用 CPU 资源,并最大限度地减少上下文切换的开销。