【作業系統】【程序管理】程序和執行緒相關知識點
併發和並行
併發:兩個或多個事件在同一時間間隔內發生;
並行:兩個或多個事件在同一時刻發生;
執行緒
執行緒是程序當中的一條執行流程。同一個程序內多個執行緒之間可以共享程式碼段、資料段、開啟的檔案等資源,但每個執行緒都有獨立一套的暫存器和棧,這樣可以確保執行緒的控制流是相對獨立的。
優點:
- 一個程序中可以同時存在多個執行緒;
- 各個執行緒之間可以併發執行;
- 各個執行緒之間可以共享地址空間和檔案等資源;
缺點:
- 當程序中的一個執行緒奔潰時,會導致其所屬程序的所有執行緒奔潰。
執行緒上下文
多執行緒程式設計中一般執行緒數目大於CPU核心的數目,而一個CPU核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU採取的策略是為每個執行緒分配時間片輪轉的形式,當一個執行緒的時間片用完的時候就會重新處於就緒狀態讓給其他執行緒使用,這個過程就屬於一次上下文切換;
總結:當前任務在執行完CPU時間片切換到另一個任務之前會先儲存自己的狀態,以便下次在切換回這個任務時,可以再載入這個任務的狀態,任務從儲存到再載入的過程就是一次上下文切換;
執行緒間同步方式
訊號量:訊號量機制廣泛使用於程序或執行緒間的同步或互斥,本質上是一個計數器,分為二值訊號量和計數訊號量,主要的方法有 sem_init(),sem_wait(),sem_post()。
互斥鎖:互斥鎖是一種簡單的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態:加鎖(lock)和解鎖(unlock);互斥鎖具有原子性、唯一性和非繁忙等待三大特性,其中唯一性是指如果一個執行緒鎖定了互斥量,在它沒有解鎖之前,其他執行緒無法再對該互斥量加鎖;非繁忙等待是指如果一個執行緒試圖去對已經加鎖的互斥量進行加鎖,那麼改執行緒將被掛起(不佔用 CPU 的任何資源),直到該鎖被釋放,那麼該執行緒將被喚醒並繼續執行。
pthread_mutex_init()
pthread_mutex_lock() // 掛起加鎖
pthread_mutex_trylock() // 非掛起加鎖,執行緒在鎖被佔據時返回EBASY的錯誤,而不是掛起
pthread_mutex_unlock()
pthread_mutex_destroy()
條件變數:條件變數與互斥鎖不同,條件變數不是用來加鎖的,而是一種等待機制。條件變數用來自動阻塞一個執行緒,直到相應的某種特殊情況發生為止。條件變數是與互斥鎖來配合使用的,條件變數使執行緒睡眠等待某種條件的出現,利用執行緒間共享全域性變數進行同步。一個執行緒等待條件成立而掛起,另一個執行緒使條件成立(給出條件成立的訊號)而喚醒其他程序。
條件的檢測是在互斥鎖的保護下進行的;在檢測條件之前,首先鎖住互斥量,然後若條件不成立那麼執行緒阻塞,並釋放互斥鎖,當另一個執行緒改變了條件,那麼將會發送訊號給關聯的條件變數,喚醒一個或多個等待它的執行緒。
pthread_cond_init()
pthread_cond_wait() // pthread_cond_wait期間會自動解鎖,等待條件觸發,然後在返回前自動加鎖,這種機制可以保證線上程加鎖互斥量和進入等待條件變數期間條件變數不被觸發
pthread_cond_singal() // 啟用一個等待該條件的執行緒
pthread_cond_boardcast() // 啟用所有等待該條件的執行緒
pthread_cond_destory()
讀寫鎖:讀寫鎖與互斥鎖類似,但它擁有更高的並行性,特點是寫獨佔、讀共享。
讀寫鎖的特徵:
- 當以寫模式加鎖時,之後所有執行緒對該鎖加鎖都會被阻塞
- 當以讀模式加鎖時,那麼執行緒以讀請求對該鎖加鎖,可以成功,其他失敗
- 當以讀模式加鎖時,既有執行緒以寫模式對該鎖加鎖,又有執行緒以讀模式對該鎖加鎖,那麼會阻塞所有的讀請求執行緒,即寫程序優先
讀寫鎖非常適合與讀的次數遠大於寫的情況。
核心執行緒同步機制
自旋鎖
自旋鎖是互斥鎖的一種實現方式,但是對於互斥鎖來說加鎖失敗會放棄 CPU 即處於掛起狀態,但是自旋鎖會加鎖失敗會一直佔用 CPU,即忙等待(自旋)的狀態,不停的迴圈並且檢測鎖的狀態。
CAS
CAS 的原理,與互斥鎖的區別,CAS 存在的問題,如何解決
為什麼要引入 CAS?CAS 的概念的引入主要是為了解決多執行緒競爭環境下,使用互斥鎖導致的效能損耗的問題。
- 在多執行緒環境下,加鎖和解鎖會導致比較多的上下文切換和排程延時,導致效能下降
- 加鎖可能會導致執行緒掛起
CAS 包括三個運算元,記憶體位置 V,期望原值,新值,如果記憶體位置的值和期望原值相同,那麼使用新值進行更新,否則處理器不做作何操作。CAS 用於無鎖程式設計,在沒有使用到鎖的情況下實現多執行緒之間變數的同步,即本執行緒不被阻塞。CAS 可認為是一個非阻塞輕量級的樂觀鎖。
使用 CAS 模擬互斥鎖的行為就是自旋鎖,就是當一個執行緒在獲取鎖的時候,如果該鎖已被其他執行緒佔用,那麼它進入迴圈等待狀態,並檢測鎖是否能夠被成功獲取,直到獲得鎖才會退出迴圈。若獲取鎖的執行緒一直處於活躍狀態,那麼進入自旋的執行緒會出現忙等待的現象,造成 CPU 的資源浪費。
CAS 與互斥鎖的區別:
在 CAS 失敗的情況下,自旋鎖會一直自旋,但是對於互斥鎖來講,加鎖失敗將會導致執行緒掛起,不消耗 CPU 資源,因此,兩者的區別就是自旋的時間和執行緒上下文切換的時間,在資源充足的情況下,自旋會更快一些;但是在競爭很激烈的情況下,可能只有一個 CAS 可以成功,其他都是失敗自旋,那麼此時 CPU 的資源消耗可以說是巨大的。解決方案的話我們可以設定超時等。
CAS 存在的問題:ABA 問題,兩個執行緒 p1 和 p2,p1 從記憶體中讀到的值是 A,p2 將值改為 B,然後再改回 A,之後又被 p1 搶佔,讀取到的值還是 A。由於 CAS 判斷的是指標的地址,若該地址被重用,那麼問題就很大了,針對此問題我們可以採用具有原子性的記憶體引用計數。
多執行緒模型
多執行緒模型有多對一模型、一對一模型和多對多模型。
- 多對一模型:多個使用者級執行緒對映到一個核心級執行緒上,那麼執行緒的管理是由使用者空間的執行緒庫來完成的,因此效率更高;但是如果一個執行緒使用系統呼叫然後被阻塞,那麼將導致整個程序被阻塞;任意時間只能有一個執行緒訪問核心,因此多個執行緒不能執行在多處理器的系統上,沒有真正意義上實現併發。
- 一對一模型:對映每一個使用者級執行緒到核心級執行緒,由於為每一個使用者級執行緒都在核心中映射了一個核心級執行緒,因此允許多個執行緒併發執行在多個處理器上,並且若其中某個執行緒使用了系統呼叫被阻塞,但是核心可以排程其他執行緒繼續執行;缺點是建立核心執行緒的開銷會影響系統的效能,因此係統一般會對使用者可以建立的執行緒數目有限制,如 Linux。
- 多對多模型:多個使用者級執行緒對映到同樣數量或更少數量的核心級執行緒上。
執行緒安全問題和可重入函式
執行緒安全就是在多執行緒併發執行某一區域的程式碼,不會出現結果不一致的情況。執行緒安全的問題一般是由於對全域性變數、靜態變數的操作造成的。
可重入是多個執行流反覆執行一段程式碼,其結果不會受其他執行流的影響而改變,通常是訪問各自的棧資源。
可重入函式的定義時某個執行流由於異常或者中斷暫時導致正在執行的函式不被執行,轉而其他執行流執行該函式,那麼後者對同一個函式的執行結果不會影響前者恢復執行後對函式結果的影響。
區別:
- 執行緒安全的函式不一定是可重入的,但是可重入函式一定是執行緒安全的
- 如果對臨界資源的訪問都加上鎖,那麼該函式是執行緒安全的,但是如果重入函式若鎖還未釋放那麼將導致死鎖,因此是不可重入的
常見的不可重入函式:
- 使用了靜態資料結構
- 呼叫了 malloc 和 free 等
- 呼叫了標準 I/O 函式
- 進行了浮點運算(浮點運算大多使用協處理器或者軟體模擬來實現)
程序
程序資料結構
程序是由PCB、程式段和相關資料段構成的;
PCB
-
程序描述資訊
- 程序識別符號:標識程序,每個程序都有一個且唯一;
- 使用者識別符號:程序歸屬的使用者,使用者識別符號主要為共享和保護服務;
-
程序控制和管理資訊
- 程序當前狀態
- 程序優先順序:程序搶佔CPU時的優先順序;
-
資源分配清單
有關記憶體地址空間或虛擬地址空間的資訊,所開啟檔案的列表和所使用的I/O裝置資訊;
-
CPU相關資訊
CPU中各個暫存器的值,當程序被切換時,CPU的狀態資訊都會被儲存在相應的PCB中,以便程序重新執行時,能從斷點處繼續執行;
PCB組織方式
- 通過連結串列進行組織,把具有相同狀態的程序鏈在一起,組成各種佇列;
- 將所有處於就緒狀態的程序鏈在一起,稱為就緒佇列;
- 把所有阻塞的程序鏈在一起,稱為阻塞佇列;
- 通過索引方式,將同一狀態的程序組織在一個索引表中,索引表項指向相應的PCB,不同狀態對應不同的索引表;
- 程序建立銷燬等狀態發生頻繁,連結串列的插入和刪除比較靈活,所以一般採用連結串列;
程序上下文切換
CPU上下文
- 大多數作業系統都是多工,通常支援大於 CPU 數量的任務同時執行。實際上,這些任務並不是同時執行的,只是因為系統在很短的時間內,讓各個任務分別在 CPU 執行,於是就造成同時執行的錯覺。任務是交給 CPU 執行的,那麼在每個任務執行前,CPU 需要知道任務從哪裡載入,又從哪裡開始執行。所以,作業系統需要事先幫 CPU 設定好 CPU 暫存器和程式計數器。
- CPU 暫存器是 CPU 內部一個容量小,但是速度極快的記憶體(快取)。我舉個例子,暫存器像是你的口袋,記憶體像你的書包,硬碟則是你家裡的櫃子,如果你的東西存放到口袋,那肯定是比你從書包或家裡櫃子取出來要快的多。
- 程式計數器則是用來儲存 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。
- 所以說,CPU 暫存器和程式計數是 CPU 在執行任何任務前,所必須依賴的環境,這些環境就叫做 CPU 上下文。
CPU上下文切換
- CPU 上下文切換就是先把前一個任務的 CPU 上下文(CPU 暫存器和程式計數器)儲存起來,然後載入新任務的上下文到這些暫存器和程式計數器,最後再跳轉到程式計數器所指的新位置,執行新任務;
- 系統核心會儲存保持下來的上下文資訊,當此任務再次被分配給 CPU 執行時,CPU 會重新載入這些上下文,這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續執行;
- 上面說到所謂的「任務」,主要包含程序、執行緒和中斷。所以,可以根據任務的不同,把 CPU 上下文切換分成:程序上下文切換、執行緒上下文切換和中斷上下文切換;
程序上下文
-
在不同時刻程序之間需要切換,讓不同的程序可以在CPU執行,這個程序切換到另一個程序執行,稱為程序的上下文切換;
-
程序是由核心管理和排程的,所以程序的切換只能發生在核心態;
-
程序的上下文切換不僅包含了虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的資源;
-
通常,會把交換的資訊儲存在程序的 PCB,當要執行另外一個程序的時候,我們需要從這個程序的 PCB 取出上下文,然後恢復到 CPU 中,這使得這個程序可以繼續執行;
程序上下文切換的場景
- 為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個程序。這樣,當某個程序的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的程序執行;
- 程序在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以執行,這個時候程序也會被掛起,並由系統排程其他程序執行;
- 當程序通過睡眠函式 sleep 這樣的方法將自己主動掛起時,自然也會重新排程;
- 當有優先順序更高的程序執行時,為了保證高優先順序程序的執行,當前程序會被掛起,由高優先順序程序來執行;
- 發生硬體中斷時,CPU 上的程序會被中斷掛起,轉而執行核心中的中斷服務程式;
程序的狀態
狀態
-
執行:程序正在CPU上執行;
-
就緒:程序處於準備執行的狀態;獲得了除CPU以外的所有資源;
-
阻塞: 程序正在等待某一時間而暫停執行;
-
建立:程序正在被建立,尚未轉到就緒狀態;
-
結束:程序正從系統中消失,退出執行;
狀態間轉換
-
就緒 =====> 執行:處於就緒狀態的程序被排程後,獲得CPU,開始執行;
-
執行 =====> 就緒:處於執行狀態的程序時間片用完,必須讓出CPU;
-
執行 =====> 阻塞:程序請求某一資源的使用和分配或等待某一時間的發生時,從執行狀態轉換成阻塞狀態;
-
阻塞 =====> 就緒:程序等待的事件到來或請求的資源得到響應;
掛起狀態
- 與阻塞狀態不是一回事兒,表示程序當前沒有佔用實體記憶體;由於虛擬記憶體管;程序所使用的空間並沒有對映到實體記憶體,而是在硬碟上,程序就會出現掛起狀態;
- 阻塞掛起:程序在外存並等待某個事件的出現;
- 就緒掛起:程序在外存,但只要程序記憶體,立刻可以執行;
殭屍程序和孤兒程序
殭屍程序,子程序先於父程序結束,但是父程序沒有呼叫 wait 或者 waitpid 回收子程序的 PCB,那麼子程序成為殭屍程序,大量的殭屍程序對系統是有害的,它會一直佔用系統的資源。
孤兒程序,父程序先於子程序結束,子程序成為孤兒程序,由 init 程序接管,由 init 程序負責子程序的回收,目的就是為了釋放核心資源,因為程序結束後,它會釋放使用者空間所佔用的資源,但是不會釋放 PCB,即核心資源,核心資源的釋放必須要由該程序的父程序來進行釋放。
回收殭屍程序的資源暴力做法是把它的父程序 kill 掉,那麼其子程序稱為孤兒程序,init 接管,其資源也會被釋放,但不是可取的行為。
正確的做法是父程序呼叫 wait()、waitpid() 方法回收子程序的資源。wait 函式會阻塞直到某個子程序終止,waitpid 提供了非阻塞的選項,並且可以等待指定的程序結束(通過 pid)。
程序通訊方式
管道
管道是一種比較原始的程序間通訊機制,它是一種以位元組流的形式在多程序之間流動,分為無名管道和有名管道,屬於半雙工通訊,管道是儲存在記憶體中的,在管道建立時,核心會給緩衝區分配一個頁面的大小。有名管道會儲存在磁碟介質上,存在於檔案系統中(p),無名管道用於具有親緣關係的程序之間通訊,有名管道可用於任意兩個程序之間的通訊,有名管道產生的 FIFO 檔案,檔案結點儲存在磁碟上,但是資料還是存放在記憶體中的。管道與檔案的區別在於管道中的資料被讀取之後,管道中就沒有資料了。管道是 Linux 核心所管理的固定大小的緩衝區,一般是一頁的大小,4KB,管道寫滿了之後,呼叫 write 會阻塞,管道被讀空了之後,呼叫 read 會阻塞。對於寫端關閉管道進行讀操作,那麼 read 會返回 0;對於讀端關閉的管道進行寫操作時,會觸發 SIGPIPE 訊號,導致程序終止。
管道為什麼是半雙工的? ????
Linux 管道的實現是一個環形緩衝區,每個緩衝區中都有 offset 和 len 標識寫入的位置,在讀寫的時候是要修改這些資訊的,如果兩個程序同時讀或者同時寫,那麼勢必會導致讀寫衝突,因此核心會對管道上鎖,因此管道只能是半雙工的。
訊號量
訊號量機制一般用在程序或執行緒之間的同步與互斥。訊號量本質上一個計數器,用於控制多程序之間對共享資料物件的讀取,它和管道有本質的不同,訊號量機制不是以傳送資料,而是以保護共享資源或保證程序同步為目標的。不儲存程序間的通訊資料。訊號量有二值訊號量和計數訊號量,二值訊號量相當於簡單的互斥鎖。
訊息佇列
訊息佇列是訊息的連結表,使用者可以從訊息佇列的末尾新增訊息,也可以在訊息佇列的頭部讀取訊息,訊息一旦被接收,就會從訊息佇列中移除,類似於 FIFO,但是它可以實現訊息的隨機查詢,比如按型別欄位取訊息。
共享記憶體
共享記憶體允許兩個或多個程序共享同一塊儲存區,通過地址對映將該塊實體記憶體對映到不同程序的地址空間中,那麼一個程序對於共享記憶體中資料的修改,另一程序可以實時的看到,從而實現的程序間的通訊機制,但是在多執行緒環境下,需要訊號量來控制對共享記憶體的互斥訪問。共享記憶體是最快的 IPC 機制,只需要兩次資料拷貝,而管道和訊息佇列需要四次資料拷貝:由於管道和訊息佇列都是核心物件,所執行的操作也都是系統呼叫,而這些資料最終是要在記憶體中儲存的,因此四次拷貝分別是
- 從使用者空間緩衝區將資料拷貝到核心空間緩衝區
- 從核心空間緩衝區將資料拷貝到記憶體
- 從記憶體將資料拷貝到核心空間緩衝區
- 從核心空間緩衝區將資料拷貝到使用者空間緩衝區
而共享記憶體使用 mmap 或者 shmget 函式,在記憶體中開闢了一塊空間,對映到不同程序的虛擬地址空間中,並且向用戶返回指向該塊記憶體的指標,因此對該記憶體可通過指標直接訪問,只需要兩次:
- 從使用者空間緩衝區拷貝資料到記憶體
- 從記憶體拷貝資料到使用者空間緩衝區
mmap() // 建立共享記憶體的對映
munmap() // 解除共享記憶體的對映
shmget() // 獲取共享記憶體的識別符號ID
shmat() // 建立對映的共享記憶體
shmdt() // 解除對映
套接字
訊號
如何檢視程序間通訊資源
ipcs 是 Linux 用來檢視程序間通訊資源的工具。
ipcs -m // 檢視系統使用的IPC共享記憶體資源
ipcs -q // 檢視系統使用的IPC訊息佇列資源
ipcs -s // 檢視系統使用的IPC訊號量資源
各個方式使用場景
程序排程方式
執行緒和程序的區別
區別
- 根本區別:程序是系統進行資源分配的基本單位,執行緒是任務排程和執行的基本單位;
- 記憶體:作業系統每建立一個程序,都會給這個程序分配不同的地址空間,來儲存程式所佔用的資源,但是執行緒由於它是共享程序的地址空間的,因此作業系統只為它分配很小一部分記憶體,TLS,執行緒區域性儲存,用來儲存執行緒獨有的資源,整體上它是共享程序的資源的。
- 開銷:程序切換的開銷很大,需要地址空間的切換,程序核心棧的切換,程序使用者堆疊以及暫存器的切換;但是執行緒可以看做是輕量級程序,共享程序地址空間,它的切換僅僅是執行緒棧和 PC 暫存器的儲存切換,因此執行緒切換的開銷小。
- 包含關係:包含關係:一個程序至少有一個執行緒,用於執行程式,稱為主執行緒,或者單執行緒程式,因此,執行緒是包含在程序中的。
- 通訊方式:程序之間的通訊需要通過程序間通訊(IPC),而同一個程序的各執行緒之間可以直接通過傳遞地址或者全域性變數的方式傳遞變數。 在 Linux 核心的實現中,並沒有單獨的執行緒概念,執行緒僅僅被視為與程序共享資源的特殊程序,clone 系統呼叫中傳入 CLONE_VM,即共享父程序地址空間;Linux 下分為使用者級執行緒、核心級執行緒和混合執行緒。
- 資源佔用:程序擁有一個完整的資源平臺,而執行緒只獨享必不可少的資源,如暫存器和棧;
- 相同之處:執行緒同樣具有就緒、阻塞、執行三種基本狀態,同樣具有狀態之間的轉換關係;
執行緒為什麼能減少開銷?
- 建立:執行緒的建立時間比程序快,因為程序在建立的過程中,還需要資源管理資訊,比如記憶體管理資訊、檔案管理資訊,而執行緒在建立的過程中,不會涉及這些資源管理資訊,而是共享它們;
- 終止:執行緒的終止時間比程序快,因為執行緒釋放的資源相比程序少很多;
- 切換:同一個程序內的執行緒切換比程序切換快,因為執行緒具有相同的地址空間(虛擬記憶體共享),這意味著同一個程序的執行緒都具有同一個頁表,那麼在切換的時候不需要切換頁表。而對於程序之間的切換,切換的時候要把頁表給切換掉,而頁表的切換過程開銷是比較大的;
- 通訊:由於同一程序的各執行緒間共享記憶體和檔案資源,那麼線上程之間資料傳遞的時候,就不需要經過核心了,這就使得執行緒之間的資料互動效率更高了;
多執行緒和多程序的區別
- 資料共享:程序資料是分開的,共享複雜,需要用 IPC,同步簡單;多執行緒共享程序資料:共享簡單,同步複雜。
- 開銷:程序建立銷燬、切換複雜,速度慢 ;執行緒建立銷燬、切換簡單,速度快
- 記憶體和CPU:程序佔用記憶體多, CPU 利用率低;執行緒佔用記憶體少, CPU 利用率高
- 實現:程序程式設計簡單,除錯簡單;執行緒程式設計複雜,除錯複雜
- 程序間不會相互影響 ;執行緒一個執行緒掛掉將導致整個程序掛掉
- 程序適應於多核、多機分佈;執行緒適用於多核
共享資源
子程序共享父程序的哪些資源
在 Linux 下,只要子程序在執行過程中,不對父程序的資料做修改,那麼子程序基本是拷貝父程序的整個頁表的,共享父程序的程式碼段、資料段、堆疊段等,即它們的實體地址是完全相同的(但注意父子程序都擁有自己的虛擬地址空間),先把頁表的對映關係建立起來,並不真正進行記憶體拷貝,如果父子程序修改資料,那麼根據寫時拷貝機制,作業系統將為子程序相應的資料段和堆疊段分配物理空間,但是程式碼段還是繼續複製父程序的地址空間(實體地址也相同)。另外子程序擁有自己的 PID、PPID 等,另外對於鎖,子程序是不繼承的。
執行緒共享程序的哪些資源
對於執行緒來講,同一個程序中的執行緒共享程序的地址空間,即執行緒共享程序擁有的所有資源。但它們有各自的私有棧,執行緒區域性儲存 TLS,執行緒切換是 TCB 切換,裡邊儲存執行緒的狀態、暫存器集等等。多個執行緒執行在單一程序的上下文中,共享這個程序虛擬地址空間的整個內容,包括它的程式碼、資料、資料、堆、共享庫和開啟的檔案。
死鎖
死鎖是多個程序由於競爭資源而形成的一種僵局,即互相等待的問題,若無外力作用,這些程序都將無法繼續執行。
- 死鎖產生的原因:系統資源的競爭、程序的推進的順序非法、死鎖產生的四個必要條件;
- 死鎖產生的四個必要條件:互斥、請求和保持、不可剝奪、迴圈等待;
- 預防死鎖:破壞死鎖產生的四個必要條件(一次性分配、資源有序分配、可剝奪資源)
- 檢測死鎖:建立資源分配表和程序等待表,發現死鎖;
- 避免死鎖,可以採用銀行家演算法來進行處理
- 死鎖解除:通過剝奪資源和撤銷程序的方式來解除死鎖
臨界區(資源)
臨界資源是一次只允許一個程序或執行緒所訪問的程式碼段。 臨界區是訪問臨界資源的那段程式。
同步和互斥
互斥指的是對於共享資源保證同一時刻只有一個程序或執行緒訪問,具有唯一性和排他性,但是無法保證對資源訪問的有序性。 同步主要指的是對資源的有序訪問,多個執行緒彼此合作,通過一定的邏輯關係來共同完成一個任務,同步基本是要求互斥的,建立在互斥的基礎上。
經典的程序同步問題
生產者和消費者
生產者消費者模型是一個典型的同步和互斥的範例,首先生產者和消費者對於緩衝區的訪問是互斥關係,同時生產者和消費者都是相互協作的關係。生產者往緩衝區中寫入資料必須要保證存在非滿緩衝區,消費者從快取區中讀取資料必須要保證存在非空緩衝區。
sem mutex = 1; // 臨界區互斥訊號量
sem full = 0; // 緩衝區初始化為空
sem empty = n; // 空閒緩衝區
//生產者:
while(1)
{
p(empty) // 獲取空緩衝區資源
p(mutex) // 進入臨界區
write data
v(mutex) // 離開臨界區,釋放互斥訊號量
v(full) // 滿緩衝區數加1
}
//消費者
while(1)
{
p(full) // 獲取滿緩衝區單元
p(mutex) // 進入臨界區
read data
v(mutex) // 離開臨界區,釋放互斥訊號量
v(empty) // 空緩衝區數加1
}
讀者寫者問題
允許多個讀者進行讀操作,只允許一個寫者進行寫操作,任一寫者在完成寫操作之前不允許其他讀者和寫者對檔案進行操作,寫者執行寫操作之前,應讓已有的讀者和寫者退出。
對於該問題,我們應該首先使用一個計數器 count 來記錄讀者的數量,需要一個互斥訊號量來保護對 count 的修改,還需要一個互斥訊號量來保證讀者寫者互斥地訪問檔案。
int count = 0;
sem mutex = 1;
sem rw = 1;
writer{
p(rw)
write
v(rw)
}
reader{
p(mutex)
if(count == 0)
p(rw) // 第一個讀程序進來阻止寫程序寫
++count
v(mutex)
read
p(mutex)
--count
if(count == 0)
v(rw) // 最後一個讀程序讀取結束釋放訊號量
v(mutex)
}
/* 上述實現有可能導致寫程序飢餓現象發生,我們可以新增一個訊號量實現寫程序優先 */
/* 下述程式主要可以保證在有寫請求發生時,之後的讀請求都被阻塞 */
int count = 0;
sem mutex = 1;
sem rw = 1;
sem w = 1; // 用於實現寫程序優先
writer{
p(w) // 在無寫程序請求時進入
p(rw)
write
v(rw)
v(w)
}
reader{
p(w) // 在無寫程序請求時進入
p(mutex)
if(count == 0)
p(rw) // 第一個讀程序進來阻止寫程序寫
++count
v(mutex)
v(w)
read
p(mutex)
--count
if(count == 0)
v(rw) // 最後一個讀程序讀取結束釋放訊號量
v(mutex)
}
哲學家就餐
sem chopstick[5] = {1,1,1,1,1};
sem mutex = 1;
eat()
{
do{
p(mutex)
p(chopstick[i])
p(chopstick[(i +1) % 5])
v(mutex)
eating
v(chopstick[i])
v(chopstick[(i + 1) % 5])
think
}while(1);
}
銀行家演算法
銀行家演算法是著名的死鎖避免演算法,作業系統按照一定的規則為程序分配資源,當程序首次申請資源時,要測試該程序對資源的最大需求量,若當前系統資源可以滿足其最大需求量,那麼按照當前的申請量來進行分配資源,否則就推遲分配。當程序在執行中再次申請資源時,要測試該程序對資源的申請量與已分配數量之和是否超過了最大需求量。若超過那麼拒絕分配資源,若為超過那麼再次測試系統的現存資源是否滿足該程序所需的最大資源量,若滿足則分配,否則也要推遲分配。
執行安全性演算法,若能找到一個安全序列,那麼系統安全,否則,系統將處於不安全狀態。
資料結構:可利用資源向量 available、已分配資源向量 allocation、需求矩陣 Need、最大需求矩陣 Max,Need = MAX - allocation。