《求職》第四部分 - 操作系統篇 - 操作系統基礎

1.計算機系統概述

1.1 基本構成

計算機的四個主要組件

  • 處理器:控制計算機的操作,執行數據處理功能。
  • 內存:存儲數據和程序。
  • I/O模塊:計算機內部和外部之間交換數據。
  • 系統總線:在CPU、內存和輸入輸出之間提供通信的設施。

1.2 指令的執行

基本指令週期,指令處理包括2步:

  • 處理器從存儲器一次讀一條指令;

  • 執行每條指令;

處理器中的PC(程序計算器)保存下一條指令的地址,IR(指令寄存器)保存當前即將執行的指令。

1.3中斷

允許“其他模塊”(I/O、存儲器)中斷“處理器”正常處理過程的機制。

1.3.1 目的

提高CPU利用率,防止一個程序壟斷CPU資源。

1.3.2 類型

1)程序中斷

2)時鐘中斷

3)I/O中斷

4)硬件失效中斷

1.3.3 中斷控制流

中斷:短I/O等待

  • 利用中斷功能,處理器可以在I/O操作的執行過程中執行其它指令:用戶程序到達系統調用WRITE處,但涉及的I/O程序僅包括準備代碼和真正的I/O命令。在這些爲數不多的幾條指令執行後,控制返回到用戶程序。在這期間,外部設備忙於從計算機存儲器接收數據並打印。這種I/O操作和用戶程序中指令的執行是併發的。

  • 當外部設備做好服務的準備時,也就是說,當它準備好從處理器接收更多的數據時,該外部設備的I/O模塊給處理器發送一箇中斷請求信號。這時處理器會做出響應,暫停當前程序的處理,轉去處理服務於特定I/O設備的程序,這個程序稱爲中斷處理程序。在對該設備的服務響應完成後,處理器恢復原先的執行。

中斷:長I/O等待

  • 對於如打印機等較慢的設備來說,I/O操作比執行一系列用戶指令的時間長得多,因此在下一次I/O操作時,前一次I/O可能還爲執行完。第二次WRITE調用時,第一次WRITE的I/O還爲執行完,結果是用戶程序會在這掛起,當前面I/O完成後,才能繼續新的WRITE調用。

1.3.4 中斷處理

中斷激活了很多事件,包括處理器硬件中的事件及軟件中的事件。
被中斷程序的信息保存與恢復:

1.3.5 多箇中斷
在處理一箇中斷的過程中,可能會發生另一箇中斷,處理多箇中斷有2種方法:

  • 當正在處理一箇中斷時,禁止再發生中斷:如果有新的中斷請求信號,處理器不予理睬。通常在處理中斷期間發生的中斷會被掛起,當處理器再次允許中斷時再處理。

  • 定義中斷優先級:允許高優先級的中斷處理打斷低優先級的中斷處理程序的允許

1.4存儲器的層次結構

在這裏插入圖片描述

從上往下看,會出現以下情況: * 每“位”的價格遞減 * 容量遞增 * 存取時間遞增 * 處理器訪問存儲器的頻率遞減(有效的基礎是訪問的局部性原理)。

1.4.1 高速緩存

內存的存儲週期跟不上處理器週期,因此,利用局部性原理在處理器和內存間提供一個容量小而速度快的存儲器,稱爲高速緩存。

上圖中高速緩存通常分爲多級:L1、L2、L3

1.5直接內存存取(DMA)

針對I/O操作有3種可能的技術

  • 可編程(程序控制)I/O(需處理器干預)
  • 中斷驅動I/O(需處理器干預)
  • 直接內存存取。

當處理器正在執行程序並遇到一個I/O相關的指令時,它通過給相應的I/O模塊發命令來執行這個指令:

1)使用可編程I/O時,I/O模塊執行請求的動作並設置I/O狀態寄存器中相應的位,但它並不進一步通 知處理器,尤其是它並不中斷處理器,因此處理器在執行I/O指令後,還需定期檢查I/O模塊的狀態。爲了確定I/O模塊是否做好了接收或發送更多數據的準備,處理器等待期間必須不斷詢問I/O模塊的狀態,這會嚴重降低整個系統的性能

2)如果是中斷驅動I/O,在給I/O模塊發送I/O命令後,處理器可以繼續做其它事。當I/O模塊準備好與處理器交換數據時,會中斷處理器並請求服務,處理器接着響應中斷,完成後再恢復以前的執行過程
儘管中斷驅動I/O比可編程I/O更有效,但是處理器仍需要主動干預在存儲器和I/O模塊直接的數據傳送,並且任何數據傳送都必須完全通過處理器。由於需要處理器干預,這兩種I/O存在下列缺陷:

  • I/O傳送速度受限於處理器測試設備和提供服務的速度(數據傳送受限於處理器)

  • 處理器忙於管理I/O傳送工作,必須執行很多指令以完成I/O傳送(處理器爲數據傳送需要做很多事)

3)因此,當需要移動大量數據時,需要使用一種更有效的技術:直接內存存取。DMA功能可以由系統總線中一個獨立的模塊完成,也可以併入到一個I/O模塊中。

DMA的工作方式如下,當處理器需要讀寫一塊數據時,它給DMA模塊產生一條命令,發送下列信息:

  • 是否請求一次讀或寫
  • 涉及的I/O設備的地址
  • 開始讀或寫的存儲器單元
  • 需要讀或寫的字數

之後處理器繼續其它工作。處理器將這個操作委託給DMA模塊,DMA模塊直接與存儲器交互,這個過程不需要處理器參與。當傳送完成後,DMA模塊發送一箇中斷信號給處理器。因此只有在開始和結束時,處理器纔會參與

2.操作系統概述

操作系統特點:併發性、共享性、虛擬性、不確定性。

2.1操作系統的目標和功能

