深入理解各種鎖

1 臨界區

1.1簡介

在早期計算機系統中,只有一個任務進程在執行,并不存在資源的共享與競爭。隨著技術和需求的飛速發展,單個CPU通過時間分片在一段時間內同時處理多個任務進程,當多個進程對共享資源進行并發訪問,就引起了進程間的競態。這其中包括了我們所熟知的SMP(對稱多處理器結構)系統,多核間的相互競爭,單CPU中斷和進程間的相互搶占等諸多問題。

更具體的說,對某段代碼而言,可能會在程序中多次被執行(多線程便是典型的場景),每次執行的過程我們稱作代碼的執行路徑,當兩個或多個代碼路徑要競爭共同的資源的時候,該代碼段就是臨界區如圖1所示


臨界區的執行

為了保護共享資源不被同時訪問,Linux內核中提供了各式各樣的同步鎖機制,包括:原子操作、自旋鎖、信號量、互斥量等等,除了原子操作無論哪種鎖機制都并非是免費的午餐,加鎖操作伴隨著用戶態到內核態切換、進程上下文切換等高消耗過程。

1.2 用戶態與內核態切換

為了集中管理,減少有限資源的訪問和使用沖突,CPU設置了多個特權級別,就Intel x86架構的CPU來說一共有0~3四個特權級,0級最高,3級最低,硬件上在執行每條指令時都會對指令所具有的特權級做相應的檢查,相關的概念有 CPL、DPL和RPL,這里不再過多闡述。為了安全考慮,Linux系統分為內核態和用戶態,分別運行在內核空間和用戶空間,對應的使用了0級特權級和3級特權級。

內核態的程序可以執行特權指令,操作系統本身也在其中運行。

用戶態則不允許直接訪問操作系統的核心數據、設備等關鍵資源,必須先通過系統調用或者中斷進入內核態才可以訪問,當系統調用或中斷返回時,重新回到用戶空間運行。

由用戶態切換到內核態的步驟主要包括:
1)從當前進程的描述符中提取其內核棧的ss0及esp0信息。
2)使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令。
3)將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到了內核態的程序執行了。
簡單來說用戶態與內核態切換一般都需要保存用戶程序得上下文(context), 在進入內核得時候需要保存用戶態得寄存器,在內核態返回用戶態得時候會恢復這些寄存器得內容,相對而言,這是一個很大的開銷。

1.3 進程上下文切換

上下文切換的定義,http://www.linfo.org/context_switch.html 此文中已做了詳細的說明,只提煉以下幾個關鍵要點:

1)進程上下文切換可以描述為kernel執行下面的操作
a. 掛起一個進程,并儲存該進程當時寄存器和程序計數器的狀態
b. 從內存中恢復下一個要執行的進程,恢復該進程原來的狀態到寄存器,返回到其上次暫停的執行代碼然后繼續執行
2)上下文切換只能發生在內核態,所以還會觸發用戶態與內核態切換

2. Linux鎖機制

2.1 自旋鎖

自旋鎖的實現是為了保護一段短小的臨界區操作代碼,保證這個臨界區的操作是原子的,從而避免并發的競爭。在Linux內核中,自旋鎖通常用于包含內核數據結構的操作,你可以看到在許多內核數據結構中都嵌入有spinlock,這些大部分就是用于保證它自身被操作的原子性,在操作這樣的結構體時都經歷這樣的過程:上鎖-操作-解鎖。如果內核控制路徑發現自旋鎖“開著”(可以獲取),就獲取鎖并繼續自己的執行。相反,如果內核控制路徑發現鎖由運行在另一個CPU上的內核控制路徑“鎖著”,就在原地“旋轉”,反復執行一條緊湊的循環檢測指令,直到鎖被釋放。 自旋鎖是循環檢測“忙等”,即等待時內核無事可做(除了浪費時間),進程在CPU上保持運行,所以它保護的臨界區必須小,且操作過程必須短。不過,自旋鎖通常非常方便,因為很多內核資源只鎖極短的時間片段,所以等待自旋鎖的釋放不會消耗太多CPU的時間。

