pthread互斥訊號量使用總結
----一年前寫的東西,重新抄錄以防遺忘。
glibc提供的pthread互斥訊號量可以用在程序內部,也可以用在程序間,可以在初始化時通過pthread_mutexattr_setpshared介面設定該訊號
量屬性,表示是程序內還是程序間。程序內的使用較為簡單,本文的總結主要是針對程序間的,程序內的也可以參考,其程式碼實現原理是類似
的。
一、實現原理
pthread mutex的實現是非常輕量級的,採用原子操作+futex系統呼叫。
在沒有競爭的情況下,即鎖空閒時,任務獲取訊號量只需要通過原子操作鎖的狀態值,把值置為佔有,再記錄其他一些俄資訊(owner
,計數,如果使能回收功能則串入任務的訊號量回收連結串列等),然後就返回了。
如果在獲取鎖時發現被佔用了,如果呼叫者需要睡眠等待,這時候會觸發futex系統呼叫,由核心繼續處理,核心會讓呼叫任務睡眠,
並在適當時候喚醒(超時或者鎖狀態為可用)。
佔用鎖的任務釋放鎖時,如果沒有任務等待這把鎖,只需要把鎖狀態置為空閒即可。如果發現有其他任務在等待此鎖,則觸發futex系
統呼叫,由核心喚醒等待任務。
由此可見,在沒有競爭的情況下,mutex只需要在使用者態操作鎖狀態值,無須陷入核心,是非常高效的。
獲取到鎖的任務沒有陷入核心,那麼當鎖支援優先順序翻轉時,高優先順序任務等待這把鎖,正常處理必須提升佔用鎖的任務優先順序。內
核又是怎麼知道是哪個任務佔用了鎖呢?實現上,複用了鎖的狀態值,該值在空閒態時為0,非空閒態則儲存了鎖的持有者ID,即PID,核心態通
過PID就知道是那個任務了。
二、核心對鎖的管理
核心維護了一個hash連結串列,每把鎖都被插入到hash連結串列中去,hash值的計算如下(參考get_futex_key):1,如果是程序內的鎖,則通
過鎖的虛擬地址+任務mm指標值+鎖在頁內偏移;2,如果是程序間的鎖,則會獲取鎖虛擬地址對應實體地址的page描述符,由page描述符構造
hash值。
這樣計算的原因是程序間的鎖在各個程序內虛擬地址可能是不同的,但都對映到同一個實體地址,對應同一個page描述符。所以,內
核使用它來定位是否同一個鎖。
這裡對程序間互斥鎖計算hash值的方法,給程序間共享鎖的使用設定了一個隱患條件。下面描述這個問題。
三、程序間互斥訊號量的使用限制:必須在系統管理的記憶體上定義mutex結構,而不能在使用者reserved的共享記憶體上定義mutex結構。
鎖要實現程序間互斥,必須各個程序都能看到這個鎖,因此,鎖結構必須放在共享記憶體上。
獲取系統的共享記憶體通過System V的API介面建立:shmget, shmat,shmdt。但是shmget的引數需要一個id值,各程序對映同一塊共享
記憶體需要同樣的ID值。如果各個程序需要共享的共享記憶體比較多,如幾千上萬個,ID值如果管理?shmget的man幫助和一些示例程式碼給出的是通
過ftok函式把一個檔案轉為ID值(實際就是把檔案對應的INODE轉為ID值),但實際應用中,如果需要的共享記憶體個數較多,難道建立成千上萬
個檔案來使用?而且怎麼保證檔案在程序的生命週期內不會被刪除或者重建?
當時開發的系統還存在另外一種共享記憶體,就是我們通過remap_pfn_range實現的,自己管理了這塊記憶體的申請釋放。申請介面引數為
字串,相同的字串表示同一塊記憶體。因此,傾向於使用自己管理的共享記憶體存放mutex結構。但在使用中,發現這種方法達不到互斥的效果
。為什麼?
原因是自己管理的共享記憶體在核心是通過remap_pfn_range實現的,核心會把這塊記憶體置為reserved,表示非核心管理,獲取鎖的HASH
值時,查詢不到page結構,返回失敗了。最後的解決方法還是通過shmget申請共享記憶體,但不是通過ftok獲取ID,而是通過字串轉為ID值並
處理衝突。
四、程序間互斥訊號量回收問題。
假設程序P1獲取了程序間訊號量,異常退出了,還沒有釋放訊號量,這時候其他程序想來獲取訊號量,能獲取的到嗎?
或者程序P1獲取了訊號量後,其他程序獲取不到進入了睡眠後,P1異常退出了,誰來負責喚醒睡眠的程序?
好在系統設計上已經考慮了這一點。
只要在訊號量初始化時呼叫pthread_mutexattr_setrobust_np設定支援訊號量回收機制,然後,在獲取訊號量時,如果原來佔有訊號
量的程序退出了,系統將會返回EOWNERDEAD,判斷是這個返回值後,呼叫pthread_mutex_consistent_np完成訊號量owner的切換工作即可。
其原理如下:
任務建立時,會註冊一個robust list(使用者態連結串列)到核心的任務控制塊TCB中期,獲取了訊號量時,會把訊號量掛入連結串列。程序復
位時,核心會遍歷此連結串列(核心必須非常小心,因為此時的連結串列資訊可能不可靠了,可不能影響到核心),置上ownerdead的標誌到鎖狀態,並
喚醒等待在此訊號量連結串列上的程序。
五、pthread介面使用說明
pthread_mutex_init: 根據指定的屬性初始化一個mutex,狀態為空閒。
pthread_mutex_destroy: 刪除一個mutex
pthread_mutex_lock/trylock/timedlock/unlock: 獲取鎖、釋放鎖。沒有競爭關係的情況下在使用者態只需要置下鎖的狀態值即返回了
,無須陷入核心。但是timedlock的入參為超時時間,一般需要呼叫系統API獲取,會導致陷入核心,效能較差,實現上,可先trylock,失敗了
再timedlock。
pthread_mutexattr_init:配置初始化
pthread_mutexattr_destroy:刪除配置初始化介面申請的資源
pthread_mutexattr_setpshared:設定mutex是否程序間共享
pthread_mutexattr_settype:設定型別,如遞迴呼叫,錯誤檢測等。
pthread_mutexattr_setprotocol:設定是否支援優先順序翻轉
pthread_mutexattr_setprioceiling:設定獲取訊號量的任務執行在最高優先順序。
每個set介面都有對應的get介面。
六、pthread結構變數說明
struct __pthread_mutex_s
{
int __lock; ----31bit:這個鎖是否有等待者;30bit:這個鎖的owner是否已經掛掉了。其他bit位:0鎖狀態空閒,非0為持
有鎖的任務PID;
unsigned int __count; ----獲取鎖的次數,支援巢狀呼叫,每次獲取到鎖值加1,釋放減1。
int __owner; ----鎖的owner
unsigned int __nusers; ----使用鎖的任務個數,通常為1(被佔用)或0(空閒)
int __kind;----鎖的屬性,如遞迴呼叫,優先順序翻轉等。
int __spins; ----SMP下,嘗試獲取鎖的次數,儘量不進入核心。
__pthread_list_t __list; ----把鎖插入回收連結串列,如果支援回收功能,每次獲取鎖時要插入任務控制塊的回收連結串列。
}__data;