操作系統是控制應用程序執行的程序,並充當應用程序和計算機硬件之間的接口。

  • 作爲用戶/計算機接口;

  • 作爲資源管理器(操作系統控制處理器使用其他系統資源,並控制其他程序的執行時機;

  • 易擴展性。

2.2操作系統的發展

  1. 串行處理:程序員直接與計算機硬件打交道,因爲當時還沒操作系統。這些機器在一個控制檯上運行,用機器代碼編寫的程序通過輸入設備載入計算機。如果發生錯誤使得程序停止,錯誤原因由顯示燈指示。如果程序正常完成,輸出結果出現在打印機中

  2. 簡單批處理系統:中心思想是使用一個稱爲監控程序的軟件。通過使用這類操作系統,用戶不再直接訪問機器,相反,用戶把卡片或磁帶中的作業提交給計算機操作員,由他把這些作業按順序組織成一批,並將整個批作業放在輸入設備上,供監控程序使用。每個程序完成處理後返回到監控程序,同時,監控程序自動加載下一個程序

  3. 多道批處理系統:簡單批處理系統提供了自動作業序列,但是處理器仍經常空閒,因爲對於I/O指令,處理器必須等到其執行完才能繼續。內存空間可以保持操作系統和一個用戶程序,假設內存空間容得下操作系統和兩個用戶程序,那麼當一個作業需要等到I/O時,處理器可以切換到另一個可能不需要等到I/O的作業。進一步還可以擴展存儲器保存三個、四個或更多的程序,並且在他們之間進行切換。這種處理稱爲多道程序設計或多任務處理,是現代操作系統的主要方案

  4. 分時系統:正如多道程序設計允許處理器同時處理多個批作業一樣,它還可以用於處理多個交互作業。對於後一種情況,由於多個用戶分享處理器時間,因而該技術稱爲分時。在分時系統中,多個用戶可以通過終端同時訪問系統,由操作系統控制每個用戶程序以很短的時間爲單位交替執行
    以下爲多道批處理系統與分時系統的比較
    批處理多道程序設計 分時
    主要目標 充分使用處理器 減小響應時間
    操作系統指令源 作業控制語言;作業提供的命令 終端輸入的命令

2.3現代操作系統

對操作系統要求上的變化速度之快不僅需要修改和增強現有的操作系統體系結構,而且需要有新的操作系統組織方法。在實驗用和商用操作系統中有很多不同的方法和設計要素,大致分爲以下幾類:

  • 微內核體系結構
  • 多線程
  • 對稱多處理
  • 分佈式操作系統
  • 面向對象設計

大內核:至今爲止大多數操作系統都有一個單體內核,操作系統應該提供的大多數功能由這些大內核提供,包括調度、文件系統、網絡、設備管理器、存儲管理等。典型情況下,這個大內核是作爲一個進程實現的,所有元素共享相同的地址空間。

微內核:微內核體系結構只給內核分配一些最基本的功能,包括地址空間,進程間通信和基本的調度。其它操作系統服務都是由運行在用戶態下且與其他應用程序類似的進程提供,這些進程可以根據特定應用和環境定製。這種方法把內核和服務程序的開發分離開,可以爲特定的應用程序或環境要求定製服務程序。可以使系統結構的設計更簡單、靈活,很適合於分佈式環境。

3.進程

3.1進程的概念

定義:進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例,包括程序計數器,寄存器和變量的當前值。能分配給處理器並由處理器執行的實體。一個具有以下特徵的活動單元:一組指令序列的執行、一個當前狀態和相關的系統資源集。程序運行時系統就會創建一個進程,併爲它分配資源,然後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。

也可以把進程視爲由程序代碼、和代碼相關聯的數據集、進程控制塊組成的實體。

進程控制塊:由操作系統創建和管理。進程控制塊包含了充分的信息,這樣就可以中斷一個進程的執行,並且在後來恢復執行進程時就好像進程未被中斷過一樣。進程控制塊是操作系統能夠支持多進程和提供多重處理技術的關鍵,進程控制塊是操作系統中最重要的數據結構,每個進程控制塊包含操作系統所需要的關於進程的所有信息

進程被中斷時,操作系統會把程序計數器和上下文數據保存到進程控制塊中的相應位置。

程序狀態字(PSW):所有處理器設計都包括一個或一組通常稱爲程序狀態字的寄存器,包含有進程的狀態信息。

【注】一個進程接到來自客戶端新的請求時,可以通過fork()複製出一個子進程讓其來處理,父進程只需負責監控請求的到來,這樣就能做到併發處理。根據寫時拷貝(copy on write)的機制,分爲兩個進程繼續運行後面的代碼。fork分別在父進程和子進程中返回,在子進程返回的值永遠是0,在父進程返回的是子進程的pid。

總結:

  1. 進程是指在系統中正在運行的一個應用程序,程序一旦運行就是進程;

  2. 進程可以認爲是程序執行的一個實例,進程是系統進行資源分配的最小單位,且每個進程擁有獨立的地址空間

  3. 一個進程無法直接訪問另一個進程的變量和數據結構,如果希望一個進程去訪問另一個進程的資源,需要使用進程間的通信,比如:管道、消息隊列等

  4. 線程是進程的一個實體,是進程的一條執行路徑;比進程更小的獨立運行的基本單位,線程也被稱爲輕量級進程,一個程序至少有一個進程,一個進程至少有一個線程;

3.2進程的狀態

3.2.1 進程的創建與終止

進程按以下步驟創建:

1.給新進程分配一個唯一的進程標識符

2.給新進程分配空間(包括進程映像中的所有元素)

3.初始化進程控制塊

4.設置正確的連接(保存到相應隊列)

會導致創建進程的事件:

會導致終止進程的事件:

3.2.2 兩狀態進程模型

在任何時候,一個進程要麼在執行要麼未執行,因此可以構建最簡單的模型。如下圖。

在這裏插入圖片描述

進程處於兩種狀態之一:運行或者未運行狀態。如圖(a)所示。操作系統創建一個新進程時,它將該進程以未運行態加入系統,操作系統知道這個進程的存在,並等待執行機會。時不時,當前正在運行的進程被中斷,此時操作系統中的分派器將選擇一個新進程運行。前一個進程運行態轉爲爲未運行狀態,後一個則進入運行狀態。

前文用提到,包含進程最重要的信息是進程控制塊,未運行的進程必須在某種類型的隊列中,並等待執行時機。如圖(b)所示,該結構有一個隊列,隊列中的每項指向進程的指針,或者隊列可由數據塊構成的鏈表組成,每個數據塊表示一個進程。

我們可以用排隊圖分配進程的執行。

3.2.3 五狀態進程模型

在這裏插入圖片描述

1)創建狀態:進程正在被創建

2)就緒狀態:進程被加入到就緒隊列中等待CPU調度運行

3)執行狀態:進程正在被運行

4)等待阻塞狀態:進程因爲某種原因,比如等待I/O,等待設備,而暫時不能運行。

5)終止狀態:進程運行完畢

2.2.4 引入”掛起態“的進程模型

考慮一個沒有使用虛擬內存的系統,每個被執行的進程必須完全載入內存,因此,2.3圖b)中,所有隊列中的所有進程必須駐留在內存中。

所有這些設計機制的原因都是由於I/O活動比計算速度慢得多,因此在單道程序系統中的處理器大多數時候是空閒的。但是2.3圖b)的方案並未完全解決這個問題。在這種情況下,內存保存有多個進程,當一個進程正在等待時,處理器可以轉移到另一個進程,但是處理器比I/O要快的多,以至於內存中所有的進程都在等待I/O的情況很常見。因此,即使是多道程序設計,大多數時候處理器仍然處於空閒。

因此,可以把內存中某個進程的一部分或全部移出到磁盤中。當內存中沒有處於就緒狀態的進程時,操作系統就把被阻塞的進程換出到磁盤中的”掛起隊列“。操作系統在此之後取出掛起隊列中的另一個進程,或者接受一個新進程的請求,將其納入內存運行。

“交換”是一個I/O操作,因而也可能使問題更加惡化。但是由於磁盤I/O一般是系統中最快的I/O(相對於磁帶或打印機I/O),所以交換通常會提高性能。

進程模型

  • 就緒/掛起->就緒:1)內存中沒有就緒態進程,需要調入一個進程繼續執行;2)處於就緒/掛起的進程具有更高優先級

  • 就緒->就緒/掛起:1)如果釋放空間以得到足夠空間的唯一方法是掛起一個就緒態的進程;2)如果操作系統確信高優先級的阻塞態進程很快將會就緒,那麼可能會掛起一個低優先級的就緒態進程而不是一個高優先級的阻塞態進程

  • 新建->就緒/掛起:進程創建需要爲其分配內存空間,如果內存中沒有足夠的空間分配給新進程,會使用”新建->就緒/掛起“轉換

  • 阻塞/掛起->阻塞:比較少見。如果一個進程終止,釋放了一些內存空間,阻塞/掛起隊列中有一個進程比就緒/掛起隊列中任何進程的優先級都要高,並且操作系統有理由相信阻塞進程的事件很快就會發生

  • 運行->就緒/掛起:如果位於阻塞/掛起隊列中的具有較高優先級的進程變得不再阻塞,操作系統搶佔這個進程,也可以直接把這個進程轉換到就緒/掛起隊列中,並釋放一些內存

3.3進程的描述

操作系統爲了管理進程和資源,必須掌握關於每個進程和資源當前狀態的信息。普遍使用的方法是:操作系統構造並維護它所管理的每個實體的信息表:

內存表用於跟蹤內(實)存和外存(虛擬內存)

使用進程映像來描述一個進程,進程鏡像包括:程序、數據、棧和進程控制塊(屬性的集合)

3.4進程控制

3.4.1 執行模式

大多數處理器至少支持兩種執行模式:

  • 用戶態

  • 內核態(系統態、控制態):軟件具有對處理器及所有指令、寄存器和內存的控制能力

使用兩種模式的原因是很顯然的,它可以保護操作系統和重要的操作系統表(如進程控制塊)不受用戶程序的干涉

處理器如何知道它正在什麼模式下執行及如何改變模式?

程序狀態字(PSW)中有一位表示執行模式,這一位應某些事件的要求而改變。在典型情況下,

  • 當用戶調用一個操作系統服務或中斷觸發系統例程的執行時,執行模式被設置爲內核態

  • 當從系統服務返回到用戶進程時,執行模式被設爲用戶態

3.4.2 進程切換

在下列事件中,進程可能把控制權交給操作系統:

  • 系統中斷

中斷:與當前正在運行的進程無關的某種類型的外部事件相關。控制首先轉移給中斷處理器,做一些基本的輔助工作後,轉到與已經發生的特定類型的中斷相關的操作系統例程

