Linux Futex的設計與實現

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

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系統調用,對進程競爭情況下的同步處理提供支持。
其原型和系統調用號爲
    #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就是用戶態下共享內存的地址,裏面存放的是一個對齊的整型計數器。
    op存放着操作類型。定義的有5中,這裏我簡單的介紹一下兩種,剩下的感興趣的自己去man futex
    FUTEX_WAIT: 原子性的檢查uaddr中計數器的值是否爲val,如果是則讓進程休眠,直到FUTEX_WAKE或者超時(time-out)。也就是把進程掛到uaddr相對應的等待隊列上去。
    FUTEX_WAKE: 最多喚醒val個等待在uaddr上進程。
    
    可見FUTEX_WAIT和FUTEX_WAKE只是用來掛起或者喚醒進程,當然這部分工作也只能在內核態下完成。有些人嘗試着直接使用futex系統調 用來實現進程同步,並寄希望獲得futex的性能優勢,這是有問題的。應該區分futex同步機制和futex系統調用。futex同步機制還包括用戶態 下的操作,我們將在下節提到。
        
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

發佈了8 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章