日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

操作系统-并发性:互斥与同步

發布時間:2025/4/16 windows 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 操作系统-并发性:互斥与同步 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

知識架構

操作系統設計中的核心問題是進程和線程的管理

那么關于進程和線程的管理涉及到的問題就包括:

  • 多道程序設計技術:管理單處理器系統中的多個進程。
  • 多處理器技術:管理多處理器系統中的多個進程。
  • 分布式處理器技術:管理多臺分布式計算機系統中多個進程的執行。最近迅猛發展的集群就是這類系統的典型例子。
  • 要處理好上面所說的問題,就不得不提到并發,并發是所有問題的基礎,也是操作系統設計的基礎。并發包括很多設計問題,其中有進程間通信、資源共享與競爭(如內存、文件、I/O訪問)、多個進程活動的同步以及給進程分配處理器時間等。我們將會看到這些問題不僅會出現在多處理器環境和分布式處理器環境中,也會出現在單處理器的多道程序設計系統中。

    并發會在以下三種不同的上下文中出現:

  • 多應用程序:多道程序設計技術允許在多個活動的應用程序間動態共享處理器時間。
  • 結構化應用程序:作為模塊化設計和結構化程序設計的擴展,一些應用程序可被有效地設計成一組并發進程。
  • 操作系統結構:同樣的結構化程序設計優點適用于系統程序,且我們已知操作系統自身常常作為一組進程或線程實現。
  • 在接下來的描述中,你將會看到以下內容:

  • 首先介紹并發的概念和多個并發進程執行的含義。我們發現,支持并發進程的基本需求是加強互斥的能力。也就是說,當一個進程被授予互斥能力時,那么在其活動期間,它具有排斥所有其他進程的能力。
  • 接下來介紹支持互斥的硬件機制。
  • 最后介紹一些不需要忙等待,由操作系統或語言編譯器支持的互斥解決方案。這里將討論三種方法:信號量、管程和消息傳遞。
  • 同時,本章通過兩個經典的并發問題(1. 生產者/消費者問題 2.讀者/寫者問題)來說明并發的概念,并對本書中使用的解決并發的各種方法進行比較。

    在介紹并發之前,我們先來看看關于并發的關鍵術語。

    5.1 并發的原理

    在單處理器多道程序設計系統中,進程會被交替地執行,因而表現出一種并發執行的外部特征。即使不能實現真正的并行處理,并且在進程間來回切換也需要一定的開銷,交替執行在處理效率和程序結構上還是會帶來很多好處。在多處理器系統中,不僅可以交替執行進程,而且可以重疊執行進程。

    從表面上看,交替和重疊代表了完全不同的執行模式和不同的問題。實際上,這兩種技術都可視為并發處理的一個實例,并且都代表了同樣的問題。在單處理器情況下,問題源于多道程序設計系統的一個基本特性:進程的相對執行速度不可預測,它取決于其他進程的活動、操作系統處理中斷的方式以及操作系統的調度策略。這就帶來了下列困難:

  • 全局資源的共享充滿了危險。
  • 操作系統很難對資源進行最優化分配。例如,進程A可能請求使用一個特定的I/O通道,并獲得控制權,但它在使用這個通道前已被阻塞,而操作系統仍然鎖定這個通道,以防止其他進程使用,這是難以令人滿意的。事實上,這種情況有可能導致死鎖,詳見第6章。
  • 定位程序設計錯誤非常困難。這是因為結果通常是不確定的和不可再現的。
  • 上述所有困難在多處理器系統中都有具體的表現,因為在這樣的系統中進程執行的相對速度也是不可預測的。多處理器系統還必須處理多個進程同時執行所引發的問題,從根本上說,這些問題和單處理器系統中的是相同的,隨著討論的深入,這些問題將逐漸明了。

    5.1.1 競爭條件

    競爭條件發生在多個進程或線程讀寫數據時,其最終結果取決于多個進程的指令執行順序。考慮下面兩個簡單的例子。

    在第一個例子中,假設兩個進程P1和P2共享全局變量a。在Pl執行的某一時刻,它將a的值更新為1,在P2執行的某一時刻,它將a的值更新為2。因此,兩個任務競爭更新變量a。在本例中,競爭的“失敗者”(即最后更新全局變量a的進程)決定了變量a的最終值。

    在第二個例子中,考慮兩個進程P3和P4共享全局變量b和c,且初始值為b=1,c=2。在某一執行時刻,P3執行賦值語句b=b+c,在另一執行時刻,P4執行賦值語句c=b+c。兩個進程更新不同的變量,但兩個變量的最終值取決于兩個進程執行賦值語句的順序。若P3首先執行賦值語句,則最終值為b=3,c=5;若P4首先執行賦值語句,則最終值為b=4,c=3。

    總結為一句話的話,就是上面關于并發的關鍵術語中所說的:多個線程或進程在讀寫一個共享數據時,結果依賴于它們執行的相對時間的情形

    操作系統必須保證一個進程的功能和輸出結果必須與執行速度無關(相對于其他并發進程的執行速度)。這是本章的主題。為理解如何解決與執行速度無關的問題,我們首先需要考慮進程間的交互方式。

    5.1.2 進程的交互

    我們可以根據進程相互之間知道對方是否存在的程度,對進程間的交互方式進行分類。下表列出了三種可能的感知程度及每種感知程度的結果。

    • 進程之間相互不知道對方的存在:這是一些獨立的進程,它們不會一起工作。關于這種情況的最好例子是多個獨立進程的多道程序設計,可以是批處理作業,也可以是交互式會話,或者是兩者的混合。盡管這些進程不會一起工作,但操作系統需要知道它們對資源的競爭情況。例如,兩個無關的應用程序可能都想訪問同一個磁盤、文件或打印機。操作系統必須控制對它們的訪問。
    • 進程間接知道對方的存在:這些進程并不需要知道對方的進程ID,但它們共享某些對象,如一個I/0緩沖區。這類進程在共享同一個對象時會表現出合作行為(cooperation)。
    • 進程直接知道對方的存在:這些進程可通過進程ID互相通信,以合作完成某些活動。同樣,這類進程表現出合作行為。
      實際條件并不總是像表5.2中給出的那么清晰,例如幾個進程可能既表現出競爭,又表現出合作。然而,對操作系統而言,分別檢查表中的每一項并確定它們的本質是必要的。接下來,我們將對上面三項關系進行一一的闡述。

    進程間的資源競爭:

    當并發進程競爭使用同一資源時,它們之間會發生沖突。我們可以把這種情況簡單描述如下:兩個或更多的進程在它們的執行過程中需要訪問一個資源,每個進程并不知道其他進程的存在,且每個進程也不受其他進程的影響。每個進程都不影響它所用資源的狀態,這類資源包括IVO設備、存儲器、處理器時間和時鐘。

    競爭進程間沒有任何信息交換,但一個進程的執行可能會影響到競爭進程的行為。特別是當兩個進程都期望訪問同一個資源時,如果操作系統把這個資源分配給一個進程,那么另一個進程就必須等待。因此,被拒絕訪問的進程的執行速度就會變慢。一種極端情況是,被阻塞的進程永遠不能訪問這個資源,因此該進程永遠不能成功結束運行。

    競爭進程面臨三個控制問題。首先需要互斥(mutual exclusion)。假設兩個或更多的進程需要訪問一個不可共享的資源,如打印機。在執行過程中,每個進程都給該IVO設備發命令,接收狀態信息,發送數據和接收數據。我們把這類資源稱為臨界資源(critical resource),使用臨界資源的那部分程序稱為程序的臨界區(critical section)。一次只允許有一個程序在臨界區中,這一點非常重要。由于不清楚詳細要求,我們不能僅僅依靠操作系統來理解和增強這個限制。例如在打印機的例子中,我們希望任何一個進程在打印整個文件時都擁有打印機的控制權,否則在打印結果中就會穿插著來自競爭進程的打印內容。

    實施互斥產生了兩個額外的控制問題。

  • 死鎖(deadlock)。例如,考慮兩個進程P1和P2,以及兩個資源R1和R2,假設每個進程為執行部分功能都需要訪問這兩個資源,那么就有可能出現下列情況:操作系統把R1分配給P2,把R2分配給P1,每個進程都在等待另一個資源,且在獲得其他資源并完成功能前,誰都不會釋放自己已擁有的資源,此時這兩個進程就會發生死鎖。

  • 饑餓(starvation)。假設有三個進程(P1、P2和P3),每個進程都周期性地訪問資源R。考慮這種情況,即P1擁有資源,P2和P3都被延遲,等待這個資源。當P1退出其臨界區時,P2和P3都允許訪問R,假設操作系統把訪問權授予P3,并在P3退出臨界區之前P1又要訪問該臨界區,若在P3結束后操作系統又把訪問權授予P1,且接下來把訪問權輪流授予P1和P3,那么即使沒有死鎖,P2也可能被無限地拒絕訪問資源。

  • 由于操作系統負責分配資源,競爭的控制不可避免地涉及操作系統。此外,進程自身需要能夠以某種方式表達互斥的需求,如在使用前對資源加鎖,但任何一種解決方案都涉及操作系統的某些支持,如提供鎖機制。

    進程間通過共享合作

    通過共享進行合作的情況,包括進程間在互相并不確切知道對方的情況下進行交互。例如,多個進程可能訪問一個共享變量、共享文件或數據庫,進程可能使用并修改共享變量而不涉及其他進程,但卻知道其他進程也可能訪問同一個數據。因此,這些進程必須合作,以確保它們共享的數據得到正確管理。控制機制必須確保共享數據的完整性。

    由于數據保存在資源(設備或存儲器)中,因此再次涉及有關互斥、死鎖和饑餓等控制問題。唯一的區別是可以按兩種不同的模式(讀和寫)訪問數據項,并且只有寫操作必須保證互斥。

    但是,除這些問題之外還有一個新要求:數據一致性。舉個簡單的例子,考慮一個關于記賬的應用程序,這個程序中可能會更新各個數據項。假設兩個數據項a和b保持著相等關系a=b,也就是說,為保持這一關系,任何一個程序如果修改了其中一個變量,也必須修改另一個變量。現在來看下面兩個進程:

    P1:a=a+1;b=b+1; P2:b=2*b;a=2*a;

    如果最初狀態是一致的,則單獨執行每個進程會使共享數據仍然保持一致狀態。現在考慮下面的并發執行,兩個進程在每個數據項(a和b)上都考慮到了互斥(也就是修改a或者b變量的時候不會被打斷):

    a=a+1;b=2*b;b=b+1;a=2*a;

    按照這一執行順序,結果不再保持條件a=b。例如,開始時有a=b=1,在這個執行序列結束時有a=4和b=3。為避免這個問題,可以把每個進程中的整個代碼序列聲明為一個臨界區。

    因此,在通過共享進行合作的情況下,臨界區的概念非常重要。但是,在這種情況下,若使用臨界區來保護數據的完整性,則沒有確定的資源或變量可作為參數。此時,可以把參數視為一個在并發進程間共享的標識符,用于標識必須互斥的臨界區(也就是需要互斥的代碼序列)。

    作為簡單的理解,可以認為進程間的資源競爭是資源互斥,而共享合作則是代碼序列互斥。前者自然是操作系統的責任,而后者則需要代碼體現希望互斥的訴求,并且操作系統能明白這種訴求。

    進程間通過通信合作:

    在前面兩種情況下,每個進程都有自己獨立的環境,不包括其他進程,進程間的交互是間接的,并且都存在共享。在競爭情況下,它們在不知道其他進程存在的情況下共享資源;在第二種情況下,它們共享變量,每個進程并未明確地知道其他進程的存在,它只知道需要維護數據的完整性。當進程通過通信進行合作時,各個進程都與其他進程進行連接。通信提供同步和協調各種活動的方法。

    典型情況下,通信可由各種類型的消息組成,發送消息和接收消息的原語由程序設計語言提供,或由操作系統的內核提供。如最linux操作系統中的ctrl+c的停止進程消息。

    由于在傳遞消息的過程中進程間未共享任何對象,因而這類合作不需要互斥,但仍然存在死鎖和饑餓問題。例如,有兩個進程可能都被阻塞,每個都在等待來自對方的通信,這時發生死鎖。作為饑餓的例子,考慮三個進程P1、P2和P3,它們都有如下特性:P1不斷地試圖與P2或P3通信,P2和P3都試圖與P1通信,如果P1和P2不斷地交換信息,而P3一直被阻塞,等待與P1通信,由于P1一直是活動的,因此雖不存在死鎖,但P3處于饑餓狀態。

    5.1.3 互斥的要求

    要提供對互斥的支持,必須滿足以下要求:

  • 必須強制實施互斥:在與相同資源或共享對象的臨界區有關的所有進程中,一次只允許一個進程進入臨界區。
  • 一個在非臨界區停止的進程不能干涉其他進程。
  • 絕不允許出現需要訪問臨界區的進程被無限延遲的情況,即不會死鎖或饑餓。
  • 沒有進程在臨界區中時,任何需要進入臨界區的進程必須能夠立即進入。
  • 對相關進程的執行速度和處理器的數量沒有任何要求和限制。
  • 一個進程駐留在臨界區中的時間必須是有限的。
  • 滿足這些互斥條件的方法有多種。第一種方法是讓并發執行的進程承擔這一責任,這類進程(不論是系統程序還是應用程序)需要與另一個進程合作,而不需要程序設計語言或操作系統提供任何支持來實施互斥。我們把這類方法稱為軟件方法。盡管這種方法已被證明會增加開銷并存在缺陷,但通過分析這類方法,可以更好地理解并發處理的復雜性。第二種方法涉及專用機器指令,這種方法的優點是可以減少開銷,但卻很難成為一種通用的解決方案,詳見5.2節。第三種方法是在操作系統或程序設計語言中提供某種級別的支持,5.3節到5.5節將介紹這類方法中最重要的三種方法。

    5.2 互斥:硬件的支持

    5.2.1 中斷禁用

    在單處理器機器中,并發進程不能重疊,只能交替。此外,一個進程在調用一個系統服務或被中斷前,將一直運行。因此,為保證互斥,只需保證一個進程不被中斷即可,這種能力可通過系統內核為啟用和禁用中斷定義的原語來提供。進程可通過如下方法實施互斥:

    whiletrue{/*禁用中斷*/;/*臨界區*/;/*啟用中斷*/;/*其余部分*/; }

    由于臨界區不能被中斷,故可以保證互斥。但是,這種方法的代價非常高。由于處理器被限制得只能交替執行程序,因此執行的效率會明顯降低。另一個問題是,這種方法不能用于多處理器體系結構中。 當一個計算機系統包括多個處理器時,通常就可能有一個以上的進程同時執行,在這種情況下,禁用中斷并不能保證互斥。

    5.2.2 專用機器指令

    在多處理器配置中,幾個處理器共享對內存的訪問。在這種情況下,不存在主/從關系,處理器間的行為是無關的,表現出一種對等關系,處理器之間沒有支持互斥的中斷機制。

    在硬件級別上,對存儲單元的訪問排斥對相同單元的其他訪問。因此,處理器的設計者人員提出了一些機器指令,用于保證兩個動作的原子性,如在一個取指周期中對一個存儲器單元的讀和寫或讀和測試。在這個指令執行的過程中,任何其他指令訪問內存都將被阻止,而且這些動作在一個指令周期中完成。

    5.3 信號量

    現在討論提供并發性的操作系統和程序設計語言的機制。表5.3總結了一般常用的并發機制。本節首先討論信號量,后續兩節分別討論管程和消息傳遞。表中其他機制這里不做說明。

    在解決并發進程問題的過程中,第一個重要的進展是1965年Dijkstra的論文。Dijkstra參與了一個操作系統的設計,這個操作系統設計為一組合作的順序進程,并為支持合作提供了有效且可靠的機制。只要處理器和操作系統使這些機制可用,它們就可以很容易地被用戶進程使用。

    基本原理如下:
    兩個或多個進程可以通過簡單的信號進行合作,可以強迫一個進程在某個位置停止,直到它接收到一個特定的信號。任何復雜的合作需求都可通過適當的信號結構得到滿足。為了發信號,需要使用一個稱為信號量的特殊變量。要通過信號量s傳送信號,進程須執行原語semsigna1(s);要通過信號量s接收信號,進程須執行原語semwait(s);若相應的信號仍未發送,則阻塞進程,直到發送完為I止。

    為達到預期效果,可把信號量視為一個值為整數的變量,整數值上定義了三個操作:

  • 一個信號量可以初始化成非負數。
  • semwait操作使信號量減1。若值變成負數,則阻塞執行semwait的進程,否則進程繼續執行。
  • semsigna1操作使信號量加1。若值小于等于零,則被semwait操作阻塞的進程解除阻塞。
  • 除了這三個操作外,沒有任何其他方法可以檢查或操作信號量。

    對這三個操作的解釋如下:開始時,信號量的值為零或正數。若值為正數,則它等于發出semwait
    操作后可立即繼續執行的進程的數量。若值為零(要么由于初始化,要么由于有等于信號量初值的進程已在等待),則發出semwait操作的下一個進程會被阻塞,此時該信號量的值變為負值。之后,每個后續的semwait操作都會使信號量的負值更大。該負值等于正在等待解除阻塞的進程的數量。在信號量為負值的情形下,每個semsigna1操作都會將等待進程中的一個進程解除阻塞。

    這里給出信號量定義的三個重要結論:

    • 通常,在進程對信號量減1之前,無法提前知道該信號量是否會被阻塞。
    • 當進程對一個信號量加1之后,會喚醒另一個進程,兩個進程繼續并發運行。而在一個單處理器系統中,同樣無法知道哪個進程會立即繼續運行。
    • 向信號量發出信號后,不需要知道是否有另一個進程正在等待,被解除阻塞的進程數要么沒有,要么是為1。

    圖5.3給出了關于信號量原語更規范的定義。semwait和semsignal原語被假設是原子操作
    圖5.4定義了稱為二元信號量(binary semaphore)的更嚴格的形式。二元信號量的值只能是0或1,可由下面三個操作定義:


    關于二元信號量

  • 二元信號量可以初始化為0或1。
  • semwaitB操作檢查信號的值。若值為0,則進程執行semwaitB就會受阻。若值為1,則將值改為0,并繼續執行該進程。
  • semsignalB操作檢查是否有任何進程在該信號上受阻。若有進程受阻,則通過semwaitB
    操作,受阻的進程會被喚醒;若沒有進程受阻,則值設置為1。
  • 理論上,二元信號量更易于實現,且可以證明它和普通信號具有同樣的表達能力(見習題5.16)。
    為了區分這兩種信號,非二元信號量也常稱為計數信號量(counting semaphore)或一般信號量(general semaphore)。

    與二元信號量相關的一個概念是互斥鎖(mutex)。互斥是一個編程標志位,用來獲取和釋放一個對象。當需要的數據不能被分享或處理,進而導致在系統中的其他地方不能同時執行時,互斥被設置為鎖定(一般為0),用于阻塞其他程序使用數據。當數據不再需要或程序運行結束時,互斥被設定為非鎖定。二元信號量和互斥量的關鍵區別在于,為互斥量加鎖(設定值為0)的進程和為互斥量解鎖(設定值為1)的進程必須是同一個進程。相比之下,可能由某個進程對二元信號量進行加鎖操作,而由另一個進程為其解鎖。

    阻塞在信號量上的進程調度問題:

    不論是計數信號量還是二元信號量,都需要使用隊列來保存于信號量上等待的進程。這就產生了一個問題:進程按照什么順序從隊列中移出?最公平的策略是先進先出(FIFO):被阻塞時間最久的進程最先從隊列釋放。采用這一策略定義的信號量稱為強信號量(strong semaphore),而沒有規定進程從隊列中移出順序的信號量稱為弱信號量(weak semaphore)。圖5.5是一個關于強信號量操作的例子。其中進程A、B和C依賴于進程D的結果,在初始時刻(1),A正在運行,B、C和D就緒,信號量為1,表示D的一個結果可用。當A執行一條semwait指令后,信號量減為0,A能繼續執行,隨后它加入就緒隊列;然后在時刻(2),B正在運行,最終執行一條semwait指令,并被阻塞;在時刻(3),允許D運行;在時刻(4),當D完成一個新結果后,它執行一條semsigna1指令,允許B移到就緒隊列中;在時刻(5),D加入就緒隊列,C開始運行,當它執行semwait指令時被阻塞。類似地,在時刻(6),A和B運行,且被阻塞在這個信號量上,允許D恢復執行。當D有一個結果后,執行一條semsigna1指令,把C移到就緒隊列中,隨后的D循環將解除A和B的阻塞狀態。


    對于下節將要講述的互斥算法(見圖5.6),強信號量保證不會饑餓,而弱信號量則無法保證。
    這里將采用強信號量,因為它們更方便,且是操作系統提供的典型信號量形式。

    5.3.1 使用信號量進行互斥

    圖5.6給出了一種使用信號量s解決互斥問題的方法。設有n個進程,用數組P(i)表示,所有進程都需要訪問共享資源。每個進程進入臨界區前執行semwait(s),若s的值為負,則進程被阻塞;若值為1,則s被減為0,進程立即進入臨界區;由于s不再為正,因而其他任何進程都不能進入臨界區。

    信號量一般初始化為1,這樣第一個執行semwait(s)的進程可立即進入臨界區,并把s的值置為0。接著任何試圖進入臨界區的其他進程,都將發現第一個進程忙,因此被阻塞,把s的值置為-1。可以有任意數量的進程試圖進入,每個不成功的嘗試都會使s的值減1,當最初進入臨界區的進程離開時,s增1,一個被阻塞的進程(如果有的話)被移出等待隊列,置于就緒態。這樣,當操作系統下一次調度時,它就可以進入臨界區。如下圖所示:

    圖5.7顯示了三個進程使用圖5.6所示互斥協議后的一種執行順序。在該例中,三個進程(A、B、C)訪問一個受信號量lock保護的共享資源。進程A執行semwait(1ock);由于信號量在本次semwait操作時值為1,因而A可以立即進入臨界區,并把信號量的值置為0;當A在臨界區中時,B和C都執行一個semwait操作并被阻塞;當A退出臨界區并執行時,隊列中的第一個進程B現在可以進入臨界區。
    圖5.6中的程序也可公平地處理一次允許多個進程進入臨界區的需求。這個需求可通過把信號量初始化成某個特定值來實現。因此,在任何時候,s.count的值都可解釋如下:

    • s.count ≥ 0:s.count 是可執行semwait(s)而不被阻塞的進程數【期間無semsignal(s)
      執行】。這種情形允許信號量支持同步與互斥。
    • s.count < 0:s.count的大小是阻塞在s.queue隊列中的進程數。

    5.3.2 生產者/消費者問題

    現在分析并發處理中最常見的一類問題:生產者(producer)/消費者(consumer)問題。這個訪問通常描述如下:有一個或多個生產者生產某種類型的數據(記錄、字符),并放置在緩沖區中;
    有一個消費者從緩沖區中取數據,每次取一項;系統保證避免對緩沖區的重復操作,即在任何時候只有一個主體(生產者或消費者)可訪問緩沖區。問題是要確保這種情況:當緩存己滿時,生產者不會繼續向其中添加數據;當緩存為空時,消費者不會從中移走數據。我們將討論該問題的多種解決方案,以證明信號量的能力和缺陷。

    首先假設緩沖區是無限的,且是一個線性的元素數組。用抽象的術語,可以定義如下的生產者和消費者函數:

    producer: whiletrue{/*生產v*/;b[in]=v;in++; }consumer: whiletrue{while(in<=out)/*不做任何事*/;w=b[out];out++;/*消費w*/; }

    圖5.8顯示了緩沖區b的結構。生產者可以按自己的步調產生項目并保存在緩沖區中。每次,緩沖區中的索引(in)增1。消費者以類似的方法繼續,但必須確保它不會從一個空緩沖區中讀取數據,因此,消費者在開始進行之前應該確保生產者已經生產(即:in>out)。

    現在用二元信號量來實現這個系統,圖5.9是第一次嘗試。這里不處理索引in和out,而用整型變量n
    (=in-out)簡單地記錄緩沖區中數據項的個數。信號量s用于實施互斥,信號量delay用于迫使消費者在緩沖區為空時等待(semwait)。

    這種方法看上去很直觀。生產者可以在任何時候自由地向緩沖區中增加數據項。它在添加數據前執行semwaitB(s),之后執行 semsignalB(s),以阻止消費者或任何其他生產者在添加操作過程中訪問緩沖區。同時,當生產者在臨界區中時,將n的值增1。若n=1,則在本次添加之前緩沖區為空,生產者執行semsigna1B(delay)以通知消費者這個事實。消費者最初就使用 semwaitB(delay)等待生產出第一個項目,然后在自己的臨界區中取到這一項并將n減1。如果生產者總能保持在消費者之前工作(一種普通情況),即n將總為正,則消費者很少會被阻塞在信號量delay上。因此,生產者和消費者都可以正常運行。

    但這個程序仍有缺陷。當消費者消耗盡緩沖區中的數據項時,需重置信號量delay,因此它被迫等待到生產者向緩沖區中放置更多的數據項,這正是語句if(n==0)semwaitB(delay)的目的。考慮表5.4中的情況,在第14行,消費者執行semwaitB操作失敗。但是消費者確實用盡了緩沖區并把n置為0(第8行),然而生產者在消費者測試到這一點(第14行)之前將n增1,結果導致semsigna1B
    和前面的semwaitB不匹配。第20行的n值為-1,表示消費者已經消費了緩沖區中不存在的一項。僅把消費者臨界區中的條件語句移出也不能解決問題,因為這將導致死鎖(如在表5.4的第8行后)。

    解決這個問題的方法是引入一個輔助變量,我們可以在消費者的臨界區中設置這個變量供以后使用,如圖5.10所示。仔細跟蹤這個邏輯過程,可以確認不會再發生死鎖。


    使用一般信號量(也稱為計數信號量),可得到一種更清晰的解決方法,如圖5.11所示。變量n為信號量,其值等于緩沖區中的項數。假設在抄錄這個程序時發生了錯誤,操作semsigna1B(s)和semsigna1B(n)被互換,這就要求生產者在臨界區中執行semsigna1B(n)操作時不會被消費者或另一個生產者打斷。這會影響程序嗎?不會,因為無論何種情況,消費者在繼續進行之前必須在兩個信號量上等待。

    現在假設semwait(n)和semwait(s)操作偶然被顛倒,這時會產生嚴重甚至致命的錯誤。如果緩沖區為空(n.count=0)時消費者曾進入過臨界區,那么任何一個生產者都不能繼續向緩沖區中添加數據項,系統發生死鎖。這是一個體現信號量的微妙之處和進行正確設計的困難之處的較好示例。
    最后,我們給生產者/消費者問題增加一個新的實際約束,即緩沖區是有限的。緩沖區被視為一個循環存儲器,如圖5.12所示,指針值必須表達為按緩沖區的大小取模,并總是保持下面的關系:


    生產者和消費者函數可表示成如下形式(變量in和out初始化為0,n代表緩沖區的大小):

    producer: whiletrue{/*生產v*/while((in+1%n==out)/*不做任何事*/;b[in]=v;in=(in+1%n; }consumer: whiletrue{while(in==out)/*不做任何事*/;w=b[out];out=(out +1%n;/*消費w*/; }

    圖5.13給出了使用一般信號量的解決方案,其中增加了信號量e來記錄空閑空間的數量。

    5.3.3 信號量的實現

    如前所述,semwait和semsignal操作必須作為原子原語實現。一種顯而易見的方法是用硬件或固件實現。如果沒有這些方案,那么還有很多其他方案。問題的本質是互斥:任何時候只有一個進程可用semwait或semsigna1操作控制一個信號量。因此,可以使用任何一種軟件方案,如Dekker
    算法或Peterson算法,這必然伴隨著處理開銷。

    另一種選擇是使用一種硬件支持實現互斥的方案。例如,圖5.14(a)顯示了使用compare&swap
    指令的實現。其中,信號量是圖5.3中的結構,但還包括一個新整型分量s.flag。誠然,這涉及某種形式的忙等待,但semsait和semsignal操作都相對較短,因此所涉及的忙等待時間量非常小。對于單處理器系統,在semwait或semsignal操作期間是可以禁用中斷的,如圖5.14(b)所示。這些操作的執行時間相對很短,因此這種方法是合理的。

    5.4 管程

    信號量為實施互斥和進程間的合作,提供了一種原始但功能強大且靈活的工具。但是,如圖5.9所示,使用信號量設計一個正確的程序是很困難的,難點在于semwait和semsignal操作可能分布在整個程序中,而很難看出信號量上的這些操作所產生的整體效果。管程是一種程序設計語言結構,它提供的功能與信號量相同,但更易于控制。管程結構在很多程序設計語言中都得以實現,包括并發 Pascal、Pascal-Plus、Modula-2、Modula-3和Java。它還被作為一個程序庫實現。這就允許我們用管程鎖定任何對象,對類似于鏈表之類的對象,可以用一個鎖鎖住整個鏈表,也可每個表用一個鎖,還可為表中的每個元素用一個鎖。

    首先介紹Hoare的方案,然后對它進行改進。

    5.4.1 使用信號的管程

    管程是由一個或多個過程、一個初始化序列和局部數據組成的軟件模塊,其主要特點如下:

  • 局部數據變量只能被管程的過程訪問,任何外部過程都不能訪問。
  • 一個進程通過調用管程的一個過程進入管程。
  • 在任何時候,只能有一個進程在管程中執行,調用管程的任何其他進程都被阻塞,以等待管程可用。
  • 前兩個特點讓人聯想到面向對象軟件中對象的特點。的確,面向對象操作系統或程序設計語言很容易把管程作為一種具有特殊特征的對象來實現。

    通過給進程強加規定,管程可以提供一種互斥機制:管程中的數據變量每次只能被一個進程訪問。因此,可以把一個共享數據結構放在管程中,從而對它進行保護。如果管程中的數據代表某些資源,那么管程為訪問這些資源提供了互斥機制。

    要進行并發處理,管程必須包含同步工具。例如,假設一個進程調用了管程,且當它在管程中時必須被阻塞,直到滿足某些條件。這就需要一種機制,使得該進程不僅被阻塞,而且能釋放這個管程,以便某些其他的進程可以進入。以后,當條件滿足且管程再次可用時,需要恢復該進程并允許它在阻塞點重新進入管程。

    管程通過使用**條件變量(condition variable)**來支持同步,這些條件變量包含在管程中,并且只有在管程中才能被訪問。有兩個函數可以操作條件變量:

    • cwait(c):調用進程的執行在條件c上阻塞,管程現在可被另一個進程使用。
    • csignal(c):恢復執行在cwait之后因某些條件而被阻塞的進程。若有多個這樣的進程,選擇其中一個;若沒有這樣的進程,什么也不做。

    圖5.15給出了一個管程的結構。盡管一個進程可以通過調用管程的任何一個過程進入管程,但我們仍可視管程有一個入口點,保證一次只有一個進程可以進入。其他試圖進入管程的進程被阻塞并加入等待管程可用的進程隊列中。當一個進程在管程中時,它可能會通過發送cwait(x)把自己暫時阻塞在條件x上,隨后它被放入等待條件改變以重新進入管程的進程隊列中,在cwait(x)調用的下一條指令開始恢復執行。

    若在管程中執行的一個進程發現條件變量x發生了變化,則它發送csignal(x),通知相應的條件隊列條件已改變。

    5.5 消息傳遞

    進程交互時,必須滿足兩個基本要求:同步和通信。為實施互斥,進程間需要同步;為實現合作,進程間需要交換信息。提供這些功能的一種方法是消息傳遞。消息傳遞還有一個優點,即它可在分布式系統、共享內存的多處理器系統和單處理器系統中實現。

    消息傳遞系統有多種形式(在很多軟件編程的書中都會提到的進程間的通信),本節簡述這類系統的典型特征。消息傳遞的實際功能以一對原語的形式提供:

    send(destination,message) receive(source,message)

    這是進程間進行消息傳遞所需的最小操作集。一個進程以消息(message)的形式給另一個指定的目標(destination)進程發送信息;進程通過執行 receive原語接收信息,receive原語中指明發送消息的源進程(source)和消息(message)。

    表5.5中列出了與消息傳遞系統相關的一些設計問題,本節的其他部分將依次分析這些問題。

    5.5.1 同步

    兩個進程間的消息通信隱含著某種同步的信息:只有當一個進程發送消息后,接收者才能接收消息。
    此外,一個進程發出send或receive原語后,我們需要確定會發生什么。

    考慮send原語。首先,一個進程執行send原語時有兩種可能:要么發送進程被阻塞直到這個消息被目標進程接收到,要么不阻塞。類似地,一個進程發出receive原語后,也有兩種可能:

  • 若一個消息在此之前已被發送,則該消息被接收并繼續執行。
  • 若沒有正等待的消息,則(a)該進程被阻塞直到所等待的消息到達,或(b)該進程繼續執行,放棄接收的努力。
  • 因此,發送者和接收者都可阻塞或不阻塞。通常有三種組合,但任何一個特定系統通常只實現一種或兩種組合:

    • 阻塞send,阻塞receive:發送者和接收者都被阻塞,直到完成信息的投遞。這種情況有時也稱為會合(rendezvous),它考慮到了進程間的緊密同步。
    • 無阻塞send,阻塞receive:盡管發送者可以繼續,但接收者會被阻塞直到請求的消息到達。這可能是最有用的一種組合,它允許一個進程給各個目標進程盡快地發送一條或多條消息。在繼續工作前必須接收到消息的進程將被阻塞,直到該消息到達。例如,一個服務器進程給其他進程提供服務或資源。
    • 無阻塞send,無阻塞 receive:不要求任何一方等待。

    對大多數并發程序設計任務來說,無阻塞send是最自然的。例如,無阻塞send用于請求一個輸出操作(如打印),它允許請求進程以消息的形式發出請求,然后繼續。無阻塞send有一個潛在的危險:錯誤會導致進程重復產生消息。由于對進程沒有阻塞的要求,這些消息可能會消耗系統資源,包括處理器時間和緩沖區空間,進而損害其他進程和操作系統。同時,無阻塞send增加了程序員的負擔,由于必須確定消息是否收到,因而進程必須使用應答消息,以證實收到了消息。

    對大多數并發程序設計任務來說,阻塞receive原語是最自然的。通常,請求一個消息的進程都需要這個期望的信息才能繼續執行下去,但若消息丟失(在分布式系統中很可能發生這種情況),或者一個進程在發送預期的消息之前失敗,則接收進程會無限期地阻塞。這個問題可以使用無阻塞receive
    來解決。但該方法的危險是,若消息在一個進程已執行與之匹配的receive之后發送,則該消息將被丟失。其他可能的方法是允許一個進程在發出receive之前檢測是否有消息正在等待,或允許進程在receive原語中確定多個源進程。若一個進程正在等待從多個源進程發來的消息,且只要有一個消息到達就可以繼續下去時,則后一種方法非常有用。

    5.5.2 尋址

    顯然,在send原語中確定哪個進程接收消息很有必要。類似地,大多數實現允許接收進程指明消息的來源。

    在send和receive原語中確定目標或源進程的方案分為兩類:直接尋址和間接尋址。對于直接尋址(direct addressing),send原語包含目標進程的標識號,而receive原語有兩種處理方式。一種是要求進程顯式地指定源進程,因此該進程必須事先知道希望得到來自哪個進程的消息,這種方式對于處理并發進程間的合作非常有效。另一種是不可能指定所期望的源進程,例如打印機服務器進程將接受來自各個進程的打印請求,對這類應用使用隱式尋址更為有效。此時,receive原語的source參數保存接收操作執行后的返回值。

    另一種常用的方法是間接尋址(indirect addressing)。此時,消息不直接從發送者發送到接收者,而是發送到一個共享數據結構,該結構由臨時保存消息的隊列組成,這些隊列通常稱為信箱(mailbox)。因此,兩個通信進程中,一個進程給合適的信箱發送消息,另一個進程從信箱中獲取這些消息。

    間接尋址通過解除發送者和接收者之間的耦合關系,可更靈活地使用消息。發送者和接收者之間的關系可以是一對一、多對一、一對多或多對多(見圖5.18)。一對一(one-to-one)關系允許在兩個進程間建立專用的通信鏈接,隔離它們間的交互,避免其他進程的錯誤干擾;多對一(many-to-one)關系對客戶服務器間的交互非常有用,一個進程給許多其他進程提供服務,這時信箱常稱為一個端口(port);一對多(one-to-many)關系適用于一個發送者和多個接收者,它對于在一組進程間廣播一條消息或某些信息的應用程序非常有用。多對多(many-to-many)關系可讓多個服務進程對多個客戶進程提供服務。

    進程和信箱的關聯既可以是靜態的,也可以是動態的。端口常常靜態地關聯到一個特定的進程上,也就是說,端口被永久地創建并指定到該進程。一對一關系就是典型的靜態和永久性關系。當有很多發送者時,發送者和信箱間的關聯可以是動態的,基于這一目的可使用諸如 connect和disconnect之類的原語。

    一個相關的問題是信箱的所有權問題。對于端口來說,信箱的所有都通常是接收進程,并由接收進程創建。因此,撤銷一個進程時,其端口也會隨之銷毀。對于通用的信箱,操作系統可提供一個創建信箱的服務,這樣信箱就可視為由創建它的進程所有,這時它們也同該進程一起終止;或視為由操作系統所有,這時銷毀信箱需要一個顯式命令。

    5.5.3 消息格式

    消息的格式取決于消息機制的目標,以及該機制是運行在一臺計算機上還是運行在分布式系統中。對某些操作系統而言,設計者會優先選用定長的短消息來減小處理和存儲的開銷。需要傳遞大量數據時,可將數據放到一個文件中,然后讓消息引用該文件。更為靈活的一種方法是使用變長消息。

    圖5.19給出了操作系統支持的變長消息的典型格式。該消息分為兩部分:包含相關信息的消息頭和包含實際內容的消息體。消息頭包含消息源和目標的標識符、長度域及判定各種消息類型的類型域,還可能含有一些額外的控制信息,例如創建消息鏈表的指針域、記錄源和目標之間所傳

    遞消息的數量、順序和序號,以及一個優先級域。

    5.5.4 排隊原則

    最簡單的排隊原則是先進先出,但當某些消息比其他消息更緊急時,僅有這一原則是不夠的。

    一個替代原則是允許指定消息的優先級,即根據消息的類型來指定或由發送者指定;另一個替代原則是允許接收者檢查消息隊列并選擇下一次接收哪個消息。

    5.5.5 使用消息來實現互斥

    圖5.20給出了用于實施互斥的消息傳遞方式。假設使用阻塞 receive原語和無阻塞 send原語,且一組并發進程共享一個信箱box,該信箱可供所有進程在發送和接收消息時使用,并初始化為一個無內容的消息。希望進入臨界區的進程首先試圖接收一條消息,若信箱為空,則阻塞該進程;一旦進程獲得消息,它就執行其臨界區,然后把該消息放回信箱。因此,消息函數可視為在進程之間傳遞的一個令牌。

    上面的解決方案假設有多個進程并發地執行接收操作,因此

    • 若有一條消息,則它僅傳遞給一個進程,而其他進程被阻塞。
    • 若消息隊列為空,則所有進程被阻塞;一條消息可用時,僅激活一個阻塞進程活,并得到這條消息。

    這些假設實際上對所有消息傳遞機制都為真。

    作為使用消息傳遞的另一個例子,圖5.21給出了解決有界緩沖區生產者/消費者問題的一種方法。
    使用消息傳遞最基本的互斥能力,該問題可通過類似于圖5.13的算法結構解決。圖5.21利用了消息傳遞的能力,除了傳遞信號之外,它還傳遞數據。它使用了兩個信箱。當生產者產生數據后,數據將作為消息發送到信箱mayconsume,只要該信箱中有一條消息,消費者就可開始消費。從此之后mayconsume用做緩沖區,緩沖區中的數據被組織成消息隊列,緩沖區的大小由全局變量 capacity確定。信箱mayproduce最初填滿空消息,空消息的數量等于信箱的容量,每次生產使得mayproduce中的消息數減少,每次消費使得mayproduce中的消息數增多。

    這種方法非常靈活,可以有多個生產者和消費者,只要它們都訪問這兩個信箱即可。系統甚至可以是分布式系統,所有生產者進程和mayproduce信箱在一個站點上,所有消費者進程和mayconsume信箱在另一個站點上。

    5.6 讀者/寫者問題

    在設計同步和并發機制時,若能與一個著名的問題關聯,檢測該問題的解決方案對原問題是否有效,則這種方法是非常有用的。很多文獻中都有一些頻繁出現的重要問題,它們不僅是普遍性的設計問題,而且具有教育價值。前面介紹的生產者/消費者問題就是這樣一個問題,本節介紹另一個經典問題:讀者/寫者問題。

    讀者/寫者問題定義如下:存在一個多個進程共享的數據區,該數據區可以是一個文件或一塊內存空間,甚至可以是一組寄存器;有些進程(reader)只讀取這個數據區中的數據,有些進程(writer)
    只往數據區中寫數據。此外,還必須滿足以下條件:

  • 任意數量的讀進程可同時讀這個文件。
  • 一次只有一個寫進程可以寫文件。
  • 若一個寫進程正在寫文件,則禁止任何讀進程讀文件。
  • 也就是說,讀進程不需要排斥其他讀進程,而寫進程需要排斥其他所有進程,包括讀進程和寫進程。
    在繼續介紹之前,首先讓我們區分這個問題和另外兩個問題:一般互斥問題和生產者/消費者問題。在讀者/寫者問題中,讀進程不會向數據區中寫數據,寫進程不會從數據區中讀數據。更一般的情況是,允許任何進程讀寫數據區,此時可以把該進程中訪問數據區的部分聲明為一個臨界區,并強行實施一般互斥問題的解決方法。之所以關注這種更受限制的情況,是因為對這種情況可以有更有效的解決方案,一般問題的低效方法由于速度過慢而很難被人們接受。 例如,假設共享區是一個圖書館目錄,普通用戶可通過讀目錄來查找一本書,一位或多位圖書管理員可以修改目錄。在一般解決方案中,每次對目錄的訪問都可視為訪問一個臨界區,并且用戶每次只能讀一個目錄,這將會帶來無法忍受的延遲。同時,避免寫進程間互相干擾非常重要,此外還要求在寫的過程中禁止讀操作,以避免訪問到不正確的信息。

    生產者/消費者問題是否可視為只有一個寫進程(生產者)和一個讀進程(消費者)的特殊讀者/寫者問題呢?答案是不能。生產者不僅僅是一個寫進程,它必須讀取隊列指針,以確定向哪里寫下一項,還必須確定緩沖區是否已滿。類似地,消費者也不僅僅是一個讀進程,它必須調整隊列指針以顯示它已從緩沖區中移走了一個單元。

    現在開始分析讀者/寫者問題的兩種解決方案。

    5.6.1 讀者優先

    圖5.22是使用信號量的一種解決方案,它給出了一個讀進程和一個寫進程的實例,該方案無須修改就可用于多個讀進程和寫進程的情況。寫進程非常簡單,信號量wsem用于實施互斥,只要一個寫進程正在訪問共享數據區,其他寫進程和讀進程就都不能訪問它。讀進程也使用wsem實施互斥,但為了允許多個讀進程,沒有讀進程正在讀時,第一個試圖讀的讀進程需要在wsem上等待。當至少已有一個讀進程在讀時,隨后的讀進程無須等待,可以直接進入。全局變量 readcount用于記錄讀進程的數量,信號量x用于確保 readcount被正確地更新。

    5.6.2 寫者優先

    在前面的解決方案中,讀進程具有優先權。一個讀進程開始訪問數據區時,只要至少有一個讀進程正在讀,就為讀進程保留對這個數據區的控制權,因此寫進程有可能處于饑餓狀態。

    圖5.23給出了另一種解決方案,它保證在一個寫進程聲明想寫時,不允許新的讀進程訪問該數據區。對于寫進程,在已有定義的基礎上還必須增加下列信號量和變量:

    • 信號量rsem:至少有一個寫進程準備訪問數據區時,用于禁止所有的讀進程。
    • 變量writecount:控制rsem的設置。
    • 信號量y:控制writecount的更新。

      對于讀進程,還需要一個額外的信號量。在rsem上不允許建造長隊列,否則寫進程將無法跳過這個隊列,因此只允許一個讀進程在rsem上排隊,而所有其他讀進程在等待rsem前,在信號量z上排隊。表5.6概括了這些可能性。

      圖5.24給出了另一種解決方案,這種方案賦予寫進程優先權,并通過消息傳遞來實現。在這種情況下,有一個訪問共享數據區的控制進程,其他要訪問這個數據區的進程給控制進程發送請求消息,若請求得到同意,則會收到應答消息“OK”,并通過“finished”消息表示訪問完成。控制進程備有三個信箱,每個信箱存放一種它可能接收到的消息。

      要賦予寫進程優先權,控制進程就要先服務于寫請求消息,后服務于讀請求消息。此外,必須實施互斥。要實現互斥,需要使用變量count,它被初始化為一個大于可能的讀進程數的最大值。
      在該例中,我們取其值為100。控制器的動作可總結如下:
    • 若count>0,則無讀進程正在等待,可能有也可能沒有活動的讀進程。要清除活動讀進程,首先要服務于所有“finished”消息,然后服務于寫請求,再服務于讀請求。
    • 若count=0,則唯一未解決的請求是寫請求。允許該寫進程繼續執行并等待“finished”消息。
    • 若count<0,則一個寫進程已發出一條請求,且正在等待消除所有活動的讀進程。因此,只有“finished”消息將得到服務。

    5.7 小結

    現代操作系統的核心是多道程序設計、多處理器和分布式處理器,這些方案和操作系統設計技術的基礎都是并發。當多個進程并發執行時,不論是在多處理器系統的情況下,還是在單處理器多道程序系統中,都會出現沖突和合作的問題。

    并發進程可按多種方式進行交互。互相之間不知道對方的進程可能需要競爭使用資源,如處理器時間或對I/O設備的訪問。進程間由于共享訪問一個公共對象,如一塊內存空間或一個文件,可能間接知道對方,這類交互中產生的重要問題是互斥和死鎖。

    互斥指的是,對一組并發進程,一次只有一個進程能夠訪問給定的資源或執行給定的功能。互斥技術可用于解決諸如資源爭用之類的沖突,也可以用于進程間的同步,使得它們能夠合作。后一種情況的例子是生產者/消費者模型(這里使用了模型這個名詞,意義為問題模型,在日后編程的時候,你可能會經常遇到這些類似的問題),在該模型中,一個進程向緩沖區中放數據,另一個或更多的進程從緩沖區中取數據。

    支持互斥的第一種方法是使用專用機器指令,這種方法能降低開銷,但由于使用了忙等待,效率較低。

    支持互斥的另一種方法是在操作系統中提供相應的功能,其中最常見的兩種技術是信號量和消息機制。信號量用于在進程間發信號,能很容易地實施一個互斥協議。消息對實施互斥很有用,還為進程間的通信提供了一種有效的方法。

    《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

    總結

    以上是生活随笔為你收集整理的操作系统-并发性:互斥与同步的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。