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;