futex


http://blog.sina.com.cn/s/blog_e59371cc0102v29b.html
https://man7.org/linux/man-pages/man7/futex.7.html
https://man7.org/linux/man-pages/man2/futex.2.html

名稱

fast user-space locking: 用戶層速度很快的鎖

簡介

Linux內核提供了futexes(fast user-space mutexes),用於實現用戶層的鎖和信號量的功能。Flutexes是很底層的功能,可以構建更高等級更抽象的鎖,比如互斥量,條件變量,讀寫鎖,barriers和信號量。

絕大多數程序都不會直接用到futexes,而是使用一些系統類庫提供的方法。

一個futex可以認定為一塊內存,在進程或線程之間是共享的。在不同的進程之間,futex不需要必須地址完全一樣。本質上來說,futex就像信號量一樣,它是一個計數器,可以增加,也可以減少,進程需要一直等待,知道計數器的值變成了正數。

Futex在整個用戶層的操作都是非競爭性的。所有競爭性的判斷都有內核完成。Futexes可以在任何非競爭性設計上表現出良好的性能。

從本質上來說,futex就相當於一個始終保持一致性的整數,它只能通過原子性的匯編操作。進程可以使用mmap,通過共享內存片段或是共享內存空間,在多線程之間共享它。

語義

futex是在用戶層啟用的,可以通過API調用訪問到內核態。

如果想啟動一個futex,就需要用適當的匯編指令,讓CPU原子的增加計數器。然后檢查一下,它是不是真的從0變成了1,在這種情況下沒有其他的等待者,這個操作順利的完成了。這是一個快速的最常見的無競爭性的例子。

對於競爭性的情況,計數器需要原子的從-1或是其他負數進行增加。如果存在這種情況,就表示有等待者。用戶層需要通知內核用FUTEX_WAKE喚起一個等待者。

完成等待futex的操作,是一個相反的過程。原子的對計數器減少,並且檢測是不是變成了0,如果是的話,就表示完成了,沒有發生競爭。其他的情況,就需要進程把計數器設置為-1,然后請求內核等待另一個進程增加futex。這就是FUTEX_WAIT的操作。

futex接口可以設置一個超時,指定內核等待多久。不過這種情況下,用法將更加負責,寫程序的時候也要注意更多細節。

注意

需要說明一下,直接使用futex是很難用的,很多時候需要使用匯編。可以使用系統提供的API調用。

接口

       #include <linux/futex.h>
       #include <sys/time.h>

       int futex(int *uaddr, int futex_op, int val,
                 const struct timespec *timeout,   /* or: uint32_t val2 */
                 int *uaddr2, int val3);

描述

futex()提供了一個等待條件變量變成true的系統調用。在共享內存同步上線文中,它通常使用阻塞的設計。當使用futex的時候,多數同步操作在用戶層實現。一個用戶層的程序,一般在長時間等待一個條件變成true的時候使用futex()系統調用。其他的一些futex()操作可以用作喚醒一個等待指定條件變量的進程或是線程。

futex是一個32位的值,它的地址被傳入到futex()。就算在64位系統上,也是一個32位的值,所有平台是一樣的。所有的futex操作都是通過這個值來處理。為了能夠在不同進程間共享futex,futex是一塊共享內存,一般通過mmapshmat創建。因為對於不同進程,這個futex可能不一樣,因為每個進程有自己的虛擬內存,但是最終指向的物理內存是同一個地方。在多線程中,futex被放到一個全局變量中,用來各個線程訪問。

當我們執行一個futex操作,需要阻塞一個線程時,僅僅在調用線程的futex的值(這個值是futex()傳遞的一個參數)是內核需要的的時候,才會阻塞。futex這個值加載的時候,會與期望的值比較,如果其他線程,對於這個futex進行一致性的操作,那么阻塞就會發生,並且是原子的。因此,這個futex的值,在用戶層是同步使用的,在內核層,會阻塞在那里,保證用戶層的同步。同樣,原子的比較和修改共享內存的操作,就是通過futex阻塞的原子的比較和修改的操作進行的。

