操作系統中的同步互斥(鎖與信號量)


互斥

操作系統的同步與互斥可以從線程和進程兩個角度進行理解。如果從線程的角度理解,這里本文以兩個線程為例,需要考慮這兩個線程是否屬於同一個進程,對於不同進程的線程來說,它們本質上和從兩個進程的角度進行理解是一樣的,在之后討論兩個進程間的同步互斥時會詳細說明。對於同一進程的兩個線程,假設有這樣一段代碼。

int res, temp=0;
res = temp++;

上文的代碼是通過C語言編寫的,需要經過編譯、鏈接之后才能執行,經過編譯后,“res=temp++;”可能被翻譯成如下的匯編指令。

load temp, reg1
store reg1, res
inc reg1
store temp, reg1

如果兩個線程同時執行這樣一段代碼,在執行過程中,可能發生線程切換,導致一個線程沒有全部執行完這4條指令,就將執行權限交到另一個線程的情況。考慮這樣一種情況,線程1在執行完inc reg1之后發生線程切換,第二個線程開始執行,如果第二個線程正常執行完畢,將temp置為1,然后切回線程1,再次將temp置為1。其實這已經和我們的初衷不符,因為正常情況下,我們通常認為temp應該等於2,而且更重要的是,這個代碼帶有不確定性,如果兩個線程執行時,temp可能為1也可能為2,res的值也不確定。

一種簡單的做法是加鎖,還是看一段代碼。

int res,temp=0;
LOCK(p);
res = temp++;
UNLOCK(p);

這里假設p是一個全局變量,初始化為1,函數LOCK(p)可以理解為讀取p的值,如果p>0則p執行自減操作,如果p=0則將當前線程睡眠一個固定的時間,然后再來查詢p的值,這個過程可以表示為如下代碼。

void LOCK(int p)
{
    while(1)
    {
        if(p > 0)
        {
            p--;
            return;
        }
        sleep(10);
    }
}

UNLOCK的代碼同理,這里不詳細寫了。看到這里讀者可能會發現,這段代碼看似解決了以前的問題,但是帶來了兩個新的問題:

  1. 這段代碼並不能真正讓多線程正確工作,比如線程1執行時,假設p=1,那么(p>0)是成立的,但是如果恰巧執行完p>0以后線程切換,線程1讓出執行權限給線程2,那么線程2在判斷p>0時也是成立的,這時兩個線程仍然同時進入到臨界區(我們把不允許多線程同時執行的區域稱為臨界區或互斥區,下同),因此不能解決上述問題。

  2. 第二個問題是,即使多個線程不會同時進入到臨界區,也會導致忙等待的問題。具體來說,如果線程1進入到臨界區,這時切換到線程2,線程2可能也執行這段代碼,當它試圖執行LOCK(p)時,它會一直輪詢p的狀態,此時線程1沒有執行,那么它這個時間片(線程2的執行時間)事實上是浪費了,如果線程2的優先級高於線程1,而且線程的調度算法是優先級高的線程總是先執行,那將產生可怕的后果,線程1永遠也不能執行,因此永遠也不會釋放鎖,而線程2永遠在輪詢,永遠在浪費時間片。

顯然,上述兩個問題是不能回避的,這兩個問題必須得到解決。針對第一個問題,事實上我們采用硬件提供的方法,由硬件確保查詢和更改操作是原子操作,簡單來說,就是判斷(p>0)和執行p--這兩個操作是原子操作,要么都做要么都不做,我記得C庫會提供一個大致叫CompareAndChange的函數來完成這個操作。
針對第二個問題,要解決起來就復雜的多。首先,操作系統將線程分為三種狀態,分別是就緒(Ready)、掛起(Suspend)、執行(Execute),事實上這三種狀態在很多地方都會用到,這里只考慮在訪問臨界區時的應用。首先介紹一下這三種狀態,就緒態的線程是指一個線程已經就緒,簡單來說就是可以被調度執行,需要注意,同一時刻可能存在多個就緒態的線程,如果當前執行的線程執行完畢后,會從當前多個就緒線程中選取一個線程(一般選擇優先級最高的)切換到執行態。執行態的線程在同一時刻只有一個(事實上執行態的線程個數取決於CPU核的個數,但又不僅僅取決於CPU核的個數,這里不詳細討論),掛起態比較特殊,這類線程往往是由於資源得不到滿足而掛起,等到資源滿足以后再被喚醒切換到就緒態。舉個簡單的掛起態的例子,比如一個線程想要讀磁盤,那么它只需要發一個系統調用告訴內核,再由內核告訴磁盤讀取指定區域的數據,但是這個讀取是需要時間的,此時這個線程就被阻塞了,因此給它時間片也沒用,所以它會被os掛起,當磁盤讀取完成后,可以告訴內核,然后由內核再將上述掛起線程喚醒。

回到這個問題,當線程1執行了LOCK(p)之后進入到臨界區以后,如果這時線程1讓出執行權限,由線程2開始執行,那么當它執行到LOCK(p)時,它不會再去輪詢p到狀態,而是會將自己從執行態(因為此時線程2在執行,所以必然處於執行態)變為掛起狀態。需要注意的是,無論線程2的優先級多么高,此時線程2再也沒有執行的可能了。接下來,如果線程1執行完畢后,它會執行UNLOCK(p),那么此時UNLOCK(p)也不能僅僅做p++了,它需要喚醒線程2,也就是喚醒等待p的線程。此時p已經不僅僅是一個整數那么簡單了,准備的說,p已經是一個信號量了,信號量肯定比一個整數要復雜很多,但從原理上講,也不需要很復雜。那么一個信號量需要什么呢?我想它應該需要兩樣東西:

  1. 一個整數記錄當前信號量的值,信號量的值不總是1,比如臨界區的代碼是操作打印機,而此時存在十個打印機,那么允許十個線程同時進入到臨界區,因此信號量可以是10,當然大多數情況下信號量只有0、1兩個取值。

  2. 應該有一個隊列作為信號量的等待隊列,簡單來說,如果線程1在臨界區中執行時讓出執行權限,在線程1再次被調度執行以前,有線程2、線程3兩個線程都試圖進入臨界區,因此這兩個線程會進入到一個隊列中,當信號量被線程1釋放時,我們一般會喚醒先等待信號量的線程,假設線程2先試圖訪問這個臨界區,那么就先喚醒線程2,等線程2再次執行完畢后再喚醒線程3.

如此,一個簡單的信號量就設計完成了,對於信號量的操作,一般稱為P和V操作,P相當於LOCK、V相當於UNLOCK。當然,現在的操作系統對於信號量的設計遠沒有這么簡單,考慮的情況也要復雜很多,這只是一個簡單的分析,如果有讀者在這方面想要交流,歡迎發郵件給我。

同步

上述考慮的是互斥的情況,下面考慮同步的情況。首先,操作系統為什么要有同步操作?舉個例子,福特是汽車行業的先驅,盡管汽車的發明者是benz(關於汽車的發明者,現在仍然爭論不休,這里不詳細說了),但是福特真正把汽車帶進了千家萬戶,他最大的貢獻就是發明了流水線作業,大幅度降低了汽車制造的成本。流水線作業的本質是每個人只負責一小部分,整個工廠像流水線一樣完成汽車制造。對於計算機來說,我們考慮這樣一種情況,假設一個音樂播放軟件,首先需要有一個線程負責告訴磁盤把音樂讀到內存中,然后另一個線程負責把內存中的數據發送到聲卡處理。那么整個音樂播放就是一個同步問題,首先需要將數據讀到內存,才能將數據發送給聲卡,播放出我們可以聽見的聲音。如果將這個問題抽象一下,可以認為有A、B、C、D四個操作,需要按照A、B、C、D的順序執行,對於這類問題,應用上述信號量的機制就可以很好解決。比如設計三個信號量,這里分別記為a,b,c。線程B等待信號量a,線程C等待信號量b,線程D等待信號量c,初始化階段將三個信號量都設置為0,因此線程B、C、D都會阻塞。當線程A執行完畢后,喚醒B,然后依次喚醒就可以讓四個線程嚴格按照順序執行。

當然,這里考慮的仍然是非常簡單的情況,讀者可以考慮按照這種思路會出現哪些無法解決的問題??或者仍有哪些問題沒有考慮到??

歡迎留言以及郵件交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM