OS / linux / 互斥锁实现原理(futex)
零、前言
所謂的鎖,在計算機里本質上就是一塊內存空間。當這個空間被賦值為 1 的時候表示加鎖了,被賦值為 0 的時候表示解鎖了,僅此而已。
多個線程搶一個鎖,就是搶著要把這塊內存賦值為 1 。在一個多核環境里,內存空間是共享的,每個核上各跑一個線程,那如何保證一次只有一個線程成功搶到鎖呢?你或許已經猜到了,這必須要硬件的某種保證。
在單核的情況下,關閉 CPU 中斷,使其不能暫停當前請求而處理其他請求,從而達到賦值“鎖”對應的內存空間的目的。
在多核的情況下,使用鎖總線和緩存一致性技術(詳情看這里),可以實現在單一時刻,只有某個CPU里面某一個核能夠賦值“鎖”對應的內存空間,從而達到鎖的目的。
實際上 mutex 底層的實現 API 為 Futex 。
一、什么是 Futex ?
Futex 是 Fast Userspace Mutexes 的縮寫。
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 。
二、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 同步機制還包括用戶態下的操作,我們將在下節提到。
三、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 是從用戶態開始,由用戶態和核心態協調完成的。
四、進 / 線程利用 futex 同步
進程或者線程都可以利用 futex 來進行同步。
對于線程,情況比較簡單,因為線程共享虛擬內存空間,虛擬地址就可以唯一的標識出 futex 變量,即線程用同樣的虛擬地址來訪問 futex 變量。
對于進程,情況相對復雜,因為進程有獨立的虛擬內存空間,只有通過 mmap() 讓它們共享一段地址空間來使用 futex 變量。每個進程用來訪問 futex 的 虛擬地址可以是不一樣的,只要系統知道所有的這些虛擬地址都映射到同一個物理內存地址,并用物理內存地址來唯一標識 futex 變量。
五、小結
關于 Futex 部分轉載于:https://www.cnblogs.com/ck1020/p/6666298.html
(SAW:Game Over!)
?
總結
以上是生活随笔為你收集整理的OS / linux / 互斥锁实现原理(futex)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OS / 总线锁和缓存一致性
- 下一篇: OS / 几个常用的操作系统进程调度算法