futexes的一個作用就是實現鎖。鎖的一個狀態,比如已經請求或是還沒有被請求,就相當於是原子性的方位一塊共享內存的flag。在非搶占的情況下,一個線程可以通過原子操作訪問或是修改鎖的狀態,比如,原子性的把鎖通過比較修改操作從沒有請求修改為已經請求。這個操作只在用戶層,內核不需要維護鎖的狀態。其他情況下,一個線程可能需要訪問一個鎖,但是這個鎖被另一個線程已經占有了。這樣就需要傳遞一個futex的值來調用futex()的等待操作,這個futex的值用來表示鎖的是否被占有的flag。如果這個鎖已經被占用了,那么futex()操作就會阻塞。也就是這個futex的值還是表明它被占用。當釋放鎖的時候,需要先重置鎖的狀態,然后調用futex的操作喚起其他等待的線程。這里將來還需要完善,避免不必要的喚起。

除了基本的等待和喚醒的功能,將來可能會支持更負責的一些操作。

不需要顯示的初始化或是析構futex,內核會在調用FUTEX_WAIT的時候進行處理。

參數

uaddr參數指向一個futex值。在任何平台上,futexes是一個四字節的整數,必須指向四字節空間的數據。對於futex的操作,定義在futex_op參數中。val的值取決於futex_op

保留參數 timeout uaddr2 val3 只有在下面介紹的一些futex操作時才用到。如果任何一個不需要,則忽略它。

對於一些阻塞的操作,timeout參數是用來定義超時時間的,它指向一個timespec結構體,定義了超時的時間。但是,盡管上面顯示了這個屬性,對於一些操作,四個字節的參數可以被一個整數替換。對於這些操作,內核先把timeout當做unsigned long,再把它當做uint32_t,在這頁內容中,這個參數被稱為val2

如果需要,uaddr2指向第二個futex值。

val3的意義也取決於本次操作的類型。

Futex操作類型

futex_op參數包含兩部分:一部分是定義這次操作的命令,另一部分是0或是本次操作的更多行為。futex_op包含如下參數:

FUTEX_PRIVATE_FLAG

這個設置位可以用於所有的futex操作。它告訴內核,這個futex是進程專有的,不可以與其他進程共享。它僅僅用作同一進程的線程間同步。可以讓內核做一些優化

為了方便,<linux/futex.h>定義了一系列的以_PRIVATE為后綴的常量, 與下面列出的傳入FUTEX_PRIVATE_FLAG參數的操作一樣。因此,有FUTEX_WAIT_PRIVATE FUTEX_WAKE_PRIVATE等等。

FUTEX_CLOCK_REALTIME

這個參數只能與FUTEX_WAIT_BITSET FUTEX_WAIT_REQUEUE_PI FUTEX_WAIT一起使用

如果設置了這個參數,內核將通過CLOCK_REALTIME測量超時。

如果沒設置這個參數,內核將通過CLOCK_MONOTONIC測量超時。

futex_op特殊的設置有如下幾個:

FUTEX_WAIT

這個操作用來檢測有uaddr指向的futex是否包含關心的數值val,如果是,則繼續sleep直到FUTEX_WAKE操作觸發。加載futex的操作是原子的。這個加載,從比較關心的數值,到開始sleep,都是原子的,與另外一個對於同一個futex的操作是線性的,串行的,嚴格按照順序來執行的。如果線程開始sleep,就表示有一個waiter在futex上。如果futex的值不匹配,回調直接返回失敗,錯誤代碼是EAGAIN

與期望值對比的目的是為了防止丟失喚醒的操作。如果另一個線程在基於前面的數值阻塞調用之后,修改了這個值,另一個線程在數值改變之后,調用FUTEX_WAIT之前執行了FUTEX_WAKE操作,這個調用的線程就會觀察到數值變換並且無法喚醒。
這里的意思是,調用FUTEX_WAIT需要做上面的一個操作,就是檢測一下這個值是不是我們需要的,如果不是就等待,如果是就直接運行下去。之所以檢測是為了避免丟失喚醒,也就是防止一直等待下去,比如我們在調用FUTEX_WAIT之前,另一個線程已經調用了FUTEX_WAKE,那么就不會有線程調用FUTEX_WAKE,調用FUTEX_WAIT的線程就永遠等不到信號了,也就永遠喚醒不了了。

如果timeout不是NULL,就表示指向了一個特定的超時時鍾。這個超時間隔使用系統時鍾的顆粒度四舍五入,可以保證觸發不會比定時的時間早。默認情況通過CLOCK_MONOTONIC測量,但是從Linux 4.5開始,可以在futex_op中設置FUTEX_CLOCK_REALTIME使用CLOCK_REALTIME測量。如果timeoutNULL,將會永遠阻塞。

注意:對於FUTEX_WAITtimeout是一個關聯的值。與其他的futex設置不同,timeout被認為是一個絕對值。使用通過FUTEX_BITSET_MATCH_ANY特殊定義的val3傳入FUTEX_WAIT_BITSET可以獲得附帶timeoutFUTEX_WAIT的值。