陷阱:與當前正在運行的進程所產生的錯誤或異常條件相關。操作系統首先確定錯誤或異常條件是否是致命的。1)如果是,當前進程被換到退出態,發生進程轉換;2)如果不是,動作取決於錯誤的種類或操作系統的設計,可能會進行一次進程切換或者繼續執行當前進程

  • 系統調用:轉移到作爲操作系統代碼一部分的一個例程上執行。通常,使用系統調用會把用戶進程置爲阻塞態

進程切換步驟如下: 1. 保存處理器上下文環境(包括程序計數器和其它寄存器) 2. 更新當前處於運行態進程的進程控制塊(狀態和其它信息) 3. 將進程控制塊移到相應隊列 4. 選擇另一個進程執行 5. 更新所選擇進程的進程控制塊(包括將狀態變爲運行態) 6. 更新內存管理的數據結構 7. 恢復處理器在被選擇的進程最近一次切換出運行狀態時的上下文環境

進程切換一定有模式切換;模式切換不一定有進程切換(中斷會發生模式切換,但是在大多數操作系統中,中斷的發生並不是必須伴隨着進程的切換的。可能是中斷處理器執行之後,當前正在運行的程序繼續執行);

3.5進程的調度

進程調度算法:先來先服務調度算法、短作業優先調度算法、非搶佔式優先級調度算法、搶佔式優先級調度算法、高響應比優先調度算法、時間片輪轉法調度算法;

3.6進程間通信

  • 管道:用於具有親緣關係進程間的通信
    • 由pipe函數創建,調用pipe函數時在內核中開闢一塊緩衝區用於通信,它有一個讀端一個寫端,然後通過filedes參數傳出給用戶程序兩個文件描述符,filedes[0]指向管道的讀端,filedes[1]指向管道的寫端。
    • 所以管道在用戶程序看來就像一個打開的文件,通過read(filedes[0])或者write(filedes[1]);向這個文件讀寫數據其實是在讀寫內核緩衝區。
    • 管道的讀寫端通過打開的文件描述符來傳遞,因此要通信的兩個進程必須從它們的公共祖先那裏繼承管道文件描述符。

① 半雙工的,具有固定的讀端和寫端;

② 只能用於具有親屬關係的進程之間的通信;

③ 可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read、write函數。但是它不是普通的文件,並不屬於其他任何文件系統,只能用於內存中。

④ Int pipe(int fd[2]);當一個管道建立時,會創建兩個文件文件描述符,要關閉管道只需將這兩個文件描述符關閉即可。

  • FIFO和Unix Domain Socket
    • 利用文件系統中的特殊文件來標識內核提供的通道
    • FIFO和Unix Domain
      Socket文件在磁盤上沒有數據塊,僅用來標識內核中的一條通道,各進程可以打開這個文件進行read和write,實際上實在讀寫內核通道,這樣就實現了進程間通信。
    • FIFO又名有名管道,每個FIFO有一個路徑名與之關聯,從而允許無親緣關係的進程訪問同一個FIFO。半雙工。

① FIFO可以再無關的進程之間交換數據,與無名管道不同;

② FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中;

③ Int mkfifo(const char* pathname,mode_t mode);

  • fork和wait
    • 父進程通過fork可以將打開的文件描述符傳遞給子進程
    • 子進程結束時,父進程調用wait可以得到子進程的終止信息
  • 信號
    • 信號又稱軟中斷,通知程序發生異步事件,程序執行中隨時被各種信號中斷,進程可以忽略該信號,也可以中斷當前程序轉而去處理信號,
  • 信號量
    • 分爲命名和匿名信號量。命名信號量通常用於不共享內存的進程之間(內核實現);匿名信號量可以用於線程通信(存放於線程共享的內存,如全局變量),或者用於進程間通信(存放於進程共享的內存,如System
      V/ Posix 共享內存)。
    • 信號量的使用主要是用來保護共享資源,使得資源在一個時刻只有一個進程(線程)所擁有。
      信號量的值爲正的時候,說明它空閒。所測試的線程可以鎖定而使用它。若爲0,說明它被佔用,測試的線程要進入睡眠隊列中,等待被喚醒。

① 信號量是一個計數器,信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據;

② 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存;

③ 信號量基於操作系統的PV操作,程序對信號量的操作都是原子操作;

  • 消息隊列
    • Linux
      中的消息可以被描述成在內核地址空間的一個內部鏈表,每一個消息隊列由一個IPC
      的標識號唯一地標識。

① 消息隊列,是消息的連接表,存放在內核中。一個消息隊列由一個標識符來標識;

② 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級;

③ 消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除;

④ 消息隊列可以實現消息的隨機查詢

  • 共享文件

    • 幾個進程可以在文件系統中讀寫某個共享文件,也可以通過給文件加鎖來實現進程間同步
  • 共享內存:通過mmap函數實現,幾個進程可以映射同一內存區

① 共享內存,指兩個或多個進程共享一個給定的存儲區;

② 共享內存是最快的一種進程通信方式,因爲進程是直接對內存進行存取;

③ 因爲多個進程可以同時操作,所以需要進行同步;

④ 信號量+共享內存通常結合在一起使用。

3.7進程之間私有和共享的資源

  • 私有:地址空間、堆、全局變量、棧、寄存器
  • 共享:代碼段,公共數據,進程目錄,進程 ID

4.線程

4.1線程的概念

線程是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位。一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。

4.2進程與線程

  • 進程是操作系統進行資源分配的基本單位;

  • 線程是調度的基本單位;

進程中的所有線程共享該進程的狀態和資源,進程和線程的關係如下圖:
在這裏插入圖片描述

從性能上比較,線程具有如下優點:

1.在一個已有進程中創建一個新線程比創建一個全新進程所需的時間要少許多,研究表明,線程創建要比在Unix中創建進程快10倍;

2.終止一個線程比終止一個進程花費的時間少;

3.同一進程內線程間切換比進程間切換花費的時間少;

4.線程提高了不同的執行程序間通信的效率(在大多數操作系統中,獨立進程間的通信需要內核的介入,以提供保護和通信所需要的機制。但是,由於在同一個進程中的線程共享內存和文件,它們無須調用內核就可以互相通信)。

線程和進程各自有什麼區別和優劣呢?

  • 進程是資源分配的最小單位,線程是程序執行的最小單位。
  • 進程有自己的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。
  • 線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點。
  • 但是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉並不會對另外一個進程造成影響,因爲進程有自己獨立的地址空間。
  • 進程有嚴格的父進程和子進程的概念,而且它們之間有很多的聯繫,父進程可以很容易地瞭解到子進程出現問題退出了,子進程退出的行爲很多時候可以不用交給程序來處理,操作系統就可以做的很好,充分利用這種機制可以獲得很好的系統可靠性。
  • Linux系統提供了豐富的進程間通信機制。在Linux下進程的執行效率與線程的執行效率基本相當。
  • 在完全不需要數據同步的基於UDP協議的大數據量讀取應用(流式視頻播放器)下,線程更爲簡單、方便且高效。

4.3線程狀態

和進程一樣,線程的關鍵狀態有運行態、就緒態和阻塞態。一般來說,掛起態對線程沒有什麼意義。這是由於此類狀態是一個進程級的概念。特別地,如果一個進程被換出,由於它的所有線程都共享該進程的地址空間,因此它們必須都被換出

有4種與線程相關的基本操作:

  • 派生:在典型情況下,當派生一個新進程時,同時也爲該進程派生了一個線程。隨後,進程中的線程可以在同一進程中派生另一個線程,併爲新線程提供指令指針和參數;新線程擁有自己的寄存器上下文和棧空間,且被放置在就緒隊列中

  • 阻塞:當線程需要等待一個事件時,它將被阻塞(保存它的用戶寄存器、程序計數器和棧指針),此時處理器轉而執行另一個處於同一進程中或不同進程中的就緒線程

  • 解除阻塞:當阻塞一個線程的事件發生時,該線程被轉移到就緒隊列中

  • 結束:當一個線程完成時,其寄存器上下文和棧都被釋放

4.4線程分類

線程的實現可以分爲兩大類:

  • 用戶級線程:有關線程管理的所有工作都由應用程序完成(使用線程庫),內核意識不到線程的存在。

  • 內核級線程:有關線程管理的所有工作都由內核完成,應用程序部分沒有進行線程管理的代碼。

