計算機作業系統 - 程序管理
計算機作業系統 - 程序管理
程序與執行緒
1. 程序
程序是資源分配的基本單位。
程序控制塊 (Process Control Block, PCB) 描述程序的基本資訊和執行狀態,所謂的建立程序和撤銷程序,都是指對 PCB 的操作。
2. 執行緒
執行緒是獨立排程的基本單位。
一個程序中可以有多個執行緒,它們共享程序資源。
QQ 和瀏覽器是兩個程序,瀏覽器程序裡面有很多執行緒,例如 HTTP 請求執行緒、事件響應執行緒、渲染執行緒等等,執行緒的併發執行使得在瀏覽器中點選一個新連結從而發起 HTTP 請求時,瀏覽器還可以響應使用者的其它事件。
3. 區別
Ⅰ 擁有資源
程序是資源分配的基本單位,但是執行緒不擁有資源,執行緒可以訪問隸屬程序的資源。
Ⅱ 排程
執行緒是獨立排程的基本單位,在同一程序中,執行緒的切換不會引起程序切換,從一個程序中的執行緒切換到另一個程序中的執行緒時,會引起程序切換。
Ⅲ 系統開銷
由於建立或撤銷程序時,系統都要為之分配或回收資源,如記憶體空間、I/O 裝置等,所付出的開銷遠大於建立或撤銷執行緒時的開銷。類似地,在進行程序切換時,涉及當前執行程序 CPU 環境的儲存及新排程程序 CPU 環境的設定,而執行緒切換時只需儲存和設定少量暫存器內容,開銷很小。
Ⅳ 通訊方面
執行緒間可以通過直接讀寫同一程序中的資料進行通訊,但是程序通訊需要藉助 IPC.
基礎補充
PCB
(1)程序識別符號(內部,外部)
(2)處理機的資訊(通用暫存器,指令計數器,PSW,使用者的棧指標)。
(3)程序排程資訊(程序狀態,程序的優先順序,程序排程所需的其它資訊,事件)
(4)程序控制資訊(程式的資料的地址,資源清單,程序同步和通訊機制,連結指標)
程式碼
struct task_struct{ ... unsigned short uid; int pid; int processor; ... volatile long state; long prority; unsighed long rt_prority; long counter; unsigned long flags; unsigned long policy; ... Struct task_struct *next_task, *prev_task; Struct task_struct *next_run,*prev_run; Struct task_struct *p_opptr,*p_pptr,*p_cptr,*pysptr,*p_ptr; ... }; 下面對部分資料成員進行說明: (1)unsigned short pid 為使用者標識 (2)int pid 為程序標識 (3)int processor標識使用者正在使用的CPU,以支援對稱多處理機方式; (4)volatile long state 標識程序的狀態,可為下列六種狀態之一: 可執行狀態(TASK-RUNING); 可中斷阻塞狀態(TASK-UBERRUPTIBLE) 不可中斷阻塞狀態(TASK-UNINTERRUPTIBLE) 僵死狀態(TASK-ZOMBLE) 暫停態(TASK_STOPPED) 交換態(TASK_SWAPPING) (5)long prority表示程序的優先順序 (6)unsigned long rt_prority 表示實時程序的優先順序,對於普通程序無效 (7)long counter 為程序動態優先順序計數器,用於程序輪轉排程演算法 (8)unsigned long policy 表示程序排程策略,其值為下列三種情況之一: SCHED_OTHER(值為0)對應普通程序優先順序輪轉法(round robin) SCHED_FIFO(值為1)對應實時程序先來先服務演算法; SCHED_RR(值為2)對應實時程序優先順序輪轉法 (9)struct task_struct *next_task,*prev_task為程序PCB雙向連結串列的前後項指標 (10)struct task_struct *next_run,*prev_run為就緒佇列雙向連結串列的前後項指標 (11)struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_ptr指明程序家族間的關係,分別為指向祖父程序、父程序、子程序以及新老程序的指標。
程序狀態的切換
- 就緒狀態(ready):等待被排程
- 執行狀態(running)
- 阻塞狀態(waiting):等待資源
應該注意以下內容:
- 只有就緒態和執行態可以相互轉換,其它的都是單向轉換。就緒狀態的程序通過排程演算法從而獲得 CPU 時間,轉為執行狀態;而執行狀態的程序,在分配給它的 CPU 時間片用完之後就會轉為就緒狀態,等待下一次排程。
- 阻塞狀態是缺少需要的資源從而由執行狀態轉換而來,但是該資源不包括 CPU 時間,缺少 CPU 時間會從執行態轉換為就緒態
程序排程演算法
不同環境的排程演算法目標不同,因此需要針對不同環境來討論排程演算法
1. 批處理系統
批處理系統沒有太多的使用者操作,在該系統中,排程演算法目標是保證吞吐量和週轉時間(從提交到終止的時間)。
1.1 先來先服務 first-come first-serverd(FCFS)
非搶佔式的排程演算法,按照請求的順序進行排程。
有利於長作業,但不利於短作業,因為短作業必須一直等待前面的長作業執行完畢才能執行,而長作業又需要執行很長時間,造成了短作業等待時間過長。
1.2 短作業優先 shortest job first(SJF)
非搶佔式的排程演算法,按估計執行時間最短的順序進行排程。
長作業有可能會餓死,處於一直等待短作業執行完畢的狀態。因為如果一直有短作業到來,那麼長作業永遠得不到排程。
1.3 最短剩餘時間優先 shortest remaining time next(SRTN)
最短作業優先的搶佔式版本,按剩餘執行時間的順序進行排程。 當一個新的作業到達時,其整個執行時間與當前程序的剩餘時間作比較。如果新的程序需要的時間更少,則掛起當前程序,執行新的程序。否則新的程序等待。
2. 互動式系統
互動式系統有大量的使用者互動操作,在該系統中排程演算法的目標是快速地進行響應。
2.1 時間片輪轉
將所有就緒程序按 FCFS 的原則排成一個佇列,每次排程時,把 CPU 時間分配給隊首程序,該程序可以執行一個時間片。當時間片用完時,由計時器發出時鐘中斷,排程程式便停止該程序的執行,並將它送往就緒佇列的末尾,同時繼續把 CPU 時間分配給隊首的程序。
時間片輪轉演算法的效率和時間片的大小有很大關係:
- 因為程序切換都要儲存程序的資訊並且載入新程序的資訊,如果時間片太小,會導致程序切換得太頻繁,在程序切換上就會花過多時間。
- 而如果時間片過長,那麼實時性就不能得到保證。
2.2 優先順序排程
為每個程序分配一個優先順序,按優先順序進行排程。
為了防止低優先順序的程序永遠等不到排程,可以隨著時間的推移增加等待程序的優先順序。
2.3 多級反饋佇列
一個程序需要執行 100 個時間片,如果採用時間片輪轉排程演算法,那麼需要交換 100 次。
多級佇列是為這種需要連續執行多個時間片的程序考慮,它設定了多個佇列,每個佇列時間片大小都不同,例如 1,2,4,8,..。程序在第一個佇列沒執行完,就會被移到下一個佇列。這種方式下,之前的程序只需要交換 7 次。
每個佇列優先權也不同,最上面的優先權最高。因此只有上一個佇列沒有程序在排隊,才能排程當前佇列上的程序。
可以將這種排程演算法看成是時間片輪轉排程演算法和優先順序排程演算法的結合。
3. 實時系統
實時系統要求一個請求在一個確定時間內得到響應。
分為硬實時和軟實時,前者必須滿足絕對的截止時間,後者可以容忍一定的超時。
程序同步
1. 臨界區
對臨界資源進行訪問的那段程式碼稱為臨界區。
為了互斥訪問臨界資源,每個程序在進入臨界區之前,需要先進行檢查。
// entry section
// critical section;
// exit section
2. 同步與互斥
- 同步:多個程序因為合作產生的直接制約關係,使得程序有一定的先後執行關係。
- 互斥:多個程序在同一時刻只有一個程序能進入臨界區。
3. 訊號量
訊號量(Semaphore)是一個整型變數,可以對其執行 down 和 up 操作,也就是常見的 P 和 V 操作。
- down : 如果訊號量大於 0 ,執行 -1 操作;如果訊號量等於 0,程序睡眠,等待訊號量大於 0;
- up :對訊號量執行 +1 操作,喚醒睡眠的程序讓其完成 down 操作。
down 和 up 操作需要被設計成原語,不可分割,通常的做法是在執行這些操作的時候遮蔽中斷。
如果訊號量的取值只能為 0 或者 1,那麼就成為了 互斥量(Mutex) ,0 表示臨界區已經加鎖,1 表示臨界區解鎖。
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 臨界區
up(&mutex);
}
void P2() {
down(&mutex);
// 臨界區
up(&mutex);
}
使用訊號量實現生產者-消費者問題
問題描述:使用一個緩衝區來儲存物品,只有緩衝區沒有滿,生產者才可以放入物品;只有緩衝區不為空,消費者才可以拿走物品。
因為緩衝區屬於臨界資源,因此需要使用一個互斥量 mutex 來控制對緩衝區的互斥訪問。
為了同步生產者和消費者的行為,需要記錄緩衝區中物品的數量。數量可以使用訊號量來進行統計,這裡需要使用兩個訊號量:empty 記錄空緩衝區的數量,full 記錄滿緩衝區的數量。其中,empty 訊號量是在生產者程序中使用,當 empty 不為 0 時,生產者才可以放入物品;full 訊號量是在消費者程序中使用,當 full 訊號量不為 0 時,消費者才可以取走物品。
注意,不能先對緩衝區進行加鎖,再測試訊號量。也就是說,不能先執行 down(mutex) 再執行 down(empty)。如果這麼做了,那麼可能會出現這種情況:生產者對緩衝區加鎖後,執行 down(empty) 操作,發現 empty = 0,此時生產者睡眠。消費者不能進入臨界區,因為生產者對緩衝區加鎖了,消費者就無法執行 up(empty) 操作,empty 永遠都為 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);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
consume_item(item);
up(&mutex);
up(&empty);
}
}
4. 管程
使用訊號量機制實現的生產者消費者問題需要客戶端程式碼做很多控制,而管程把控制的程式碼獨立出來,不僅不容易出錯,也使得客戶端程式碼呼叫更容易。
c 語言不支援管程,下面的示例程式碼使用了類 Pascal 語言來描述管程。示例程式碼的管程提供了 insert() 和 remove() 方法,客戶端程式碼通過呼叫這兩個方法來解決生產者-消費者問題。
monitor ProducerConsumer
integer i;
condition c;
procedure insert();
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
管程有一個重要特性:在一個時刻只能有一個程序使用管程。程序在無法繼續執行的時候不能一直佔用管程,否則其它程序永遠不能使用管程。
管程引入了 條件變數 以及相關的操作:wait() 和 signal() 來實現同步操作。對條件變數執行 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;
經典同步問題
生產者和消費者問題前面已經討論過了。
1. 哲學家進餐問題
五個哲學家圍著一張圓桌,每個哲學家面前放著食物。哲學家的生活有兩種交替活動:吃飯以及思考。當一個哲學家吃飯時,需要先拿起自己左右兩邊的兩根筷子,並且一次只能拿起一根筷子。
下面是一種錯誤的解法,如果所有哲學家同時拿起左手邊的筷子,那麼所有哲學家都在等待其它哲學家吃完並釋放自己手中的筷子,導致死鎖。
#define N 5
void philosopher(int i) {
while(TRUE) {
think();
take(i); // 拿起左邊的筷子
take((i+1)%N); // 拿起右邊的筷子
eat();
put(i);
put((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; // 臨界區的互斥,臨界區是 state 陣列,對其修改需要互斥
semaphore s[N]; // 每個哲學家一個訊號量
void philosopher(int i) {
while(TRUE) {
think(i);
take_two(i);
eat(i);
put_two(i);
}
}
void take_two(int i) {
down(&mutex);
state[i] = HUNGRY;
check(i);
up(&mutex);
down(&s[i]); // 只有收到通知之後才可以開始吃,否則會一直等下去
}
void put_two(i) {
down(&mutex);
state[i] = THINKING;
check(LEFT); // 嘗試通知左右鄰居,自己吃完了,你們可以開始吃了
check(RIGHT);
up(&mutex);
}
void eat(int i) {
down(&mutex);
state[i] = EATING;
up(&mutex);
}
// 檢查兩個鄰居是否都沒有用餐,如果是的話,就 up(&s[i]),使得 down(&s[i]) 能夠得到通知並繼續執行
void check(i) {
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
state[i] = EATING;
up(&s[i]);
}
}
2. 讀者-寫者問題
允許多個程序同時對資料進行讀操作,但是不允許讀和寫以及寫和寫操作同時發生。
一個整型變數 count 記錄在對資料進行讀操作的程序數量,一個互斥量 count_mutex 用於對 count 加鎖,一個互斥量 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--;
if(count == 0) up(&data_mutex);
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
程序通訊
程序同步與程序通訊很容易混淆,它們的區別在於:
- 程序同步:控制多個程序按一定順序執行;
- 程序通訊:程序間傳輸資訊。
程序通訊是一種手段,而程序同步是一種目的。也可以說,為了能夠達到程序同步的目的,需要讓程序進行通訊,傳輸一些程序同步所需要的資訊。
1. 管道
管道是通過呼叫 pipe 函式建立的,fd[0] 用於讀,fd[1] 用於寫。
#include <unistd.h>
int pipe(int fd[2]);
它具有以下限制:
- 只支援半雙工通訊(單向交替傳輸);
- 只能在父子程序或者兄弟程序中使用。
2. FIFO
也稱為命名管道,去除了管道只能在父子程序中使用的限制。
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
FIFO 常用於客戶-伺服器應用程式中,FIFO 用作匯聚點,在客戶程序和伺服器程序之間傳遞資料。
3. 訊息佇列
相比於 FIFO,訊息佇列具有以下優點:
- 訊息佇列可以獨立於讀寫程序存在,從而避免了 FIFO 中同步管道的開啟和關閉時可能產生的困難;
- 避免了 FIFO 的同步阻塞問題,不需要程序自己提供同步方法;
- 讀程序可以根據訊息型別有選擇地接收訊息,而不像 FIFO 那樣只能預設地接收。
4. 訊號量
它是一個計數器,用於為多個程序提供對共享資料物件的訪問。
5. 共享儲存
允許多個程序共享一個給定的儲存區。因為資料不需要在程序之間複製,所以這是最快的一種 IPC。
需要使用訊號量用來同步對共享儲存的訪問。
多個程序可以將同一個檔案對映到它們的地址空間從而實現共享記憶體。另外 XSI 共享記憶體不是使用檔案,而是使用記憶體的匿名段。
6. 套接字
與其它通訊機制不同的是,它可用於不同機器間的程序通訊。