uaddr2val3是被忽略的。

FUTEX_WAKE

這個喚醒的操作將在大多數情況下喚起val中對uaddr指向的futex的等待者。大多數情況下,val的值是1(喚起一個)或是INT_MAX喚起所有。並沒有指定,這次喚起肯定會喚起某一個等待者(換句話說,有着高優先級調度的等待者並不能保證一定比低優先級調度的等待者先喚起)。

timeout uaddr2 val3被忽略。

FUTEX_FD

創建一個與futex中uaddr關聯的文件描述符。使用完成后,需要關閉返回的文件描述符。當另一個線程或是進程在futex上執行了FUTEX_WAKE,文件描述符會被select poll epoll捕獲到可讀的消息。

這個文件描述符可以用作獲取異步的通知:如果val是非零的,當另一個線程或是進程執行FUTEX_WAKE的時候,調用者會收到通過val傳遞的信號。

timeout uaddr2 val3被忽略。

由於不常用從Linux 2.6.26起被移除了。

FUTEX_REQUEUE

與下面將要介紹的FUTEX_CMP_REQUEUE功能一樣,只不過沒有使用val3做檢查。

FUTEX_CMP_REQUEUE

這個操作先檢查uaddr是否包含val3。如果沒有,這個操作失敗,返回EAGAIN。如果有,就喚醒在uaddrval中最大的等待者。如果還有其他的等待者,這些等待者會從uaddr的等待隊列中移除,然后增加到uaddr2的等待隊列中。val2制訂了在uaddr2中等待隊列的上限。

uaddr中加載是一個原子性的內存操作,也就是使用了不同平台對應的原子機器指令。這次加載,與val3比較,並且所有等待者的隊列化,都是原子性的,如果有其他對於同一個futex的操作,都將被嚴格的按照順序執行。

一般情況,val的值是0或是1.定義為INT_MAX是無效的,因為它會使FUTEX_CMP_REQUEUE的操作與FUTEX_WAKE一樣。val2設定的值一般是·或是INT_MAX。定義0是無效的,因為它會把FUTEX_CMP_REQUEUE操作當做FUTEX_WAIT

FUTEX_CMP_REQUEUE操作會替換掉前面的FUTEX_REQUEUE操作。不同的地方就是對uaddr的檢查可以保證請求隊列僅僅在特定條件下觸發,避免一些條件觸發。

FUTEX_REQUEUEFUTEX_CMP_REQUEUE都可以避免在所有的等待者都等待同一個futex時,調用FUTEX_WAKE導致的驚群效應。比如下面的情景,多個等待者線程在等待B,一個使用futex實現的等待隊列:

lock(A)
while (!check_value(V)) {
    unlock(A);
    block_on(B);
    lock(A);
};
unlock(A);

如果一個線程調用FUTEX_WAKE,所有的等待B的等待者都會喚醒,然后試圖獲取A的鎖。但是這樣是無意義的,因為只有一個會立馬在鎖住A的時候掛起。但是呢,請求操作值喚起了一個等待着,然后把其他所有鎖A的移除,直到喚醒的等待者釋放了A,下一個才會繼續執行。

FUTEX_WAKE_OP

支持用戶空間用例,同一時間處理多個futex。最有名的例子就是pthread_cond_signal的實現,需要請求兩個futexes,一個用mutex實現,一個用條件變量的等待隊列實現。FUTEX_WAKE_OP可以實現這種功能,不會導致過多的競爭和上下文切換。

FUTEX_WAKE_OP操作等同於下面代碼原子的操作。在兩個futex之間嚴格的順序執行。

int oldval = *(int *) uaddr2;
*(int *) uaddr2 = oldval op oparg;
futex(uaddr, FUTEX_WAKE, val, 0, 0, 0);
if (oldval cmp cmparg)
    futex(uaddr2, FUTEX_WAKE, val2, 0, 0, 0);

換句話說,FUTEX_WAKE_OP做了如下操作:

  • 把原來futex的值保存在uaddr2,修改這個值,這些都是原子的操作

  • 喚起uaddr上指定的futex上的val中的最大的等待者

  • 依賴於與原來在uaddr2中的futex值比較的結果,喚醒uaddr2指向的futex中在val2里面最大的等待者

操作和比較都是基於val3中數據位運算實現的,大體結構如下:

+---+---+-----------+-----------+
|op |cmp| oparg | cmparg |
+---+---+-----------+-----------+
4 4 12 12 <== # of bits

用代碼表示,編碼為:

#define FUTEX_OP(op, oparg, cmp, cmparg) \
                 (((op & 0xf) << 28) | \
                 ((cmp & 0xf) << 24) | \
                 ((oparg & 0xfff) << 12) | \
                 (cmparg & 0xfff))

op對應的值如下:

 FUTEX_OP_SET        0  /* uaddr2 = oparg; */
 FUTEX_OP_ADD        1  /* uaddr2 += oparg; */
 FUTEX_OP_OR         2  /* uaddr2 |= oparg; */
 FUTEX_OP_ANDN       3  /* uaddr2 &= ~oparg; */
 FUTEX_OP_XOR        4  /* uaddr2 ^= oparg; */

此外,如果1 << opargop會有如下或位運算:

 FUTEX_OP_ARG_SHIFT  8  /* Use (1 << oparg) as operand */

cmp的值如下:

 FUTEX_OP_CMP_EQ     0  /* if (oldval == cmparg) wake */
 FUTEX_OP_CMP_NE     1  /* if (oldval != cmparg) wake */
 FUTEX_OP_CMP_LT     2  /* if (oldval < cmparg) wake */
 FUTEX_OP_CMP_LE     3  /* if (oldval <= cmparg) wake */
 FUTEX_OP_CMP_GT     4  /* if (oldval > cmparg) wake */
 FUTEX_OP_CMP_GE     5  /* if (oldval >= cmparg) wake */

FUTEX_WAKE_OP的返回值是在uaddruaddr2上指定的等待隊列的個數。

FUTEX_WAIT_BITSET

這個與FUTEX_WAIT一樣,只不過val3傳入了一個32位的標識給內核。這個標識,至少要有一位設置了,在內核中標示等待者的狀態。

如果timeout不是NULL,那就根據指定的結構體定義一個超時時間,如果是NULL,就表示無限等待下去。

uaddr2被忽略。

FUTEX_WAKE_BITSET

這個操作與FUTEX_WAKE一樣,除了使用了val3,這個參數是一個32位的標識,傳入到內核中,至少設置了一位,用來標明哪一個等待者可以被喚醒。選擇喚醒的等待者,通過和喚醒位的標識進行與位運算計算得到。這個標識存在內核中,用來表示等待者的狀態。這個標識通過FUTEX_WAIT_BITSET設置。所有和喚醒位進行與位運算不是0的就可以喚醒,其余的繼續等待。

FUTEX_WAIT_BITSETFUTEX_WAKE_BITSET的作用就是選擇喚醒被同一個futex阻塞的符合條件的等待者。但是,需要注意,基於這種由位預算實現的多路復用的功能,可能比使用多個futexes效率差一些, 因為基於位運算的多路復用,內核需要每次都檢測所有的等待者的狀態,包括那些,沒有相關等待位標識的等待者,也就是與這次喚醒無關的等待者,內核也需要判斷,因為內核也不知道這個等待者到底與這次喚醒有沒有關系。

常量FUTEX_BITSET_MATCH_ANY,可以匹配32位標識中所有的位,可以用作val3的參數,傳入到FUTEX_WAIT_BITSETFUTEX_WAKE_BITSET的調用中。除了timeout參數的區別外,FUTEX_WAIT與設置參數val3FUTEX_BITSET_MATCH_ANYFUTEX_WAIT_BITSET是一樣的。可以讓任何一個喚醒者喚醒。FUTEX_WAKE與設置val3FUTEX_BITSET_MATCH_ANYFUTEX_WAKE_BITSET一樣,可以喚醒任何一個等待着。

忽略uaddr2參數

優先級繼承的futexes

Linux支持優先級繼承(PI)的futexes,避免使用常規的futex鎖導致的優先級反轉的問題。優先級反轉的問題是,一個高優先級的任務等待一個被低優先級占用的鎖,而中間優先級的任務繼續占用低優先級的CPU處理時間。所以低優先級的任務無法釋放鎖,高優先級的任務也被阻塞。

優先級繼承是為了解決優先級反轉的問題設計的。如果高優先級的任務被低優先級的任務阻塞,那么這時,低優先級的任務會臨時的提升為高優先級的任務,所以就不會被中間優先級的任務搶占了,這樣就可以讓程序繼續執行,釋放鎖。如果要使這個設計生效,優先級繼承必須是可傳遞的,也就是如果一個高優先級的任務被一個低優先級的任務阻塞,而低優先級的任務又被一個中優先級的任務阻塞,以此類推,對於任意長度的鏈,這兩個任務,所有說,更多的任務,也就死這個鎖鏈上的所有的任務,都會提升到高優先級的權限。

