前言
在多進程的運行環境下,進程是並發執行的,不同進程間存在着不同的相互制約關系。為了協調進程之間的相互制約關系,達到資源共享和進程協作,避免進程之間的沖突,引入了進程同步的概念。
臨界資源
多個進程可以共享系統中的各種資源,但其中許多資源一次只能為一個進程所使用,我們把一次只允許一個進程使用的資源成為臨界資源。
對臨界資源的訪問,必須互斥的進行。每個進程中,訪問臨界資源的那段代碼成為臨界區。
為了保證臨界資源的正確使用,可以把臨界資源的訪問過程分為四個部分。
- 進入區。為了進入臨界區使用臨界資源,在進入去要檢查可否進入臨界區。
- 臨界區。進程中訪問臨界資源的那段代碼。
- 退出區。將正在訪問臨界區的標志清除。
- 剩余區。代碼中的其余部分。
一般實現進程的同步有這幾種方法:
- 提過硬件提供的實現
- 信號量
- 管程
生產者-消費者實例
下面的代碼就分別是生產者進程和消費者進程,而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;
}
}
小結
首先竟然有了多進程,那在訪問共享資源的時候自然就會發生制約關系,所以才引入了進程同步的概念。
而進程同步的關鍵就是對臨界區的保護,信號量就是一種可以很好的實現對臨界區保護的方法