1. 互斥鎖 (Mutex) - 檔案室的“智能鎖”
首先,我們給之前討論的那些“鎖”一個正式的名字:互斥鎖 (Mutex)。
- 概念:你可以把它簡單理解成檔案室門上的一把“智能鎖”。它只有兩種狀態:
locked
?(已上鎖) 或?unlocked
?(未上鎖)。 - 操作:它提供兩個標準操作:
acquire()
?(或?lock()
):嘗試獲取鎖。如果鎖是開著的,你就把它鎖上并進去。如果鎖已經是鎖著的狀態,你就得等待。release()
?(或?unlock()
):你從檔案室出來后,把鎖打開。
這是一個非常通用的概念。之前我們學的所有方法,無論是軟件的 Peterson 算法,還是硬件的 TSL/Swap 指令,它們本質上都是在實現一個“互斥鎖”。
2. 互斥鎖的“壞脾氣”:忙等待與自旋鎖 (Spinlock)
現在,關鍵問題來了:當?acquire()
?失敗時,進程該如何“等待”?
之前我們學到的所有軟件和硬件實現方法,都有一個共同的特點:它們采用的是一種非常“執著”的等待方式。
- 忙等待 (Busy-Waiting):當進程發現門是鎖著的,它不會走開,而是在門口不停地、反復地檢查:“門開了嗎?開了嗎?現在呢?”。這個過程,進程的CPU并沒有閑著,而是在一個死循環里空轉。
- 自旋鎖 (Spinlock):因為這種等待方式就像一個陀螺在原地不停地“旋轉”一樣,所以,我們把采用這種“忙等待”策略來實現的互斥鎖,特別稱為“自旋鎖”。
所以,視頻里提到的TSL指令、Swap指令,甚至Peterson算法,它們實現的都是自旋鎖。它們都存在忙等待問題,違反了“讓權等待”原則。
3. 自旋鎖的成本與收益:一場“空轉”與“切換”的賽跑
既然自旋鎖會導致CPU空轉,浪費資源,為什么我們還要用它呢?難道就沒有更好的辦法嗎?
有!更好的辦法就是我們之前提到的“讓權等待”:進程發現門鎖著,就去旁邊的休息室睡覺(進入阻塞態),把CPU讓給別人。等門開了,再由別人喚醒。
但是,“去睡覺再被叫醒”這個過程是有成本的,這個成本叫做“進程上下文切換”。它非常昂貴,好比:
- 你把辦公桌上所有文件、電腦狀態全部打包收好(保存現場)。
- 走到休息室(切換到內核態)。
- 找到一個空沙發躺下(進入阻塞隊列)。
- 等別人叫醒你后,你再走回辦公室(切換回用戶態)。
- 再把所有文件和電腦狀態全部恢復原樣(恢復現場)。
這個過程比你單純在門口站著“空轉”幾圈要復雜得多!
于是,我們就面臨一個選擇:
- 選擇自旋:付出“CPU空轉”的代價。
- 選擇睡眠:付出“兩次上下文切換”的代價。
到底哪個更劃算?這取決于門要鎖多久。
4. 場景決定策略:單核 vs. 多核
這個選擇在單核與多核系統上,答案是截然不同的。
在單處理機系統(一個打工人)
- 場景:辦公室只有一個打工人。他想進檔案室,發現門被另一個任務鎖著了。如果他選擇“自旋”,會發生什么?
- 災難性后果:他會一直占用著辦公室里唯一的CPU資源,在門口空轉。而那個鎖著門的任務,因為得不到CPU,根本無法運行,也就永遠無法出來開門!只有等這個自旋的進程時間片用完,被強制換下,那個鎖門進程才有機會上CPU去開鎖。
- 結論:在單核系統里,自旋等待毫無意義,純屬浪費。因為你等的那個鎖,絕對不可能在你自旋的時候被解開。所以,在單核系統里,等待時必須“讓權等待”(去睡覺)。
在多處理機系統(多個打工人)
- 場景:辦公室里有兩個打工人(CPU 0 和 CPU 1)。進程A在CPU 0上運行,它想進檔案室,發現門被正在CPU 1上運行的進程B鎖著了。如果進程A選擇“自旋”,會發生什么?
- 可能的高效結果:
- 進程A在CPU 0上開始自旋,占用了CPU 0。
- 與此同時,進程B正在CPU 1上繼續運行!
- 如果進程B在檔案室里的工作很簡單,可能只需要幾微秒就完成了。它在CPU 1上運行完,把門打開。
- CPU 0上的進程A在下一圈檢查時,立刻就發現門開了,馬上就能進去。
- 結論:在這種情況下,進程A只“空轉”了非常短的時間,這個代價遠比進行一次昂貴的“上下文切換”要小得多。
- 適用性:因此,自旋鎖非常適合多處理器系統,但有一個重要前提:我們能預測鎖被占用的時間非常短。比如,內核里修改一個指針,可能就幾條指令的時間,用自旋鎖就非常劃算。
必會題與詳解
題目一:什么是自旋鎖?它與我們常說的“睡眠鎖”(采用讓權等待的鎖)最核心的區別是什么?
答案詳解:
- 自旋鎖 (Spinlock):是一種互斥鎖的實現方式。當一個進程嘗試獲取鎖失敗時,它不會放棄CPU,而是進入一個“忙等待”循環,反復檢查鎖的狀態,直到獲取成功。
- 核心區別:它們在獲取鎖失敗后的等待策略不同。
- 自旋鎖采用忙等待。進程保持在運行態,持續占用CPU進行空轉。
- 睡眠鎖(如Semaphore、Mutex的非自旋實現)采用讓權等待。進程會放棄CPU,從運行態轉為阻塞態,進入等待隊列,直到被其他進程喚醒。
- 這個區別導致了它們的性能代價不同:自旋鎖的代價是CPU空轉時間,睡眠鎖的代價是進程上下文切換的開銷。
題目二:為什么說“自旋鎖是為多處理器系統量身定做的”?在什么情況下,在多處理器系統中使用自旋鎖是高效的?
答案詳解:
原-因:自旋鎖的有效性依賴于一個核心前提:一個進程在等待鎖的時候,另一個持有鎖的進程能夠同時在運行,以便盡快釋放鎖。這個“同時運行”的條件只有在多處理器系統中才能滿足。在單處理器系統中,持有鎖的進程無法與等待鎖的進程同時運行,導致自旋等待變得毫無意義。
高效的情況:在多處理器系統中,當能夠合理預期鎖被占用的時間非常短時,使用自旋鎖是高效的。因為如果鎖很快被釋放,那么等待進程自旋所消耗的CPU時間成本,將遠小于進行兩次昂貴的進程上下文切換(一次睡眠,一次喚醒)的成本。反之,如果鎖被占用的時間很長,那么長時間的CPU空轉會造成巨大浪費,此時采用睡眠鎖讓出CPU會更劃算。
題目三:一個進程在使用自旋鎖進行忙等待時,是否會一直霸占CPU直到它獲得鎖為止?請解釋原因。
答案詳解:
不會。?一個用戶態進程(或即使是內核態任務,在可搶占內核中)在使用自旋鎖忙等待時,并不會無限期地霸占CPU。
原因是操作系統基于時間片輪轉的搶占式調度機制依然在起作用。當該進程的時間片用完后,無論它是否在忙等待,時鐘中斷都會發生,調度程序會被觸發,并強制將該進程從運行態切換下來,讓它回到就緒隊列。然后調度另一個進程上CPU運行。
所以,進程的忙等待只是在其被分配到的CPU時間片內進行空轉。它并不能破壞操作系統的調度公平性,但它確實浪費了它“本應”用來做有意義計算的CPU時間。