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是一塊共享內存,一般通過mmap
或shmat
創建。因為對於不同進程,這個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
測量。如果timeout
是NULL
,將會永遠阻塞。
注意:對於FUTEX_WAIT
,timeout
是一個關聯的值。與其他的futex設置不同,timeout
被認為是一個絕對值。使用通過FUTEX_BITSET_MATCH_ANY
特殊定義的val3
傳入FUTEX_WAIT_BITSET
可以獲得附帶timeout
的FUTEX_WAIT
的值。
uaddr2
和val3
是被忽略的。
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
。如果有,就喚醒在uaddr
中val
中最大的等待者。如果還有其他的等待者,這些等待者會從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_REQUEUE
和FUTEX_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 << oparg
,op
會有如下或位運算:
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
的返回值是在uaddr
和uaddr2
上指定的等待隊列的個數。
FUTEX_WAIT_BITSET
這個與FUTEX_WAIT
一樣,只不過val3
傳入了一個32位的標識給內核。這個標識,至少要有一位設置了,在內核中標示等待者的狀態。
如果timeout
不是NULL
,那就根據指定的結構體定義一個超時時間,如果是NULL
,就表示無限等待下去。
uaddr2
被忽略。
FUTEX_WAKE_BITSET
這個操作與FUTEX_WAKE
一樣,除了使用了val3
,這個參數是一個32位的標識,傳入到內核中,至少設置了一位,用來標明哪一個等待者可以被喚醒。選擇喚醒的等待者,通過和喚醒位的標識進行與位運算計算得到。這個標識存在內核中,用來表示等待者的狀態。這個標識通過FUTEX_WAIT_BITSET
設置。所有和喚醒位進行與位運算不是0的就可以喚醒,其余的繼續等待。
FUTEX_WAIT_BITSET
和FUTEX_WAKE_BITSET
的作用就是選擇喚醒被同一個futex阻塞的符合條件的等待者。但是,需要注意,基於這種由位預算實現的多路復用的功能,可能比使用多個futexes效率差一些, 因為基於位運算的多路復用,內核需要每次都檢測所有的等待者的狀態,包括那些,沒有相關等待位標識的等待者,也就是與這次喚醒無關的等待者,內核也需要判斷,因為內核也不知道這個等待者到底與這次喚醒有沒有關系。
常量FUTEX_BITSET_MATCH_ANY
,可以匹配32位標識中所有的位,可以用作val3
的參數,傳入到FUTEX_WAIT_BITSET
和FUTEX_WAKE_BITSET
的調用中。除了timeout
參數的區別外,FUTEX_WAIT
與設置參數val3
為FUTEX_BITSET_MATCH_ANY
的FUTEX_WAIT_BITSET
是一樣的。可以讓任何一個喚醒者喚醒。FUTEX_WAKE
與設置val3
為FUTEX_BITSET_MATCH_ANY
的FUTEX_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_PI
和FUTEX_TRYLOCK_PI
與FUTEX_UNLOCK_PI
是成對出現的。futex所屬的調用線程必須調用FUTEX_UNLOCK_PI
,不這樣操作的話就會導致一個EPERM
錯誤。 -
FUTEX_WAIT_REQUEUE_PI
與FUTEX_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_FIFO
和SCHED_RR
。所有者繼承了等待者的CPU帶寬,如果等待者設定了SCHED_DEADLINE
權限,或是等待者的權限,如果等待者設定了SCHED_RR
或SCHED_FIFO
權限。這個繼承是延續在鎖的鏈表中,防止嵌套的鎖和死鎖。
timeout
參數提供了一個超時。如果不是NULL
,它指向一個專有的超時結構體,通過CLOCK_REALTIME
測量。如果是NULL
,表示永久等待。
忽略uaddr2
val
val3
參數。
FUTEX_TRYLOCK_PI
這個操作一般用在用戶層原子的嘗試請求一個uaddr
指向的鎖,但是由於futex的值不是0,返回失敗。
因為內核比用戶層可以得到更多的信息,所以從內核請求這個鎖,如果futex字段包含了原先的狀態,比如FUTEX_WAITERS
或FUTEX_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
的等待隊列中。
val2
和val3
與FUTEX_CMP_REQUEUE
中的作用相同。
FUTEX_WAIT_REQUEUE_PI
在uaddr
上等待的non-PI futex有可能會被另一個任務通過FUTEX_CMP_REQUEUE_PI
在uaddr2
上請求。這個在uaddr
上等待的操作與FUTEX_WAIT
一樣。
在uaddr
上的等待者可以在刪除后不妨去uaddr2
的隊列,這時候FUTEX_WAIT_REQUEUE_PI
會報錯,返回EAGAIN
。
同樣,如果timeout
不是NULL
,那么就表示指定了一個超時計時器,如果是NULL
就表示永久等待。
忽略val3
FUTEX_WAIT_REQUEUE_PI
和FUTEX_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
返回被喚醒所有等待者的數字。是在uaddr
和uaddr2
指向的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_WAIT
或FUTEX_WAIT_BITSET
操作被一個信號中斷。
EINVAL
futex_op
設置的超時參數是非法的,tv_sec
是負數或者tv_nsec
大於1,000,000,000
EINVAL
futex_op
操作的uaddr
和uaddr2
中一個或是兩個指針指向的結構體是非法的,沒有四字節對齊。
EINVAL (FUTEX_WAIT_BITSET, FUTEX_WAKE_BITSET)
val3
設置的位標記是0
EINVAL (FUTEX_CMP_REQUEUE_PI)
uaddr
與uaddr2
的值相等,也就是放入到同一個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_WAIT
或FUTEX_WAIT_BITSET
的等待者不一致。
EINVAL (FUTEX_CMP_REQUEUE_PI)
內核檢測到,uaddr2
指向的,用戶層和內核層的狀態不一致。也就是內核檢測到FUTEX_WAIT
或FUTEX_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);
}