[轉載] Linux Futex的設計與實現

Linux Futex的設計與實現

引子

在編譯2.6內核的時候,你會在編譯選項中看到[*] Enable futex support這一項,上網查,有的資料會告訴你"不選這個內核不必定能正確的運行使用glibc的程序",那futex是什麼?和glibc又有什麼關係呢?linux

1. 什麼是Futex

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。多線程

2. 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

3. Futex同步機制

全部的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是從用戶態開始,由用戶態和核心態協調完成的。

4. 進/線程利用futex同步

進程或者線程均可以利用futex來進行同步。
對於線程,狀況比較簡單,由於線程共享虛擬內存空間,虛擬地址就能夠惟一的標識出futex變量,即線程用一樣的虛擬地址來訪問futex變量。
對 於進程,狀況相對複雜,由於進程有獨立的虛擬內存空間,只有經過mmap()讓它們共享一段地址空間來使用futex變量。每一個進程用來訪問futex的 虛擬地址能夠是不同的,只要系統知道全部的這些虛擬地址都映射到同一個物理內存地址,並用物理內存地址來惟一標識futex變量。

小結:

  1. Futex變量的特徵:1)位於共享的用戶空間中 2)是一個32位的整型 3)對它的操做是原子的
  2. Futex在程序low-contention的時候能得到比傳統同步機制更好的性能。
  3. 不要直接使用Futex系統調用。
  4. Futex同步機制能夠用於進程間同步,也能夠用於線程間同步。

Linux中的線程同步機制(二)--In Glibc

在linux中進行多線程開發,同步是不可迴避的一個問題。在POSIX標準中定義了三種線程同步機制: Mutexes(互斥量), Condition Variables(條件變量)和POSIX Semaphores(信號量)。NPTL基本上實現了POSIX,而glibc又使用NPTL做爲本身的線程庫。所以glibc中包含了這三種同步機制 的實現(固然還包括其餘的同步機制,如APUE裏提到的讀寫鎖)。

Glibc中經常使用的線程同步方式舉例:

Semaphore

變量定義: sem_t sem;

初始化: sem_init(&sem,0,1);

進入加鎖: sem_wait(&sem);

退出解鎖: sem_post(&sem);

Mutex

變量定義: pthread_mutex_t mut;

初始化: pthread_mutex_init(&mut,NULL);

進入加鎖: pthread_mutex_lock(&mut);

退出解鎖: pthread_mutex_unlock(&mut);

這些用於同步的函數和futex有什麼關係?下面讓咱們來看一看:

以Semaphores爲例,

進入互斥區的時候,會執行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的程序"。

小結:

  1. Glibc中的所提供的線程同步方式,如你們所熟知的Mutex,Semaphore等,大多都構造於futex之上了,除了特殊狀況,你們不必再去實現本身的futex同步原語。
  2. 你們要作的事情,彷佛就是按futex的manual中所說得那樣: 正確的使用Glibc所提供的同步方式,並在使用它們的過程當中,意識到它們是利用futex機制和linux配合完成同步操做就能夠了。

Linux中的線程同步機制(三)--Practice

上回說到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系統調用,咱們來分析一下這分別是什麼緣由形成的。

1. 出人意料的"sem_post()"

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機制。

2. 半路殺出的"pthread_join()"

那另外一個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()阻塞當前進程了。

3. 更多的競爭。

咱們把上面的程序稍微改改:
在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系統調用。

1) sem_wait()

第一個進入的線程不會調用futex,而其餘的線程由於要阻塞而調用,所以sem_wait會形成3次futex(FUTEX_WAIT)調用。

2) sem_post()

全部線程都會在sem_post的時候調用futex, 所以會形成4次futex(FUTEX_WAKE)調用。

3) pthread_join()

別忘了還有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個子線程)

4. 更多的問題

事 情到這裏就結束了嗎? 若是咱們把semaphore換成Mutex試試。你會發現當自始自終沒有競爭的時候,mutex會徹底符合futex機制,不論是lock仍是 unlock都不會調用futex系統調用。有競爭的時候,第一次pthread_mutex_lock的時候不會調用futex調用,看起來還正常。但 是最後一次pthread_mutex_unlock的時候,雖然已經沒有線程在等待mutex了,可仍是會調用futex(FUTEX_WAKE)。緣由是什麼?歡迎討論!!!

小結:

  1. 雖然semaphore,mutex等同步方式構建在futex同步機制之上。然而受其語義等的限制,並無徹底按futex最初的設計實現。
  2. pthread_join()等函數也是調用futex來實現的。
  3. 不一樣的同步方式都有其不一樣的語義,不一樣的性能特徵,適合於不一樣的場景。咱們在使用過程當中要知道他們的共性,也得了解它們之間的差別。這樣才能更好的理解多線程場景,寫出更高質量的多線程程序。

轉載地址:

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

Linux中的線程同步機制(四)--C語言實現

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

相關文章
相關標籤/搜索