Linux中的同步機制 — Futex

來源:互聯網
上載者:User
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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.