4.3.1 用戶級線程

在用戶級線程中,進程和線程的狀態可能有如下轉換:

a)->b):線程2中執行的應用程序代碼進行系統調用,阻塞了進程B。例如,進行一次I/O調用。這導致控制轉移到內核,內核啓動I/O操作,把進程B置於阻塞狀態,並切換到另一個進程。在此期間,根據線程庫維護的數據結構,進程B的線程2仍處於運行狀態。值得注意的是,從處理器上執行的角度看,線程2實際上並不處於運行態,但是在線程庫看來,它處於運行態。

a)->c):時鐘中斷把控制傳遞給內核,內核確定當前正在運行的進程B已經用完了它的時間片。內核把進程B置於就緒態並切換到另一個進程。同時,根據線程庫維護的數據結構,進程B的線程2仍處於運行態

a)->d):線程2運行到需要進程B的線程1執行某些動作的一個點。此時,線程2進入阻塞態,而線程1從就緒態轉換到運行態。進程自身保留在運行態

在前兩種情況中,當內核把控制切換回進程B時,線程2會恢復執行

還需注意,進程在執行線程庫中的代碼時可以被中斷,或者是由於它的時間片用完了,或者是由於被一個更高優先級的進程所搶佔。因此在中斷時,進程可能處於線程切換的中間時刻。當該進程被恢復時,線程庫得以繼續運行,並完成線程切換和把控制轉移給另一個線程

用戶級線程的優點

1.由於所有線程管理數據結構都在一個進程的用戶地址空間中,線程切換不需要內核態特權,節省了兩次狀態轉換的開銷

2.調度可以是應用程序相關的(一個應用程序可能更適合簡單的輪轉調度,另一個可能更適合基於優先級的調度),可以爲應用量身定做調度算法而不擾亂底層操作系統調度程序

3.可以在任何操作系統中運行,不需要對底層內核進行修改以支持用戶級線程

用戶級線程的缺點

1.當用戶級線程執行一個系統調用時,不僅這個線程會被阻塞,進程中的所有線程都會被阻塞;

2.一個多線程應用程序不能利用多處理技術。內核一次只把一個進程分配給一個處理器,因此一次進程中只有一個線程可以執行(事實上,在一個進程內,相當於實現了應用程序級別的多道程序)。

4.3.2 內核級線程

內核級線程的優點

1.內核可以同時把同一進程中的多個線程調度到多個處理器中同時運行;

2.如果進程中一個線程被阻塞,內核可以調度其它線程;

3.內核例程自身也可以使用多線程。

內核級線程的缺點

把控制從一個線程轉移到用一進程的另一線程時,需要到內核的狀態切換。

4.3.3 混合方案

可以混合使用用戶級和內核級線程。在混合方案中,同一應用程序中的多個線程可以在多個處理器上並行地運行,某個會引起阻塞的系統調用不會阻塞整個進程。

如果設計正確,該方法將會結合純粹用戶級線程和內核級線程方法的優點,同時克服它們的缺點。

4.5線程之間的通信方式

  • 鎖機制:包括互斥鎖/量(mutex)、讀寫鎖(reader-writer lock)、自旋鎖(spin lock)、條件變量(condition)
    • 互斥鎖/量(mutex):提供了以排他方式防止數據結構被併發修改的方法。
    • 讀寫鎖(reader-writer lock):允許多個線程同時讀共享數據,而對寫操作是互斥的。
    • 自旋鎖(spin lock)與互斥鎖類似,都是爲了保護共享資源。互斥鎖是當資源被佔用,申請者進入睡眠狀態;而自旋鎖則循環檢測保持着是否已經釋放鎖。
    • 條件變量(condition):可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。
  • 信號量機制(Semaphore)
    • 無名線程信號量
    • 命名線程信號量
  • 信號機制(Signal):類似進程間的信號處理
  • 屏障(barrier):屏障允許每個線程等待,直到所有的合作線程都達到某一點,然後從該點繼續執行。

線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制

4.6線程之間私有和共享的資源

  • 私有:線程棧,寄存器,程序寄存器
  • 共享:堆,地址空間,全局變量,靜態變量

4.7多進程與多線程間的對比、優劣與選擇

對比
對比維度 多進程 多線程 總結
數據共享、同步 數據共享複雜,需要用 IPC;數據是分開的,同步簡單 因爲共享進程數據,數據共享簡單,但也是因爲這個原因導致同步複雜 各有優勢
內存、CPU 佔用內存多,切換複雜,CPU 利用率低 佔用內存少,切換簡單,CPU 利用率高 線程佔優
創建銷燬、切換 創建銷燬、切換複雜,速度慢 創建銷燬、切換簡單,速度很快 線程佔優
編程、調試 編程簡單,調試簡單 編程複雜,調試複雜 進程佔優
可靠性 進程間不會互相影響 一個線程掛掉將導致整個進程掛掉 進程佔優
分佈式 適應於多核、多機分佈式;如果一臺機器不夠,擴展到多臺機器比較簡單 適應於多核分佈式 進程佔優
優劣
優劣 多進程 多線程
優點 編程、調試簡單,可靠性較高 創建、銷燬、切換速度快,內存、資源佔用小
缺點 創建、銷燬、切換速度慢,內存、資源佔用大 編程、調試複雜,可靠性較差
選擇
  • 需要頻繁創建銷燬的優先用線程
  • 需要進行大量計算的優先使用線程
  • 強相關的處理用線程,弱相關的處理用進程
  • 可能要擴展到多機分佈的用進程,多核分佈的用線程
  • 都滿足需求的情況下,用你最熟悉、最拿手的方式

多進程與多線程間的對比、優劣與選擇來自:[多線程還是多進程的選擇及區別](

4.8協程

  • 協程是什麼?
    • 子程序,或者稱爲函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最後是A執行完畢。
    • 所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。
    • 子程序調用總是一個入口,一次返回,調用順序是明確的。
    • 而協程的調用和子程序不同。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然後轉而執行別的子程序,在適當的時候再返回來接着執行。
    • 在一個子程序中中斷,去執行其他子程序,不是函數調用,有點類似CPU的中斷
  • 協程的特點在於是一個線程執行,那和多線程比,協程有何優勢?
    • 最大的優勢就是協程極高的執行效率。因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
    • 第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
  • 因爲協程是一個線程執行,那怎麼利用多核CPU呢?
    • 最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
    • Python對協程的支持還非常有限,用在generator中的yield可以一定程度上實現協程。雖然支持不完全,但已經可以發揮相當大的威力了。
  • 來看例子:
    • 傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。
    • 如果改用協程,生產者生產消息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高:
import time

def consumer():
  r = ''
  while True:
	  n = yield r
	  if not n:
		  return
	  print('[CONSUMER] Consuming %s...' % n)
	  time.sleep(1)
	  r = '200 OK'

def produce(c):
  c.next()
  n = 0
  while n < 5:
	  n = n + 1
	  print('[PRODUCER] Producing %s...' % n)
	  r = c.send(n)
	  print('[PRODUCER] Consumer return: %s' % r)
  c.close()

if __name__=='__main__':
  c = consumer()
  produce(c)

執行結果:
[PRODUCER] Producing 1…
[CONSUMER] Consuming 1…
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2…
[CONSUMER] Consuming 2…
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3…
[CONSUMER] Consuming 3…
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4…
[CONSUMER] Consuming 4…
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5…
[CONSUMER] Consuming 5…
[PRODUCER] Consumer return: 200 OK

注意到consumer函數是一個generator(生成器),把一個consumer傳入produce後:

  1. 首先調用c.next()啓動生成器;
  2. 然後,一旦生產了東西,通過c.send(n)切換到consumer執行;
  3. consumer通過yield拿到消息,處理,又通過yield把結果傳回;
  4. produce拿到consumer處理的結果,繼續生產下一條消息;
  5. produce決定不生產了,通過c.close()關閉consumer,整個過程結束。

整個流程無鎖,由一個線程執行,produce和consumer協作完成任務,所以稱爲"協程",而非線程的搶佔式多任務。

5.併發

5.1互斥

可以根據進程相互之間知道對方是否存在的程度,對進程間的交互進行分類:

  • 進程間的資源競爭:每個進程不影響它所使用的資源,這類資源包括I/O設備、存儲器、處理器時間和時鐘。首先需要提供互斥要求(比方說,如果不提供對打印機的互斥訪問,打印結果會穿插)。實施互斥又產生了兩個額外的控制問題:死鎖和飢餓

進程間的資源競爭:每個進程不影響它所使用的資源,這類資源包括I/O設備、存儲器、處理器時間和時鐘。首先需要提供互斥要求(比方說,如果不提供對打印機的互斥訪問,打印結果會穿插)。實施互斥又產生了兩個額外的控制問題:死鎖和飢餓

  • 進程間通過共享的合作:進程可能使用並修改共享變量而不涉及其他進程,但卻知道其他進程也可能訪問同一數據。因此,進程必須合作,以確保共享的數據得到正確管理。由於數據保存在資源中(設備或存儲器),因此再次涉及有關互斥、死鎖、飢餓等控制問題,除此之外,還有一個新要求:數據的一致性

  • 進程間通過通信的合作:由於在傳遞消息的過程中,進程間未共享任何對象,因而這類合作不需要互斥,但是仍然存在死鎖和飢餓問題(死鎖舉例:兩個進程可能都被阻塞,每個都在等待來自對方的通信;飢餓舉例:P1,P2,P3,P1不斷試圖與P2,P3通信,P2和P3都試圖與P1通信,如果P1和P2不斷交換信息,而P3一直被阻塞,等待與P1通信,由於P1一直是活躍的,P3處於飢餓狀態)

5.1.1 互斥的硬件支持

1) 中斷禁用(只對單處理器有效):爲保證互斥,只需保證一個進程不被中斷即可

