这两天复习分布式系统,顺便看了一下 Linux 下的 futex(2) 同步机制。
简单来说,futex(2) 是一个新的同步机制,在作用方面与一般的 mutex 比较相似。使用 futex(2) 进行同步的两个执行体必须通过一段共享的内存空间关联起来——这段内存空间可以具有相同或者不同的进程中地址,只要它们映射到同一块核心中内存地址即可——因此,线程和进程(通过 sysvshm 关联)都可以利用 futex 进行同步。
futex(2) 支持两个操作,分别是等待(wait)和唤醒(wake)。futex(2) 的系统接口是 int futex (void *futex, int op, int val, const struct timespec *timeout) ,这里第一个参数 futex 指向被映射的内存地址,op 表示操作类型,timeout 表示阻塞时间,而 val 对于不同的值具有不同的意义。在使用者角度,futex(2) 相比 pthread_mutex / pthread_condition_variable 和 sysv semaphore,有一些独一无二的特色。
futex 的等待操作可以指定条件。将常量 FUTEX_WAIT 作为参数传递给 futex 作为第二个参数 op 的值,这个时候第三个参数 op 的值将作为一个比较对象。当 *(int *)futex == val 的时候,调用体将被阻塞直到 timeout 时间到达,或者被其它执行体唤醒。如果 *(int *)futex != val,则锁定过程会被直接跳过。系统通过精心编写的汇编代码,保证了这个 load-and-compare 过程的原子性。这个进行比较的过程是完全在用户态完成的,也就是说,如果值不相等,不符合等待的条件,则这个“系统调用”并不会切换进内核态执行,也就比其它的同步方式少了至少一次内核态往返切换的损耗。这也是 futex 名字(Fast Userspace muTEX)的来历。
futex 的 wake 可以指定被唤醒的进程数。用常量 FUTEX_WAKE 传递给 op 表示唤醒操作,第三个值 val 表示了最多被唤醒的执行体数量。因此,用 futex 实现 semaphore 的“广播”和单一执行体的唤醒都是非常灵活便捷的。
futex(2) 和 futex(4) 一堆废话,偏偏没有一个示例代码。我在我的代码中
#include <linux/futex.h>
之后直接使用
futex( (void *)p, FUTEX_{WAIT|WAKE}, 0, (struct timespec *)NULL )
之后来回报了一堆错误。加上 -I /usr/src/linux/include 之后的错误是,连接器找不到 futex 函数的实现?!
从 rusty 的 kernel.org 页面下载了 futex-2.2.tar.gz,看了一下,里面是直接用 _syscall 定义了一个 sys_futex
_syscall4(int,sys_futex,
int *, futex,
int, op,
int, val,
struct timespec *, rel);
但是我用这个声明就怎么也过不去,错误就在 _syscall4 一行。从 Google 搜索了一下,发现有人说在旧版内核中 futex 接口没有被正式 export,只提供了 sys_futex 这个系统调用。于是我尝试把 _syscall 的第二个参数 sys_futex 改成 futex,这样才编译通过。看来是说在我的内核(2.6.11,FC4 标准内核)中,futex 已经被正式 export 了,只是暂时还没有被引入 Glibc 中。
一个简单的 post / wait 操作:
int wait_always( void *futexpos ) {
// 0 if woken by FUTEX_WAKE
return futex( futexpos, FUTEX_WAIT, *(int *)futexpos, NULL );
}
int wait_eq( void *futexpos, int val ) {
return futex( futexpos, FUTEX_WAIT, val, NULL );
}
int wake( void *futexpos, int num ) {
// return #processes been woken up
return futex( futexpos, FUTEX_WAKE, num, NULL );
}
上面代码中,传递给 wait_* 和 signal 的 futex 参数必须:1、是多线程程序中的同一地址;或 2、是某个 sysv shm 映射得到的内存地址中的同一地址;3、对某个非 /dev/zero 的文件(目前只测试了普通文件和/dev/zero)内存映射得到的地址,其中 mmap 必须具有 PROT_READ 权限。