http://blog.sina.com.cn/s/blog_e59371cc0102v29b.html
https://man7.org/linux/man-pages/man7/futex.7.html
https://man7.org/linux/man-pages/man2/futex.2.html
名称
fast user-space locking: 用户层速度很快的锁
简介
Linux内核提供了futexes(fast user-space mutexes),用于实现用户层的锁和信号量的功能。Flutexes是很底层的功能,可以构建更高等级更抽象的锁,比如互斥量,条件变量,读写锁,barriers和信号量。
绝大多数程序都不会直接用到futexes,而是使用一些系统类库提供的方法。
一个futex可以认定为一块内存,在进程或线程之间是共享的。在不同的进程之间,futex不需要必须地址完全一样。本质上来说,futex就像信号量一样,它是一个计数器,可以增加,也可以减少,进程需要一直等待,知道计数器的值变成了正数。
Futex在整个用户层的操作都是非竞争性的。所有竞争性的判断都有内核完成。Futexes可以在任何非竞争性设计上表现出良好的性能。
从本质上来说,futex就相当于一个始终保持一致性的整数,它只能通过原子性的汇编操作。进程可以使用mmap
,通过共享内存片段或是共享内存空间,在多线程之间共享它。
语义
futex是在用户层启用的,可以通过API调用访问到内核态。
如果想启动一个futex,就需要用适当的汇编指令,让CPU原子的增加计数器。然后检查一下,它是不是真的从0变成了1,在这种情况下没有其他的等待者,这个操作顺利的完成了。这是一个快速的最常见的无竞争性的例子。
对于竞争性的情况,计数器需要原子的从-1或是其他负数进行增加。如果存在这种情况,就表示有等待者。用户层需要通知内核用FUTEX_WAKE
唤起一个等待者。
完成等待futex的操作,是一个相反的过程。原子的对计数器减少,并且检测是不是变成了0,如果是的话,就表示完成了,没有发生竞争。其他的情况,就需要进程把计数器设置为-1,然后请求内核等待另一个进程增加futex。这就是FUTEX_WAIT
的操作。
futex接口可以设置一个超时,指定内核等待多久。不过这种情况下,用法将更加负责,写程序的时候也要注意更多细节。
注意
需要说明一下,直接使用futex是很难用的,很多时候需要使用汇编。可以使用系统提供的API调用。
接口
#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);
描述
futex()
提供了一个等待条件变量变成true的系统调用。在共享内存同步上线文中,它通常使用阻塞的设计。当使用futex的时候,多数同步操作在用户层实现。一个用户层的程序,一般在长时间等待一个条件变成true的时候使用futex()
系统调用。其他的一些futex()
操作可以用作唤醒一个等待指定条件变量的进程或是线程。
futex是一个32位的值,它的地址被传入到futex()
。就算在64位系统上,也是一个32位的值,所有平台是一样的。所有的futex操作都是通过这个值来处理。为了能够在不同进程间共享futex,futex是一块共享内存,一般通过mmap
或shmat
创建。因为对于不同进程,这个futex可能不一样,因为每个进程有自己的虚拟内存,但是最终指向的物理内存是同一个地方。在多线程中,futex被放到一个全局变量中,用来各个线程访问。
当我们执行一个futex操作,需要阻塞一个线程时,仅仅在调用线程的futex的值(这个值是futex()
传递的一个参数)是内核需要的的时候,才会阻塞。futex这个值加载的时候,会与期望的值比较,如果其他线程,对于这个futex进行一致性的操作,那么阻塞就会发生,并且是原子的。因此,这个futex的值,在用户层是同步使用的,在内核层,会阻塞在那里,保证用户层的同步。同样,原子的比较和修改共享内存的操作,就是通过futex阻塞的原子的比较和修改的操作进行的。
futexes的一个作用就是实现锁。锁的一个状态,比如已经请求或是还没有被请求,就相当于是原子性的方位一块共享内存的flag。在非抢占的情况下,一个线程可以通过原子操作访问或是修改锁的状态,比如,原子性的把锁通过比较修改操作从没有请求修改为已经请求。这个操作只在用户层,内核不需要维护锁的状态。其他情况下,一个线程可能需要访问一个锁,但是这个锁被另一个线程已经占有了。这样就需要传递一个futex的值来调用futex()
的等待操作,这个futex的值用来表示锁的是否被占有的flag。如果这个锁已经被占用了,那么futex()
操作就会阻塞。也就是这个futex的值还是表明它被占用。当释放锁的时候,需要先重置锁的状态,然后调用futex的操作唤起其他等待的线程。这里将来还需要完善,避免不必要的唤起。
除了基本的等待和唤醒的功能,将来可能会支持更负责的一些操作。
不需要显示的初始化或是析构futex,内核会在调用FUTEX_WAIT
的时候进行处理。
参数
uaddr
参数指向一个futex值。在任何平台上,futexes是一个四字节的整数,必须指向四字节空间的数据。对于futex的操作,定义在futex_op
参数中。val
的值取决于futex_op
。
保留参数 timeout
uaddr2
val3
只有在下面介绍的一些futex操作时才用到。如果任何一个不需要,则忽略它。
对于一些阻塞的操作,timeout
参数是用来定义超时时间的,它指向一个timespec
结构体,定义了超时的时间。但是,尽管上面显示了这个属性,对于一些操作,四个字节的参数可以被一个整数替换。对于这些操作,内核先把timeout
当做unsigned long
,再把它当做uint32_t
,在这页内容中,这个参数被称为val2
。
如果需要,uaddr2
指向第二个futex值。
val3
的意义也取决于本次操作的类型。
Futex操作类型
futex_op
参数包含两部分:一部分是定义这次操作的命令,另一部分是0或是本次操作的更多行为。futex_op
包含如下参数:
FUTEX_PRIVATE_FLAG
这个设置位可以用于所有的futex操作。它告诉内核,这个futex是进程专有的,不可以与其他进程共享。它仅仅用作同一进程的线程间同步。可以让内核做一些优化
为了方便,<linux/futex.h>
定义了一系列的以_PRIVATE
为后缀的常量, 与下面列出的传入FUTEX_PRIVATE_FLAG
参数的操作一样。因此,有FUTEX_WAIT_PRIVATE
FUTEX_WAKE_PRIVATE
等等。
FUTEX_CLOCK_REALTIME
这个参数只能与FUTEX_WAIT_BITSET
FUTEX_WAIT_REQUEUE_PI
FUTEX_WAIT
一起使用
如果设置了这个参数,内核将通过CLOCK_REALTIME
测量超时。
如果没设置这个参数,内核将通过CLOCK_MONOTONIC
测量超时。
futex_op
特殊的设置有如下几个:
FUTEX_WAIT
这个操作用来检测有uaddr
指向的futex是否包含关心的数值val
,如果是,则继续sleep直到FUTEX_WAKE
操作触发。加载futex的操作是原子的。这个加载,从比较关心的数值,到开始sleep,都是原子的,与另外一个对于同一个futex的操作是线性的,串行的,严格按照顺序来执行的。如果线程开始sleep,就表示有一个waiter在futex上。如果futex的值不匹配,回调直接返回失败,错误代码是EAGAIN
。
与期望值对比的目的是为了防止丢失唤醒的操作。如果另一个线程在基于前面的数值阻塞调用之后,修改了这个值,另一个线程在数值改变之后,调用FUTEX_WAIT
之前执行了FUTEX_WAKE
操作,这个调用的线程就会观察到数值变换并且无法唤醒。
这里的意思是,调用FUTEX_WAIT
需要做上面的一个操作,就是检测一下这个值是不是我们需要的,如果不是就等待,如果是就直接运行下去。之所以检测是为了避免丢失唤醒,也就是防止一直等待下去,比如我们在调用FUTEX_WAIT
之前,另一个线程已经调用了FUTEX_WAKE
,那么就不会有线程调用FUTEX_WAKE
,调用FUTEX_WAIT
的线程就永远等不到信号了,也就永远唤醒不了了。
如果timeout
不是NULL
,就表示指向了一个特定的超时时钟。这个超时间隔使用系统时钟的颗粒度四舍五入,可以保证触发不会比定时的时间早。默认情况通过CLOCK_MONOTONIC
测量,但是从Linux 4.5开始,可以在futex_op
中设置FUTEX_CLOCK_REALTIME
使用CLOCK_REALTIME
测量。如果timeout
是NULL
,将会永远阻塞。
注意:对于FUTEX_WAIT
,timeout
是一个关联的值。与其他的futex设置不同,timeout
被认为是一个绝对值。使用通过FUTEX_BITSET_MATCH_ANY
特殊定义的val3
传入FUTEX_WAIT_BITSET
可以获得附带timeout
的FUTEX_WAIT
的值。
uaddr2
和val3
是被忽略的。
FUTEX_WAKE
这个唤醒的操作将在大多数情况下唤起val
中对uaddr
指向的futex的等待者。大多数情况下,val
的值是1(唤起一个)或是INT_MAX
唤起所有。并没有指定,这次唤起肯定会唤起某一个等待者(换句话说,有着高优先级调度的等待者并不能保证一定比低优先级调度的等待者先唤起)。
timeout
uaddr2
val3
被忽略。
FUTEX_FD
创建一个与futex中uaddr
关联的文件描述符。使用完成后,需要关闭返回的文件描述符。当另一个线程或是进程在futex上执行了FUTEX_WAKE
,文件描述符会被select
poll
epoll
捕获到可读的消息。
这个文件描述符可以用作获取异步的通知:如果val
是非零的,当另一个线程或是进程执行FUTEX_WAKE
的时候,调用者会收到通过val
传递的信号。
timeout
uaddr2
val3
被忽略。
由于不常用从Linux 2.6.26起被移除了。
FUTEX_REQUEUE
与下面将要介绍的FUTEX_CMP_REQUEUE
功能一样,只不过没有使用val3
做检查。
FUTEX_CMP_REQUEUE
这个操作先检查uaddr
是否包含val3
。如果没有,这个操作失败,返回EAGAIN
。如果有,就唤醒在uaddr
中val
中最大的等待者。如果还有其他的等待者,这些等待者会从uaddr
的等待队列中移除,然后增加到uaddr2
的等待队列中。val2
制订了在uaddr2
中等待队列的上限。
从uaddr
中加载是一个原子性的内存操作,也就是使用了不同平台对应的原子机器指令。这次加载,与val3
比较,并且所有等待者的队列化,都是原子性的,如果有其他对于同一个futex的操作,都将被严格的按照顺序执行。
一般情况,val
的值是0或是1.定义为INT_MAX
是无效的,因为它会使FUTEX_CMP_REQUEUE
的操作与FUTEX_WAKE
一样。val2
设定的值一般是·或是INT_MAX
。定义0是无效的,因为它会把FUTEX_CMP_REQUEUE
操作当做FUTEX_WAIT
。
FUTEX_CMP_REQUEUE
操作会替换掉前面的FUTEX_REQUEUE
操作。不同的地方就是对uaddr
的检查可以保证请求队列仅仅在特定条件下触发,避免一些条件触发。
FUTEX_REQUEUE
和FUTEX_CMP_REQUEUE
都可以避免在所有的等待者都等待同一个futex时,调用FUTEX_WAKE
导致的惊群效应。比如下面的情景,多个等待者线程在等待B,一个使用futex实现的等待队列:
lock(A)
while (!check_value(V)) {
unlock(A);
block_on(B);
lock(A);
};
unlock(A);
如果一个线程调用FUTEX_WAKE
,所有的等待B的等待者都会唤醒,然后试图获取A的锁。但是这样是无意义的,因为只有一个会立马在锁住A的时候挂起。但是呢,请求操作值唤起了一个等待着,然后把其他所有锁A的移除,直到唤醒的等待者释放了A,下一个才会继续执行。
FUTEX_WAKE_OP
支持用户空间用例,同一时间处理多个futex。最有名的例子就是pthread_cond_signal
的实现,需要请求两个futexes,一个用mutex实现,一个用条件变量的等待队列实现。FUTEX_WAKE_OP
可以实现这种功能,不会导致过多的竞争和上下文切换。
FUTEX_WAKE_OP
操作等同于下面代码原子的操作。在两个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
,修改这个值,这些都是原子的操作 -
唤起
uaddr
上指定的futex上的val
中的最大的等待者 -
依赖于与原来在
uaddr2
中的futex值比较的结果,唤醒uaddr2
指向的futex中在val2
里面最大的等待者
操作和比较都是基于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
对应的值如下:
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; */
此外,如果1 << oparg
,op
会有如下或位运算:
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
的返回值是在uaddr
和uaddr2
上指定的等待队列的个数。
FUTEX_WAIT_BITSET
这个与FUTEX_WAIT
一样,只不过val3
传入了一个32位的标识给内核。这个标识,至少要有一位设置了,在内核中标示等待者的状态。
如果timeout
不是NULL
,那就根据指定的结构体定义一个超时时间,如果是NULL
,就表示无限等待下去。
uaddr2
被忽略。
FUTEX_WAKE_BITSET
这个操作与FUTEX_WAKE
一样,除了使用了val3
,这个参数是一个32位的标识,传入到内核中,至少设置了一位,用来标明哪一个等待者可以被唤醒。选择唤醒的等待者,通过和唤醒位的标识进行与位运算计算得到。这个标识存在内核中,用来表示等待者的状态。这个标识通过FUTEX_WAIT_BITSET
设置。所有和唤醒位进行与位运算不是0的就可以唤醒,其余的继续等待。
FUTEX_WAIT_BITSET
和FUTEX_WAKE_BITSET
的作用就是选择唤醒被同一个futex阻塞的符合条件的等待者。但是,需要注意,基于这种由位预算实现的多路复用的功能,可能比使用多个futexes效率差一些, 因为基于位运算的多路复用,内核需要每次都检测所有的等待者的状态,包括那些,没有相关等待位标识的等待者,也就是与这次唤醒无关的等待者,内核也需要判断,因为内核也不知道这个等待者到底与这次唤醒有没有关系。
常量FUTEX_BITSET_MATCH_ANY
,可以匹配32位标识中所有的位,可以用作val3
的参数,传入到FUTEX_WAIT_BITSET
和FUTEX_WAKE_BITSET
的调用中。除了timeout
参数的区别外,FUTEX_WAIT
与设置参数val3
为FUTEX_BITSET_MATCH_ANY
的FUTEX_WAIT_BITSET
是一样的。可以让任何一个唤醒者唤醒。FUTEX_WAKE
与设置val3
为FUTEX_BITSET_MATCH_ANY
的FUTEX_WAKE_BITSET
一样,可以唤醒任何一个等待着。
忽略uaddr2
参数
优先级继承的futexes
Linux支持优先级继承(PI)的futexes,避免使用常规的futex锁导致的优先级反转的问题。优先级反转的问题是,一个高优先级的任务等待一个被低优先级占用的锁,而中间优先级的任务继续占用低优先级的CPU处理时间。所以低优先级的任务无法释放锁,高优先级的任务也被阻塞。
优先级继承是为了解决优先级反转的问题设计的。如果高优先级的任务被低优先级的任务阻塞,那么这时,低优先级的任务会临时的提升为高优先级的任务,所以就不会被中间优先级的任务抢占了,这样就可以让程序继续执行,释放锁。如果要使这个设计生效,优先级继承必须是可传递的,也就是如果一个高优先级的任务被一个低优先级的任务阻塞,而低优先级的任务又被一个中优先级的任务阻塞,以此类推,对于任意长度的链,这两个任务,所有说,更多的任务,也就死这个锁链上的所有的任务,都会提升到高优先级的权限。
从用户层看,futex的PI意思,从用户层到底层是共识的。不同于其他的futex操作,PI-futex操作是一个非常具体的设计。
下面介绍的PI-futex操作与其他的futex操作不一样, 在使用下面的futex字段是,增加了权限:
-
如果锁没有获取到,futex的字段应该是0
-
如果锁获取到了,futex的字段应该是所属线程的ID
-
如果锁已经被占有,当其他的线程竞争这个锁时,
FUTEX_WAITERS
的位应该设置到futex的字段中,也就是设置为如下的数据:
FUTEX_WAITERS | TID
注意,如果PI futex没有所属者并且没有设置FUTEX_WAITERS
位,那么这个PI futex是无效的。
有了这个设计,用户层的应用可以原子的请求一个没有被请求的锁或是释放一个锁。比如在x86架构上的cmpxchg
,比较和交换的操作。请求一个锁就是使用比较和交换原子的设置futex字段位调用者的TID,如果原来是0的话。释放锁就是使用比较和交换,把futex字段设置位0,如果当前字段的值与TID相同的话。
如果futex已经被请求了,也就是非0,等待者必须使用FUTEX_LOCK_PI
操作去获得锁。如果其他线程等待锁,FUTEX_WAITERS
字段已经被设置,锁的所有者必须使用FUTEX_UNLOCK_PI
来释放锁。
在这种情况下,调用者被强制发送到内核,比如futex()
调用,然后他们直接处理一个所谓的RT-mutex,一个内核锁机制,使用了权限继承的设计。当一个RT-mutex被捕获到,在调用线程返回到用户层之前,futex的值会被修改。
要记住,内核会在返回到用户层之前修改futex的值。这是为了防止futex在结束的时候是一个非法的值,比如有所有者,但是数值是0,或是有所有者,但是没有对应的FUTEX_WAITERS
标识位。
如果futex在内核中有一个相关的RT-mutex,也就是阻塞了等待者,但是futex/RT-mutex的所有者销毁了,内核会清理RT-mutex并且把它赋给下一个等待者。这一轮用户层请求的数值也会根据这个改变。为了标明这个是需要的,内核会给新的拥有的线程的futex字段设置FUTEX_OWNER_DIED
位。用户层可以通过FUTEX_OWNER_DIED
位来判断这种情况发生,这样就可以清理销毁所有者的状态。
PI futexes使用在futex_op
中如下特殊的参数。记住,PI futex操作必须是成对出现的,是一些特殊请求的子操作:
-
FUTEX_LOCK_PI
和FUTEX_TRYLOCK_PI
与FUTEX_UNLOCK_PI
是成对出现的。futex所属的调用线程必须调用FUTEX_UNLOCK_PI
,不这样操作的话就会导致一个EPERM
错误。 -
FUTEX_WAIT_REQUEUE_PI
与FUTEX_CMP_REQUEUE_PI
成对出现。这个实现,non-PI futex必须与 PI futex区别开来,不然就会报EINVAL
错误。除此之外,val
(准备唤醒的等待者的数字)必须是1,不然也会报EINVAL
的错误。
PI futex的操作如下:
FUTEX_LOCK_PI
这个操作用在,尝试通过原子的方式在用户层获取锁的时候,因为futex字段已经是非零的值,特别是已经包含了拥有锁的TID,失败的情况下。
这个操作会检测futex的字段,如果是0,内核尝试原子的设置futex的值为当前调用的TID。如果是非零,内核原子的设置FUTEX_WAITERS
的位,标明futex的所有者不能通过在用户层原子的解锁futex。然后,内核要做如下操作:
-
尝试找到与这个TID相关的线程
-
代表所有者创建或是重用内核状态。如果这个是第一个等待者,那么futex就没有内核状态,内核就会通过RT-mutex的锁创建一个,futex的所有者也变成RT-mutex的所有者。如果已经有了等待者,现有的状态就会被重用
-
把等待者附到futex上。也就是等待者放到RT-mutex的等待队列中。
如果有多个等待者存在,放到队列中的等待者的优先级是递减的。更多关于权限排序的信息,可以参考sched
中的SCHED_DEADLINE
SCHED_FIFO
和SCHED_RR
。所有者继承了等待者的CPU带宽,如果等待者设定了SCHED_DEADLINE
权限,或是等待者的权限,如果等待者设定了SCHED_RR
或SCHED_FIFO
权限。这个继承是延续在锁的链表中,防止嵌套的锁和死锁。
timeout
参数提供了一个超时。如果不是NULL
,它指向一个专有的超时结构体,通过CLOCK_REALTIME
测量。如果是NULL
,表示永久等待。
忽略uaddr2
val
val3
参数。
FUTEX_TRYLOCK_PI
这个操作一般用在用户层原子的尝试请求一个uaddr
指向的锁,但是由于futex的值不是0,返回失败。
因为内核比用户层可以得到更多的信息,所以从内核请求这个锁,如果futex字段包含了原先的状态,比如FUTEX_WAITERS
或FUTEX_OWNER_DIED
,有可能会是成功的。当futex的所有者销毁的时候,会发生这种情况。用户层无法处理这种没有宿主的情况,但是内核可以解决并请求futex。
忽略uaddr2
val
timeout
val3
FUTEX_UNLOCK_PI
唤起uaddr
指向的futex中在FUTEX_LOCK_PI
中等待的最高权限的等待者。
当不能原子把在uaddr
中用户层的数据从TID修改为0的时候,调用这个接口。
忽略uaddr2
val
timeout
val3`
FUTEX_CMP_REQUEUE_PI
这个操作是FUTEX_CMP_REQUEUE
的PI关心的变种。它把因为FUTEX_WAIT_REQUEUE_PI
阻塞的在uaddr
中的等待者从non-PI(uaddr)放到PI队列(uaddr2)中。
如果用FUTEX_CMP_REQUEUE
,会唤醒在uaddr
中最大数值的等待者。但是,调用FUTEX_CMP_REQUEUE_PI
,需要把val
设置位1,主要目的就是为了避免经群效用。剩下的等待者会从uaddr
中删除,天见到uaddr2
的等待队列中。
val2
和val3
与FUTEX_CMP_REQUEUE
中的作用相同。
FUTEX_WAIT_REQUEUE_PI
在uaddr
上等待的non-PI futex有可能会被另一个任务通过FUTEX_CMP_REQUEUE_PI
在uaddr2
上请求。这个在uaddr
上等待的操作与FUTEX_WAIT
一样。
在uaddr
上的等待者可以在删除后不妨去uaddr2
的队列,这时候FUTEX_WAIT_REQUEUE_PI
会报错,返回EAGAIN
。
同样,如果timeout
不是NULL
,那么就表示指定了一个超时计时器,如果是NULL
就表示永久等待。
忽略val3
FUTEX_WAIT_REQUEUE_PI
和FUTEX_CMP_REQUEUE_PI
是为了一个特殊的用法:支持优先级继承相关的POSIX的线程条件变量。目的就是为了保证用户层和内核层同步这些操作总是成对出现的。因此,在FUTEX_WAIT_REQUEUE_PI
操作中,用户层的应用需要提前定义在FUTEX_CMP_REQUEUE_PI
中的队列。
返回值
出错的时候,所有的操作都会返回-1,然后设置errno
。
成功的返回值各不相同,详细的介绍如下:
FUTEX_WAIT
如果调用者被唤醒,返回0。需要注意,在正常的不相关的futex的使用中,也会因为使用了原来的futex数值的内存空间而触发唤醒。一般是基于futex的一些pthread mutexes的实现,会在一些情况下导致这个问题。因此,调用者需要特别关系返回0时的情况,有可能是一个假的唤醒,需要判断是否阻塞还是向下运行。
FUTEX_WAKE
返回被唤醒的等待者的数字
FUTEX_FD
返回一个与futex相关的文件描述符
FUTEX_REQUEUE
返回被唤醒的等待者的数字
FUTEX_CMP_REQUEUE
返回所有的被唤醒或是放到uaddr2
中的等待者的数字,如果大于val
,就返回被放到uaddr2
中等待者的数字
FUTEX_WAKE_OP
返回被唤醒所有等待者的数字。是在uaddr
和uaddr2
指向的futex的总和
FUTEX_WAIT_BITSET
如果被唤醒,返回0
FUTEX_WAKE_BITSET
返回被唤醒的等待者的数字
FUTEX_LOCK_PI
如果加锁成功,返回0
FUTEX_TRYLOCK_PI
如果加锁成功,返回0
FUTEX_UNLOCK_PI
如果解锁成功返回0
FUTEX_CMP_REQUEUE_PI
返回所有唤醒或是放到uaddr2
中的等待者数字。如果返回值比val
大,返回放到uaddr2
中指向的futex的数字。
FUTEX_WAIT_REQUEUE_PI
如果成功的请求到uaddr2
中的futex,返回0
错误
EACCES
无法读内存中futex的值
EAGAIN (FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI)
uaddr
指向的数字与期望的数字不同
注意:在Linux下,可能是EAGAIN
,也可能是EWOULDBLOCK
,这两个是同一个意思,不同的内核可能不一样。
EAGAIN (FUTEX_CMP_REQUEUE, FUTEX_CMP_REQUEUE_PI)
uaddr
指向的数字与val3
设定的不一致
EAGAIN (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)
uaddr
指向的,在FUTEX_CMP_REQUEUE_PI
中是uaddr2
指向的,futex所属的线程ID,已经退出了,但是底层没有清理,再试一次。
EDEADLK (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)
uaddr
指向的futex已经被调用者锁住了
EDEADLK (FUTEX_CMP_REQUEUE_PI)
把一个等待着放到uaddr2
指向的PI futex中,出发了死锁
EFAULT
指针参数,比如uaddr
uaddr2
timeout
指向了非法的地址
EINTR
FUTEX_WAIT
或FUTEX_WAIT_BITSET
操作被一个信号中断。
EINVAL
futex_op
设置的超时参数是非法的,tv_sec
是负数或者tv_nsec
大于1,000,000,000
EINVAL
futex_op
操作的uaddr
和uaddr2
中一个或是两个指针指向的结构体是非法的,没有四字节对齐。
EINVAL (FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET)
val3
设置的位标记是0
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
指向的状态,用户层和内核层不一致。也就是在FUTEX_LOCK_PI
中由uaddr
指向的等待者不一致
EINVAL (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI)
内核检测到uaddr
指向的状态,用户层和内核层不一致。这表示有可能是状态损坏了或是内核发现在uaddr
上通过FUTEX_WAIT
或FUTEX_WAIT_BITSET
的等待者不一致。
EINVAL (FUTEX_CMP_REQUEUE_PI)
内核检测到,uaddr2
指向的,用户层和内核层的状态不一致。也就是内核检测到FUTEX_WAIT
或FUTEX_WAIT_BITSET
调用中uaddr2
指向的等待者不一致
EINVAL (FUTEX_CMP_REQUEUE_PI)
内核检测到,uaddr
指向的,用户层和内核层的状态不一致。
EINVAL (FUTEX_CMP_REQUEUE_PI)
内核检测到,uaddr
指向的,用户层和内核层的状态不一致。
EINVAL (FUTEX_CMP_REQUEUE_PI)
试图把一个等待着放入到futex队列。但是futex是由该等待者调用FUTEX_WAIT_REQUEUE_PI
请求的
EINVAL (FUTEX_CMP_REQUEUE_PI)
val
不是1
EINVAL
错误的参数
ENFILE (FUTEX_FD)
打开的文件描述符超过了系统的最大限制
ENOMEM (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)
内核无法为状态信息申请内存
ENOSYS
futex_op
中定义的非法的操作
ENOSYS
在futex_op
中定义了FUTEX_CLOCK_REALTIME
操作,但是对应的调用不是FUTEX_WAIT
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_CMP_REQUEUE_PI
中是uaddr
,指向的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
定义的操作超时
注意
Glibc不支持对系统调用的封装,因为这个是系统调用,所以Glibc不支持futex
一些更高级的代码设计师基于futexes,包括POSIX的信号量,还有各种各样的POSIX线程同步机制,比如互斥量,条件变量,读写锁,屏障
示例
下面的示例,父进程和子进程使用一对放在共享匿名mapping中的futexes进行同步访问共享资源,一个terminal。两个进程都写nloops消息,一个命令行参数,默认是5,到terminal上,然后同步保证交替的写。跑起来的结果如下:
$ ./futex_demo
Parent (18534) 0
Child (18535) 0
Parent (18534) 1
Child (18535) 1
Parent (18534) 2
Child (18535) 2
Parent (18534) 3
Child (18535) 3
Parent (18534) 4
Child (18535) 4
源码
/* futex_demo.c
Usage: futex_demo [nloops]
(Default: 5)
Demonstrate the use of futexes in a program where parent and child
use a pair of futexes located inside a shared anonymous mapping to
synchronize access to a shared resource: the terminal. The two
processes each write 'num-loops' messages to the terminal and employ
a synchronization protocol that ensures that they alternate in
writing messages.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <stdatomic.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;
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, uaddr2, 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;
/* atomic_compare_exchange_strong(ptr, oldval, newval)
atomically performs the equivalent of:
if (*ptr == *oldval)
*ptr = newval;
It returns true if the test yielded true and *ptr was updated. */
while (1) {
/* Is the futex available? */
const int one = 1;
if (atomic_compare_exchange_strong(futexp, &one, 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 the wake any futex waiters,
so that if the peer is blocked in fpost(), it can proceed. */
static void
fpost(int *futexp)
{
int s;
/* atomic_compare_exchange_strong() was described in comments above */
const int zero = 0;
if (atomic_compare_exchange_strong(futexp, &zero, 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
", (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
", (long)getpid(), j);
fpost(futex1);
}
wait(NULL);
exit(EXIT_SUCCESS);
}