從用戶層看,futex的PI意思,從用戶層到底層是共識的。不同於其他的futex操作,PI-futex操作是一個非常具體的設計。

下面介紹的PI-futex操作與其他的futex操作不一樣, 在使用下面的futex字段是,增加了權限:

  • 如果鎖沒有獲取到,futex的字段應該是0

  • 如果鎖獲取到了,futex的字段應該是所屬線程的ID

  • 如果鎖已經被占有,當其他的線程競爭這個鎖時,FUTEX_WAITERS的位應該設置到futex的字段中,也就是設置為如下的數據:

  FUTEX_WAITERS | TID

注意,如果PI futex沒有所屬者並且沒有設置FUTEX_WAITERS位,那么這個PI futex是無效的。

有了這個設計,用戶層的應用可以原子的請求一個沒有被請求的鎖或是釋放一個鎖。比如在x86架構上的cmpxchg,比較和交換的操作。請求一個鎖就是使用比較和交換原子的設置futex字段位調用者的TID,如果原來是0的話。釋放鎖就是使用比較和交換,把futex字段設置位0,如果當前字段的值與TID相同的話。

如果futex已經被請求了,也就是非0,等待者必須使用FUTEX_LOCK_PI操作去獲得鎖。如果其他線程等待鎖,FUTEX_WAITERS字段已經被設置,鎖的所有者必須使用FUTEX_UNLOCK_PI來釋放鎖。

在這種情況下,調用者被強制發送到內核,比如futex()調用,然后他們直接處理一個所謂的RT-mutex,一個內核鎖機制,使用了權限繼承的設計。當一個RT-mutex被捕獲到,在調用線程返回到用戶層之前,futex的值會被修改。

要記住,內核會在返回到用戶層之前修改futex的值。這是為了防止futex在結束的時候是一個非法的值,比如有所有者,但是數值是0,或是有所有者,但是沒有對應的FUTEX_WAITERS標識位。

如果futex在內核中有一個相關的RT-mutex,也就是阻塞了等待者,但是futex/RT-mutex的所有者銷毀了,內核會清理RT-mutex並且把它賦給下一個等待者。這一輪用戶層請求的數值也會根據這個改變。為了標明這個是需要的,內核會給新的擁有的線程的futex字段設置FUTEX_OWNER_DIED位。用戶層可以通過FUTEX_OWNER_DIED位來判斷這種情況發生,這樣就可以清理銷毀所有者的狀態。

PI futexes使用在futex_op中如下特殊的參數。記住,PI futex操作必須是成對出現的,是一些特殊請求的子操作:

  • FUTEX_LOCK_PIFUTEX_TRYLOCK_PIFUTEX_UNLOCK_PI是成對出現的。futex所屬的調用線程必須調用FUTEX_UNLOCK_PI,不這樣操作的話就會導致一個EPERM錯誤。

  • FUTEX_WAIT_REQUEUE_PIFUTEX_CMP_REQUEUE_PI成對出現。這個實現,non-PI futex必須與 PI futex區別開來,不然就會報EINVAL錯誤。除此之外,val(准備喚醒的等待者的數字)必須是1,不然也會報EINVAL的錯誤。

PI futex的操作如下:

FUTEX_LOCK_PI

這個操作用在,嘗試通過原子的方式在用戶層獲取鎖的時候,因為futex字段已經是非零的值,特別是已經包含了擁有鎖的TID,失敗的情況下。

這個操作會檢測futex的字段,如果是0,內核嘗試原子的設置futex的值為當前調用的TID。如果是非零,內核原子的設置FUTEX_WAITERS的位,標明futex的所有者不能通過在用戶層原子的解鎖futex。然后,內核要做如下操作:

  • 嘗試找到與這個TID相關的線程

  • 代表所有者創建或是重用內核狀態。如果這個是第一個等待者,那么futex就沒有內核狀態,內核就會通過RT-mutex的鎖創建一個,futex的所有者也變成RT-mutex的所有者。如果已經有了等待者,現有的狀態就會被重用

  • 把等待者附到futex上。也就是等待者放到RT-mutex的等待隊列中。