2.2.1 自旋鎖需要做的工作

從保證臨界區訪問原子性的目的來考慮,自旋鎖應該阻止在代碼運行過程中出現的任何并發干擾。這些“干擾”包括:

  1. 中斷,包括硬件中斷和軟件中斷 (僅在中斷代碼可能訪問臨界區時需要) 這種干擾存在于任何系統中,一個中斷的到來導致了中斷例程的執行,如果在中斷例程中訪問了臨界區,原子性就被打破了。所以如果在某種中斷例程中存在訪問某個臨界區的代碼,那么就必須用spinlock保護。對于不同的中斷類型(硬件中斷和軟件中斷)對應于不同版本的自旋鎖實現,其中包含了中斷禁用和開啟的代碼。但是如果你保證沒有中斷代碼會訪問臨界區,那么使用不帶中斷禁用的自旋鎖API即可。

  2. 內核搶占(僅存在于可搶占內核中) 在2.6以后的內核中,支持內核搶占,并且是可配置的。這使UP系統和SMP類似,會出現內核態下的并發。這種情況下進入臨界區就需要避免因搶占造成的并發,所以解決的方法就是在加鎖時禁用搶占(preempt_disable(); ),在開鎖時開啟搶占(preempt_enable();注意此時會執行一次搶占調度)

  3. 其他處理器對同一臨界區的訪問 (僅SMP系統) 在SMP系統中,多個物理處理器同時工作,導致可能有多個進程物理上的并發。這樣就需要在內存加一個標志,每個需要進入臨界區的代碼都必須檢查這個標志,看是否有進程已經在這個臨界區中。這種情況下檢查標志的代碼也必須保證原子和快速,這就要求必須精細地實現,正常情況下每個構架都有自己的匯編實現方案,保證檢查的原子性。

根據上的介紹,我們很容易知道自旋鎖的操作包括:

中斷控制(僅在中斷代碼可能訪問臨界區時需要)
搶占控制(僅存在于可搶占內核中需要)
自旋鎖標志控制 (僅SMP系統需要)
中斷控制是按代碼訪問臨界區的不同而在編程時選用不同的變體,有些API中有,有些沒有。
而搶占控制和自旋鎖標志控制依據內核配置(是否支持內核搶占)和硬件平臺(是否為SMP)的不同而在編譯時確定。如果不需要,相應的控制代碼就編譯為空函數。 對于非搶占式內核,由自旋鎖所保護的每個臨界區都有禁止內核搶占的API,但是為空操作。由于UP系統不存在物理上的并行,所以可以閹割掉自旋的部分,剩下搶占和中斷操作部分即可。

有些人會以為自旋鎖的自旋檢測可以用for實現,這種想法“Too young, too simple, sometimes naive”!你可以在理論上用C去解釋,但是如果用for,起碼會有如下兩個問題:
1)你如何保證在SMP下其他處理器不會同時訪問同一個的標志呢?(也就是標志的獨占訪問)
2)必須保證每個處理器都不會去讀取高速緩存而是真正的內存中的標志(可以實現,編程上可以用volitale)要根本解決這個問題,需要在芯片底層實現物理上的內存地址獨占訪問,并且在實現上使用特殊的匯編指令訪問。請看參考資料中對于自旋鎖的實現分析。以arm為例,從存在SMP的ARM構架指令集開始(V6、V7),采用LDREX和STREX指令實現真正的自旋等待。

2.2.2 自旋鎖變體的使用規則

不論是搶占式UP、非搶占式UP還是SMP系統,只要在某類中斷代碼可能訪問臨界區,就需要控制中斷,保證操作的原子性。所以這個和模塊代碼中臨界區的訪問還有關系,是否可能在中斷中操作臨界區,只有程序員才知道。所以自旋鎖API中有針對不同中斷類型的自旋鎖變體:
不會在任何中斷例程中操作臨界區