while(true){
     /* 禁用中斷 */
     /* 臨界區   */
     /* 啓用中斷  */
     /* 其餘部分  */
 }

問題

  • 處理器被限制於只能交替執行程序,因此執行的效率將會有明顯的降低。

  • 該方法不能用於多處理器結構中。

2) 專用機器指令

  • 比較和交換指令

  • 交換指令

在硬件級別上,對存儲單元的訪問排斥對相同單元的其它訪問。基於這一點,處理器的設計者提出了一些機器指令,用於保證兩個動作的原子性。在指令執行的過程中,任何其它指令訪問內存將被阻止

比較和交換指令

 int bolt;
 void P(int i)
 {
     while(true){
         while(compare_and_swap(&bolt,0,1) == 1)
             */***不做任何事***/*;
         */***臨界區***/*
         bolt = 0;
         */***其餘部分***/*
     }
 }

 int compare_and_swap(int *word,int testval,int newval)
 {
     int oldval;
     oldval = *word;
     if(oldval == testval) *word = newval;
     return oldval;
 }

交換指令

int bolt;
 void P(int i)
 {
     int keyi = 1;
     while(true){
         do exchange (&keyi,&bolt);
         while(keyi != 0);
         /*臨界區*/
         bolt = 0;
         /*其餘部分*/
     }
 }

 void exchange (int *register,int *memory)
 {
     int temp;
     temp = *memory;
     *memory = *register;
     *register = temp;
 }

優點

  • 適用於單處理器或共享內存的多處理上的任何數目的進程

  • 簡單且易於證明

  • 可用於支持多個臨界區(每個臨界區可以用它自己的變量定義)

缺點

  • 使用了忙等待(進入臨界區前會一直循環檢測,會銷燬處理器時間)

  • 可能飢餓(忙等的進程中可能存在一些進程一直無法進入臨界區)

  • 可能死鎖(P1在臨街區中時被更高優先級的P2搶佔,P2請求相同的資源)

5.1.2 互斥的軟件支持

軟件支持包括操作系統和用於提供併發性的程序設計語言機制,常見如下表:

1)信號量

通常稱爲計數信號量或一般信號量

可把信號量視爲一個具有整數值的變量,在它之上定義三個操作:

1.一個信號量可以初始化爲非負數(表示發出semWait操作後可立即執行的進程數量)

2.semWait操作使信號量減1。若值爲負數,執行該操作進程被阻塞。否則進程繼續執行

3.semSignal操作使信號量加1。若值小於或等於0,則被semWait阻塞的進程被解除阻塞

信號量原語的定義:

struct semaphore{
     int count;
     queueType queue;
 };

 void semWait(semaphore s)
 {
     s.count--;
     if(s.count < 0){
         /*把當前進程插入到隊列當中*/;
         /*阻塞當前進程*/;
     }
 }

 void semSignal(semaphore s)
 {
     s.count++;
     if(s.count <= 0){
         /*把進程P從隊列中移除*/;
         /*把進程P插入到就緒隊列*/;
     }
 }

2)二元信號量

二元信號量是一種更特殊的信號量,它的值只能是0或1。

可以使用下面3種操作:

1.可以初始化爲0或1。

2.semWaitB操作檢查信號的值,如果爲0,該操作會阻塞進程。如果值爲1,將其改爲0後進程繼續執行。

3.semSignalB操作檢查是否有任何進程在信號上阻塞。有則通過semSignalB操作,受阻進程會被喚醒,如果沒有,那麼設置值爲1。

二元信號量的原語定義:

struct binary_semaphore{
     enum {zero,one} value;
     queueType queue;
 };

 void semWaitB(binary_semaphore s)
 {
     if(s.value == one)
         s.value = zero;
     else{
         /*把當前進程插入到隊列當中*/;
         /*阻塞當前進程*/;
     }
 }

 void semSignalB(binary_semaphore s)
 {
     if(s.queue is empty())
         s.value = one;
     else
     {
         /*把進程P從等待隊列中移除*/;
         /*把進程P插入到就緒隊列*/;
     }
 }
  • 強信號量:隊列設計爲FIFO,被阻塞最久的進程最先從隊列中釋放(保證不會飢餓)。

  • 弱信號量:沒有規定進程從隊列中移出順序。

使用信號量的互斥(這裏是一般信號量,不是二元信號量)

 const int n = */***進程數***/*
 semaphore s = 1;

 void P(int i)
 {
     while(true){
         semWait(s);
         */***臨界區***/*;
         semSignal(s);
         */***其它部分***/*;
     }
 }

 void main()
 {
     parbegin(P(1),P(2),...,P(n));
 }

下圖爲三個進程使用了上述互斥協議後,一種可能的執行順序:

信號量爲實施互斥及進程間合作提供了一種原始但功能強大且靈活的工具,但是,使用信號量設計一個正確的程序是很困難的,其難點在於semWait和semSignal操作可能分佈在整個程序中,卻很難看出這些在信號量上的操作所產生的整體效果(詳見1.3 經典互斥問題中的“生產者/消費者“問題)

3)互斥量

互斥量和二元信號量關鍵的區別在於:互斥量加鎖的進程和解鎖的進程必須是同一進程

4)管程

管程是一個程序設計語言結構,它提供了與信號量同樣的功能,但更易於控制。它是由一個或多個過程一個初始化序列局部數據組成的軟件模塊,主要特點如下:

1.局部數據變量只能被管程的過程訪問,任何外部過程都不能訪問

2.一個進程通過調用管程的一個過程進入管程

3.在任何時候,只能有一個進程在管程中執行,調用管程的其它進程都被阻塞,等待管程可用

爲進行併發處理,管程必須包含同步工具(例如:一個進程調用了管程,並且當它在管程中時必須被阻塞,直到滿足某些條件。這就需要一種機制,使得該進程在管程內被阻塞時,能釋放管程,以便其它進程可以進入。以後,當條件滿足且管程在此可用時,需要恢復進程並允許它在阻塞點重新進入管程)

管程通過使用條件變量提供對同步的支持,這些條件變量包含在管程中,並且只有在管程中才能被訪問。有2個操作:

  • cwait©:調用進程的執行在條件c上阻塞,管程現在可被另一個進程使用

  • csignal©:恢復執行在cwait後因某些條件被阻塞的進程。如果有多個則選擇其一;如果沒有則什麼也不做

管程的結構如下:

管程優於信號量之處在於,所有的同步機制都被限制在管程內部,因此,不但易於驗證同步的正確性,而且易於檢查出錯誤。此外,如果一個管程被正確編寫,則所有進程對保護資源的訪問都是正確的;而對於信號量,只有當所有訪問資源的進程都被正確地編寫時,資源訪問纔是正確的

5)消息傳遞

最小操作集:

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

