程序管理(二)
- 程序與執行緒
- 程序
- 執行緒
- 區別
- 程序狀態的切換
- 程序排程演算法
- 批處理演算法
- 互動式系統
- 實時系統
- 程序同步
- 臨界區
- 同步和互斥
- 訊號量
- 管程
- 經典同步問題
- 哲學家進餐問題
- 讀者——寫者問題
- 程序通訊
- 管道
- FIFO
- 訊息佇列
- 訊號量
- 共享儲存
- 套接字
程序與執行緒
1.程序
程序是資源分配的基本單元。
程序控制塊(Process Control Block, PCB)描述程序的基本資訊和執行狀態,所謂的建立程序和撤銷程序,都是針對PCB的操作。
下圖顯示了四個程式建立了四個程序,這4各程序可以併發地執行。
2.執行緒
執行緒是獨立排程地基本單位。
一個程序中可以有多個執行緒,它們共享程序資源。
例如,QQ和瀏覽器是兩個程序,瀏覽器程序裡面有很多執行緒,例如HTTP請求執行緒、事件響應執行緒、渲染執行緒等,執行緒地併發執行使得在瀏覽器中點選一個新連結從而發起HTTP請求時,瀏覽器還可以響應使用者地其他事件。
3.區別
- 擁有資源:程序擁有資源,執行緒不擁有資源,執行緒可以訪問隸屬程序地資源。
- 排程:執行緒是獨立排程地基本單位,在同一程序中,執行緒的切換不會引起程序切換。從一個程序中的執行緒切換到另一個程序中的執行緒時,會引起程序切換。
- 系統開銷:由於建立或撤銷程序時,系統都要為之分配或回收資源,如記憶體空間、I/O裝置等,所付出的開銷遠大於建立或撤銷執行緒時的開銷。類似的,在進行程序切換時,涉及當前執行程序CPU環境的儲存及新排程程序CPU環境的設定,而執行緒切換時只需儲存和設定少量暫存器內容,開銷很小。
- 通訊:執行緒間可以通過直接讀寫同一程序中的資料進行通訊,但是程序通訊需要藉助IPC。
程序狀態的切換
- 就緒狀態(ready):等待被排程
- 執行狀態(running)
- 阻塞狀態(waiting):等待資源
應該注意以下內容:
- 只有就緒態和執行態可以相互轉換,其他的都是單向轉換。就緒狀態的程序通過排程演算法從而獲得CPU時間,轉為執行狀態;而執行狀態的程序,在分配給它的CPU時間片用完之後就會轉為就緒狀態,等待下一次排程。
- 阻塞狀態是缺少需要的資源從而由執行狀態轉換而來,但是該資源不包括CPU時間,缺少CPU時間會從執行態轉換為就緒態。
程序排程演算法
不同環境的排程演算法目標不同,因此需要針對不同環境來討論排程演算法。
1.批處理系統
批處理系統沒有太多的使用者操作,在該系統中,排程演算法的目標是保證吞吐量和週轉時間(從提交到終止的時間)
- 先來先服務 first-come first-serverd (FCFS) : 非搶佔式,不利於短作業。因為短作業必須等前面的長作業執行完畢才能執行,而長作業又需要執行很長時間,造成了短作業執行時間過長。
- 短作業優先 shortest job first (SJF) : 非搶佔式,長作業有可能餓死。如果一直有短作業來,長作業永遠得不到排程。
- 最短剩餘時間優先 shortest remaining time next (SRTN) : 短作業優先的搶佔式版本,按剩餘執行時間的順序進行排程。當一個新的作業到達時,其整個執行時間與當前程序的剩餘時間做比較。如果新的程序需要的時間更少,則掛起當前程序,執行新的程序。否則新的程序等待。
2.互動式系統
互動式系統有大量的使用者互動操作,在該系統中排程演算法的目標是快速地進行響應。
- 時間片輪轉:效率和時間片的大小有很大關係,如果時間片過小,會導致程序切換頻繁;如果時間片過大,那麼實時性就不能得到保證。
- 優先順序排程:為每個程序分配一個優先順序,按優先順序進行排程。為防止低優先順序地程序永遠等不到排程,可以隨著時間推移增加等待程序的優先順序。
- 多級反饋佇列:可以看成是時間片輪轉和優先順序排程地結合。多級佇列設定了多個佇列,每個佇列時間片大小都不同,例如 1,2,4,8···。程序在一個佇列排程完,會進入下一級佇列。每個佇列優先順序不同,時間片越小的優先順序越高。
3.實時系統
要求一個請求在確定時間內得到響應。分為硬實時和軟實時,前者必須滿足絕對地截至時間,後者可以容忍一定地超時。
程序同步
1.臨界區 critical section
對臨界資源進行訪問的程式碼稱為臨界區。為了互斥訪問臨界資源,每個程序進入臨界區之前,需要先進行檢查。
// 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記錄滿緩衝區的數量。
注意,不能先對快取區加鎖,再測試訊號量。如先執行down(mutex),再執行down(empty)。
#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 #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); } }
第一個例子可能導致,寫者的等待時間遠遠超過合適時間。當有讀者先於寫者訪問,且持續有讀者訪問時,寫者會一直掛起。
int readcount, writecount; //(initial value = 0) semaphore rmutex, wmutex, readLock, resource; //(initial value = 1) //READER void reader() { <ENTRY Section> down(&readLock); // reader is trying to enter down(&rmutex); // lock to increase readcount readcount++; if (readcount == 1) down(&resource); //if you are the first reader then lock the resource up(&rmutex); //release for other readers up(&readLock); //Done with trying to access the resource <CRITICAL Section> //reading is performed <EXIT Section> down(&rmutex); //reserve exit section - avoids race condition with readers readcount--; //indicate you're leaving if (readcount == 0) //checks if you are last reader leaving up(&resource); //if last, you must release the locked resource up(&rmutex); //release exit section for other readers } //WRITER void writer() { <ENTRY Section> down(&wmutex); //reserve entry section for writers - avoids race conditions writecount++; //report yourself as a writer entering if (writecount == 1) //checks if you're first writer down(&readLock); //if you're first, then you must lock the readers out. Prevent them from trying to enter CS up(&wmutex); //release entry section <CRITICAL Section> down(&resource); //reserve the resource for yourself - prevents other writers from simultaneously editing the shared resource //writing is performed up(&resource); //release file <EXIT Section> down(&wmutex); //reserve exit section writecount--; //indicate you're leaving if (writecount == 0) //checks if you're the last writer up(&readLock); //if you're last writer, you must unlock the readers. Allows them to try enter CS for reading up(&wmutex); //release exit section }
這個例子導致只要有寫者在,讀者就要一直等待。
下面的方案添加了不允許執行緒餓死的約束條件,也就是說,獲取共享資料鎖的操作總是會在有限的時間內終止。
int readCount; // init to 0; number of readers currently accessing resource // all semaphores initialised to 1 Semaphore resourceAccess; // controls access (read/write) to the resource Semaphore readCountAccess; // for syncing changes to shared variable readCount Semaphore serviceQueue; // FAIRNESS: preserves ordering of requests (signaling must be FIFO) void writer() { down(&serviceQueue); // wait in line to be servicexs // <ENTER> down(&resourceAccess); // request exclusive access to resource // </ENTER> up(&serviceQueue); // let next in line be serviced // <WRITE> writeResource(); // writing is performed // </WRITE> // <EXIT> up(&resourceAccess); // release resource access for next reader/writer // </EXIT> } void reader() { down(&serviceQueue); // wait in line to be serviced down(&readCountAccess); // request exclusive access to readCount // <ENTER> if (readCount == 0) // if there are no readers already reading: down(&resourceAccess); // request resource access for readers (writers blocked) readCount++; // update count of active readers // </ENTER> up(&serviceQueue); // let next in line be serviced up(&readCountAccess); // release access to readCount // <READ> readResource(); // reading is performed // </READ> down(&readCountAccess); // request exclusive access to readCount // <EXIT> readCount--; // update count of active readers if (readCount == 0) // if there are no readers left: up(&resourceAccess); // release resource access for all // </EXIT> up(&readCountAccess); // release access to readCount }
程序通訊
- 程序同步:控制多個程序按一定順序執行
- 程序通訊:程序間傳輸資訊
程序通訊是一種手段,而程序同步是一種目的。可以說,為了達到程序同步的目的,需要讓程序進行通訊,傳輸一些程序同步所需要的資訊。
1.管道
管道是通過pipe函式建立的,fd [ 0 ]用於讀,fd[1]用於寫。
#include <unistd.h> int pipe(int fd[2]);
它有以下限制:
- 只支援半雙工通訊(單向交替傳輸)
- 只能在父子程序或者兄弟程序中使用。
2.FIFO
也稱為命名管道(named PIPE),去除了管道只能在父子程序中使用的限制。
#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.套接字
它可用於不同機器間的程序通訊。