static inline void spin_lock(spinlock_t *lock)

static inline void spin_unlock(spinlock_t *lock)

如果在軟件中斷中操作臨界區:

static inline void spin_lock_bh(spinlock_t *lock)
static inline void spin_unlock_bh(spinlock_t *lock)

bh代表bottom half,也就是中斷中的底半部,因內核中斷的底半部一般通過軟件中斷(tasklet等)來處理而得名。
如果在硬件中斷中操作臨界區:

static inline void spin_lock_irq(spinlock_t *lock)
static inline void spin_unlock_irq(spinlock_t *lock)

如果在控制硬件中斷的時候需要同時保存中斷狀態:

spin_lock_irqsave(lock, flags)
static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

這些情況描訴似乎有點簡單,我在網上找到了一篇使用規則((轉)自旋鎖(spinlock) 解釋得經典,透徹),非常詳細。我稍作修改,轉載如下:

獲得自旋鎖和釋放自旋鎖有好幾個版本,因此讓讀者知道在什么樣的情況下使用什么版本的獲得和釋放鎖的宏是非常必要的。

如果被保護的共享資源只在進程上下文訪問和軟中斷(包括tasklet、timer)上下文訪問,那么當在進程上下文訪問共享資源時,可能被軟中斷打斷,從而可能進入軟中斷上下文來對被保護的共享資源訪問,因此對于這種情況,對共享資源的訪問必須使用spin_lock_bh和spin_unlock_bh來保護。當然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當的,它比其他兩個快。

如果被保護的共享資源只在兩個或多個tasklet或timer上下文訪問,那么對共享資源的訪問僅需要用spin_lock和spin_unlock來保護,不必使用_bh版本,因為當tasklet或timer運行時,不可能有其他tasklet或timer在當前CPU上運行。

如果被保護的共享資源只在一個tasklet或timer上下文訪問,那么不需要任何自旋鎖保護,因為同一個tasklet或timer只能在一個CPU上運行,即使是在SMP環境下也是如此。實際上tasklet在調用tasklet_schedule標記其需要被調度時已經把該tasklet綁定到當前CPU,因此同一個tasklet決不可能同時在其他CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經被幫定到當前CPU,所以同一個timer絕不可能運行在其他CPU上。當然同一個tasklet有兩個實例同時運行在同一個CPU就更不可能了。

如果被保護的共享資源只在一個軟中斷(tasklet和timer除外)上下文訪問,那么這個共享資源需要用spin_lock和spin_unlock來保護,因為同樣的軟中斷可以同時在不同的CPU上運行。

如果被保護的共享資源在兩個或多個軟中斷上下文訪問,那么這個共享資源當然更需要用spin_lock和spin_unlock來保護,不同的軟中斷能夠同時在不同的CPU上運行。

如果被保護的共享資源在軟中斷(包括tasklet和timer)或進程上下文和硬中斷上下文訪問,那么在軟中斷或進程上下文訪問期間,可能被硬中斷打斷,從而進入硬中斷上下文對共享資源進行訪問,因此,在進程或軟中斷上下文需要使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。

而在中斷處理句柄中使用什么版本,需依情況而定,如果只有一個中斷處理句柄訪問該共享資源,那么在中斷處理句柄中僅需要spin_lock和spin_unlock來保護對共享資源的訪問就可以了。因為在執行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進程打斷。

但是如果有不同的中斷處理句柄訪問該共享資源,那么需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。

在使用spin_lock_irq和spin_unlock_irq的情況下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應該使用哪一個也需要依情況而定,如果可以確信在對共享資源訪問前中斷是使能的,那么使用spin_lock_irq更好一些。因為它比spin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因為它將恢復訪問共享資源前的中斷標志而不是直接使能中斷。