阻塞:

  • 當一個進程執行send原語時,有2種可能:

– 發送進程被阻塞直到這個消息被目標進程接收

– 不阻塞

  • 當一個進程執行receive原語後,也有2種可能:

– 如果一個消息在此之前被髮送,該消息被正確接收並繼續執行

– 沒有正在等待的消息,則a)進程阻塞直到等待的消息到達,b)繼續執行,放棄接收的努力

消息傳遞過程中需要識別消息的源或目的地,這個過程稱爲尋址,可分爲兩類: 1. 直接尋址 * 對於send:包含目標進程的標識號 * 對於receive:1)進程顯示指定源進程;2)不可能指定所希望的源進程時,通過source參數保存相應信息 2. 間接尋址(解除了發送者/接收者的耦合性,更靈活) * 消息發送到一個共享數據結構,稱爲”信箱“。發送者和接收者直接有”一對一“、”多對一“、”一對多“和”多對多“的對應關係(典型的”多對一“如客戶端/服務器,此時”信箱“就是端口)

消息傳遞實現互斥(消息函數可視爲在進程直接傳遞的一個令牌):

const int n = */***進程數***/*;
 void P(int i)
 {
     message msg;
     while(true){
         receive(box,msg);
         */***臨界區***/*;
         send(box,msg);
         */***其它部分***/*;
     }
 }

 void main()
 {
     create mailbox (box);
     send(box,null);
     parbegin(P(1),P(2),...,P(n));
 }

可以使用消息傳遞處理”生產者/消費者問題“,可以有多個消費者和生產者,系統甚至可以是分佈式系統,代碼見1.3

5.1.3 經典問題

在設計同步和併發機制時,可以與一些經典問題聯繫起來,以檢測該問題的解決方案對原問題是否有效

1)生成者/消費者問題

有一個或多個生產者生產某種類型的數據,並放置在緩衝區中;有一個消費者從緩衝區中取數據,每次取一項;

任何時候只有一個主體(生產者或消費者)可以訪問緩衝區。要確保緩存滿時,生產者不會繼續添加,緩存爲空時,消費者不會從中取數據

實現代碼:

  • 當緩衝無限大時(二元信號量,對應圖5.10;信號量,對應圖5.11)

  • 當緩衝有限時(信號量,對應圖5.13;管程,對應圖5.16;消息傳遞,對應圖5.21)

2)讀者/寫者問題

有一個由多個進程共享的數據區,一些進程只讀取這個數據區中的數據,一些進程只往數據區中寫數據;此外還滿足以下條件:

  • 任意多的讀進程可以同時讀

  • 一次只有一個進程可以寫

  • 如果一個進程正在寫,禁止所有讀;

實現代碼:

  • 讀優先:只要至少有一個讀進程正在讀,就爲進程保留對這個數據區的控制權(信號量,對應圖5.22)

  • 寫優先:保證當有一個寫進程聲明想寫時,不允許新的讀進程訪問該數據區(信號量,對應圖5.23)

5.2死鎖

5.2.1死鎖的概念

死鎖定義:指多個進程因競爭共享資源而造成的一種僵局,若無外力作用,這些進程都將永遠不能再向前推進。

假設兩個進程的資源請求和釋放序列如下:

下圖是相應的聯合進程圖,顯示了進程競爭資源的進展情況:

敏感區域:路徑3,4進入的區域。敏感區域的存在依賴於兩個進程的邏輯關係。然而,如果另個進程的交互過程創建了能夠進入敏感區的執行路徑,那麼死鎖就必然發生

死鎖問題中的資源分類

  • 可重用資源:一次只能供一個進程安全地使用,並且不會由於使用而耗盡的資源(包括處理器、I/O通道、內外存、設備等)

  • 可消耗資源:可以被進程創建和消耗的資源。通常對某種類型可消耗資源的數目沒有限制,一個無阻塞的生產進程可以創建任意數目的這類資源(包括中斷、信號、消息和I/O緩衝中的信息)

資源分配圖

  • 進程到資源:進程請求資源但還沒得到授權

  • 資源到進程:請求資源已被授權

  • 資源中的“點”:表示該類資源的一個實例

5.2.2死鎖的條件

1.互斥:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一進程所佔用。

2.佔有且等待:當進程因請求資源而阻塞時,對已獲得的資源保持不放。

3.不可搶佔:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。

4.循環等待:存在一個封閉的進程鏈,使得每個進程至少佔有此鏈中下一個進程所需的一個資源。

條件1~3是死鎖的必要條件,條件4是前3個條件的潛在結果,即假設前3個條件存在,可能發生的一系列事件會導致不可解的循環等待。這個不可解的循環等待實際上就是死鎖的定義。之所以不可解是因爲有前3個條件的存在。因此,4個條件連在一起構成了死鎖的充分必要條件。

【注】死鎖產生的原因:

  • 系統資源不足;
  • 資源分配不當;
  • 進程運行推進順序不合適。

5.2.3死鎖預防

死鎖預防是通過約束資源請求,使得4個死鎖條件中的至少1個被破壞,從而防止死鎖發生

  • 間接的死鎖預防(防止死鎖條件1~3)

  • 預防互斥:一般來說,不可能禁止

  • 預防佔有且等待:可以要求進程一次性地請求所有需要的資源,並且阻塞進程直到所有請求都同時滿足。這種方法在兩個方面是低效的:1)爲了等待滿足其所有請求的資源,進程可能被阻塞很長時間。但實際上只要有一部分資源,就可以繼續執行;2)分配的資源有可能有相當長的一段時間不會被使用,且在此期間,這些資源不能被其它進程使用;除此之外,一個進程可能事先並不會知道它所需要的所有資源。

  • 預防不可搶佔:有幾種方法:1)如果佔用某些資源的進程進一步申請資源時被拒,則釋放其佔用的資源;2)如果一個進程請求當前被另一個進程佔有的一個資源,操作系統可以搶佔另一個進程,要求它釋放資源(方法2只有在任意兩個進程優先級不同時,才能預防死鎖);此外,通過預防不可搶佔來預防死鎖的方法,只有在資源狀態可以很容易保存和恢復的情況下才實用。

  • 直接的死鎖預防(防止死鎖條件4)

    • 預防循環等待:可以通過定義資源類型的線性順序來預防,如果一個進程已經分配到了R類型的資源,那麼它接下來請求的資源只能是那些排在R類型之後的資源;這種方法可能是低效的,會使進程執行速度變慢,並且可能在沒有必要的情況下拒絕資源訪問都會導致低效的資源使用和低效的進程運行。

5.3.4死鎖避免

死鎖避免允許3個必要條件,但通過明智選擇,確保永遠不會到達死鎖點。

由於需要對是否會引起死鎖進行判斷,因此死鎖避免需要知道將來的進程資源請求的情況。

2種死鎖避免的方法:

1.進程啓動拒絕:如果一個進程的請求會導致死鎖,則不啓動此進程

2.資源分配拒絕:如果一個進程增加的資源請求會導致死鎖,則不允許此分配

1)進程啓動拒絕

一個有n個進程,m種不同類型資源的系統。定義如下向量和矩陣:

從中可以看出以下關係成立:

對於進程n+1,僅當對所有j,以下關係成立時,才啓動進程n+1:

2)資源分配拒絕(銀行家算法)

當進程請求一組資源時,假設同意該請求,從而改變了系統的狀態,然後確定其結果是否還處於安全狀態。如果是,同意這個請求;如果不是,阻塞該進程直到同意該請求後系統狀態仍然是安全的。

  • 安全狀態:至少有一個資源分配序列不會導致死鎖(即所有進程都能運行直到結束)

  • 不安全狀態:非安全的一個狀態(所有分配序列都不可行)

下圖爲一個安全序列:

下圖爲一個不安全序列:

這個不安全序列並不是一個死鎖狀態,僅僅是有可能死鎖。例如,如果P1從這個狀態開始運行,先釋放一個R1和R3,後來又再次需要這些資源,一旦這樣做,則系統將到達一個安全狀態

優點

  • 不需要死鎖預防中的搶佔和回滾進程,並且比死鎖預防的限制少。比死鎖預防允許更多的併發

缺點

  • 必須事先聲明每個進程請求的最大資源;
  • 所討論的進程必須是無關的,也就是說,他們執行的順序必須沒有任何同步要求的限制;
  • 分配的資源數目必須是固定的;
  • 在佔有資源時,進程不能退出。

