一、NAME
futex - 快速用户空间锁定
二、SYNOPSIS
#include <linux/futex.h> #include <sys/time.h> int futex(int *uaddr, int futex_op, int val, const struct timespec *timeout, /* or: uint32_t val2 */int *uaddr2, int val3); /* 使用时需自己封装 */ static int futex(int *uaddr, int futex_op, int val, const struct timespec *timeout, int *uaddr2, int val3) { return syscall(SYS_futex, uaddr, futex_op, val, timeout, uaddr, val3); }
注意:这个系统调用没有 glibc 包装器; 见注释。
三、DESCRIPTION
futex() 系统调用提供了一种等待某个条件成立的方法。 它通常用作共享内存同步上下文中的阻塞构造。 使用 futex 时,大多数同步操作都是在用户空间中执行的。
用户空间程序仅在程序可能必须阻塞更长的时间直到条件变为真时才使用 futex() 系统调用。 其他 futex() 操作可用于唤醒任何等待特定条件的进程或线程。
futex 是一个 32 位的值 —— 下面称为 futex 字 —— 它的地址被提供给 futex() 系统调用。 (Futex 在所有平台上都是 32 位大小,包括 64 位系统。)所有 futex
操作都受此值控制。 为了在进程之间共享 futex,将 futex 放置在共享内存区域中,使用(例如)mmap(2) 或 shmat(2) 创建。 (因此,futex 字在不同的进程中可
能有不同的虚拟地址,但这些地址都指向物理内存中的同一位置。)在多线程程序中,将 futex 字放在所有线程共享的全局变量中就足够了。
当执行一个请求阻塞线程的 futex 操作时,仅当 futex 字具有调用线程提供的值(作为 futex() 调用的参数之一)作为 futex 字的预期值时,内核才会阻塞。 futex
字的值的加载、该值与预期值的比较以及实际的阻塞将自动发生,并且将相对于其他线程在同一个 futex 字上执行的并发操作完全排序。 因此,futex 字用于连接用户
空间的同步和内核的阻塞实现。 类似于可能更改共享内存的原子比较和交换操作,通过 futex 阻塞是原子比较和阻塞操作。
futexes 的一种用途是实现锁。锁的状态(即获得或未获得)可以表示为共享内存中的原子访问标志。在无竞争的情况下,线程可以使用原子指令访问或修改锁定状态,
例如使用原子比较和交换指令将其从未被获取状态原子地更改为已被获取状态。 (这些指令完全在用户模式下执行,内核不维护有关锁状态的信息。)另一方面,一个线
程可能无法获取到锁,因为锁已经被另一个线程获取。然后它可以将锁的标志作为一个 futex 字和表示获取状态的值作为预期值传递给 futex() 等待操作。当且仅当锁仍
被获取时(即 futex 字中的值仍与“之前获取的状态”匹配),此 futex() 操作才会阻塞。释放锁时,线程必须首先将锁状态重置为未被获取的状态,然后执行 futex 操作,
唤醒阻塞在 futex 字的锁定标志上的线程(这可以进一步优化以避免不必要的唤醒)。有关如何使用 futex 的更多详细信息,请参阅 futex(7)。
除了基本的等待和唤醒 futex 功能外,还有更多 futex 操作旨在支持更复杂的用例。
请注意,使用 futex 不需要显式初始化或销毁; 只有在下面描述的 FUTEX_WAIT 等操作正在对特定的 futex 字执行时,内核才维护一个 futex(即内核内部的实现工件)。
3.1. Arguments
uaddr 参数指向 futex 字。 在所有平台上,futex 都是四字节整数,并且必须在四字节边界上对齐。 对 futex 执行的操作在 futex_op 参数中指定; val 是一个值,
其意义和目的取决于 futex_op。
剩下的参数(timeout、uaddr2 和 val3)仅对下面描述的某些 futex 操作是必需的。 如果不需要这些参数之一,则将其忽略。
对于几个阻塞操作, timeout 参数是一个指向 timespec 结构的指针,该结构指定该操作的超时时间。 然而,尽管有上面显示的原型,对于某些操作,最低有效四个字
节被用作整数,其含义由操作确定。 对于这些操作,内核首先将超时值转换为 unsigned long,然后转换为 uint32_t,并且在本页的其余部分中,当以这种方式解释时,
此参数称为 val2。
如果需要,uaddr2 参数是指向操作使用的第二个 futex 字的指针。 最后一个整数参数 val3 的解释取决于是什么操作。
3.2. Futex operations
futex_op 参数由两部分组成:指定要执行的操作的命令,与修改操作行为的零个或多个选项进行按位或运算。 futex_op 中可能包含的选项如下:
(1) FUTEX_PRIVATE_FLAG (since Linux 2.6.22)
此选项位可用于所有 futex 操作。 它告诉内核 futex 是进程私有的,不与其它进程共享(即,它仅用于同一进程的线程之间的同步)。 这允许内核进行一些额外的性能优化。
为方便起见,<linux/futex.h> 定义了一组带有后缀 _PRIVATE 的常量,它们等效于下面列出的所有操作,但是和 FUTEX_PRIVATE_FLAG 位或成的常量值。 因此,有 FUTEX_WAIT_PRIVATE、FUTEX_WAKE_PRIVATE 等等。
(2) FUTEX_CLOCK_REALTIME (since Linux 2.6.28)
此选项位只能用于 FUTEX_WAIT_BITSET 和 FUTEX_WAIT_REQUEUE_PI 操作。
如果设置了此选项,内核将超时视为基于 CLOCK_REALTIME 的绝对时间。
如果未设置此选项,内核将超时视为相对时间,基于 CLOCK_MONOTONIC 时钟测量。
futex_op 中指定的操作是以下之一:
(1) FUTEX_WAIT (since Linux 2.6.0)
此操作测试地址 uaddr 指向的 futex 字处值是否仍然包含预期值 val,如果是,则休眠等待对 futex 字的 FUTEX_WAKE 操作。 对 futex 字的值的加载是原子内存访问(即,使用相应架构的原子机器指令)。
这个加载、与期望值的比较以及开始睡眠都是原子性地执行的,并且相对于同一个 futex 字上的其他 futex 操作是完全有序的。 如果线程开始休眠,则认为它是这个 futex 字的等待者。 如果 futex 值
与 val 不匹配,则调用将立即失败并返回错误 EAGAIN。
与期望值比较的目的是防止丢失唤醒。 如果在调用线程根据之前的值决定阻塞之后,另一个线程改变了futex字的值,并且如果另一个线程在值改变之后和这个 FUTEX_WAIT 操作之前,执行了一个 FUTEX_WAKE
操作(或类似的唤醒),那么调用线程将观察到值的变化并且不会开始休眠。
如果 timeout 参数为非 NULL,则其内容指定根据 CLOCK_MONOTONIC 时钟测量的相对超时时间。 (此时间间隔将向上舍入到系统时钟粒度,并保证不会提前到期。)如果 timeout 为 NULL,则调用将无限期阻塞。
参数 uaddr2 和 val3 被忽略。
(2) FUTEX_WAKE (since Linux 2.6.0)
这个操作最多唤醒 val 个正在等待 uaddr 指向的 futex 字的等待者(例如,在 FUTEX_WAIT 中)。 最常见的是,val 被指定为 1(唤醒单个waiter)或 INT_MAX(唤醒所有waiter)。 不保证唤醒哪些等
待者(例如,不能保证具有较高调度优先级的等待者优先于具有较低优先级的等待者被唤醒)。
参数 timeout、uaddr2 和 val3 被忽略。
(3) FUTEX_FD (从 Linux 2.6.0 到并包括 Linux 2.6.25)
此操作创建一个与 uaddr 指向的 futex 关联的文件描述符。 在使用后调用者必须关闭返回的文件描述符。 当另一个进程或线程对 futex 字执行 FUTEX_WAKE 时,文件描述符通
过 select(2)、poll(2) 和 epoll(7) 表示为可读
文件描述符可用于获取异步通知:如果 val 不为零,则当另一个进程或线程执行 FUTEX_WAKE 时,调用者将接收到 val 中传递的信号值。
参数 timeout、uaddr2 和 val3 被忽略。
因为它本质上是活泼的,所以 FUTEX_FD 已从 Linux 2.6.26 开始删除。
(4) FUTEX_REQUEUE (since Linux 2.6.0)
此操作执行与 FUTEX_CMP_REQUEUE 相同的任务(见下文),除了不使用 val3 中的值进行检查。(参数 val3 被忽略。)
(5) FUTEX_CMP_REQUEUE (since Linux 2.6.7)
此操作首先检查位置 uaddr 是否仍包含值 val3。 如果不是,则操作失败并显示错误 EAGAIN。 否则,该操作最多会唤醒在 uaddr 处等待 futex 的 val 个waiters。 如果有超过 val 的等待者,则剩余的
等待者将重 uaddr 处的 futex 的等待队列中移除,并添加到在 uaddr2 处的 futex 的等待队列中。 val2 参数指定重新排队到 uaddr2 处的 futex 的waiters个数的上限。
来自 uaddr 的加载是原子内存访问(即,使用相应架构的原子机器指令)。 这种加载、与 val3 的比较以及任何等待者的重新排队都是原子地执行的,并且相对于同一个 futex 字上的其他操作是完全有序的。
为 val 指定的典型值是 0 或 1。(指定 INT_MAX 没有用,因为它会使 FUTEX_CMP_REQUEUE 操作等同于 FUTEX_WAKE。)通过 val2 指定的限制值通常是 1 或 INT_MAX。(将参数指定为 0 没有用,因为它会
使 FUTEX_CMP_REQUEUE 操作等同于 FUTEX_WAIT。)
添加了 FUTEX_CMP_REQUEUE 操作以替代早期的 FUTEX_REQUEUE。 不同之处在于检查 uaddr 处的值可用于确保仅在某些条件下才发生重新排队,从而可以在某些用例中避免竞争。
FUTEX_REQUEUE 和 FUTEX_CMP_REQUEUE 都可以用来避免使用 FUTEX_WAKE 在所有被唤醒的waiters都需要获取另一个 futex 的情况下可能发生的“thundering herd”唤醒。 考虑以下场景,其中多个等待者线程
在 B 上等待,这是一个使用 futex 实现的等待队列:
lock(A) while (!check_value(V)) { unlock(A); block_on(B); lock(A); }; unlock(A);
如果一个执行唤醒的线程使用了 FUTEX_WAKE,那么所有等待 B 的等待者都会被唤醒,他们都会尝试获取锁 A。但是,以这种方式唤醒所有线程将毫无意义,因为除了一个线程之外的所有线程都会立即再次
阻塞在锁 A 上。 相比之下,重新排队操作只唤醒一个等待者并将其他等待者移动到锁 A 上,当被唤醒的等待者释放锁 A 时,下一个等待者可以继续运行。
(6) FUTEX_WAKE_OP (since Linux 2.6.14)
添加此操作是为了支持一些必须同时处理多个 futex 的用户空间用例。 最值得注意的例子是 pthread_cond_signal(3) 的实现,它需要对两个 futex 进行操作,一个用于实现互斥锁,一个用于实现与条件
变量关联的等待队列。 FUTEX_WAKE_OP 允许在不导致高竞争率和上下文切换的情况下实施此类情况。
FUTEX_WAKE_OP 操作等价于对两个提供的 futex 字中的任何一个上的其他 futex 操作,以原子方式和完全排序的方式执行以下代码:
int oldval = *(int *) uaddr2; *(int *) uaddr2 = oldval op oparg; futex(uaddr, FUTEX_WAKE, val, 0, 0, 0); if (oldval cmp cmparg) futex(uaddr2, FUTEX_WAKE, val2, 0, 0, 0);
换句话说,FUTEX_WAKE_OP 做了以下事情:
* 将 futex 字的原始值保存在 uaddr2,并进行修改 uaddr2 的 futex 值的操作; 这是一个原子 读取-修改-写入内存 访问(即,使用相应架构的原子机器指令)
* 为 uaddr 处的 futex 字唤醒 futex 上的最多 val 个waiters; 和
* 根据 uaddr2 处 futex 字的原始值的测试结果,为 uaddr2 处的 futex 字唤醒最多 val2 个waiters。
要执行的操作和比较被编码在参数 val3 的位中。 如图所示,编码为:
+---+---+-----------+-----------+ |op |cmp| oparg | cmparg | +---+---+-----------+-----------+ 4 4 12 12 <== # of bits
用代码表示,编码为:
#define FUTEX_OP(op, oparg, cmp, cmparg) \ (((op & 0xf) << 28) | \ ((cmp & 0xf) << 24) | \ ((oparg & 0xfff) << 12) | \ (cmparg & 0xfff))
上面的 op 和 cmp 分别是下面列出的代码之一。 oparg 和 cmparg 组件是文字数值,除非下面注明。
op 组件具有以下值之一:
FUTEX_OP_SET 0 /* uaddr2 = oparg; */ FUTEX_OP_ADD 1 /* uaddr2 += oparg; */ FUTEX_OP_OR 2 /* uaddr2 |= oparg; */ FUTEX_OP_ANDN 3 /* uaddr2 &= ~oparg; */ FUTEX_OP_XOR 4 /* uaddr2 ^= oparg; */
此外,将以下值按位或运算到 op 会导致 (1 << oparg) 用作操作数:
FUTEX_OP_ARG_SHIFT 8 /* Use (1 << oparg) as operand */
cmp 字段是以下之一:
FUTEX_OP_CMP_EQ 0 /* if (oldval == cmparg) wake */ FUTEX_OP_CMP_NE 1 /* if (oldval != cmparg) wake */ FUTEX_OP_CMP_LT 2 /* if (oldval < cmparg) wake */ FUTEX_OP_CMP_LE 3 /* if (oldval <= cmparg) wake */ FUTEX_OP_CMP_GT 4 /* if (oldval > cmparg) wake */ FUTEX_OP_CMP_GE 5 /* if (oldval >= cmparg) wake */
FUTEX_WAKE_OP 的返回值是 futex uaddr 上唤醒的等待者数量加上 futex uaddr2 上唤醒的等待者数量之和。
(7) FUTEX_WAIT_BITSET (since Linux 2.6.25)
此操作类似于 FUTEX_WAIT,只是 val3 用于向内核提供 32 位掩码。 该位掩码存储在waiter的内核内部状态中。 有关详细信息,请参阅 FUTEX_WAKE_BITSET 的描述。
FUTEX_WAIT_BITSET 操作对超时参数的解释也与 FUTEX_WAIT 不同。 请参阅上面对 FUTEX_CLOCK_REALTIME 的讨论。
uaddr2 参数被忽略。
(8) FUTEX_WAKE_BITSET (since Linux 2.6.25)
此操作与 FUTEX_WAKE 相同,只是 val3 参数用于向内核提供 32 位位掩码。 该位掩码用于选择应该唤醒哪些 waiters。 选择是通过“wake”位掩码(即 val3 中的值)和存储在waiter的内核内部状态中的
位掩码(即"wait"位掩码,即使用 FUTEX_WAIT_BITSET 设置)的位与操作。 位与结果非零的所有waiters将被唤醒,剩下的waiter将继续休眠。
FUTEX_WAIT_BITSET 和 FUTEX_WAKE_BITSET 的作用是允许在同一个 futex 上阻塞的多个等待者之间进行选择性唤醒。 但是,请注意,根据用例,在 futex 上使用这种位掩码多路复用功能可能比简单地使用
多个 futex 效率低,因为使用位掩码多路复用需要内核检查 futex 上的所有等待者,包括那些对被唤醒不感兴趣的人(即,他们没有在他们的“等待”位掩码中设置相关位)。
uaddr2 和 timeout 参数被忽略。
FUTEX_WAIT 和 FUTEX_WAKE 操作对应于位掩码全为 1 的 FUTEX_WAIT_BITSET 和 FUTEX_WAKE_BITSET 操作。
3.3. 优先级继承 futex
Linux 支持优先级继承 (PI) futex 以处理普通 futex 锁可能遇到的优先级反转问题。 优先级反转是当一个高优先级任务被阻塞等待获取一个低优先级任务持有的锁,而处于中等优先级的任务不断地从 CPU 抢占
低优先级任务时发生的问题。 因此,低优先级任务在释放锁方面没有进展,而高优先级任务保持阻塞状态。
优先级继承是一种处理优先级反转问题的机制。通过这种机制,当高优先级任务被低优先级任务持有的锁阻塞时,低优先级任务的优先级会暂时提升到高优先级任务的优先级,这样它就不会被任何中间级别优先级
的任务抢占,因此可以在释放锁方面取得进展。为了有效,优先级继承必须是可传递的,这意味着如果一个高优先级任务阻塞在了一个低优先级任务持有的锁上,而该低优先级任务本身又被另一个中等优先级任务
持有的锁阻塞(依此类推,对于任意长度的链),那么这两个任务(或更一般地说,锁链中的所有任务)都将它们的优先级提高到与高优先级任务相同。
从用户空间的角度来看,让 futex PI 感知的是用户空间和内核之间关于 futex 字的值的策略协议(如下所述),以及下面描述的 PI-futex 操作的使用。(与上述其他 futex 操作不同,PI-futex 操作旨在实
现非常具体的 IPC 机制。)
下面描述的 PI-futex 操作与其他 futex 操作的不同之处在于它们对 futex 字的值的使用施加了策略:
* 如果没有获得锁,futex 字的值为 0。
* 如果获得了锁,则 futex 字的值应为拥有线程的线程 ID(TID;参见 gettid(2))。
* 如果锁是被持有的并且有线程争用这个锁,则 FUTEX_WAITERS 位应设置在 futex 字的值中; 换句话说,这个值是:FUTEX_WAITERS | TID
(请注意,对于没有所有者和 FUTEX_WAITERS 设置的 PI futex 字,这是无效的。)
有了这个策略,用户空间应用程序可以使用在用户模式下执行的原子指令获取未获取的锁或释放锁(例如,比较和交换操作,例如 x86 架构上的 cmpxchg)。 获取锁只需使用比较和交换将 futex 字的值自动
设置为调用者的 TID,如果其先前的值为 0。如果先前的值是预期的 TID,释放锁需要使用比较和交换指令将 futex 字的值设置为 0。
如果一个 futex 已经被获取(即,有一个非零值),等待者必须使用 FUTEX_LOCK_PI 操作来获取锁。 如果其他线程正在等待锁,则在 futex 值中设置 FUTEX_WAITERS 位; 在这种情况下,锁的拥有者必须使
用 FUTEX_UNLOCK_PI 操作来释放锁。
在调用者被迫进入内核的情况下(即,需要执行 futex() 调用),它们会直接处理所谓的 RT-mutex,这是一种内核锁定机制,实现了所需的优先级继承语义 . 获取到 RT-mutex 后,在调用线程返回用户空间之前
futex 值会相应更新。
需要注意的是,内核会在返回用户空间之前更新 futex 字的值。(这可以防止 futex 字的值最终处于无效状态的可能性,例如具有owner但值为 0,或者具有waiters但未设置 FUTEX_WAITERS 位。)
如果 futex 在内核中有一个关联的 RT-mutex(即有阻塞的等待者)并且 futex/RT-mutex 的所有者意外死亡,那么内核会清理 RT-mutex 并将其交给下一个等待者 . 这反过来又要求相应地更新用户空间值。
为了表明这是必需的,内核设置 futex 字中的 FUTEX_OWNER_DIED 位以及新所有者的线程 ID。 用户空间可以通过判断 FUTEX_OWNER_DIED 位是否存在来检测这种情况,然后负责清理已经死去的owner留下的陈
旧状态。
通过在 futex_op 中指定下面列出的值之一来操作 PI futex。 请注意,PI futex 操作必须作为配对操作使用,并且需要满足一些附加要求:
* FUTEX_LOCK_PI 和 FUTEX_TRYLOCK_PI 与 FUTEX_UNLOCK_PI 配对。 FUTEX_UNLOCK_PI 只能在调用线程拥有的 futex 上调用,如值策略所定义,否则会导致错误 EPERM。
* FUTEX_WAIT_REQUEUE_PI 与 FUTEX_CMP_REQUEUE_PI 配对。 这必须从非 PI futex 执行到不同的 PI futex(或错误 EINVAL 结果)。 此外,val(要唤醒的waiter数量)必须为 1(或错误 EINVAL 结果)。
PI futex 的操作如下:
(1) FUTEX_LOCK_PI (since Linux 2.6.18)
在尝试通过原子用户模式指令获取锁失败后使用此操作,因为 futex 字具有非零值 —— 特别是因为它包含锁所有者的(PID 命名空间特定的)TID。
该操作检查地址 uaddr 处的 futex 字的值。 如果该值为 0,则内核尝试自动将 futex 值设置为调用者的 TID。 如果 futex 字的值非零,内核会自动设置 FUTEX_WAITERS 位,这会通知 futex 的拥有者
它无法通过将 futex 值设置为 0 来自动解锁用户空间中的 futex。之后,内核:
1. 尝试查找与所有者 TID 关联的线程。
2. 代表所有者创建或重用内核状态。(如果这是第一个等待者,则此 futex 没有内核状态,因此通过锁定 RT-mutex 来创建内核状态,并使 futex 所有者成为 RT-mutex 的所有者。如果存在现有等待
者,则现有状态被重用。)
3. 将等待者附加到 futex(即,等待者在 RT-mutex 等待者列表中排队)。
如果存在多个waiters,则waiters的排队按优先级降序排列。(有关优先级排序的信息,请参阅 sched(7) 中对 SCHED_DEADLINE、SCHED_FIFO 和 SCHED_RR 调度策略的讨论。)所有者要么继承waiter
的 CPU 带宽(如果waiter在 SCHED_DEADLINE 策略下调度)或waiter的优先级 (如果服务员是在 SCHED_RR 或 SCHED_FIFO 策略下调度的)。在嵌套锁定的情况下,此继承遵循锁定链并执行
死锁检测。
timeout 参数为锁定尝试提供超时。 它被解释为绝对时间,根据 CLOCK_REALTIME 时钟测量。 如果 timeout 为 NULL,则操作将无限期阻塞。
uaddr2、val 和 val3 参数被忽略。
(2) FUTEX_TRYLOCK_PI (since Linux 2.6.18)
此操作尝试获取 uaddr 处的锁。 当用户空间原子获取,由于 futex 字不为 0 而未成功时调用它。
因为内核可以访问比用户空间更多的状态信息,如果在 futex 字(即,可访问使用空间的状态信息)包含陈旧状态(FUTEX_WAITERS 和 / 或 FUTEX_OWNER_DIED)。 当 futex 的所有者去世时,可能会发生
这种情况。 用户空间不能以无竞争的方式处理这种情况,但内核可以解决这个问题并获取 futex。
uaddr2、val、timeout 和 val3 参数被忽略。
(3) FUTEX_UNLOCK_PI (since Linux 2.6.18)
此操作唤醒在 uaddr 地址处的 futex 上的 FUTEX_LOCK_PI 中等待的最高优先级的waiter。
当 uaddr 处的用户空间值不能从(所有者的)TID 原子地更改为 0 时,将调用此方法。
uaddr2、val、timeout 和 val3 参数被忽略。
(4) FUTEX_CMP_REQUEUE_PI (since Linux 2.6.31)
此操作是 FUTEX_CMP_REQUEUE 的 PI-aware 变体。 它将被 uaddr 处的 FUTEX_WAIT_REQUEUE_PI 阻塞的waiters 从非 PI 源 futex (uaddr) 重新排队到 PI 目标 futex (uaddr2) 上。
与 FUTEX_CMP_REQUEUE 一样,此操作最多会唤醒在 uaddr 处等待 futex 的 val 个等待者。 但是,对于 FUTEX_CMP_REQUEUE_PI,val 必须为 1(因为主要是为了避免thundering herd)。剩余的等待者
将从 uaddr 处的源 futex 的等待队列中移除,然后添加到 uaddr2 处的目标 futex 的等待队列中。
val2 和 val3 参数的用途与 FUTEX_CMP_REQUEUE 相同。
(5) FUTEX_WAIT_REQUEUE_PI (since Linux 2.6.31)
在 uaddr 等待非 PI futex,并可能被重新排队(通过另一个任务中的 FUTEX_CMP_REQUEUE_PI 操作)到 uaddr2 处的 PI futex。 uaddr 上的等待操作与 FUTEX_WAIT 相同。
通过另一个任务中的 FUTEX_WAKE 操作,可以将等待者从 uaddr 上的等待中移除,而无需在 uaddr2 上重新排队。 在这种情况下,FUTEX_WAIT_REQUEUE_PI 操作失败并出现错误 EAGAIN。
如果 timeout 不为 NULL,则它指定的是等待操作的超时时间; 这个超时被解释为上面对 FUTEX_CLOCK_REALTIME 选项的描述。 如果 timeout 为 NULL,则操作可以无限期地阻塞。
val3 参数被忽略。
添加了 FUTEX_WAIT_REQUEUE_PI 和 FUTEX_CMP_REQUEUE_PI 以支持一个相当具体的用例:支持优先级继承感知 POSIX 线程条件变量。 这个想法是这些操作需要总是成对的,以确保用户空间和内核保持同步。
因此,在 FUTEX_WAIT_REQUEUE_PI 操作中,用户空间应用程序预先指定在 FUTEX_CMP_REQUEUE_PI 操作中发生的重新排队的目标。
四、RETURN VALUE
如果发生错误(并假设 futex() 是通过 syscall(2) 调用的),所有操作都返回 -1 并设置 errno 以指示错误的原因。
成功的返回值取决于操作,如下表所述:
4.1. FUTEX_WAIT
如果调用者被唤醒,则返回 0。 请注意,唤醒也可能是由不相关代码中的常见 futex 使用模式引起的,这些代码恰好以前使用过 futex 字的内存位置(例如,在某些情况下,典型的基于 futex 的
Pthreads 互斥锁实现可能会导致这种情况)。 因此,调用者应始终保守地假设返回值 0 可能意味着虚假唤醒,并使用 futex 字的值(即用户空间同步方案)来决定是否继续阻塞。
4.2. FUTEX_WAKE
返回被唤醒的waiters的数量。
4.3. FUTEX_FD
返回与 futex 关联的新文件描述符。
4.4. FUTEX_REQUEUE
返回被唤醒的waiters的数量。
4.5. FUTEX_CMP_REQUEUE
返回 uaddr2 处的 futex 字被唤醒或重新排队到 futex 的 waiter 的总数。 如果此值大于 val,则差值是重新排队到 uaddr2 处的 futex 字的 waiter 的数量。
4.6. FUTEX_WAKE_OP
返回被唤醒的waitersd的总数。 这是 uaddr 和 uaddr2 两处的 futex 词的两个 futex 上的唤醒的waiters的总和。
4.7. FUTEX_WAIT_BITSET
如果调用者被唤醒,则返回 0。 请参阅 FUTEX_WAIT 以了解如何在实践中正确解释这一点。
4.8. FUTEX_WAKE_BITSET
返回被唤醒的waiters的数量。
4.9. FUTEX_LOCK_PI
如果 futex 被成功锁定,则返回 0。
4.10. FUTEX_TRYLOCK_PI
如果 futex 成功被锁定,则返回 0。
4.11. FUTEX_UNLOCK_PI
如果 futex 成功解锁,则返回 0。
4.12. FUTEX_CMP_REQUEUE_PI
返回 uaddr2 处的 futex 字被唤醒或重新排队到 futex 的waiters的总数。 如果此值大于 val,则差值是 uaddr2 处的 futex 字重新排队到 futex 的waiters的数量。
4.13. FUTEX_WAIT_REQUEUE_PI
如果调用者成功地重新排队到 uaddr2 处的 futex 字的 futex 上,则返回 0。
五、ERRORS
EACCES 对 futex 字的内存没有读取访问权限。
EAGAIN (FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI) uaddr 指向的值不等于调用时的预期值 val。注意:在 Linux 上,符号名称 EAGAIN 和 EWOULDBLOCK(两者都出现在内核 futex 代码的不同部分)具有相同的值。
EAGAIN (FUTEX_CMP_REQUEUE, FUTEX_CMP_REQUEUE_PI) uaddr 指向的值不等于期望值 val3。
EAGAIN (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) uaddr 的 futex 所有者线程 ID(对于 FUTEX_CMP_REQUEUE_PI:uaddr2)即将退出,但尚未处理内部状态清理。 再试一次。
EDEADLK (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) uaddr 处的 futex 字已经被调用者锁定住了。
EDEADLK (FUTEX_CMP_REQUEUE_PI) 在 uaddr2 将等待者重新排队到 PI futex 以获取 futex 字时,内核检测到死锁。
EFAULT 所需的指针参数(即 uaddr、uaddr2 或 timeout)未指向有效的用户空间地址。
EINTR FUTEX_WAIT 或 FUTEX_WAIT_BITSET 操作被信号中断(参见 signal(7))。 在 Linux 2.6.22 之前的内核中,此错误也可能在虚假唤醒时返回; 从 Linux 2.6.22 开始,这种情况不再发生。
EINVAL futex_op 中的操作是使用超时的操作之一,但提供的超时参数无效(tv_sec 小于零,或 tv_nsec 不小于 1,000,000,000)。
EINVAL futex_op 中指定的操作使用指针 uaddr 和 uaddr2 之一或两者,但其中之一不指向有效对象——即地址不是四字节对齐的。
EINVAL (FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET) val3 中提供的位掩码为零。
EINVAL (FUTEX_CMP_REQUEUE_PI) uaddr 等于 uaddr2(即尝试重新排队到同一个 futex)。
EINVAL (FUTEX_FD) val 中提供的信号编号无效。
EINVAL (FUTEX_WAKE, FUTEX_WAKE_OP, FUTEX_WAKE_BITSET, FUTEX_REQUEUE, FUTEX_CMP_REQUEUE) 内核检测到 uaddr 的用户空间状态和内核状态之间的不一致 —— 也就是说,它检测到一个在 uaddr 上的 FUTEX_LOCK_PI 中等待的 waiter。
EINVAL (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI) 内核检测到 uaddr 的用户空间状态和内核状态之间存在不一致。这表明状态损坏或内核在 uaddr 上发现了一个等待者,该等待者正在通过 FUTEX_WAIT 或 FUTEX_WAIT_BITSET 等待。
EINVAL (FUTEX_CMP_REQUEUE_PI) 内核检测到 uaddr2 的用户空间状态与内核状态不一致;也就是说,内核检测到一个等待者,它通过 uaddr2 上的 FUTEX_WAIT 或 FUTEX_WAIT_BITSET 等待。
EINVAL (FUTEX_CMP_REQUEUE_PI) 内核检测到 uaddr 的用户空间状态与内核状态不一致;也就是说,内核通过 uaddr 上的 FUTEX_WAIT 或 FUTEX_WAIT_BITSET 检测到一个等待者。
EINVAL (FUTEX_CMP_REQUEUE_PI) 内核检测到 uaddr 的用户空间状态与内核状态不一致;也就是说,内核检测到一个通过 FUTEX_LOCK_PI(而不是 FUTEX_WAIT_REQUEUE_PI)等待 uaddr 的等待者。
EINVAL (FUTEX_CMP_REQUEUE_PI) 试图将一个waiter重新排队到一个 futex,而不是由该服务员的匹配 FUTEX_WAIT_REQUEUE_PI 调用指定的。
EINVAL (FUTEX_CMP_REQUEUE_PI) val 参数不是 1。
EINVAL 参数无效。
ENOMEM (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) 内核无法分配内存来保存状态信息。
ENFILE (FUTEX_FD) 已达到打开文件总数的系统范围限制。
ENOSYS futex_op 中指定的操作无效。
ENOSYS 在 futex_op 中指定了 FUTEX_CLOCK_REALTIME 选项,但伴随的操作既不是 FUTEX_WAIT_BITSET 也不是 FUTEX_WAIT_REQUEUE_PI。
ENOSYS (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI, FUTEX_CMP_REQUEUE_PI, FUTEX_WAIT_REQUEUE_PI) 运行时检查确定操作不可用。 PI-futex 操作并非在所有架构上都实现,并且在某些 CPU 变体上不受支持。
EPERM (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) 不允许调用者将自己附加到 uaddr 的 futex(对于 FUTEX_CMP_REQUEUE_PI:uaddr2 的 futex)。 (这可能是由用户空间中的状态损坏引起的。)
EPERM (FUTEX_UNLOCK_PI) 调用者不拥有由 futex 字表示的锁。
ESRCH (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI) uaddr 处的 futex 字中的线程 ID 不存在。
ESRCH (FUTEX_CMP_REQUEUE_PI) uaddr2 处的 futex 字中的线程 ID 不存在。
ETIMEDOUT futex_op 中的操作使用了 timeout 中指定的超时时间,并且在操作完成之前超时时间已到。
六、VERSIONS
Futex 最初是在 Linux 2.6.0 的稳定内核版本中提供的。
Linux 2.5.7 中合并了最初的 futex 支持,但语义与上述内容不同。 Linux 2.5.40 中引入了具有本页所述语义的四参数系统调用。 Linux 2.5.70 中添加了第五个参数,Linux 2.6.7 中添加了第六个参数。
七、CONFORMING TO
此系统调用是特定于 Linux 的。
八、NOTES
Glibc 没有为这个系统调用提供包装器; 使用 syscall(2) 调用它。
几个更高级别的编程抽象是通过 futexes 实现的,包括 POSIX 信号量和各种 POSIX 线程同步机制(互斥锁、条件变量、读写锁和屏障)。
九、EXAMPLE
下面的程序演示了 futex 在程序中的使用,其中父进程和子进程使用位于共享匿名映射内的一对 futex 来同步对共享资源终端的访问。 这两个进程分别向终端写入 nloops(一个命令行参数,如果省略,则默认为 5)
消息,并使用同步协议确保它们在写入消息时交替。 运行此程序后,我们会看到如下输出:
~/origin_tmp/4.futex$ ./pp Parent (611045) 0 Child (611046) 0 Parent (611045) 1 Child (611046) 1 Parent (611045) 2 Child (611046) 2 Parent (611045) 3 Child (611046) 3 Parent (611045) 4 Child (611046) 4
futex_demo.c:
#define _GNU_SOURCE #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/mman.h> #include <sys/syscall.h> #include <linux/futex.h> #include <sys/time.h> #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0) static int *futex1, *futex2, *iaddr; /* 到目前为止,还必须得这样用,否则报“undefined reference to `futex'” */ static int futex(int *uaddr, int futex_op, int val, const struct timespec *timeout, int *uaddr2, int val3) { return syscall(SYS_futex, uaddr, futex_op, val, timeout, uaddr, val3); } /* * Acquire the futex pointed to by 'futexp': wait for its value to * become 1, and then set the value to 0. */ static void fwait(int *futexp) { int s; /* * __sync_bool_compare_and_swap(ptr, oldval, newval) is a gcc * built-in function. It atomically performs the equivalent of: * * if (*ptr == oldval) * *ptr = newval; * * It returns true if the test yielded true and *ptr was updated. * The alternative here would be to employ the equivalent atomic * machine-language instructions. For further information, see * the GCC Manual. */ while (1) { /* Is the futex available? */ if (__sync_bool_compare_and_swap(futexp, 1, 0)) break; /* Yes */ /* Futex is not available; wait */ s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0); if (s == -1 && errno != EAGAIN) errExit("futex-FUTEX_WAIT"); } } /* * Release the futex pointed to by 'futexp': if the futex currently * has the value 0, set its value to 1 and wake any futex waiters, * so that if the peer is blocked in fpost(), it can proceed. */ static void fpost(int *futexp) { int s; /* __sync_bool_compare_and_swap() was described in comments above */ if (__sync_bool_compare_and_swap(futexp, 0, 1)) { s = futex(futexp, FUTEX_WAKE, 1, NULL, NULL, 0); if (s == -1) errExit("futex-FUTEX_WAKE"); } } int main(int argc, char *argv[]) { /* 这些栈空间的变量,父子进程各有一份,互不干扰 */ pid_t childPid; int j, nloops; setbuf(stdout, NULL); nloops = (argc > 1) ? atoi(argv[1]) : 5; /* * Create a shared anonymous mapping that will hold the futexes. * Since the futexes are being shared between processes, we * subsequently use the "shared" futex operations (i.e., not the * ones suffixed "_PRIVATE") */ iaddr = mmap(NULL, sizeof(int) * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); if (iaddr == MAP_FAILED) errExit("mmap"); futex1 = &iaddr[0]; futex2 = &iaddr[1]; *futex1 = 0; /* State: unavailable */ *futex2 = 1; /* State: available */ /* Create a child process that inherits the shared anonymous mapping */ childPid = fork(); if (childPid == -1) errExit("fork"); if (childPid == 0) { /* Child */ for (j = 0; j < nloops; j++) { fwait(futex1); printf("Child (%ld) %d\n", (long) getpid(), j); fpost(futex2); } exit(EXIT_SUCCESS); } /* Parent falls through to here */ for (j = 0; j < nloops; j++) { fwait(futex2); printf("Parent (%ld) %d\n", (long) getpid(), j); fpost(futex1); } wait(NULL); exit(EXIT_SUCCESS); }
十、SEE ALSO
get_robust_list(2), restart_syscall(2), pthread_mutexattr_getprotocol(3), futex(7), sched(7)
The following kernel source files:
* Documentation/pi-futex.txt
* Documentation/futex-requeue-pi.txt
* Documentation/locking/rt-mutex.txt
* Documentation/locking/rt-mutex-design.txt
* Documentation/robust-futex-ABI.txt
Franke, H., Russell, R., and Kirwood, M., 2002. Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux (from proceedings of the Ottawa Linux Symposium 2002),
⟨http://kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf⟩
Hart, D., 2009. A futex overview and update, ⟨http://lwn.net/Articles/360699/⟩
Hart, D. and Guniguntala, D., 2009. Requeue-PI: Making Glibc Condvars PI-Aware (from proceedings of the 2009 Real-Time Linux Workshop), ⟨http://lwn.net/images/conf/rtlws11/papers/proc/p10.pdf⟩
Drepper, U., 2011. Futexes Are Tricky, ⟨http://www.akkadia.org/drepper/futex.pdf⟩
Futex example library, futex-*.tar.bz2 at
⟨ftp://ftp.kernel.org/pub/linux/kernel/people/rusty/⟩
十一、COLOPHON
此页面是 Linux 手册页项目 4.04 版的一部分。 可以在 http://www.kernel.org/doc/man-pages/ 找到项目的描述、关于报告错误的信息以及此页面的最新版本。
标签:PI,futex,uaddr2,uaddr,FUTEX,REQUEUE,Futex,man From: https://www.cnblogs.com/hellokitty2/p/16879264.html