在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这个内核不必定能正确的运行使用glibc的程序",那futex是什么?和glibc又有什么关系呢?linux
Futex 是Fast Userspace muTexes的缩写,由Hubertus Franke, Matthew Kirkwood, Ingo Molnar and Rusty Russell共同设计完成。几位都是linux领域的专家,其中可能Ingo Molnar你们更熟悉一些,毕竟是O(1)调度器和CFS的实现者。程序员
Futex按英文翻译过来就是快速用户空间互斥体。其设计思想其实 不难理解,在传统的Unix系统中,System V IPC(inter process communication),如 semaphores, msgqueues, sockets还有文件锁机制(flock())等进程间同步机制都是对一个内核对象操做来完成的,这个内核对象对要同步的进程都是可见的,其提供了共享 的状态信息和原子操做。当进程间要同步的时候必需要经过系统调用(如semop())在内核中完成。但是经研究发现,不少同步是无竞争的,即某个进程进入 互斥区,到再从某个互斥区出来这段时间,经常是没有进程也要进这个互斥区或者请求同一同步变量的。可是在这种状况下,这个进程也要陷入内核去看看有没有人 和它竞争,退出的时侯还要陷入内核去看看有没有进程等待在同一同步变量上。这些没必要要的系统调用(或者说内核陷入)形成了大量的性能开销。为了解决这个问 题,Futex就应运而生,Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间经过mmap共享一段内存,futex变量就位于这段共享 的内存中且操做是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,若是没有竞争发生,则只修改futex,而不 用再执行系统调用了。当经过访问futex变量告诉进程有竞争发生,则仍是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是经过在用户态的检查,(motivation)若是了解到没有竞争就不用陷入内核了,大大提升了low-contention时候的效率。 Linux从2.5.7开始支持Futex。多线程
Futex是一种用户态和内核态混合机制,因此须要两个部分合做完成,linux上提供了sys_futex系统调用,对进程竞争状况下的同步处理提供支持。socket
#include <linux/futex.h> #include <sys/time.h> int futex (int *uaddr, int op, int val, const struct timespec *timeout,int *uaddr2, int val3); #define __NR_futex 240
虽然参数有点长,其实经常使用的就是前面三个,后面的timeout你们都能理解,其余的也常被ignore。函数
uaddr就是用户态下共享内存的地址,里面存放的是一个对齐的整型计数器。post
op存放着操做类型。定义的有5中,这里我简单的介绍一下两种,剩下的感兴趣的本身去man futex性能
FUTEX_WAIT: 原子性的检查uaddr中计数器的值是否为val,若是是则让进程休眠,直到FUTEX_WAKE或者超时(time-out)。也就是把进程挂到uaddr相对应的等待队列上去。ui
FUTEX_WAKE: 最多唤醒val个等待在uaddr上进程。atom
可见FUTEX_WAIT和FUTEX_WAKE只是用来挂起或者唤醒进程,固然这部分工做也只能在内核态下完成。有些人尝试着直接使用futex系统调 用来实现进程同步,并寄但愿得到futex的性能优点,这是有问题的。应该区分futex同步机制和futex系统调用。futex同步机制还包括用户态 下的操做,咱们将在下节提到。spa
全部的futex同步操做都应该从用户空间开始,首先建立一个futex同步变量,也就是位于共享内存的一个整型计数器。
当 进程尝试持有锁或者要进入互斥区的时候,对futex执行"down"操做,即原子性的给futex同步变量减1。若是同步变量变为0,则没有竞争发生, 进程照常执行。若是同步变量是个负数,则意味着有竞争发生,须要调用futex系统调用的futex_wait操做休眠当前进程。
当进程释放锁或 者要离开互斥区的时候,对futex进行"up"操做,即原子性的给futex同步变量加1。若是同步变量由0变成1,则没有竞争发生,进程照常执行。如 果加以前同步变量是负数,则意味着有竞争发生,须要调用futex系统调用的futex_wake操做唤醒一个或者多个等待进程。
这里的原子性加减一般是用CAS(Compare and Swap)完成的,与平台相关。CAS的基本形式是:CAS(addr,old,new),当addr中存放的值等于old时,用new对其替换。在x86平台上有专门的一条指令来完成它: cmpxchg。
可见: futex是从用户态开始,由用户态和核心态协调完成的。
进程或者线程均可以利用futex来进行同步。
对于线程,状况比较简单,由于线程共享虚拟内存空间,虚拟地址就能够惟一的标识出futex变量,即线程用一样的虚拟地址来访问futex变量。
对 于进程,状况相对复杂,由于进程有独立的虚拟内存空间,只有经过mmap()让它们共享一段地址空间来使用futex变量。每一个进程用来访问futex的 虚拟地址能够是不同的,只要系统知道全部的这些虚拟地址都映射到同一个物理内存地址,并用物理内存地址来惟一标识futex变量。
在linux中进行多线程开发,同步是不可回避的一个问题。在POSIX标准中定义了三种线程同步机制: Mutexes(互斥量), Condition Variables(条件变量)和POSIX Semaphores(信号量)。NPTL基本上实现了POSIX,而glibc又使用NPTL做为本身的线程库。所以glibc中包含了这三种同步机制 的实现(固然还包括其余的同步机制,如APUE里提到的读写锁)。
变量定义: sem_t sem;
初始化: sem_init(&sem,0,1);
进入加锁: sem_wait(&sem);
退出解锁: sem_post(&sem);
变量定义: pthread_mutex_t mut;
初始化: pthread_mutex_init(&mut,NULL);
进入加锁: pthread_mutex_lock(&mut);
退出解锁: pthread_mutex_unlock(&mut);
进入互斥区的时候,会执行sem_wait(sem_t *sem),sem_wait的实现以下:
int sem_wait (sem_t *sem) { int *futex = (int *) sem; if (atomic_decrement_if_positive (futex) > 0) return 0; int err = lll_futex_wait (futex, 0); return -1; )
atomic_decrement_if_positive()的语义就是若是传入参数是正数就将其原子性的减一并当即返回。若是信号量为正,在Semaphores的语义中意味着没有竞争发生,若是没有竞争,就给信号量减一后直接返回了。
若是传入参数不是正数,即意味着有竞争,调用lll_futex_wait(futex,0),lll_futex_wait是个宏,展开后为:
#define lll_futex_wait(futex, val) \ ({ \ ... __asm __volatile (LLL_EBX_LOAD \ LLL_ENTER_KERNEL \ LLL_EBX_LOAD \ : "=a" (__status) \ : "0" (SYS_futex), LLL_EBX_REG (futex), "S" (0), \ "c" (FUTEX_WAIT), "d" (_val), \ "i" (offsetof (tcbhead_t, sysinfo)) \ : "memory"); \ ... \ })
能够看到当发生竞争的时候,sem_wait会调用SYS_futex系统调用,并在val=0的时候执行FUTEX_WAIT,让当前线程休眠。
从 这个例子咱们能够看出,在Semaphores的实现过程当中使用了futex,不只仅是说其使用了futex系统调用(再重申一遍只使用futex系统调 用是不够的),而是整个创建在futex机制上,包括用户态下的操做和核心态下的操做。其实对于其余glibc的同步机制来讲也是同样,都采纳了 futex做为其基础。因此才会在futex的manual中说:对于大多数程序员不须要直接使用futexes,取而代之的是依靠创建在futex之上 的系统库,如NPTL线程库(most programmers will in fact not be using futexes directly but instead rely on system libraries built on them, such as the NPTL pthreads implementation)。因此才会有若是在编译内核的时候不 Enable futex support,就"不必定能正确的运行使用Glibc的程序"。
上回说到Glibc中(NPTL)的线程同步方式如Mutex,Semaphore等都使用了futex做为其基础。那么实际使用是什么样子,又会碰到什么问题呢?
先来看一个使用semaphore同步的例子。
sem_t sem_a; void *task1(); int main(void){ int ret=0; pthread_t thrd1; sem_init(&sem_a,0,1); ret=pthread_create(&thrd1,NULL,task1,NULL); //建立子线程 pthread_join(thrd1,NULL); //等待子线程结束 } void *task1() { int sval = 0; sem_wait(&sem_a); //持有信号量 sleep(5); //do_nothing sem_getvalue(&sem_a,&sval); printf("sem value = %d\n",sval); sem_post(&sem_a); //释放信号量 }
程序很简单,咱们在主线程(执行main的线程)中建立了一个线程,并用join等待其结束。在子线程中,先持有信号量,而后休息一下子,再释放信号量,结束。
由于这段代码中只有一个线程使用信号量,也就是没有线程间竞争发生,按照futex的理论,由于没有竞争,因此全部的锁操做都将在用户态中完成,而不会执行系统调用而陷入内核。咱们用strace来跟踪一下这段程序的执行过程当中所发生的系统调用:
... 20533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL <unfinished ...> 20534 futex(0x8049870, FUTEX_WAKE, 1) = 0 20533 <... futex resumed> ) = 0 ...
20533是main线程的id,20534是其子线程的id。出乎咱们意料以外的是这段程序仍是发生了两次futex系统调用,咱们来分析一下这分别是什么缘由形成的。
20534 futex(0x8049870, FUTEX_WAKE, 1) = 0
子 线程仍是执行了FUTEX_WAKE的系统调用,就是在sem_post(&sem_a);的时候,请求内核唤醒一个等待在sem_a上的线程, 其返回值是0,表示如今并无线程等待在sem_a(这是固然的,由于就这么一个线程在使用sem_a),此次futex系统调用白作了。这彷佛和 futex的理论有些出入,咱们再来看一下sem_post的实现。
int sem_post (sem_t *sem) { int *futex = (int *) sem; int nr = atomic_increment_val (futex); int err = lll_futex_wake (futex, nr); return 0; }
咱们看到,Glibc在实现sem_post的时候给futex原子性的加上1后,无论futex的值是什么,都执行了lll_futex_wake(),即futex(FUTEX_WAKE)系统调用。
在 第二部分中(见前文),咱们分析了sem_wait的实现,当没有竞争的时候是不会有futex调用的,如今看来真的是这样,可是在sem_post的时 候,不管有无竞争,都会调用sys_futex(),为何会这样呢?我以为应该结合semaphore的语义来理解。在semaphore的语义 中,sem_wait()的意思是:"挂起当前进程,直到semaphore的值为非0,它会原子性的减小semaphore计数值。" 咱们能够看到,semaphore中是经过0或者非0来判断阻塞或者非阻塞线程。即不管有多少线程在竞争这把锁,只要使用了 semaphore,semaphore的值都会是0。这样,当线程推出互斥区,执行sem_post(),释放semaphore的时候,将其值由0改 1,并不知道是否有线程阻塞在这个semaphore上,因此只好无论怎么样都执行futex(uaddr, FUTEX_WAKE, 1)尝试着唤醒一个进程。而相反的,当sem_wait(),若是semaphore由1变0,则意味着没有竞争发生,因此没必要去执行futex系统调 用。咱们假设一下,若是抛开这个语义,若是容许semaphore值为负,则也能够在sem_post()的时候,实现futex机制。
那另外一个futex系统调用是怎么形成的呢? 是由于pthread_join();
在Glibc中,pthread_join也是用futex系统调用实现的。程序中的pthread_join(thrd1,NULL); 就对应着
20533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL <unfinished ...>
很 好解释,主线程要等待子线程(id号20534上)结束的时候,调用futex(FUTEX_WAIT),并把var参数设置为要等待的子线程号 (20534),而后等待在一个地址为0xb7db1be8的futex变量上。当子线程结束后,系统会负责把主线程唤醒。因而主线程就
20533 <... futex resumed> ) = 0
恢复运行了。
要注意的是,若是在执行pthread_join()的时候,要join的线程已经结束了,就不会再调用futex()阻塞当前进程了。
咱们把上面的程序稍微改改:
在main函数中:
int main(void){ ... sem_init(&sem_a,0,1); ret=pthread_create(&thrd1,NULL,task1,NULL); ret=pthread_create(&thrd2,NULL,task1,NULL); ret=pthread_create(&thrd3,NULL,task1,NULL); ret=pthread_create(&thrd4,NULL,task1,NULL); pthread_join(thrd1,NULL); pthread_join(thrd2,NULL); pthread_join(thrd3,NULL); pthread_join(thrd4,NULL); ... }
这样就有更的线程参与sem_a的争夺了。咱们来分析一下,这样的程序会发生多少次futex系统调用。
第一个进入的线程不会调用futex,而其余的线程由于要阻塞而调用,所以sem_wait会形成3次futex(FUTEX_WAIT)调用。
全部线程都会在sem_post的时候调用futex, 所以会形成4次futex(FUTEX_WAKE)调用。
别忘了还有pthread_join(),咱们是按thread1, thread2, thread3, thread4这样来join的,可是线程的调度存在着随机性。若是thread1最后被调度,则只有thread1这一次futex调用,因此 pthread_join()形成的futex调用在1-4次之间。(虽然不是必然的,可是4次更常见一些)
因此这段程序至多会形成3+4+4=11次futex系统调用,用strace跟踪,验证了咱们的想法。
19710 futex(0xb7df1be8, FUTEX_WAIT, 19711, NULL <unfinished ...> 19712 futex(0x8049910, FUTEX_WAIT, 0, NULL <unfinished ...> 19713 futex(0x8049910, FUTEX_WAIT, 0, NULL <unfinished ...> 19714 futex(0x8049910, FUTEX_WAIT, 0, NULL <unfinished ...> 19711 futex(0x8049910, FUTEX_WAKE, 1 <unfinished ...> 19710 futex(0xb75f0be8, FUTEX_WAIT, 19712, NULL <unfinished ...> 19712 futex(0x8049910, FUTEX_WAKE, 1 <unfinished ...> 19710 futex(0xb6defbe8, FUTEX_WAIT, 19713, NULL <unfinished ...> 19713 futex(0x8049910, FUTEX_WAKE, 1 <unfinished ...> 19710 futex(0xb65eebe8, FUTEX_WAIT, 19714, NULL <unfinished ...> 19714 futex(0x8049910, FUTEX_WAKE, 1) = 0 (19710是主线程,19711,19712,19713,19714是4个子线程)
事 情到这里就结束了吗? 若是咱们把semaphore换成Mutex试试。你会发现当自始自终没有竞争的时候,mutex会彻底符合futex机制,不论是lock仍是 unlock都不会调用futex系统调用。有竞争的时候,第一次pthread_mutex_lock的时候不会调用futex调用,看起来还正常。但 是最后一次pthread_mutex_unlock的时候,虽然已经没有线程在等待mutex了,可仍是会调用futex(FUTEX_WAKE)。缘由是什么?欢迎讨论!!!
http://blog.csdn.net/Javadino/archive/2008/09/06/2891385.aspx
http://blog.csdn.net/Javadino/archive/2008/09/06/2891388.aspx
http://blog.csdn.net/Javadino/archive/2008/09/06/2891399.aspx
futex 的逻辑能够用以下C语言表示
int val = 0; void lock() { int c if ((c = cmpxchg(val, 0, 1)) != 0) { if (c != 2) c = xchg(val, 2); while (c != 0) { futex_wait((&val, 2); c = xchg(val, 2); } } } void unlock() { if (atomic_dec(val) != 1) futex_wake(&val, 1); } val 0: unlock val 1: lock, no waiters val2 : lock , one or more waiters
参见: futex are tricky