我是如何學習寫一個作業系統(七):程序的同步與訊號量
前言
在多程序的執行環境下,程序是併發執行的,不同程序間存在著不同的相互制約關係。為了協調程序之間的相互制約關係,達到資源共享和程序協作,避免程序之間的衝突,引入了程序同步的概念。
臨界資源
多個程序可以共享系統中的各種資源,但其中許多資源一次只能為一個程序所使用,我們把一次只允許一個程序使用的資源成為臨界資源。
對臨界資源的訪問,必須互斥的進行。每個程序中,訪問臨界資源的那段程式碼成為臨界區。
為了保證臨界資源的正確使用,可以把臨界資源的訪問過程分為四個部分。
- 進入區。為了進入臨界區使用臨界資源,在進入去要檢查可否進入臨界區。
- 臨界區。程序中訪問臨界資源的那段程式碼。
- 退出區。將正在訪問臨界區的標誌清除。
- 剩餘區。程式碼中的其餘部分。
一般實現程序的同步有這幾種方法:
- 提過硬體提供的實現
- 訊號量
- 管程
生產者-消費者例項
下面的程式碼就分別是生產者程序和消費者程序,而buffer就是臨界資源
當生產者要訪問臨界資源時,會先判斷buffer是不是已經滿了,而消費者則判斷buffer是不是空的,這就是訪問臨界資源的進入區
而中間對buffer的操作就是臨界區
最後對counter的加減就是設定對臨界區訪問的標誌
但是這裡依舊也有可能出現問題,比如當程序走到in = (in + 1) % BUFFER_SIZE;的時候,這時候作業系統進行排程,這時候的counter的值可能還是0,所以消費者程序可能就會出現問題
這裡的處理可以是利用關閉中斷來限制程序的切換,但是在多核CPU下一樣不管用
這裡就涉及到了臨界區的保護了
#define BUFFER_SIZE 10
typedef struct { . . . } item;
item buffer[BUFFER_SIZE];
int in = out = counter = 0
while (true) {
while(counter== BUFFER_SIZE)
;
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
while (true) { while(counter== 0) ; item = buffer[out]; out = (out + 1) % BUFFER_SIZE; counter--; }
訊號量
什麼是訊號量
為了防止出現因多個程式同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒訪問程式碼的臨界區。
為了避免像上面一樣會發生競爭條件,程式對訊號量訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和傳送(即V(訊號變數))資訊操作。
訊號量的工作原理
由於訊號量只能進行兩種操作等待和傳送訊號,即P(sv)和V(sv),他們的行為是這樣的:
- P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該程序的執行
- V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行,如果沒有程序因等待sv而掛起,就給它加1.
舉個例子,就是兩個程序共享訊號量sv,一旦其中一個程序執行了P(sv)操作,它將得到訊號量,並可以進入臨界區,使sv減1。而第二個程序將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個程序離開臨界區域並執行V(sv)釋放訊號量,這時第二個程序就可以恢復執行。
Linux 0.11的程序同步
在Linux 0.11裡是沒有實現訊號量的,考慮後面會自己實現一個。這裡先看一下Linux 0.11裡用來進行程序同步的兩個函式
sleep_on
p實際上指的是一個等待佇列
如果當前程序是程序0或者無效,就直接退出
然後把要等待的程序放到等待佇列的頭節點,把狀態設定為不可中斷的等待狀態
這裡佇列的形成非常非常的隱蔽,首先把用tmp指向之前的程序,在把當前要睡眠的程序放入,而之所以能形成佇列,是因為現在放入佇列的程序的tmp作為區域性變數是儲存在這個程序的堆疊中的,這樣在把程序切換回來的時候,tmp就自然的指向上一個程序了。
最後當這個程序被喚醒的時候,會回到if語句喚醒等待佇列中所有程序
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp)
tmp->state=0;
}
wake_up
- 喚醒 p 指向的任務。 p是任務等待佇列頭指標。由於新等待任務是插入在等待佇列頭指標處的,
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; // 置為就緒(可執行)狀態TASK_RUNNING.
*p=NULL;
}
}
小結
首先竟然有了多程序,那在訪問共享資源的時候自然就會發生制約關係,所以才引入了程序同步的概念。
而程序同步的關鍵就是對臨界區的保護,訊號量就是一種可以很好的實現對臨界區保護的方