5.2.5死鎖檢測

死鎖檢測不限制資源訪問或約束進程行爲。只要有可能,被請求的資源就被分配給進程。操作系統週期性地執行一個算法檢測死鎖條件4(循環等待)

常見死鎖檢測算法

這種算法的策略是查找一個進程,使得可用資源可以滿足該進程的資源請求,然後假設同意這些資源,讓該進程運行直到結束,再釋放它的所有資源。然後算法再尋找另一個可以滿足資源請求的進程

這個算法並不能保證防止死鎖,是否死鎖要取決於將來同意請求的次序,它所做的一切是確定當前是否存在死鎖

恢復

一旦檢測到死鎖,就需要某種策略以恢復死鎖,有下列方法(複雜度遞增):

  • 取消所有死鎖進程(操作系統最常用)
  • 回滾每個死鎖進程到前面定義的某些檢測點
  • 連續取消死鎖進程直到不再存在死鎖(基於某種最小代價原則)
  • 連續搶佔資源直到不再存在死鎖(基於代價選擇,每次搶佔後需重新調用算法檢測,被搶佔的進程需回滾)

5.2.6經典問題(哲學家就餐問題)

就餐需要使用盤子和兩側的叉子,設計一套算法以允許哲學家喫飯。算法必須保證互斥(沒有兩位哲學家同時使用同一把叉子),同時還要避免死鎖和飢餓

方法一(基於信號量,可能死鎖):每位哲學家首先拿起左邊的叉子,然後拿起右邊的叉子。吃完麪後,把兩把叉子放回。如果哲學家同時拿起左邊的叉子,會死鎖

方法二(基於信號量,不會死鎖):增加一位服務員,只允許4位哲學家同時就座,因而至少有一位哲學家可以拿到兩把叉子

方法三(基於管程,不會死鎖):和方法一類似,但和信號量不同的是,因爲同一時刻只有一個進程進入管程,所以不會發生死鎖

6.內存管理

單道程序設計中:內存被劃分爲兩部分,一部分供操作系統使用(駐留監控程序、內核),一部分供當前正在執行的程序使用

多道程序設計中:必須在內存中進一步細分“用戶”部分,以滿足多個進程的要求,細分的任務由操作系統動態完成,稱爲內存管理

內存管理的需求

重定位:程序在從磁盤換入內存時,可以被裝載到內存中的不同區域

保護:處理器必須保證進程以外的其它進程不能未經授權地訪問該進程的內存單元

共享:任何保護機制都必須具有一定靈活性,以允許多個進程訪問內存的同一部分

邏輯組織

物理組織

內存管理中的地址

邏輯地址:指與當前數據在內存中的物理分配地址無關的訪問地址,執行對內存訪問前必須轉換成物理地址

相對地址:邏輯地址的一個特例,是相對於某些已知點(通常是程序開始處)的存儲單元

物理地址**(絕對地址)**:數據在內存中的實際位置

虛擬地址:虛擬內存中的邏輯地址

內存管理單元**(MMU)**:CPU中的一個模塊,將虛擬地址轉換成實際物理地址

6.1內存管理中的數據塊

頁框:內存中一個固定長度的塊

:二級存儲(如磁盤)中一個固定長度的數據塊

:二級存儲中一個變長的數據塊

6.2內存分區

6.2.1 固定分區

系統生成階段,內存被劃分成許多靜態**(大小,容量固定不變)**分區,兩種固定分區:

分區大小相等

分區大小不等

放置策略

對於分區大小相等的固定分區

– 只要存在可用分區,就可以分配給進程

對於分區大小不等的固定分區

每個進程分配到能容納它的最小分區:每個分區維護一個隊列(較多小進程時,大分區會空閒)

每個進程分配到能容納它的最小可用分區:只需一個隊列

存在內部碎片;活動進程數固定

6.2.2 動態分區

並不進行預先分區,在每次需要爲進程分配時動態劃分

外部碎片(隨着時間推移,內存中產生了越來越多”空洞“):

可以使用壓縮解決外部碎片,但是非常耗時

放置算法:由於壓縮十分耗時,因而需要巧妙地把進程分配到內存中,塞住內存中的”洞“

最佳適配:選擇與要求大小最接近的塊(通常性能最差,儘管每次浪費的空間最小,但結果卻使得內存中很快產生許多碎片)

首次適配:選擇大小足夠的第一個塊(不僅最簡單,通常也是最好、最快的;容易在首部產生碎片)

下次適配:從上次放置的位置起,第一個大小足夠的塊(比首次適配差,常常會在尾部產生碎片)

維護複雜,且會產生外部碎片

6.2.3 夥伴系統

內存最小塊和最大塊的尺寸是M和L。在爲一個進程分配空間時,如果需要的內存大於L/2,則分配L的內存,否則,將大小爲L的塊分成兩個L/2的塊,繼續上述步驟;如果兩個相鄰的塊(夥伴)都未分配出去(如前面的進程釋放後),則將其合併

下圖爲一個夥伴系統的例子:

夥伴系統是一種折中方案,克服了固定分區和動態分區方案的缺陷。但在當前操作系統中,基於分頁和分段機制的虛擬內存更好。夥伴系統在並行系統中有很多應用

6.2.4 分區中的地址轉換

邏輯地址->物理地址的轉換如下

基址寄存器:被載入程序在內存中的起始地址

界限寄存器:程序的終止位置

這種轉換方式適用於程序運行時,被加載到內存中連續區域的情況。對於分頁和分段,由於一個程序可以加載到內存的不同區域,所以需要使用另外的機制進行轉換

6.3分頁

用戶程序的地址空間被劃分成若干固定大小的區域,稱爲"頁",相應地,內存空間分成若干個物理塊,頁和塊的大小相等。可將用戶程序的任一頁放在內存的任一塊中,實現了離散分配。將整個內存劃分成許多大小相等的頁面,每個進程的地址空間可以由多個頁面構成。

內存被劃分爲大小固定的塊,且塊相對比較小,每個進程也被分成同樣大小的小塊,那麼進程中稱爲頁的塊可以指定到內存中稱爲頁框的可用塊。和固定分區的不同在於:一個程序可以佔據多個分區,這些分區不要求連續

使用分頁技術在內存中每個進程浪費的空間,僅僅是最後一頁的一小部分(內部碎片)

6.3.1 分頁中的地址轉換

由於進程的頁可能不連續,因此僅使用一個簡單的基址寄存器是不夠的,操作系統需要爲每個進程維護一個頁表。頁表項是進程每一頁與內存頁框的映射

6.4分段

將用戶程序地址空間分成若干個大小不等的段,每段可以定義一組相對完整的邏輯信息。存儲分配時,以段爲單位,段與段在內存中可以不相鄰接,也實現了離散分配。

將整個內存劃分爲大小不同的段,每個進程的地址空間處於不同的獨立段中。

段有一個最大長度限制,但不要求所有程序的所有段長度都相等。分段類似於動態分區,區別在於:一個程序可以佔據多個不連續的分區

分段同樣會產生外部碎片,但是進程被劃分成多個小塊,因此外部碎片也會很小

6.4.1 分段中的地址轉換

由於進程的段可能不連續,因此也不能僅靠一個簡單的基址寄存器,地址轉換通過段表實現。由於段的大小不同,因此段表項中還包括段的大小

如果偏移大於段的長度,則這個地址無效.

  • 應用場景
    • 進程與進程之間可以讓虛擬地址相同,但是物理地址不同而達到空間上的真正分離。
    • 進程自己並不能看到自己的真實物理地址,而且即便物理地址不存在,也可以通過頁面交換技術讓它存在,那麼操作系統就可以欺騙進程擁有很多的內存可用。
    • 利用頁面交換技術,可以將一個文件映射到內存中,使得mmap這樣的系統調用可以實現。
    • 將虛擬地址轉換成相同的物理地址,就可以做到數據的共享,線程就是這麼幹的。
    • 將硬件設備的控制存儲區域反映到虛擬內存上,就可以實現通過內存訪問就達到控制硬件的目的。
  • 分頁與分段的主要區別
    • 頁是信息的物理單位,分頁是爲了實現非連續分配,以便解決內存碎片問題,或者說分頁是由於系統管理的需要.段是信息的邏輯單位,它含有一組意義相對完整的信息,分段的目的是爲了更好地實現共享,滿足用戶的需要.
    • 頁的大小固定,由系統確定,將邏輯地址劃分爲頁號和頁內地址是由機器硬件實現的.而段的長度卻不固定,決定於用戶所編寫的程序,通常由編譯程序在對源程序進行編譯時根據信息的性質來劃分.
    • 分頁的作業地址空間是一維的.分段的地址空間是二維的。

