本文是基於操作系統概念(第七版)第六章的學習總結,不足之處歡迎批評指正。
一、什麼是臨界區問題?
臨界區是指在該區域中,進程可能改變共同變量、更新一個表、寫一個文件等。如果多個進程進入臨界區進行修改,那麼將會引起混亂。
典型進程的通用結構:
do{
進入區;
臨界區;
退出區;
剩餘區;
}while(true);
臨界區問題的解答必須滿足下面三個要求:
1、互斥。一個進程在臨界區執行,那麼其他進程是不允許進入的。
2、前進。若臨界區沒有進程執行,那麼其他想進入的進程可以進行選擇。
3、有限等待。
二、處理操作系統的臨界區問題:
1、搶佔內核——適合實時編程,搶佔內核的響應更快。
2、非搶佔內核——從根本上來說不會引起競爭。
三、peterson算法(這是一種基於軟件的臨界區問題的解答)
flag——表示哪個進程想要進入臨界區。
turn——表示哪個進程可以進入臨界區。
下面是進程pi的結構:
do{
flag[i]=true;
turn=j;
while(flag[i]&&turn==j);
臨界區;
flag[i]=false;
剩餘區;
}while(true);
說明:當i想進入臨界區但是不允許進入臨界區,那麼程序就一直進行while循環,進入不了臨界區。如果此時turn==i,即i可以進入臨界區了,那麼進程i進入臨界區,但是此時進程j是進入不了臨界區的。執行完畢後,重新設置進程i,含義爲此時i不想進入臨界區了。peterson算法的關鍵在於藉助了turn這個變量,大家慢慢體會。
四、硬件同步
採用鎖的臨界區問題的解答
do{
請求鎖;
臨界區;
釋放鎖;
剩餘區;
}
這裏我們將用簡單硬件指令來解決臨界區問題。
單處理系統——在修改共享變量是禁止中斷出現即可。
多處理系統——上面的方法不可行,因爲要將消息傳遞給多處理器,費時。
幸運的是,現代計算機系統大多數提供了特殊硬件指令以原子地檢查和修改字的內容或者交換兩個字的內容。
什麼是原子地?
原子操作即不可中斷。
第一個指令testandset()——指令的作用是返回lock的bool值,並設置其爲true。因此如果原來爲true,則只是簡單返回,否則要設置從false->true。
bool testandset(bool *lock){
bool temp=*lock;
*lock=true;
return temp;
}
那麼如何利用testandset()來實現互斥呢?
初始化一個全局變量lock,初始化爲false;需要說明的lock爲false,代表沒有上鎖,因此資源可用。
爲什麼需要將lock設置爲true呢,因此如果進程進入了,那麼需要上鎖,從而使得其他進程進入不了臨界區。
do{
while(testandset(&lock));
臨界區;
lock=false;
剩餘區;
}while(true);
第二個指令——swap()
void swap(bool* a, bool *b){
bool temp;
temp=*a;
*a=*b;
*b=temp;
}
那麼如何利用swap()來實現互斥呢?
聲明一個全局變量lock,初始化爲false,並且每個進程中有一個局部bool變量key。
do{
key=true;
while(key==true) swap(&lock,&key);
臨界區;
lock=false;
}while(true);
首先lock爲false,資源可用,執行swap指令之後,lock=true,資源上鎖,key=false;這裏我覺得若key一直爲false,那麼進程會一直執行臨界區,所以在剩餘去肯定會對key進行重新設置。
但是上面兩個操作雖然解決互斥問題,但並未解決有限等待問題,可能一個進程會一直運行許多次,而另一個等待進程一直在等待。因此可對testandset指令進行改進,
首先引入兩個全局變量:
bool waiting[n];
bool lock;
waiting[i]=false變量代表級進程無需等待,可以進入臨界區,waiting[i]=true,則表示需要等待。每一次只有一個waiting被設置爲false,以滿足互斥要求。key的作用是實現無限循環。
只有第一個進程進入時纔會發現key==false(testandset的作用),之後lock被設置爲true,因此其他進程都必須等待。當進程進入臨界區執行完之後,會查找其他是否有其他進程想進入臨界區,若沒有,則直接釋放鎖,否則將找到的進程waiting設置爲false,這樣這個進程就可以進入臨界區了。所以任何等待臨界區的進程最多只需要等待n-1次,因爲是循環等待。
do{
waiting[i]=true;
key=true;
while(waiting[i]&&key)
key=testandset(&lock);
waiting[i]=true;
臨界區;
j=(i+1)%n;
while(j!=i&&!waiting[j]) j=(j+1)%n;
if(j==i)
lock=false;
esle
waiting[j]=false;
剩餘區;
}while(true);
五、信號量
信號量的引入原因:由於基於硬件的臨界區問題的解決方案對於程序員而言,使用比較複雜。
信號量s是個整數變量,除了初始化外,只能通過兩個原子操作來訪問,wait()和signal(),即P,V操作。
wait操作的定義:
s可以理解爲現可用資源的數量,s<=0代表沒有資源可以使用。wait()——獲取資源,signal()——釋放資源。
wait(s){
while(s<=0);
s--;
}
signal(s){
s++;
}
通常os區分計算信號量和二進制信號量,計數信號量不受值域限制,二進制信號量的值只能爲0或者1。
二進制信號量又稱爲互斥鎖。下面程序就可以使用二進制信號量實現n個進程的臨界區問題。
do{
wait(mutex);
臨界區;
signal(mutex);
剩餘區;
}while(true);
六、信號量的實現
上述信號量的主要缺點:忙等待,即任何想進入臨界區的進程必須在進入代碼中進行連續循環。這種類型的信號量也成爲自旋鎖,這是因爲進程在等待鎖是還在運行。
爲了克服忙等待,那麼如何做呢?
進程不是忙等待,而是阻塞自己,阻塞操作將一個進程放入信號量相關的等待隊列中,並將該進程的狀態切換成等待狀態,這樣就是忙等待了。
修改原來原來wait和signal的定義:
typedef struct{
int value;
struct process* list;
}semaphore;
每個信號量都有一個整型值和一個進程鏈表,signal會從等待進程鏈表中取一個喚醒。
wait(semaphore* s){
s->value--;
if(s->value<0){
將該進程插入等待進程鏈表中,
block();——掛起進程。
}
}
signal(semaphore* s){
s->value++;
if(s->value<=0){
從等待進程鏈表中移除進程p;
wakeup(p);——喚起進程p到就緒隊列中。
}
}
死鎖和飢餓現象:
死鎖:兩個或多個進程無限地等待一個事件,而這個事件又是由這些等待進程中的一個產生,是一種互相等待的現象。具體死鎖問題和算法將在下一篇博文中介紹。