進程互斥同步及通信死鎖問題【操作系統】

(一) 進程間的互斥關係

(1) 電影院多線程問題引入

由於我們今天的問題是基於多個線程併發的,所以我簡單的通過一個 Java 多線程的例子來引入今天的內容(今天主要講的是進程,這裏的多線程問題,體會一下出現的問題就好了)

在SellTicket類中添加sleep方法,延遲一下線程,拖慢一下執行的速度

public class SellTickets implements Runnable {
    //電影票數
    private int ticket = 10;

    @Override
    public void run() {
        while (true){
            if (tickets > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() 
                                   + "正在出售第" + (tickets--) + "張票");
            }
        }
    }
}

我這裏爲了篇幅只講票數定義到了10張,自己測試可以設置的稍微多一些,更好看出結果

public class SellTicketsTest {
    public static void main(String[] args) {
        //創建資源對象
        SellTickets st = new SellTickets();

        //創建線程對象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        //啓動線程
        t1.start();
        t2.start();
        t3.start();
    }
}

執行測試代碼看一下結果

窗口3正在出售第10張票
窗口2正在出售第8張票
窗口1正在出售第9張票
窗口3正在出售第7張票
窗口2正在出售第5張票
窗口1正在出售第6張票
窗口2正在出售第4張票
窗口3正在出售第3張票
窗口1正在出售第3張票
窗口2正在出售第2張票
窗口1正在出售第1張票
窗口3正在出售第1張票

僅僅通過10張票,就能看到問題,首先票的順序亂了,理應是10、9、8 … 2、1 但是出現了 10、8、9這種問題,其次例如 1 和 3 這兩章票,卻賣了多次,顯然是極其不合理的,如果數量多的情況下,有時候還可能出現負數票,具體原因大家可以去看我 Java基礎教程中的多線程入門那篇

我簡單摘一下:

線程1執行的同時線程2也可能在執行,所以可能導致在讀取 tickets-- 時原來的數值和減1過程的中間擠進了兩個線程而出現重複,這就是重複票的問題,歸根結底是因爲多個線程不加控制的訪問操作 ticket 這個變量

但是我們要解決這個問題怎麼弄呢,當然有很多種方法,我們後面也會介紹,而 Java 中常用的一種方式就是加鎖