當然,有些情況下需要在訪問共享資源時必須中斷失效,而訪問完后必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。

spin_lock用于阻止在不同CPU上的執行單元對共享資源的同時訪問以及不同進程上下文互相搶占導致的對共享資源的非同步訪問,而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對共享資源的非同步訪問。

2.2.3 自旋鎖使用及注意事項

自旋鎖使用如下;

//1.分配自旋鎖
spinlock_t lock;
//2.初始化自旋鎖
spin_lock_init(&lock);
//3.訪問臨界區之前獲取鎖:
spin_lock(&lock);  //獲取自旋鎖,立即返回,如果沒有獲取鎖,將進行忙等待
 或者
spin_trylock(&lock); //獲取鎖,返回true,否則返回false,所以這個函數一定要對返回值進行判斷!
//4 .訪問臨界區
//5.釋放自旋鎖
spin_unlock(&lock);

自旋鎖的注意事項:

  1. 自旋鎖使CPU處于忙等狀態,因此臨界區執行時間應該盡量短;

  2. 自旋鎖是不可重入的;

  3. 自旋鎖保護的臨界區不應該有睡眠操作:

    1)對于開中斷的自旋鎖來說,睡眠操作可能發生如下兩種情況:
    a. 死鎖:任務A獲得自旋鎖之后睡眠,接著又發生了中斷,而中斷處理程序內部又打算獲取同一個自旋鎖,則此時會發生自死鎖 —— 自旋鎖是不可重入的。
    b. CPU浪費:倘若中斷處理程序內部沒有獲取同一個自旋鎖的操作,則理論上可以產生調度。假設進程B打算獲取CPU的控制權,但由于此時是關搶占的(因為進程A還沒有解自旋鎖,此時依舊處于自旋鎖的臨界區中),導致進程B無法運行。也就是說CPU將無法運行任何程序,一直處于無事可做的狀態,造成CPU的浪費。

    2)對于順帶關中斷的自旋鎖來說,顯而易見在臨界區內使不能睡眠的,因為喚醒一個睡眠的進程依賴于調度器,而調度器是通過時鐘中斷來判斷合適喚醒進程的,倘若在關閉中斷的時候進程睡眠,則調度器將再也無法收到時鐘中斷(因為開中斷的操作也是由該進程控制的),從而永遠都無法喚醒睡眠的進程。也就是說該進程將處于睡死狀態。

簡單來說,自旋鎖的初衷就是:在短期間內進行輕量級的鎖定。一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用的期間進行自旋(特別浪費處理器時間),所以自旋鎖不應該被持有時間過長。如果需要長時間鎖定的話, 最好使用信號量。

2.3 信號量

信號量是用來協調不同進程間的數據對象的,而最主要的應用是共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。一般說來,為了獲得共享資源,進程需要執行下列操作:
1) 測試控制該資源的信號量。   
2) 若此信號量的值為正,則允許進行使用該資源。進程將信號量減1。   
3) 若此信號量為0,則該資源目前不可用,進程進入睡眠狀態,直至信號量值大于0,進程被喚醒,轉入步驟(1)。   
4) 當進程不再使用一個信號量控制的資源時,信號量值加1。如果此時有進程正在睡眠等待此信號量,則喚醒此進程。

維護信號量狀態的是Linux內核操作系統而不是用戶進程。我們可以從頭文件/usr/src/linux/include/linux/sem.h 中看到內核用來維護信號量狀態的各個結構的定義。信號量是一個數據集合,用戶可以單獨使用這一集合的每個元素。要調用的第一個函數是semget,用以獲得一個信號量ID。Linux2.6.26下定義的信號量結構體:

struct semaphore {
    spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};

從以上信號量的定義中,可以看到信號量底層使用到了spin lock的鎖定機制,這個spinlock主要用來確保對count成員的原子性的操作(count–)和測試(count > 0)。

