進程的調度、同步、通信
進程的組成
- PCB(進程控制塊): 爲了描述控制進程的運行,系統中存放進程的管理和控制信息的數據結構稱爲進程控制塊(Process Control Block)。它是進程實體的一部分,是操作系統中最重要的記錄性數據結構。它是進程管理和控制的最重要的數據結構,每一個進程均有一個PCB,在創建進程時,建立PCB,伴隨進程運行的全過程,直到進程撤消而撤消。PCB是進程存在的唯一標識,所謂的創建進程和撤銷進程,都是指對 PCB 的操作。
- 程序段: 存放執行的代碼;
- 數據段: 存放程序運行過程中處理的各種數據;
進程的特徵
- 動態性:進程是程序的一次執行過程,動態產生、變化和滅亡的。
- 併發性:內存中各進程可併發執行。
- 獨立性:進程是系統進行資源分配、調度的獨立單位。
- 異步性:個進程以不可預知速度向前推進,導致運行結果的不確定性。
- 結構性:每個進程都有一個PCB,進程由PCB、程序段、數據段組成。
進程的狀態與轉換
進程三種基本狀態:
- 運行:佔有CPU,並在CPU上運行
- 就緒:已經具備運行條件,但是沒有空閒CPU
- 阻塞:因等待某一事件而暫時不能運行
注意: - 只有就緒態和運行態可以相互轉換,其它的都是單向轉換。就緒狀態的進程通過調度算法從而獲得 CPU 時間,轉爲運行狀態;
- 而運行狀態的進程,在分配給它的 CPU 時間片用完之後就會轉爲就緒狀態,等待下一次調度。阻塞狀態是缺少需要的資源從而由運行狀態轉換而來,但是該資源不包括 CPU 時間,缺少 CPU 時間會從運行態轉換爲就緒態;
- 進程只能自己阻塞自己,因爲只有進程自身才知道何時需要等待某種事件的發生;
線程
概念:
QQ 和瀏覽器是兩個進程,瀏覽器進程裏面有很多線程,線程的併發執行使得在瀏覽器可以打開一個窗口後繼續響應其他事件,QQ可以同時發信息和上傳文件。
實現方式:
- 用戶級線程
把整個線程包放在用戶空間中,內核對線程包一無所知,線程切換可以在用戶態下直接完成,無需操作系統干預。在用戶看來,是有多個線程,從內核角度考慮,意識不到線程存在。優點:用戶線程包可以在不支持線程的操作系統上實現。 - 內核級線程
內核級線程的管理工作由操作系統內核完成。線程調度、切換等工作都由內核負責,因此,內核級線程的切換需要在覈心態才能完成。優點:內核線程不需要任何新的、非阻塞系統調用,如果某個進程中的線程引起了頁面故障,內核可以很方便檢查該進程是否有任何其他可運行的線程。缺點:系統調用代價比較大。 - 兩者混合
在同時支持用戶級線程和內核級線程的系統中,採用兩者組合的方式,將n個用戶級線程映射到m個內核級線程上(n >=m),大大提高了靈活度。在這種方法中,內核只識別內核級線程,並對其進行調度,其中一些內核級線程會被多個用戶級線程多路複用,每個內核級線程有一個可以輪流使用的用戶及線程集合。
進程與線程的異同
一個線程只能屬於一個進程,但一個進程中可以有多個線程,它們共享進程資源。
- 擁有資源: 進程是資源分配的基本單位,但是線程不擁有資源,線程可以訪問隸屬進程的資源;
- 調度: 線程是獨立調度的基本單位,在同一進程中,線程的切換不會引起進程切換,從一個進程內的線程切換到另一個進程中的線程時,會引起進程切換。
- 系統開銷: 由於創建或撤銷進程時,系統都要爲之分配或回收資源,如內存空間、I/O 設備等,所付出的開銷遠大於創建或撤銷線程時的開銷。類似地,在進行進程切換時,涉及當前執行進程 CPU 環境的保存及新調度進程 CPU 環境的設置,而線程切換時只需保存和設置少量寄存器內容,開銷很小。
- 通信: 進程間通信 (IPC) 需要進程同步和互斥手段的輔助,以保證數據的一致性。而線程間可以通過直接讀/寫同一進程中的數據段(如全局變量)來進行通信。
進程調度算法
概念:
操作系統管理了系統的有限資源,當有多個進程或線程同時競爭CPU時,因爲資源的有限性,必須按照一定的原則選擇進程(請求)來佔用資源,只要有兩個或更多的進程處於就緒狀態,如果只有一個CPU可用,那麼就必須選擇下一個要運行的進程,這就是調度,其中使用的算法就是調度算法。
不同環境的調度算法目標不同,因此需要針對不同環境來討論調度算法。
1. 批處理系統
批處理系統沒有太多的用戶操作,在該系統中,調度算法目標是保證吞吐量和週轉時間(從提交到終止的時間),有以下三種:
-
先來先服務 first-come first-serverd(FCFS)
即按照請求的順序進行調度,非搶佔式。
分析:有利於長作業,但不利於短作業,因爲短作業必須一直等待前面的長作業執行完畢才能執行,而長作業又需要執行很長時間,造成了短作業等待時間過長。 -
短作業優先 shortest job first(SJF)
按估計運行時間最短的順序進行調度,非搶佔式。
分析:長作業有可能會餓死,處於一直等待短作業執行完畢的狀態。因爲如果一直有短作業到來,那麼長作業永遠得不到調度。 -
最短剩餘時間優先 shortest remaining time next(SRTN)
按估計剩餘時間最短的順序進行調度,搶佔式。
2. 交互式系統
交互式系統有大量的用戶交互操作,在該系統中調度算法的目標是快速地進行響應。
- 時間片輪轉
將所有就緒進程按 FCFS 的原則排成一個隊列,每次調度時,把 CPU 時間分配給隊首進程,該進程可以執行一個時間片。當時間片用完時,由計時器發出時鐘中斷,調度程序便停止該進程的執行,並將它送往就緒隊列的末尾,同時繼續把 CPU 時間分配給隊首的進程,搶佔式。
時間片輪轉算法的效率和時間片的大小有很大關係:
因爲進程切換都要保存進程的信息並且載入新進程的信息,如果時間片太小,會導致進程切換得太頻繁,在進程切換上就會花過多時間。
而如果時間片過長,那麼實時性就不能得到保證。
- 優先級調度
爲每個進程分配一個優先級,按優先級進行調度,搶佔式和非搶佔式都有。
爲了防止低優先級的進程永遠等不到調度,可以隨着時間的推移增加等待進程的優先級。
- 多級反饋隊列
如果一個進程需要執行 100 個時間片,採用時間片輪轉調度算法,那麼需要交換 100 次。
多級隊列是爲這種需要連續執行多個時間片的進程考慮,它設置了多個隊列,優先級從高到低,時間片從小到大,每個隊列時間片大小都不同,例如 1,2,4,8,…。進程在第一個隊列沒執行完,就會被移到下一個隊列。這種方式下,之前的進程只需要交換 7 次,搶佔式。
每個隊列優先權也不同,最上面的優先權最高。因此只有上一個隊列沒有進程在排隊,才能調度當前隊列上的進程。
可以將這種調度算法看成是時間片輪轉調度算法和優先級調度算法的結合。
3. 實時系統
實時系統要求一個請求在一個確定時間內得到響應。
分爲硬實時和軟實時,前者必須滿足絕對的截止時間,後者可以容忍一定的超時。
進程同步
同步 互斥概念
進程同步:指相互合作去完成相同的任務的進程間,由同步機構對執行次序進行協調。(在多道程序環境下,進程是併發執行的,不同進程之間存在着不同的相互制約關係。);
進程互斥:指多個進程在對臨界資源進行訪問的時候,應採用互斥方式;
簡單來說,同步:多個進程按一定順序執行;互斥:多個進程在同一時刻只有一個進程能進入臨界區。
信號量
信號量(Semaphore)是一個整型變量,可以對其執行 down 和 up 操作,也就是常見的 P 和 V 操作。
- down : 如果信號量大於 0 ,執行 -1 操作;如果信號量等於 0,進程睡眠,等待信號量大於 0;
- up :對信號量執行 +1 操作,喚醒睡眠的進程讓其完成 down 操作。
down 和 up 操作需要被設計成原語,不可分割,保證一旦一個信號量操作開始,則在該操作完成或阻塞之前,其他進程均不允許訪問該信號量,這種原子性對解決同步問題和避免競爭條件是絕對必要的。通常的做法是在執行這些操作的時候屏蔽中斷。
如果信號量的取值只能爲 0 或者 1,那麼就成爲了 互斥量(Mutex) ,0 表示臨界區已經加鎖,1 表示臨界區解鎖。
使用信號量實現生產者-消費者問題
問題描述:
生產者、消費者共享一個初始爲空、大小爲n的緩衝區;
只有緩衝區沒滿時,生產者纔可以放入物品,否則必須等待;
只有緩衝區不爲空,消費者纔可以拿走物品,否則必須等待;
緩衝區屬於臨界資源,各進程必須互斥地訪問;
分析:用信號量機制(P、V操作)實現生產者、消費者互斥、同步:
semaphore mutex = 1; //互斥信號量 mutex 來控制對緩衝區的互斥訪問
semaphore empty= 0; //同步信號量,表示空閒緩衝區的數量,當 empty 不爲 0 時,生產者纔可以放入物品
semaphore full = 0; //同步信號量,表示產品的數量,當 full 信號量不爲 0 時,消費者纔可以取走物品
#define N 100 /*緩衝區的槽數目 */
typedef int semaphore;
semaphore mutex = 1; /*控制對臨界區的訪問*/
semaphore empty = N; /*計數緩衝區的空槽數目 */
semaphore full = 0; /*計數緩衝區的滿槽數目,即產品數量 */
void producer() {
while(TRUE) {
int item = produce_item();
down(&empty); /*將空槽數目減1 */
down(&mutex); /*進入臨界區 */
insert_item(item); /*將新數據項放到緩衝區中*/
up(&mutex); /*離開臨界區*/
up(&full); /*將滿槽數目加1*/
}
}
void consumer() {
while(TRUE) {
down(&full); /*將滿槽數目減1 */
down(&mutex); /*進入臨界區 */
int item = remove_item(); /*從緩衝區中取出數據項 */
consume_item(item); /*處理數據項*/
up(&mutex); /*離開臨界區*/
up(&empty); /*將空槽數目加1*/
}
}
**注意:**不能先對緩衝區進行加鎖,再測試信號量。也就是說,不能先執行 down(mutex) 再執行 down(empty)。如果這麼做了,那麼可能會出現這種情況:生產者對緩衝區加鎖後,執行 down(empty) 操作,發現 empty = 0,此時生產者睡眠。消費者不能進入臨界區,因爲生產者對緩衝區加鎖了,消費者就無法執行 up(empty) 操作,empty 永遠都爲 0,導致生產者永遠等待下,不會釋放鎖,消費者因此也會永遠等待下去,出現“死鎖”現象。
因此:實現互斥的down(P)操作一定要在實現同步的down(P)操作之後,up(V)操作不會導致進程阻塞,因此兩個V操作順序可以交換。
生產者消費者問題分析:
- 關係分析:找出各個進程,分析它們之間同步、互斥關係;
- 根據各進程操作流程確定P、V操作的大致順序;
- 設置信號量,並確定信號量初值;通常互斥信號量初值一般爲1,同步信號量的初始值要看對應資源的初始值是多少。
類似的經典同步問題還有:
1. 讀者-寫者問題:
允許多個進程同時對數據進行讀操作,但是不允許讀和寫以及寫和寫操作同時發生。
互斥關係:寫進程——寫進程、寫進程——讀進程、讀進程與讀進程不存在互斥問題。
一個整型變量 count 記錄在對數據進行讀操作的進程數量
一個互斥量 count_mutex 用於對 count 加鎖(防止兩個讀進程併發執行,這樣兩個進程先後執行down(&data_mutex),從而使第二個線程阻塞)
一個互斥量 data_mutex 用於對讀寫的數據加鎖,保證對文件的互斥訪問。
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(TRUE) {
down(&count_mutex);
count++;
if(count == 1) down(&data_mutex); // 第一個讀者需要對數據進行加鎖,防止寫進程訪問
up(&count_mutex);
read();
down(&count_mutex);
count--; // 訪問文件的讀進程數-1
if(count == 0) up(&data_mutex); // 最後一個進程負責解鎖
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
以上這種算法潛在的問題:只要讀進程還在讀,寫進程就要一直阻塞等待,可能“餓死”。因此,這種算法中,讀進程是優先的。
防止寫進程餓死的方法:
即在第一個讀者到達,且一個寫者在等待時,讀者在寫者之後被掛起,而不是立即允許進入。用這種方式,在一個寫者到達時如果有正在工作的讀者,那麼該寫者只要等待這個讀者完成,而不用等候其後面到來的讀者。但是該方案併發度和效率較低。
2. 哲學家進餐問題:
五個哲學家圍着一張圓桌,每個哲學家面前放着食物。哲學家的生活有兩種交替活動:喫飯以及思考。當一個哲學家喫飯時,需要先拿起自己左右兩邊的兩根筷子(一邊一根),並且一次只能拿起一根筷子。
下面是一種錯誤的解法,考慮到如果所有哲學家同時拿起左手邊的筷子,那麼就無法拿起右手邊的筷子,造成死鎖。
#define N 5 // 哲學家個數
void philosopher(int i) // 哲學家編號:0 - 4
{
while(TRUE)
{
think(); // 哲學家在思考
take_fork(i); // 去拿左邊的叉子
take_fork((i + 1) % N); // 去拿右邊的叉子
eat(); // 喫飯
put_fork(i); // 放下左邊的叉子
put_fork((i + 1) % N); // 放下右邊的叉子
}
}
爲了防止死鎖的發生,可以設置兩個條件(臨界資源):
- 必須同時拿起左右兩根筷子;
- 只有在兩個鄰居都沒有進餐的情況下才允許進餐。
#define N 5
#define LEFT (i + N - 1) % N // 左鄰居
#define RIGHT (i + 1) % N // 右鄰居
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N]; // 跟蹤每個哲學家的狀態
semaphore mutex = 1; // 臨界區的互斥
semaphore s[N]; // 每個哲學家一個信號量
void philosopher(int i) {
while(TRUE) {
think(); // 思考
take_two(i); // 拿起兩個筷子
eat();
put_two(i);
}
}
void take_two(int i) {
down(&mutex); // 進入臨界區
state[i] = HUNGRY; // 我餓了
test(i); // 試圖拿兩隻筷子
up(&mutex); // 退出臨界區
down(&s[i]); // 沒有筷子便阻塞
}
void put_two(i) {
down(&mutex);
state[i] = THINKING;
test(LEFT); // 左邊的人嘗試
test(RIGHT); //右邊的人嘗試
up(&mutex);
}
void test(i) { // 嘗試拿起兩把筷子
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
state[i] = EATING;
up(&s[i]); // 通知第i個人可以喫飯了
}
}
哲學家問題關鍵在於解決進程死鎖;
這些進程之間只存在互斥關係,但是和之前的互斥關係不同的是: 每個進程都需要同時持有兩個臨界資源,因此有死鎖的可能;
管程
管程是一個由過程、變量及數據結構等組成的一個集合,他們組成一個特殊的模塊或軟件包。
使用用信號量機制實現的生產者消費者問題需要客戶端代碼做很多控制而且編程麻煩,而管程把控制的代碼獨立出來,不僅不容易出錯,也使得客戶端代碼調用更容易。
c 語言不支持管程,下面的示例代碼使用了類 Pascal 語言來描述管程。示例代碼的管程提供了 insert() 和 remove() 方法,客戶端代碼通過調用這兩個方法來解決生產者-消費者問題。
monitor ProducerConsumer
integer i;
condition c;
procedure insert();
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
管程有基本特性:
- 每次只能有一個進程使用管程。這一特性使管程能有效完成互斥。進程在無法繼續執行的時候不能一直佔用管程,否則其它進程永遠不能使用管程。
- 各外部進程/線程只能通過管程提供的特定“入口”才能訪問共享數據。
管程引入了 條件變量 以及相關的操作:wait() 和 signal() 來實現同步操作。
- 對條件變量執行 wait() 操作會導致調用進程阻塞,把管程讓出來給另一個進程持有,比如生產者發現緩衝區已滿,它會在某個條件變量上如(full)執行wait操作。
- signal() 操作用於喚醒被阻塞的進程。
使用管程實現生產者-消費者問題 :
一次只能有一個管程過程活躍
// 管程
monitor ProducerConsumer
condition full, empty;
integer count := 0;
condition c;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty);
end;
function remove: integer;
begin
if count = 0 then wait(empty);
remove = remove_item;
count := count - 1;
if count = N -1 then signal(full);
end;
end monitor;
// 生產者客戶端
procedure producer
begin
while true do
begin
item = produce_item;
ProducerConsumer.insert(item);
end
end;
// 消費者客戶端
procedure consumer
begin
while true do
begin
item = ProducerConsumer.remove;
consume_item(item);
end
end;
進程的通信
進程間通信(IPC,InterProcess Communication)是指在不同進程之間傳播或交換信息。
IPC的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
1.管道:
管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。
特點:
-
它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
-
它只能用於父子進程之間的通信。
-
當一個管道建立時,它會創建兩個文件描述符:fd[0]爲讀而打開,fd[1]爲寫而打開,要關閉管道只需將這兩個文件描述符關閉即可。
1 #include <unistd.h>
2 int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
2.FIFO:
FIFO,也稱爲命名管道,它是一種文件類型,去除了管道只能在父子進程中使用的限制,FIFO可以在無關的進程之間交換數據。
FIFO 常用於客戶-服務器應用程序中,FIFO 用作匯聚點,在客戶進程和服務器進程之間傳遞數據。
3. 消息隊列:
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
1、特點
消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
4. 信號量:
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
特點:
-
信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
-
信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
-
每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
-
支持信號量組。
5. 共享存儲:
特點:
-
共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
-
因爲多個進程可以同時操作,所以需要進行同步。
-
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
總結:
-
管道:速度慢,容量有限,只有父子進程能通訊
-
FIFO:任何進程間都能通訊,但速度慢
-
消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題
-
信號量:不能傳遞複雜消息,只能用來同步
-
共享內存:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存
死鎖:
可參考書本或者cyc
死鎖: 如果一個進程集合裏面的每個進程都在等待只能由這個進程集合中的其他一個進程(包括他自身)才能引發的事件,那麼該進程集合就是死鎖的。
必要條件:
- 互斥:每個資源要麼已經分配給了一個進程,要麼就是可用的。
- 佔有和等待:已經得到了某個資源的進程可以再請求新的資源。
- 不可搶佔:已經分配給一個進程的資源不能強制性地被搶佔,它只能被佔有它的進程顯式地釋放。
- 環路等待:有兩個或者兩個以上的進程組成一條環路,該環路中的每個進程都在等待下一個進程所佔有的資源。
死鎖發生時,以上四個條件一定是同時滿足的,缺一不可。
四種處理死鎖策略:
- 鴕鳥策略
- 檢測死鎖並恢復
- 動態避免死鎖
- 預防死鎖產生
鴕鳥策略
忽略該問題,把頭埋在沙子裏,假裝根本沒發生問題。
因爲解決死鎖問題的代價很高,因此鴕鳥策略這種不採取任務措施的方案會獲得更高的性能。
當發生死鎖時不會對用戶造成多大影響,或發生死鎖的概率很低,可以採用鴕鳥策略。
檢測死鎖並恢復
系統並不試圖阻止死鎖的產生,而是允許死鎖發生,當檢測到死鎖發生時,採取措施進行恢復。
- 每種類型一個資源的死鎖檢測
- 每種類型多個資源的死鎖檢測
死鎖恢復:
- 利用搶佔恢復(將某個資源從它的當前所有者那裏轉移給另一個進程)
- 利用回滾恢復(將進程復位到一個更早的狀態)
- 通過殺死進程恢復(最簡單解決死鎖方法,可以殺掉環中的一個進程)
死鎖避免
安全狀態:
如果沒有死鎖發生,並且即使所有進程突然請求對資源的最大需求,也仍然存在某種調度次序能夠使得每一個進程運行完畢,則稱該狀態是安全的。
單個資源的銀行家算法:
該模型基於一個小城鎮的銀行家,他向一羣客戶分別承諾了一定的貸款額度,算法要做的是判斷對請求的滿足是否會進入不安全狀態,如果是,就拒絕請求;否則予以分配。
多個資源的銀行家算法:
預防死鎖產生
破壞死鎖產生必要條件,使死鎖不會產生。
1. 破壞互斥條件:
將臨界資源改造爲可共享使用的資源(如SPOOLing技術)
缺點:可行性不高,很多時候無法破壞互斥條件
2. 破壞佔有和等待條件:
一種實現方式是規定所有進程在開始執行前請求所需要的全部資源,之後一直保持。
缺點:資源利用率低;可能導致飢餓
3. 破壞不可搶佔條件:
一種實現方式是申請的資源得不到滿足時,立即釋放擁有的所有資源
缺點:實現複雜,反覆申請和釋放導致系統開銷大
4. 破壞環路等待條件:
給資源編號,進程必須按照編號的順序申請資源
缺點:不方便增加新設備;會導致資源浪費;用戶編程麻煩
參考書籍:《現代操作系統》