下面是一個 Lock鎖 的實例,當然還有其他的鎖,例如同步鎖等等,但是 Java 中的具體實現不是我們這篇文章想要講的,我們對於這裏看一下效果就好了

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTickets2 implements Runnable {

    private int tickets = 10;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                ;
                if (tickets > 0) {
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "張票");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

執行一下

窗口1正在出售第10張票
窗口1正在出售第9張票
窗口2正在出售第8張票
窗口2正在出售第7張票
窗口2正在出售第6張票
窗口2正在出售第5張票
窗口2正在出售第4張票
窗口2正在出售第3張票
窗口2正在出售第2張票
窗口2正在出售第1張票

果然上面的問題得到了解決

進程也是這樣,在兩個進程併發執行的過程中,如果一個進程對共享變量(例如:ticket)訪問還沒有完全結束,另外一個進程就開始訪問的話,就會產生數據衝突。像這樣的共享變量一次只允許一個進程訪問,這樣,兩個進程在使用這種共享變量的時候就產生一種競爭關係,也就是進程間的互斥關係。如果在進程併發執行的過程中,沒有考慮這種互斥關係,從而沒有加以有效控制的話,就會出現問題

(2) 互斥

一組併發進程中的一個或多個程序段,因共享某一公有資源而導致它們必須以一個不允許交叉執行的單位執行
不允許兩個以上的共享該資源的進程同時進入臨界區

由於各進程要求共享資源,而有些資源需要互斥使用,即多個進程不能同時使用同一個資源,因此各進程間競爭使用這些資源,進程的這種關係爲進程的互斥

(3) 臨界區(Critical Section)

把不允許多個併發進程交叉執行的一段程序稱作臨界區

系統中某些資源一次只允許一個進程使用,這樣的資源爲臨界資源(critical resource) 或互斥資源或共享變量

(4) 臨界區的訪問過程

這些名詞會在介紹互斥方法的時候默認使用喔 ~

  • 進入區:在進入臨界區之前,檢查是否可以進入臨界區的一段代碼,如果可以,設置正在訪問臨界區標誌
  • 臨界區:進程中訪問臨界資源的一段代碼
  • 退出區:用於將正在訪問臨界區標誌刪除
  • 剩餘區:代碼中的其餘部分

(5) 臨界區準則

  • 有空讓進:當無進程在互斥區時,任何有權使用互斥區的進程可進入
  • 無空等待:不允許兩個以上的進程同時進入互斥區
  • 多中擇一:當沒有進程在臨界區,而同時有多個進程要求進入臨界區,只能讓其中之一進入臨界區,其他進程必須等待
  • 有限等待:任何進入互斥區的要求應在有限的時間內得到滿足
  • 讓權等待:處於等待狀態的進程應放棄佔用 CPU
  • 平等競爭:任何進程無權停止其它進程的運行,進程之間相對運行速度無硬性規定

(二) 加鎖實現互斥的方式

(1) 單標誌法

基本思想是設立一個公用整型變量 turn,描述允許進入臨界區的進程標識

進入臨界區之前先檢查turn,如果等於進程號,就可以進入臨界區,否則循環等待,因此可以實現互斥

這個圖簡單說一下,例如進程 Pi先進來,現在由於 turn = i 所以不進循環,直接進入臨界區,如果這個時候進程 Pj 也想進來,但是卻因爲 turn = i所以只能進入 循環 while(tuen != j) 直到進程 Pi 從臨界區出來後,重新將 trun 設置爲 j 後面 Pj 就可以進入臨界區了

特點:

  • 等待期間會耗費處理器時間

  • 兩個進程交替使用處理器,執行速度取決於慢的進程

  • 如果一個終止(無論是在臨界區內還是臨界區外),另一個會被永遠阻塞

(2) 雙標誌法(先檢查)

雙標誌是設立一個標誌數組 flag[]:描述進程是否在臨界區,初值均爲 FALSE,表示進程不在臨界區

其基本思想是:

  • 先檢查,後修改:在進入區檢查另一個進程是否在臨界區,不在時修改本進程在臨界區的標誌;

  • 在退出區修改本進程在臨界區的標誌

優點:不用交替進入,可連續使用;

缺點:Pi 和 Pj可能同時進入臨界區。在Pi 和 Pj都不在臨界區時,假設 Pi Pj 進入區的兩部併發執行時,會同時進入臨界區。即在檢查對方 flag 之後發生進程切換,結果都通過檢查。這裏的問題出在檢查和修改操作不能連續進行

(3) 雙標誌法(後檢查)

算法 3 類似於算法 2,與算法 2 的區別在於先修改後檢查。可防止兩個進程同時進入臨

界區。其缺點爲:Pi和 Pj 可能都進入不了臨界區。在 Pi和 Pj 都不在臨界區時,假設按 "Pi (第一步)

Pj (第一步) Pi (第二步) Pj (第二步)"順序併發執行時,會都進不了臨界區,即在設置進入臨界區的標誌後發生

進程切換,結果檢查都不能通過

(4) 雙標誌法改進版

算法 4 結合算法 1 和算法 3,是正確的算法。當同時修改標誌時,採用標誌 turn 描述可進入的進程

其主要思想是在進入區先修改後檢查,並檢查併發修改的先後

  • 檢查對方 flag,如果不在臨界區則自己進入——空閒則入

  • 否則再檢查 turn:保存的是較晚的一次賦值,則較晚的進程等待,較早的進程進入,即先到先入,後到等待

(三) 信號量實現互斥的方式

前面的互斥算法都存在問題,它們是平等進程間的一種協商機制,需要一個地位高於進程的管理者來解決公有資源的使用問題。OS 可從進程管理者的角度來處理互斥的問題,信號量就是 OS 提供的管理公有資源的有效手段。信號量的值代表可用資源實體的數量

每個信號量 s 除了一個整數值 s.count(計數), 還有一個進程等待隊列 s.queue,其中存儲的是阻塞在該信號量的各個進程的標識。

信號量只能通過初始化和兩個標準的原語(P、V原語)來訪問——作爲 OS 核心代碼執行,不受進程調度的打斷

信號量在始化時被指定一個非負整數值,表示空閒資源總數(又稱爲“資源信號量”)

在進程執行過程中,信號量的值(即其計數值)可能發生變化

  • 若爲非負值,表示當前的空閒資源數

  • 若爲負值,其絕對值表示當前等待臨界區的進程數

(1) P 原語

在互斥問題中,申請使用臨界資源時調用 P 原語,其實現原理爲:

P(Semaphore s) {
	--s.count; //表示申請一個資源;
 	if (s.count <0) { //表示沒有空閒資源;
		調用進程進入等待隊列 s.queue;
		阻塞調用進程;
	}
 }

(2) V 原語

V 原語通常喚醒進程等待隊列中的頭一個進程,其實現原理爲:

V(Semaphore s ) {
	++s.count; //表示釋放一個資源;
	if (s.count <= 0) { //表示有進程處於阻塞狀態;
		從等待隊列 s.queue 中取出一個進程 P;
		進程 P 進入就緒隊列;
	}
 }

(四) 加鎖法和信號量法的區別

加鎖法 信號量法
1、加鎖過程可以中斷 採用P、V原語
2、循環檢測鎖,系統開銷大, 系統開銷小
3、未進入臨界區的進程無排隊等待機制 未進入臨界區的進程必須在等待隊列中等待

(五) 進程同步

(1) 基本概念

進程同步:把異步環境下的一組併發進程,因直接制約互相發送消息而進行的相互合作、相互等待,並使進程按照一定的順序和速度執行的過程

合作進程:具有同步關係的一組進程

消息:合作進程互相發送的信號,則可使用以下過程:

  • Wait(消息名):表示進程等待合作進程發來消息
  • Signal(消息名):表示向合作進程發送消息

(2) 信號量分類

私用信號量:也可以把各進程之間發送的消息作爲信號量看待

公用信號量:互斥時使用的信號量稱爲公用信號量

(六) 經典互斥同步問題

(1) 生產者消費者問題

**問題描述:**若干進程通過有限的共享緩衝區交換數據。其中,"生產者"進程不斷寫入,

而"消費者"進程不斷讀出;共享緩衝區共有 N 個;任何時刻只能有一個進程可對共享緩衝區

進行操作。

  • 任何時刻只能有一個進程可對共享緩衝區進行操作,可知使用共享緩衝區的生產者與生產者之間、生產者與消費者之間以及消費者與消費者之間存在互斥關係。

  • 緩衝區不滿,生產者才能寫入;緩衝區不空,消費者才能讀出,可知生產者與消費者之間存在同步關係。

(2) 讀寫問題

**問題描述:**對共享資源的讀寫操作,任一時刻“寫者”最多隻允許一個,而“讀者”則允許

多個,要求:

  • “讀-寫” 互斥

  • “寫-寫” 互斥

  • “讀-讀” 允許

(3) 哲學家就餐問題

就餐條件

哲學家想吃飯,先提出吃飯要求

提出吃飯要求後,並拿到兩雙筷子後,方可吃飯。如果筷子被他人獲得,則必須等待此人吃完後,才能獲取筷子

對於已經申請吃飯的任意一個哲學家在自己未拿到兩隻筷子吃飯之前,不放下自己的筷子

剛開始就餐時,只允許兩個哲學家請求吃飯

要考慮的問題是如何保證哲學家們的動作有序進行?如:

  • 不出現相鄰者同時要求進餐;
  • 不出現有人永遠拿不到筷子

試着解答這幾個問題:

(1)描述一個保證不會出現兩個鄰座同時要求吃飯的通信算法。

(2)描述一個既沒有兩鄰座同時吃飯,又沒有人餓死(永遠拿不到筷子)的算法。

(3)在什麼情況下,5個哲學家全部吃不上飯?

解答第一問:

(1) 首先分析,5只筷子都屬於臨界資源,所以五個哲學家 philosopher(i) 取筷子的時候就需要互斥,爲滿足第一點所以就需要按一定順序取筷子

設公用信號量 fork[i] 其初值爲 1 ,i 的取值爲 0-4,規定先取右邊的,再取左邊的 當 i = 4 左邊的筷子是 (i + 1) mod 5 即等於0

算法結果如下:

i = 0,1,2,3,4
philosopher(i)
	begin
		//思考
		//就餐
		P(fork[i])
			P(fork[(i + 1) mod 5])
				eat()
			V(fork[(i + 1) mod 5])
		V(fork[i])
	end

上述的代碼可以保證不會有兩個相鄰的哲學家同時進餐,但卻可能引起死鎖的情況。假如五位哲學家同時飢餓而都拿起的左邊的筷子,就會使五個信號量chopstick都爲0,當他們試圖去拿右手邊的筷子時,都將無筷子而陷入無限期的等待

解答第二問:

防止死鎖策略1

(2) 這種情況,只需要有一位哲學家按相反的順序取筷子

i = 0,1,2,3
philosopher(i)
	begin
		//思考
		//就餐
		P(fork[i])
			P(fork[i + 1])
				eat()
			V(fork[i + 1])
		V(fork[i])
	end

前三位依舊是先拿右邊,再拿左邊,而最後一位規定,先拿左邊,再拿右邊

philosopher(4)
	begin
		//思考
		//就餐
		P(fork[0])
			P(fork[4])
				eat()
			V(fork[4])
		V(fork[0])
	end

解答第三問:

(3) 第二種情況,解決了出現餓死的現象,第三種情況,就是在第一種情況下出現的,也就是當每位哲學家都取到了左邊的筷子,試圖去取右邊的筷子,就會出現五位哲學家都吃不上飯的情況

當然防止死鎖的方法有很多種,如果有興趣,可以自己寫成具體的代碼試一試,以及參考一下別人一些好的算法

(七) 進程通信

(1) 分類

進程通信共有四種方式

A:主從式

主進程可以自由使用從進程資源

從進程的動作受到主進程的限制

主進程和從進程關係固定

應用於終端控制進程和終端進程

B:會話式

使用進程在使用服務進程提供的服務前,必須得到許可

服務進程根據使用進程的要求提供服務,單控制權屬於服務進程本身使用進程和服務進程關係固定

應用於用戶進程和磁盤管理

C:消息或郵箱機制

只要存在空緩存區或郵箱,發送進程就可以發送消息

發送進程和接受進程無直接聯繫

發送進程和接受進程之間存在緩衝區或郵箱用來存放被傳輸的消息

D:共享內存機制

共享內存方式不要求數據移動,兩個需要互相交互的信息的進程通過對同一共享數據區(shared memory)的操作來達到互相通信的目的

(2) 消息緩衝機制

系統在操作系統空間設置一組緩衝區,其中每個 BUFFER 可以存放一個消息

當發送進程需要發送消息時,執行 send 系統調用,操作系統爲發送進程分配一個空緩衝區,並將所發送的消息從發送進程 copy 到緩衝區中,然後將該載有消息的緩衝區連接到接收進程的消息鏈鏈尾,如此就完成了發送過程

在以後某個時刻,當接收進程執行到 receive 接收原語時,由操作系統將載有消息的緩衝區從消息鏈中取出,並把消息內容 copy 到接收進程空間,之後收回緩衝區,如此就完成了消息的接收

由於消息緩衝機制中使用的緩衝區是公用緩衝區,使用消息緩衝機制傳送數據時,通信進程應滿足的條件:

進程對緩衝區的操作必須互斥
當緩衝區中無消息存在時,接收進程不能接受到任何消息
設置公用信號量mutex,接收進程私用信號量SM,消息m

Send (m):
Begin
向系統申請一個消息緩衝區
P(mutex)
將消息m發送到新申請的消息緩衝區
V(mutex)
V(SM)

Receive(m):
Begin
P(SM)
P(mutex)
將消息m從緩衝區複製到接收區並釋放緩衝區
V(mutex)

(3) 郵箱通信

對於只有一個發送進程和一個接收進程使用的郵箱,進程間通信應滿足以下條件:

  • 發送進程發送消息時,郵箱中至少有一個存儲消息的單元
  • 接收進程接收消息時,郵箱中至少有一個消息存在

(4) 管道

Unix 系統從System V開始,提供有名管道和無名管道兩種數據通信方式
無名管道爲建立管道的進程及其子進程提供一條以比特流方式傳送消息的通信管道。該管道在邏輯上是管道文件,物理上則由文件系統的高速緩衝區構成

管道是一條在進程間以字節流方式傳送的通信通道,它由 OS 核心的緩衝區(通常幾十KB)來實現,是單向的。在使用管道前要建立相應的管道,然後纔可使用。

UNIX 系統中,通過 pipe 系統調用創建無名管道,得到兩個文件描述符,分別用於寫和讀。具體調用形式爲:

  • int pipe(int fildes[2]);

其中,文件描述符 fildes[0]爲讀端,fildes[1]爲寫端

通信時,通過系統調用 write 和 read 進行管道的寫和讀

如果進程間需要雙向通信,通常需要兩個管道。

UNIX 無名管道只適用於父子進程之間或父進程安排的各個子進程之間(只有相關進程可以共享無名管道);

UNIX 中的命名管道,可通過 mknod 系統調用建立(不相關進程只能共享命名管道,指定 mode 爲 S_IFIFO),具體形式爲:

  • int mknod(const char *path, mode_t mode, dev_t dev);

1 利用無名管道實現父子進程通信

#include <stdio.h>
main() { 
	int x,fd[2];
	char buf[30],s[30];
 	pipe(fd);
 	while((x=fork()==-1);
 	if(x==0) { 
		sprintf(buf, “This is an example\n”);
     	write(fd[1],buf,30);
 		exit(0);
 	} else { 
 		wait(0);
		read(fd[0],s,30);
 		printf(“%s”,s);
 	} 
}

(八) 死鎖

(1) 基本概念

死鎖的定義:當多個進程因競爭資源而造成的一種僵局,在無外力作用下,這些進程將永遠不能繼續向前推進,這種現象稱爲死鎖

死鎖的起因:併發進程的資源競爭、進程推進順序不當

(2) 產生條件

  • 互斥條件:資源的排他性

  • 不剝奪條件:進程對獲得的資源在未使用完畢前,不可被其他進程剝奪使用權利

  • 部分分配條件:進程每次申請新資源時,同時還要佔用已分配的資源

  • 環路條件:存在進程循環鏈,鏈中每個進程已獲得的資源同時被下一個進程申請

(3) 死鎖的排除方法

A: 死鎖預防

  • 一次性分配法

  • 資源順序分配法

  • 先釋放,後申請

B:死鎖避免:

  • 動態預防,系統根據某種算法在動態分配資源時,預測出死鎖發生的可能性,並加以預防

C:死鎖的檢測與解除

  • 一個給定的進程-資源圖最終是可以化簡的
  • 剝奪資源
  • 撤銷進

(九) 結尾

如果文章中有什麼不足,歡迎大家留言交流,感謝朋友們的支持!

如果能幫到你的話,那就來關注我吧!如果您更喜歡微信文章的閱讀方式,可以關注我的公衆號

在這裏的我們素不相識,卻都在爲了自己的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章