2.3.1 信號量的P操作

  1. void down(struct semaphore *sem);
  2. int down_interruptible(struct semaphore *sem);
  3. int down_trylock(struct semaphore *sem);

函數1表示當信號申請不到時會進程會休眠;對于函數(2)來說,它表示如果當進程因申請不到信號量而進入睡眠后,能被信號打斷,這里所說的信號是指進程間通信的信號,比如我們的Ctrl+C,但這時候這個函數的返回值不為0;

int down_interruptible(struct semaphore *sem)
{
        unsigned long flags;
        int result = 0;
  
        spin_lock_irqsave(&sem->lock, flags);
        if (likely(sem->count > 0))
                sem->count--;
        else
                result = __down_interruptible(sem);
        spin_unlock_irqrestore(&sem->lock, flags);
  
        return result;
}

對此函數的理解:在保證原子操作的前提下,先測試count是否大于0,如果是說明可以獲得信號量,這種情況下需要先將count--,以確保別的進程能否獲得該信號量,然后函數返回,其調用者開始進入臨界區。如果沒有獲得信號量,當前進程利用struct semaphore 中wait_list加入等待隊列,開始睡眠。

對于需要休眠的情況,在__down_interruptible()函數中,會構造一個struct semaphore_waiter類型的變量(struct semaphore_waiter定義如下:

struct semaphore_waiter
{
    struct list_head list;
    struct task_struct *task;
    int up;
};

將當前進程賦給task,并利用其list成員將該變量的節點加入到以sem中的wait_list為頭部的一個列表中,假設有多個進程在sem上調用down_interruptible,則sem的wait_list上形成的隊列如下圖:

等待隊列示意圖

(注:將一個進程阻塞,一般的經過是先把進程放到等待隊列中,接著改變進程的狀態,比如設為TASK_INTERRUPTIBLE,然后調用調度函數schedule(),后者將會把當前進程從cpu的運行隊列中摘下)
函數(3)試圖去獲得一個信號量,如果沒有獲得,函數立刻返回1而不會讓當前進程進入睡眠狀態。

2.3.2 信號量的V操作

void up(struct semaphore *sem);
原型如下:

void up(struct semaphore *sem)
{
    unsigned long flags;
    spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))
            sem->count++;
    else
            __up(sem);
    spin_unlock_irqrestore(&sem->lock, flags);
}

如果沒有其他線程等待在目前即將釋放的信號量上,那么只需將count++即可。如果有其他線程正因為等待該信號量而睡眠,那么調用__up.
__up的定義:

static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
    list_del(&waiter->list);
    waiter->up = 1;
    wake_up_process(waiter->task);
}

這個函數首先獲得sem所在的wait_list為頭部的鏈表的第一個有效節點,然后從鏈表中將其刪除,然后喚醒該節點上睡眠的進程。 由此可見,對于sem上的每次down_interruptible調用,都會在sem的wait_list鏈表尾部加入一新的節點。對于sem上的每次up調用,都會刪除掉wait_list鏈表中的第一個有效節點,并喚醒睡眠在該節點上的進程。

2.3.3 信號量的使用

//1.分配信號量對象
  struct semaphore sema;
//2.初始化為互斥信號量
  init_MUTEX(&sema);
或者:
  DECLARE_MUTEX(sema);
//3.訪問臨界區之前獲取信號量
  down(&sema);
  //如果獲取信號量,立即返回
  //如果信號量不可用,進程將在此休眠,并且休眠的狀態是 [ 不可中斷的休眠狀態 TASK_UNINTERRUPTIBLE] !
  或者
  down_interruptible(&sema);
  //如果信號量不可用,進程將進入 [ 可中斷的休眠狀態 TASK_INTERRUPTIBLE ],如果返回0表示正常獲取信號,如果返回非0,表示接受到了信號
  down_trylock();
  //獲取信號,如果信號量不可用,返回非0,如果信號量可用,返回0;不會引起休眠,可以在中斷上下文使用。返回值也要做判斷!
//4.訪問臨界區:臨界區可以休眠
//5.釋放信號量
  up(&sema);
  //不僅僅釋放信號量,然后喚醒休眠的進程,讓這個進程去獲取信號量來訪問臨界區

2.4 互斥量

互斥體實現了“互相排斥”(mutual exclusion)同步的簡單形式(所以名為互斥體(mutex))。互斥體禁止多個線程同時進入受保護的代碼“臨界區”(critical section)。因此,在任意時刻,只有一個線程被允許進入這樣的代碼保護區。任何線程在進入臨界區之前,必須獲取(acquire)與此區域相關聯的互斥體的所有權。如果已有另一線程擁有了臨界區的互斥體,其他線程就不能再進入其中。這些線程必須等待,直到當前的屬主線程釋放(release)該互斥體。

Linux 2.6.26中mutex的定義:

struct mutex {
        /* 1: unlocked, 0: locked, negative: locked, possible waiters */
        atomic_t                  count;
        spinlock_t                wait_lock;
        struct list_head          wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
        struct thread_info        *owner;
        const char                *name;
        void                      *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map         dep_map;
#endif
};

對比前面的struct semaphore,struct mutex除了增加了幾個作為debug用途的成員變量外,和semaphore幾乎長得一樣。但是mutex的引入主要是為了提供互斥機制,以避免多個進程同時在一個臨界區中運行。

如果靜態聲明一個count=1的semaphore變量,可以使用DECLARE_MUTEX(name),DECLARE_MUTEX(name)實際上是定義一個semaphore,所以它的使用應該對應信號量的P,V函數.

如果要定義一個靜態mutex型變量,應該使用DEFINE_MUTEX

如果在程序運行期要初始化一個mutex變量,可以使用mutex_init(mutex),mutex_init是個宏,在該宏定義的內部,會調用__mutex_init函數。

#define mutex_init(mutex) \
do { \
    static struct lock_class_key __key; \
    \
    __mutex_init((mutex), #mutex, &__key); \
} while (0)
  
void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
    atomic_set(&lock->count, 1);
    spin_lock_init(&lock->wait_lock);
    INIT_LIST_HEAD(&lock->wait_list);
    debug_mutex_init(lock, name, key);
  
}

從__mutex_init的定義可以看出,在使用mutex_init宏來初始化一個mutex變量時,應該使用mutex的指針型。
mutex上的P,V操作:void mutex_lock(struct mutex *lock)和void __sched mutex_unlock(struct mutex *lock)

從原理上講,mutex實際上是count=1情況下的semaphore,所以其PV操作應該和semaphore是一樣的。但是在實際的Linux代碼上,出于性能優化的角度,并非只是單純的重用down_interruptible和up的代碼。以ARM平臺的mutex_lock為例,實際上是將mutex_lock分成兩部分實現:fast path和slow path,主要是基于這樣一個事實:在絕大多數情況下,試圖獲得互斥體的代碼總是可以成功獲得。所以Linux的代碼針對這一事實用ARM V6上的LDREX和STREX指令來實現fast path以期獲得最佳的執行性能。
mutux底層支持:

Linux:底層的pthread mutex采用futex(2)(fast userspace mutex)實現,不必每次加鎖、解鎖都陷入系統調用(從用戶態切換到內核態)。

futex(2):快速用戶態互斥鎖(fast userspace mutex),在非競態(或非鎖爭用,表示申請即能立即拿到鎖而不用等待)的情況下,futex操作完全在用戶態下進行,內核態只負責處理競態(或鎖爭用,表示申請了但有其它線程提前拿到了鎖,需要等待鎖被釋放后才能拿到)下的操作(調用相應的系統調用來仲裁),futex有兩個主要的函數:

futex_wait(addr, val)   //檢查*addr == val ?,若相等則將當前線程放入等待隊列addr中隨眠(陷入到內核態等待喚醒),否則調用失敗并返回錯誤(依舊處于用戶態)
futex_wake(addr, val)   //喚醒val個處于等待隊列addr中的線程(從內核態回到用戶態

因此采用futex(2)的互斥鎖不必每次加解鎖都從用戶態切換到內核態,效率較高.
Windows:底層的CRITICAL_SECTION嵌入了一小段自旋鎖,如果不能立即拿到鎖,它先會自旋一小段時間,如果還拿不到,才掛起當前線程.

2.4.1 互斥量的使用

pthread mutex接口函數:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
    
int pthread_mutex_lock(pthread_mutex_t *mutex);
    
int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
int pthread_mutex_destroy(pthread_mutex_t *mutex);

3. 各種鎖的區別

3.1 信號量/互斥體和自旋鎖的區別

信號量/互斥體允許進程睡眠屬于睡眠鎖,自旋鎖則不允許調用者睡眠,而是讓其循環等待,所以有以下區別應用 :

  1. 信號量和讀寫信號量適合于保持時間較長的情況,它們會導致調用者睡眠,因而自旋鎖適合于保持時間非常短的情況
  2. 自旋鎖可以用于中斷,不能用于進程上下文(會引起死鎖)。而信號量不允許使用在中斷中,而可以用于進程上下文
  3. 自旋鎖保持期間是搶占失效的,自旋鎖被持有時,內核不能被搶占,而信號量和讀寫信號量保持期間是可以被搶占的

另外需要注意的是:

  1. 信號量鎖保護的臨界區可包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區,因為阻塞意味著要進行進程的切換,如果進程被切換出去后,另一進程企圖獲取本自旋鎖,死鎖就會發生。
  2. 在你占用信號量的同時不能占用自旋鎖,因為在你等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。

3.2 信號量和互斥體之間的區別

概念上的區別:
信號量:是進程間(線程間)同步用的,一個進程(線程)完成了某一個動作就通過信號量告訴別的進程(線程),別的進程(線程)再進行某些動作。有二值和多值信號量之分。
互斥鎖:是線程間互斥用的,一個線程占用了某一個共享資源,那么別的線程就無法訪問,直到這個線程離開,其他的線程才開始可以使用這個共享資源。可以把互斥鎖看成二值信號量。

上鎖時:
信號量: 只要信號量的value大于0,其他線程就可以sem_wait成功,成功后信號量的value減一。若value值不大于0,則sem_wait阻塞,直到sem_post釋放后value值加一。
互斥鎖: 只要被鎖住,其他任何線程都不可以訪問被保護的資源。如果沒有鎖,獲得資源成功,否則進行阻塞等待資源可用。
使用場所: 信號量主要適用于進程間通信,當然,也可用于線程間通信。而互斥鎖只能用于線程間通信。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容

  • 在Linux內核中,有很多同步機制。比較經典的有原子操作、spin_lock(忙等待的鎖)、mutex(互斥鎖)、...
    扎Zn了老Fe閱讀 5,063評論 0 5
  • 目錄 1.并發編程的目標 2.并行訪問控制 - 是什么使并行編程變得復雜? 3.關于硬件 - 對并行編程造成的障礙...
    初級造輪師閱讀 5,933評論 0 15
  • 前言 如何正確有效的保護共享數據是編寫并行程序必須面臨的一個難題,通常的手段就是同步。同步可分為阻塞型同步(Blo...
    尉剛強閱讀 2,492評論 3 8
  • POSIX threads(簡稱Pthreads)是在多核平臺上進行并行編程的一套常用的API。線程同步(Thre...
    北辰青閱讀 997評論 0 1
  • 年關將至,匯總記忘。一年來,收獲最多的是育女之喜,最難度過的是無為之苦。忙碌著,沒有任何績效,收獲的,是一份成長。...
    敘事小丑閱讀 330評論 0 0