如果有多個等待者存在,放到隊列中的等待者的優先級是遞減的。更多關於權限排序的信息,可以參考sched中的SCHED_DEADLINE SCHED_FIFOSCHED_RR。所有者繼承了等待者的CPU帶寬,如果等待者設定了SCHED_DEADLINE權限,或是等待者的權限,如果等待者設定了SCHED_RRSCHED_FIFO權限。這個繼承是延續在鎖的鏈表中,防止嵌套的鎖和死鎖。

timeout參數提供了一個超時。如果不是NULL,它指向一個專有的超時結構體,通過CLOCK_REALTIME測量。如果是NULL,表示永久等待。

忽略uaddr2 val val3參數。

FUTEX_TRYLOCK_PI

這個操作一般用在用戶層原子的嘗試請求一個uaddr指向的鎖,但是由於futex的值不是0,返回失敗。

因為內核比用戶層可以得到更多的信息,所以從內核請求這個鎖,如果futex字段包含了原先的狀態,比如FUTEX_WAITERSFUTEX_OWNER_DIED,有可能會是成功的。當futex的所有者銷毀的時候,會發生這種情況。用戶層無法處理這種沒有宿主的情況,但是內核可以解決並請求futex。

忽略uaddr2 val timeout val3

FUTEX_UNLOCK_PI

喚起uaddr指向的futex中在FUTEX_LOCK_PI中等待的最高權限的等待者。

當不能原子把在uaddr中用戶層的數據從TID修改為0的時候,調用這個接口。

