在編譯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