6.5頁面置換算法

  • 最佳置換算法OPT:不可能實現
  • 先進先出FIFO
  • 最近最久未使用算法LRU:最近一段時間裏最久沒有使用過的頁面予以置換.
  • clock算法

6.6內存安全

6.6.1 緩衝區溢出

緩衝區溢出是指輸入到一個緩衝區或者數據保存區域的數據量超過了其容量,從而導致覆蓋了其它區域數據的狀況。攻擊者造成並利用這種狀況使系統崩潰或者通過插入特製的代碼來控制系統

被覆蓋的區域可能存有其它程序的變量、參數、類似於返回地址或指向前一個棧幀的指針等程序控制流數據。緩衝區可以位於堆、棧或進程的數據段。這種錯誤可能產生如下後果:

1.破壞程序的數據

2.改變程序的控制流,因此可能訪問特權代碼

最終很有可能造成程序終止。當攻擊者成功地攻擊了一個系統之後,作爲攻擊的一部分,程序的控制流可能會跳轉到攻擊者選擇的代碼處,造成的結果是被攻擊的進程可以執行任意的特權代碼(比如通過判斷輸入是否和密碼匹配來訪問特權代碼,如果存在緩衝區漏洞,非法輸入導致存放“密碼”的內存區被覆蓋,從而使得“密碼”被改寫,因此判斷爲匹配進而獲得了特權代碼的訪問權)

緩衝區溢出攻擊是最普遍和最具危害性的計算機安全攻擊類型之一

6.6.2 預防緩衝區溢出

廣義上分爲兩類:

  • 編譯時防禦系統,目的是強化系統以抵禦潛伏於新程序中的惡意攻擊

  • 運行時預防系統,目的是檢測並終止現有程序中的惡意攻擊

儘管合適的防禦系統已經出現幾十年了,但是大量現有的脆弱的軟件和系統阻礙了它們的部署。因此運行時防禦有趣的地方是它能夠部署在操作系統中,可以更新,並能爲現有的易受攻擊的程序提供保護

7.其他

7.1 Linux與windows

  • Linux:
    • 以進程爲主,強調任務的獨立性
    • 線程方面的處理:NPTL原生POSIX線程庫
      • 一個線程與一個內核的調度實體一一對應
      • 新的線程同步機制:futex(快速用戶空間互斥體)
    • Linux處理進程和線程的機制就是是否開啓COW
      • 子進程先跟父進程共享內存,採用COW及術後,子進程還需要拷貝父進程的頁面表。
  • Windows
    • 以線程爲主,強調任務的協同性
  • windows的調度實體就是線程,進程只是一堆數據結構。而Linux不是。Linux將進程和線程做了同等對待,進程和線程在內核一級沒有差別,只是通過特殊的內存映射方法使得它們從用戶的角度上看來有了進程和線程的差別。
  • Windows至今也沒有真正的多進程概念,創建進程的開銷遠大於創建線程的開銷。Linux則不然。Linux在內核一級並不區分進程和線程,這使得創建進程的開銷和創建線程的開銷差不多。
  • Windows和Linux的任務調度策略也不盡相同。Windows會隨着線程越來越多而變得越來越慢,這也是爲什麼Windows服務器在運行一段時間後必須重啓的原因。Linux可以持續運行很長時間,系統的效率也不會有什麼變化。

7.2內核態和用戶態

  • 內核態和用戶態的區別
    • 當進程執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核狀態。此時處理器處於特權級最高的(0級)內核代碼。當進程處於內核態時,執行的內核代碼會使用當前的內核棧。每個進程都有自己的內核棧。
    • 當進程在執行用戶自己的代碼時,則稱其處於用戶態。即此時處理器在特權級最低的用戶代碼中運行。
    • 當正在執行用戶程序而突然中斷時,此時用戶程序也可以象徵性地處於進程的內核態。因爲中斷處理程序將使用當前進程的內核態。
    • 內核態與用戶態是操作系統的兩種運行級別,跟intel
      cpu沒有必然聯繫,intel
      cpu提供Ring0-Ring3三種級別運行模式,Ring0級別最高,Ring3級別最低。Linux使用了Ring3級別運行用戶態。Ring0作爲內核態,沒有使用Ring1和Ring2。Ring3不能訪問Ring0的地址空間,包括代碼和數量。Linux進程的4GB空間,3G-4G部分大家是共享的,是內核態的地址空間,這裏存放在整個內核代碼和所有的內核模塊,以及內核所維護的數據。用戶運行一程序,該程序所創建的進程開始是運行在用戶態的,如果要執行文件操作,網絡數據發送等操作,必須通過write,send等系統調用,這些系統會調用內核中的代碼來完成操作,這時必須切換到Ring0,然後進入3GB-4GB中的內核地址空間去執行這些代碼完成操作,完成後,切換Ring3,回到用戶態。這樣,用戶態的程序就不能隨意操作內核地址空間,具有一定的安全保護作用。
  • 用戶態和內核態的轉換
    • 用戶態切換到內核態的3種方式
      • 系統調用
        • 這是用戶進程主動要求切換到內核態的一種方式,用戶進程通過系統調用申請操作系統提供的服務程序完成工作。而系統調用的機制其核心還是使用了操作系統爲用戶特別開放的一箇中斷來實現,例如Linux的ine
          80h中斷。
      • 異常
        • 當CPU在執行運行在用戶態的程序時,發現了某些事件不可知的異常,這是會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就到了內核態,比如缺頁異常。
      • 外圍設備的中斷
        • 當外圍設備完成用戶請求的操作之後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條將要執行的指令轉而去執行中斷信號的處理程序,如果先執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了有用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。
    • 具體的切換操作
      • 從出發方式看,可以在認爲存在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一樣的,沒有任何區別,都相當於執行了一箇中斷響應的過程,因爲系統調用實際上最終是中斷機制實現的,而異常和中斷處理機制基本上是一樣的,用戶態切換到內核態的步驟主要包括:
      • (1)從當前進程的描述符中提取其內核棧的ss0及esp0信息。
      • (2)使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧找到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令。
      • (3)將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到了內核態的程序執行了。

7.3變量存儲區域

  • 棧:
    • 由編譯器在需要的時候分配,在不需要的時候自動清楚的變量的存儲區。
    • 地址是不固定的。
    • 存儲的變量通常是局部變量、函數參數等。
  • 堆:
    • 由new分配的內存塊,它們的釋放編譯器不去管,而是由應用程序去控制,一般一個new就要對應一個delete。
    • 如果程序員沒有釋放掉,那麼在程序結束後,操作系統會自動回收。
  • 自由存儲區:
    • 由malloc等分配的內存塊,和堆是十分類似,不過它是用free來結束自己的生命的。
  • 全局存儲區(靜態存儲區):
    • 全局變量和靜態變量的存儲是放在一塊的。
    • 初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。
    • 程序結束後由系統釋放。
  • 常量存儲區:
    • 這是一塊比較特殊的存儲區,位置是固定的。
    • 這裏面存放的是常量,不允許修改。

7.4 Linux 內核的同步方式

原因

在現代操作系統裏,同一時間可能有多個內核執行流在執行,因此內核其實象多進程多線程編程一樣也需要一些同步機制來同步各執行單元對共享數據的訪問。尤其是在多處理器系統上,更需要一些同步機制來同步不同處理器上的執行單元對共享的數據的訪問。

同步方式

  • 原子操作
  • 信號量(semaphore)
  • 讀寫信號量(rw_semaphore)
  • 自旋鎖(spinlock)
  • 大內核鎖(BKL,Big Kernel Lock)
  • 讀寫鎖(rwlock)
  • 大讀者鎖(brlock-Big Reader Lock)
  • 讀-拷貝修改(RCU,Read-Copy Update)
  • 順序鎖(seqlock)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章