忽略uaddr2 val timeout val3`

FUTEX_CMP_REQUEUE_PI

這個操作是FUTEX_CMP_REQUEUE的PI關心的變種。它把因為FUTEX_WAIT_REQUEUE_PI阻塞的在uaddr中的等待者從non-PI(uaddr)放到PI隊列(uaddr2)中。

如果用FUTEX_CMP_REQUEUE,會喚醒在uaddr中最大數值的等待者。但是,調用FUTEX_CMP_REQUEUE_PI,需要把val設置位1,主要目的就是為了避免經群效用。剩下的等待者會從uaddr中刪除,天見到uaddr2的等待隊列中。

val2val3FUTEX_CMP_REQUEUE中的作用相同。

FUTEX_WAIT_REQUEUE_PI

uaddr上等待的non-PI futex有可能會被另一個任務通過FUTEX_CMP_REQUEUE_PIuaddr2上請求。這個在uaddr上等待的操作與FUTEX_WAIT一樣。

uaddr上的等待者可以在刪除后不妨去uaddr2的隊列,這時候FUTEX_WAIT_REQUEUE_PI會報錯,返回EAGAIN

同樣,如果timeout不是NULL,那么就表示指定了一個超時計時器,如果是NULL就表示永久等待。

忽略val3

FUTEX_WAIT_REQUEUE_PIFUTEX_CMP_REQUEUE_PI是為了一個特殊的用法:支持優先級繼承相關的POSIX的線程條件變量。目的就是為了保證用戶層和內核層同步這些操作總是成對出現的。因此,在FUTEX_WAIT_REQUEUE_PI操作中,用戶層的應用需要提前定義在FUTEX_CMP_REQUEUE_PI中的隊列。

返回值

出錯的時候,所有的操作都會返回-1,然后設置errno

成功的返回值各不相同,詳細的介紹如下:

FUTEX_WAIT

如果調用者被喚醒,返回0。需要注意,在正常的不相關的futex的使用中,也會因為使用了原來的futex數值的內存空間而觸發喚醒。一般是基於futex的一些pthread mutexes的實現,會在一些情況下導致這個問題。因此,調用者需要特別關系返回0時的情況,有可能是一個假的喚醒,需要判斷是否阻塞還是向下運行。

FUTEX_WAKE

返回被喚醒的等待者的數字

FUTEX_FD

返回一個與futex相關的文件描述符

FUTEX_REQUEUE

返回被喚醒的等待者的數字

FUTEX_CMP_REQUEUE

返回所有的被喚醒或是放到uaddr2中的等待者的數字,如果大於val,就返回被放到uaddr2中等待者的數字

FUTEX_WAKE_OP

返回被喚醒所有等待者的數字。是在uaddruaddr2指向的futex的總和

FUTEX_WAIT_BITSET

如果被喚醒,返回0

FUTEX_WAKE_BITSET

返回被喚醒的等待者的數字

FUTEX_LOCK_PI

如果加鎖成功,返回0

FUTEX_TRYLOCK_PI

如果加鎖成功,返回0

FUTEX_UNLOCK_PI

如果解鎖成功返回0

FUTEX_CMP_REQUEUE_PI

返回所有喚醒或是放到uaddr2中的等待者數字。如果返回值比val大,返回放到uaddr2中指向的futex的數字。

FUTEX_WAIT_REQUEUE_PI

如果成功的請求到uaddr2中的futex,返回0

錯誤

EACCES

無法讀內存中futex的值

EAGAIN (FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI)

uaddr指向的數字與期望的數字不同

注意:在Linux下,可能是EAGAIN,也可能是EWOULDBLOCK,這兩個是同一個意思,不同的內核可能不一樣。

EAGAIN (FUTEX_CMP_REQUEUE, FUTEX_CMP_REQUEUE_PI)

uaddr指向的數字與val3設定的不一致

EAGAIN (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)

uaddr指向的,在FUTEX_CMP_REQUEUE_PI中是uaddr2指向的,futex所屬的線程ID,已經退出了,但是底層沒有清理,再試一次。

EDEADLK (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)

uaddr指向的futex已經被調用者鎖住了

EDEADLK (FUTEX_CMP_REQUEUE_PI)

把一個等待着放到uaddr2指向的PI futex中,出發了死鎖

EFAULT

指針參數,比如uaddr uaddr2 timeout指向了非法的地址

EINTR

FUTEX_WAITFUTEX_WAIT_BITSET操作被一個信號中斷。

EINVAL

futex_op設置的超時參數是非法的,tv_sec是負數或者tv_nsec大於1,000,000,000

EINVAL

futex_op操作的uaddruaddr2中一個或是兩個指針指向的結構體是非法的,沒有四字節對齊。

EINVAL (FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET)

val3設置的位標記是0

EINVAL (FUTEX_CMP_REQUEUE_PI)

uaddruaddr2的值相等,也就是放入到同一個futex隊列

EINVAL (FUTEX_FD)

val中的信號數字是非法的

EINVAL (FUTEX_WAKE, FUTEX_WAKE_OP, FUTEX_WAKE_BITSET, FUTEX_REQUEUE, FUTEX_CMP_REQUEUE)

內核檢測到uaddr指向的狀態,用戶層和內核層不一致。也就是在FUTEX_LOCK_PI中由uaddr指向的等待者不一致

EINVAL (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI)

內核檢測到uaddr指向的狀態,用戶層和內核層不一致。這表示有可能是狀態損壞了或是內核發現在uaddr上通過FUTEX_WAITFUTEX_WAIT_BITSET的等待者不一致。

EINVAL (FUTEX_CMP_REQUEUE_PI)

內核檢測到,uaddr2指向的,用戶層和內核層的狀態不一致。也就是內核檢測到FUTEX_WAITFUTEX_WAIT_BITSET調用中uaddr2指向的等待者不一致

EINVAL (FUTEX_CMP_REQUEUE_PI)

內核檢測到,uaddr指向的,用戶層和內核層的狀態不一致。

EINVAL (FUTEX_CMP_REQUEUE_PI)

內核檢測到,uaddr指向的,用戶層和內核層的狀態不一致。

EINVAL (FUTEX_CMP_REQUEUE_PI)

試圖把一個等待着放入到futex隊列。但是futex是由該等待者調用FUTEX_WAIT_REQUEUE_PI請求的

EINVAL (FUTEX_CMP_REQUEUE_PI)

val不是1

EINVAL

錯誤的參數

ENFILE (FUTEX_FD)

打開的文件描述符超過了系統的最大限制

ENOMEM (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)

內核無法為狀態信息申請內存

ENOSYS

futex_op中定義的非法的操作

ENOSYS

futex_op中定義了FUTEX_CLOCK_REALTIME操作,但是對應的調用不是FUTEX_WAIT FUTEX_WAIT_BITSET FUTEX_WAIT_REQUEUE_PI的任何一個

ENOSYS (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI, FUTEX_CMP_REQUEUE_PI, FUTEX_WAIT_REQUEUE_PI)

程序運行時檢測發現操作不可用。PI-futex在一些架構和CPU上不支持

EPERM (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)

調用者不能把自己附加到uaddr,在FUTEX_CMP_REQUEUE_PI中是uaddr,指向的futex上。這樣會在用戶層導致狀態失效

EPERM (FUTEX_UNLOCK_PI)

調用者沒有futex的鎖

ESRCH (FUTEX_LOCK_PI, FUTEX_TRYLOCK_PI, FUTEX_CMP_REQUEUE_PI)

uaddr指向的futex,其線程ID不存在

ESRCH (FUTEX_CMP_REQUEUE_PI)

uaddr2指向的futex,其線程ID不存在

ETIMEDOUT

futex_op定義的操作超時

注意

Glibc不支持對系統調用的封裝,因為這個是系統調用,所以Glibc不支持futex

一些更高級的代碼設計師基於futexes,包括POSIX的信號量,還有各種各樣的POSIX線程同步機制,比如互斥量,條件變量,讀寫鎖,屏障

示例

下面的示例,父進程和子進程使用一對放在共享匿名mapping中的futexes進行同步訪問共享資源,一個terminal。兩個進程都寫nloops消息,一個命令行參數,默認是5,到terminal上,然后同步保證交替的寫。跑起來的結果如下:

$ ./futex_demo
Parent (18534) 0
Child  (18535) 0
Parent (18534) 1
Child  (18535) 1
Parent (18534) 2
Child  (18535) 2
Parent (18534) 3
Child  (18535) 3
Parent (18534) 4
Child  (18535) 4

源碼

/* futex_demo.c

   Usage: futex_demo [nloops]
                    (Default: 5)

   Demonstrate the use of futexes in a program where parent and child
   use a pair of futexes located inside a shared anonymous mapping to
   synchronize access to a shared resource: the terminal. The two
   processes each write 'num-loops' messages to the terminal and employ
   a synchronization protocol that ensures that they alternate in
   writing messages.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/futex.h>
#include <sys/time.h>

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                               } while (0)

static int *futex1, *futex2, *iaddr;

static int
futex(int *uaddr, int futex_op, int val,
    const struct timespec *timeout, int *uaddr2, int val3)
{
    return syscall(SYS_futex, uaddr, futex_op, val,
        timeout, uaddr2, val3);
}

/* Acquire the futex pointed to by 'futexp': wait for its value to
   become 1, and then set the value to 0. */

static void
fwait(int *futexp)
{
    int s;

    /* atomic_compare_exchange_strong(ptr, oldval, newval)
       atomically performs the equivalent of:

           if (*ptr == *oldval)
               *ptr = newval;

       It returns true if the test yielded true and *ptr was updated. */

    while (1) {

        /* Is the futex available? */
        const int one = 1;
        if (atomic_compare_exchange_strong(futexp, &one, 0))
            break;      /* Yes */

        /* Futex is not available; wait */

        s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0);
        if (s == -1 && errno != EAGAIN)
            errExit("futex-FUTEX_WAIT");
    }
}

/* Release the futex pointed to by 'futexp': if the futex currently
   has the value 0, set its value to 1 and the wake any futex waiters,
   so that if the peer is blocked in fpost(), it can proceed. */

static void
fpost(int *futexp)
{
    int s;

    /* atomic_compare_exchange_strong() was described in comments above */

    const int zero = 0;
    if (atomic_compare_exchange_strong(futexp, &zero, 1)) {
        s = futex(futexp, FUTEX_WAKE, 1, NULL, NULL, 0);
        if (s == -1)
            errExit("futex-FUTEX_WAKE");
    }
}

int
main(int argc, char *argv[])
{
    pid_t childPid;
    int j, nloops;

    setbuf(stdout, NULL);

    nloops = (argc > 1) ? atoi(argv[1]) : 5;

    /* Create a shared anonymous mapping that will hold the futexes.
       Since the futexes are being shared between processes, we
       subsequently use the "shared" futex operations (i.e., not the
       ones suffixed "_PRIVATE") */

    iaddr = mmap(NULL, sizeof(int) * 2, PROT_READ | PROT_WRITE,
        MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if (iaddr == MAP_FAILED)
        errExit("mmap");

    futex1 = &iaddr[0];
    futex2 = &iaddr[1];

    *futex1 = 0;        /* State: unavailable */
    *futex2 = 1;        /* State: available */

    /* Create a child process that inherits the shared anonymous
       mapping */

    childPid = fork();
    if (childPid == -1)
        errExit("fork");

    if (childPid == 0) {        /* Child */
        for (j = 0; j < nloops; j++) {
            fwait(futex1);
            printf("Child  (%ld) %d\n", (long)getpid(), j);
            fpost(futex2);
        }

        exit(EXIT_SUCCESS);
    }

    /* Parent falls through to here */

    for (j = 0; j < nloops; j++) {
        fwait(futex2);
        printf("Parent (%ld) %d\n", (long)getpid(), j);
        fpost(futex1);
    }

    wait(NULL);

    exit(EXIT_SUCCESS);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM