在
OS
中引入程式後,系統中的多道程式可以併發執行,但系統卻變得更加複雜,為使程式有序執行,引入了同步機制。在程式之間傳送大量資料,也需要利用程式通訊工具。這篇文章總結了程式的幾種同步方式和程式之間的通訊方式。
1. 程式間同步
1.1 基本概念
為避免競爭條件,作業系統需要利用同步機制在併發執行時,保證對臨界區的互斥訪問。程式同步的解決方案主要有:訊號量和管程。
對於同步機制,需要遵循以下四個規則:
- 空閒則入:沒有程式在臨界區時,任何程式可以進入;
- 忙則等待:有程式在臨界區時,其他程式均不能進入臨界區;
- 有限等待:等待進入臨界區的程式不能無限期等待;
- 讓權等待(可選):不能進入臨界區的程式,應該釋放
CPU
,如轉換到阻塞態;
1.2 訊號量
訊號量機制(semaphore
)是一種協調共享資源訪問的方法。訊號量由一個變數 semaphore
和兩個原子操作組成,訊號量只能通過 P
和 V
操作來完成,而且 P
和 V
操作都是原子操作。
將訊號量表示如下:
typedef struct {
int value;
struct process_control_block *list;
} semaphore;
複製程式碼
相應的 P(wait)
操作和 V(signal)
操作如下實現:
wait(semaphore *S) {
S->value--;
if(S->value < 0) {
block(S->list);
}
}
signal(semaphore *S) {
S->value++;
if(S->value <= 0) {
wakeup(S->list);
}
}
複製程式碼
訊號量可分為兩類:互斥訊號量,訊號量大小為為 0
或 1
,用來實現程式的互斥訪問;資源訊號量,訊號量大小為資源數,用來表示系統資源數目。
資源訊號量
代表資源訊號量時,S->value
初值表示系統資源的數目,P
操作意味著程式請求一個資源,於是系統中可分配的資源數減一,如果 S->value < 0
,表示該類資源已分配完畢,因此阻塞該程式,並插入訊號量連結串列 S->list
中。小於 0
時,S->value
的絕對值表示該訊號量連結串列中阻塞的程式數。
V
操作表示程式釋放一個資源,於是系統中可分配的資源數加一,如果增加一後仍然 S->value <= 0
,表示該訊號量連結串列中仍然有阻塞的程式,因此呼叫 wakeup
,將 S->list
中的第一個程式喚醒。
互斥訊號量
代表互斥訊號量時,S->value
初值為 1
,表示只允許一個程式訪問該資源。
利用訊號量實現兩個程式互斥描述如下:
semaphore mutex = 1;
P() {
wait(mutex);
臨界區;
signal(mutex);
}
複製程式碼
當 mutex = 1
時,表示兩個程式都沒有進入臨界區,當 mutex = 0
時,表示一個程式進入臨界區執行;當 mutex = -1
時,表示一個程式進入臨界區執行,另一個程式被阻塞在訊號量佇列中。
1.3 管程
管程採用物件導向思想,將表示共享資源的資料結構及相關的操作,包括同步機制,都集中並封裝到一起。所有程式都只能通過管程間接訪問臨界資源,而管程只允許一個程式進入並執行操作,從而實現程式互斥。
Monitor monitor_name {
share variable declarations;
condition declarations;
public:
void P1(···) {
···
}
{
initialization code;
}
}
複製程式碼
管程中設定了多個條件變數,表示多個程式被阻塞或掛起的條件,條件變數的形式為 condition x, y;
,它也是一種抽象資料型別,每個變數儲存了一條連結串列,記錄因該條件而阻塞的程式,與條件變數相關的兩個操作:condition.cwait
和 condition.csignal
。
condition.cwait
:正在呼叫管程的程式因condition
條件需要被阻塞,則呼叫condition.cwait
將自己插入到condition
的等待佇列中,並釋放管程。此時其他程式可以使用該管程。condition.csignal
:正在呼叫管程的程式發現condition
條件發生變化,則呼叫condition.csignal
喚醒一個因condition
條件而阻塞的程式。如果沒有阻塞的程式,則不產生任何結果。
2. 經典同步問題
2.1 生產者-消費者問題
生產者-消費者問題描述的是:生產者和消費者兩個執行緒共享一個公共的固定大小的緩衝區,生產者在生成產品後將產品放入緩衝區;而消費者從緩衝區取出產品進行處理。
它需要保證以下三個問題:
- 在任何時刻只能有一個生產者或消費者訪問緩衝區(互斥訪問);
- 當緩衝區已滿時,生產者不能再放入資料,必須等待消費者取出一個資料(條件同步);
- 而當緩衝區為空時,消費者不能讀資料,必須等待生產者放入一個資料(條件同步)。
利用訊號量解決
用訊號量解決生產者-消費者問題,使用了三個訊號量:
- 互斥訊號量
mutex
:用來保證生產者和消費者對緩衝區的互斥訪問; - 資源訊號量
full
:記錄已填充的緩衝槽數目; - 資源訊號量
empty
:記錄空的緩衝槽數目。
#define N 10
int in = 0, out = 0;
item buffer[N];
semaphere mutex = 1, full = 0, empty = N;
void producer(void) {
while(TRUE) {
item nextp = produce_item();
wait(empty);
wait(mutex);
buffer[in] = nextp;
in = (in + 1) % N;
signal(mutex);
signal(full);
}
}
void consumer(void) {
while(TRUE) {
wait(full);
wait(mutex);
item nextc = buffer[out];
out = (out + 1) % N;
signal(mutex);
signal(empty);
consume_item(nextc);
}
}
複製程式碼
需要注意的是程式中的多個 wait
操作順序不能顛倒,否則可能造成死鎖。例如在生產者中,當系統中沒有空的緩衝槽時,生產者程式的 wait(mutex)
獲取了緩衝區的訪問權,但 wait(empty)
會阻塞,這樣消費者也無法執行。
利用管程解決
利用管程解決時,需要為它們建立一個管程,其中 count
表示緩衝區中已有的產品數目,條件變數 full
和 empty
有 cwait
和 csignal
兩個操作,另外還包括兩個過程:
put(x)
:生產者將自己生產的產品放入到緩衝區中,而如果count >= N
,表示緩衝區已滿,生產者需要等待;get(x)
:消費者從緩衝區中取出一個產品,如果count <= 0
,表示緩衝區為空,消費者應該等待;
Monitor producerconsumer {
item buffer[N];
int in, out;
condition full, emtpy;
int count;
public:
void put(item x) {
if(count >= N) {
cwait(full);
}
buffer[in] = x;
in = (in + 1) % N;
count++;
csignal(emtpy);
}
item get() {
if(count <= 0) {
cwait(emtpy);
}
x = buffer[out];
out = (out + 1) % N;
count--;
csignal(full);
}
{ in = 0; out = 0; count = 0; }
}
複製程式碼
於是生產者和消費者可描述為:
void producer() {
while(TRUE) {
item nextp = produce_item();
producerconsumer.put(nextp);
}
}
void consumer() {
while(TRUE) {
item nextc = producerconsumer.get();
consume_item(nextc);
}
}
複製程式碼
2.2 哲學家就餐問題
哲學家就餐問題描述的是:有五個哲學家共用一個圓桌,分別坐在周圍的五張椅子上,在圓桌上有五個碗和五隻筷子,他們交替地進行思考和進餐。哲學家在平時進行思考,在飢餓時試圖獲取左右兩隻筷子,拿到兩隻筷子才能進餐,進餐完後放下筷子繼續思考。
為實現筷子的互斥使用,可以用一個訊號量表示一隻筷子,五個訊號量構成訊號量陣列,也都被初始化為 1
。
semaphore chopstick[5] = {1, 1, 1, 1, 1};
複製程式碼
第 i
位哲學家的活動可描述為:
void philosopher(int i) {
while(TRUE) {
wait(chopstick[i]);
wait(chopstick[(i + 1) % 5]);
// eat
signal(chopstick[i]);
signal(chopstick[(i + 1) % 5]);
// think
}
}
複製程式碼
上述解法中,如果五位哲學家同時飢餓而都拿起左邊的筷子,再試圖去拿右邊的筷子時,會出現無限期等待而引起死鎖。
2.3 讀者-寫者問題
讀者-寫者問題描繪的是:一個檔案可以被多個程式共享,允許多個 Reader
程式同時讀這個檔案,但不允許 Wirter
程式和其他 Reader
程式或 Writer
程式同時訪問這個檔案。所以讀者-寫者需要保證一個 Writer
程式必須與其他程式互斥地訪問共享物件。
解決這個問題需要設定兩個互斥訊號量和一個整形變數:
- 互斥訊號量
wmutext
:實現Reader
程式和Writer
程式在讀或寫時的互斥; - 整形變數
readcount
:正在讀的程式數目; - 互斥訊號量
rmutext
:實現多個Reader
程式對readcount
變數的互斥訪問;
semaphore rmutex = 1, wmutex = 1;
int readcount = 0;
void Reader() {
while(TRUE) {
wait(rmutex);
if(readcount == 0) {
wait(wmutex);
}
readcount++;
signal(rmutex);
// perform read opertaion
wait(rmutex);
readcount--;
if(readcount == 0) {
signal(wmutex);
}
signal(rmutex);
}
}
void Writer() {
while(TRUE) {
wait(wmutex);
// perform wirte opertaion
signal(wmutex);
}
}
複製程式碼
只要有一個 Reader
程式在讀,便不允許 Writer
程式去寫。所以,僅當 readcount = 0
,表示沒有 Reader
程式在讀時,Reader
程式才需要執行 wait(wmutex)
操作,而 readcount != 0
時,表示有其他 Reader
程式在讀,也就肯定沒有 Writer
在寫。同理,僅當 readcount = 0
時,才執行 signal(wmutex)
類似。
3. 程式通訊
程式通訊是指程式之間的資訊交換。在程式間要傳送大量資料時,應利用高階通訊方法。
3.1 共享記憶體
在共享記憶體系統中,多個通訊的程式共享某些資料結構或儲存區,程式之間能夠通過這些空間進行通訊。
可分為兩種型別:
- 基於共享資料結構的通訊方式。多個程式共用某些資料結構,實現程式之間的資訊交換,例如生產者-消費者問題中的緩衝區。這種方式僅適用於少量的資料,通訊效率低下。
- 基於共享儲存區的通訊方式。在記憶體中分配一塊共享儲存區,多個程式可通過對該共享區域的讀或寫交換資訊。通訊的程式在通訊前,需要先向系統申請共享儲存區的一個分割槽,以便對其中的資料進行讀寫。
3.2 管道
管道(Pipe
)是指用於連線一個讀程式和一個寫程式以實現程式間通訊的一個共享檔案。傳送程式以字元形式將資料送入管道,而接收程式則從管道中接收資料。
管道機制提供了三方面的協調能力:
- 互斥:當一個程式對管道執行讀或寫操作時,其他程式必須等待;
- 同步:當寫程式把一定數量的資料寫入管道,便睡眠等待,直到讀程式取走資料後再把它喚醒;
- 確定對方是否存在,只有確定對方存在才能通訊。
3.3 訊息傳遞
訊息傳遞機制中,程式以格式化的訊息為單位,將通訊的資料封裝在訊息中,並利用作業系統提供的原語,在程式之間進行訊息傳遞,完成程式間資料交換。
按照實現方式,可分為兩類:
- 直接通訊方式:傳送程式利用作業系統提供的傳送原語,直接把訊息傳送給程式,接收程式則利用接收原語來接收訊息;
- 間接通訊方式:傳送和接收程式,通過共享中間實體方式進行訊息的傳送和接收,完成程式間的通訊。