重拾作業系統

fitzeng發表於2017-08-31

主要是把在重讀 《現代作業系統》 中覺得有價值的東西,以 Tips 的形式記錄下來。不可能面面俱到,但是如果有一定的基礎應該是會回想起很多知識的。具體解釋將會以連結形式補充。GitHub
相關閱讀 : 重拾資料結構

1. 程式與執行緒

1.1 程式

主要是為了支援偽併發能力

  • 執行態 : 實際佔用 CPU 資源
  • 就緒態 : 可執行,但是由於沒有時間片而被暫停等待 CPU 重新排程
  • 阻塞態 : 外部某種事件導致(資源不足)不可執行

CPU 利用率 = 1 - p ^ n

p : IO 等待時間比

n : 程式數

1.2 執行緒

每一個程式有一個地址空間和一個控制執行緒,主要是使某個程式內的任務之間不被相互阻塞,實現一種程式內並行操作的假象。建立銷燬更加輕量級。

共享一組資源,協同完成任務。每個執行緒有自己的堆疊區(因為要區分同一程式內的執行緒,CPU 排程要進行狀態的儲存)

執行緒模型
使用者空間中實現執行緒
核心中實現執行緒
混合實現

1.3 程式間通訊(IPC)

1.競爭條件

兩個或者多個程式讀寫某些共享資料

2.臨界區

將共享記憶體的訪問程式碼稱為臨界區,確保在每個時刻兩個程式不可能同時處於臨界區中,這樣可以避免競爭條件。核心思想為互斥。

併發程式準確高效要滿足一下四個條件

  • 任何兩個程式不能同時處於其臨界區
  • 不應對 CPU 的速度和數量做任何假設
  • 臨界區外執行的程式不得阻塞其他程式
  • 不得使程式無限期等待進入臨界區
3.忙等待的互斥

互斥實現方案

遮蔽中斷

每個程式進入臨界區後立即遮蔽所有中斷,這樣 CPU 無法進行程式切換,就要離開臨界區是開啟中斷。

鎖變數

設定一個共享鎖變數,初始值為 0。當一個程式想要進入臨界區,必須檢測鎖的值是否為 0,是則置 1 進入臨界區。不是則等待其他程式退出臨界區時釋放鎖直到自己能獲取到鎖開始進入臨界區。

鎖變數還是會產生競爭條件

嚴格輪換法

一直迴圈等待直到出現允許該程式進入臨界區的條件才開始執行,十分消耗 CPU 資源。

避免了競爭條件,但是臨界區外執行的程式會發生阻塞

用於忙等待的鎖稱為自旋鎖。

A:
while (TRUE) {
    while (turn != 0);
    critical_region();
    turn = 1;
    noncritical_region();
}

B:
while (TRUE) {
    while (turn != 1); 
    critical_region();
    turn = 0;
    noncritical_region();
}複製程式碼
Peterson 解法

一種互斥演算法

#define FALSE 0
#define TRUE 1
#define N 2

int turn;
int interested[N];

void enter_region(int process) {
    int other;
    other = 1 - process;
    interested[process] = TRUE;
    turn = process;
    // 如果有另一個程式進入臨界區的話則一直空迴圈
    while (turn == process && interested[other] == TRUE);
}

void leave_region(int process) {
    interested[process] = FALSE;
}複製程式碼
4.睡眠與喚醒

前面的弊端是忙等待會消耗 CPU 資源。如果在等待進入臨界區時可以掛起,等到某個訊號到達再喚醒就可以避免這種情況了。

生產者-消費者問題

利用資源緩衝區實現程式間的協調

#define N 100 
int count = 0;

void producer(void) {
    int item;
    while (TURE) {
        item = produce_item();
        if (count == N) {
            sleep();
        }
        insert_item(item);
        count = count + 1;
        if (count == 1) {
            wakeup(consumer);
        }
    }
}

void consumer(void) {
    int item;
    while (TURE) {
        if (count == 0) {
            sleep();
        }
        item = remove_item();
        count = count - 1;
        if (count == N - 1) {
            wakeup(producer);
        }
        consume_item(item);
    }
}複製程式碼
5.訊號量

引入一個訊號量來累計喚醒次數,可以為 0 或正數

使用 down 和 up 操作代替 sleep 和 wakeup

#define N 100
typedef int semaphore
semaphore mutex = 1;  // 控制對臨界區的訪問
semaphore empty = N; // 計數緩衝區的空槽數目
semaphore full = 0; // 計數緩衝區的滿槽數目

void producer(void) {
    int item;
    while (TRUE) {
        utem = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer(void) {
    int item;
    while (TRUE) {
        down(&full);
        down(&mutex);
        item = remove_item();
        up(&mutex);
        up(&empty);
        consume_item(item);
    }
}複製程式碼
  • mutex : 用於互斥,保證任一時刻只有一個程式讀寫緩衝區
  • full && empty : 實現同步,保證某種時間的順序發生或者不發生
6.互斥量

僅僅適用於管理共享資源或一小段程式碼

7.管程
8.訊息傳遞
9.屏障
1.4 排程

當有多個程式處於就緒態時就面臨排程的選擇。

核心管理執行緒時排程可以認為是執行緒級別的。

程式行為有 計算密集型I/O 密集型。而現在由於 CPU 改進速度加快,程式行為更傾向於後者,所以應該多執行該類程式保持 CPU 的利用率。

排程演算法
  1. 批處理

    • 先來先服務
    • 最短作業優先
    • 最短剩餘時間優先
  2. 互動式

    • 輪轉排程(每個程式一個時間片,用完就輪轉)
    • 優先順序排程
    • 多級佇列
    • 最短程式優先 (aT0 + (1 - a)T1
    • 保證優先
    • 彩票排程
    • 公平分享排程
  3. 實時

執行緒排程

和系統支援的執行緒實現方式有關(理解 : 執行緒表存在哪的區別)

使用者級執行緒 : 核心並不知道,核心只是選中該程式,至於程式中跑哪個執行緒由使用者態排程決定。

核心級執行緒 : 直接排程某個程式內的執行緒。

以上兩種方式在效能上有主要差別 : 前面提及 I/O 操作其實是很耗時的,所以在程式間切換比線上程間切換更加耗時。因為執行緒輕量,而程式完成切換要完整的山下文切換,修改記憶體映像。而且同一程式內的執行緒 I/O 訪問 cache 的區域性性更高,不同程式間切換的清理快取,這也會消耗時間。

2. 儲存管理

主要思想就是記憶體抽象

2.1 空閒記憶體管理

使用點陣圖的儲存管理
使用連結串列的儲存管理

2.2 虛擬記憶體

程式產生的地址為虛擬地址,在沒有虛擬記憶體的作業系統上,直接將地址輸送到記憶體匯流排上。而有虛擬記憶體的作業系統上,把虛擬地址輸送到 MMU(Memory Management Unit),由 MMU 將虛擬地址對映為為實體地址。

分頁

虛擬地址空間 : 頁面 實體記憶體地址 : 葉框 4k大小

虛擬地址 = 虛擬頁號(高位) + 偏移量(低位)

頁表 : 把虛擬地址的頁面對映為頁框,每個程式都有自己的頁表

加速分頁方法 : 轉換檢測緩衝區(TLB)主要是優先在 TLB 中查詢頁面號。

大記憶體頁表

  1. 多級頁表
  2. 倒排頁表 : 每個頁框一個表項 + TLB 快表

2.3 頁面置換演算法

最優頁面置換演算法不可實現,因為無法確定未來。

1.最近未使用頁面置換演算法(NRU)

設定訪問(讀、寫)位 R,頁面修改位 M。

2.先進先出頁面置換演算法(FIFO)
3.第二次機會頁面置換演算法

設定一個檢測最老頁面位 R

4.時鐘頁面置換演算法

連結串列實現頁面選擇

5.最近最少使用頁面置換演算法(LRU)

利用矩陣模擬 : 增加自身權重減少其他權重,行置 1,列置 0。

6.用軟體模擬 LRU

老化演算法

7.工作集頁面置換演算法
8.工作集時鐘頁面置換演算法
演算法 註釋
最優演算法 不可實現,但可作為基準
NRU(最近未使用)演算法 LRU 的很粗糙近似
FIFO(先進先出)演算法 可能拋棄重要頁面
第二次機會演算法 比 FIFO 有大的改善
時鐘演算法 現實的
LRU(最近最少使用)演算法 很優秀,但很難實現
NFU(最不經常使用)演算法 LRU 的相對粗略的近似
老化演算法 非常近似 LRU 的有效演算法
工作集演算法 實現起來開銷很大
工作集時鐘演算法 好的有效演算法

2.4 記憶體對映檔案

程式發起系統呼叫,把檔案對映到其虛擬地址空間的一部分。一般實現是開始不載入,在程式訪問時在按頁載入。

// Linux 待填

2.5 實現

分頁工作
  • 程式建立時 : 作業系統要確定程式和資料在初始時有多大,併為它們建立一個頁表,作業系統還要在記憶體中為頁表分配空間並對其進行初始化。
  • 程式執行時 : 頁表必須在記憶體中(反之不需要),並且在磁碟交換區中分配空間。
  • 排程一個程式執行時 : 為新程式充值 MMU,重新整理 TLB,更換頁表。
  • 缺頁中斷髮生時 : 作業系統必須通過讀硬體暫存器確定是哪個虛擬地址造成了缺頁中斷通過該資訊計算需要哪個頁面,定位磁碟位置並找到合適的頁框來存放新頁面,必要的話要置換老頁面,然後把所需頁面讀入頁框。最後,備份程式計數器,是程式計數器指向引起缺頁中斷的指令,並重新執行該指令。
  • 程式退出時 : 釋放頁表,頁面和頁面在硬碟上佔的空間。
缺頁中斷處理
  1. 硬體陷入核心,在堆疊中儲存程式計數器。大多數機器將當前的指令的各種狀態資訊儲存在特殊的 CPU 暫存器中。
  2. 啟動一個彙編程式碼例程儲存通用暫存器和其他易失資訊,以免被作業系統破壞。這個例程將作業系統做為一個函式來呼叫。
  3. 當作業系統發現一個缺頁中斷時,嘗試發現需要哪個虛擬頁面。通常一個硬體暫存器包含了這一資訊,如果沒有的話,作業系統必須檢索程式計數器,取出這條指令,用軟體分析這條指令,看看他在缺頁中斷時正在做什麼。
  4. 一旦知道了發生缺頁中斷的虛擬地址,作業系統檢查這個地址是否有效,並檢查存取與保護是否一致,如果不一致,向程式發出一個訊號或殺掉該程式。如果地址有效且沒有保護錯誤發生,系統則檢查是否有空閒頁框。如果沒有空閒頁框,執行頁面置換演算法尋找一個頁面來淘汰。
  5. 如果選擇的頁框“髒”了,安排該頁面寫回磁碟,併發生一次上下文切換,掛起產生缺頁中斷的程式,讓其他程式執行直至磁碟傳輸結束。無論如何,該頁框被標記為忙,以免因為其他原因而被其他程式佔用。
  6. 一旦頁框“乾淨”後(無論是立刻還是在寫回磁碟後),作業系統查詢所需頁面在磁碟上的地址,通過磁碟操作將其裝入。該頁面被裝入後,產生缺頁中斷的程式仍然被掛起,並且如果有其他可執行使用者程式,則選擇另一個使用者程式執行。
  7. 當磁碟中斷髮生時,表明該頁已被裝入,頁表已經更新可以反映他的位置,頁框也被標記為正常狀態。
  8. 恢復發生缺頁中斷指令以前的狀態,程式計數器重新定向這條指令。
  9. 排程引發缺頁中斷的程式,作業系統返回撥用他的組合語言例程。
  10. 該例程恢復暫存器和其他狀態資訊,放回到使用者空間繼續執行,就好像缺頁中斷沒有發生過一樣。

2.6 分段

段是邏輯實體,大小不固定。

2.7 分段和分頁結合 : MULTICS

還有 Intel Pentuium 未介紹

34 位的 MULTICS 虛擬地址

段號 頁號 頁內偏移
18 6 10
  1. 根據段號找到段描述符
  2. 檢查該段的頁表是否存在記憶體中。如果在,則找到他;如果不再,則產生一個段錯誤。如果訪問違反了段的保護要求就要求發出一個越界錯誤(陷阱)。
  3. 檢查所請求虛擬頁面的頁表項,如果該頁面不再記憶體中則產生一個缺頁中斷,如果在記憶體就從頁表中取出這個頁面在記憶體中的起始地址。
  4. 把偏移量加到頁面的起始地址上,得到要訪問的字在記憶體中的地址。
  5. 最後進行讀或寫操作。

3. 檔案系統

檔案系統存放在磁碟上。多數磁碟劃分為一個或多個分割槽,每個分割槽中有一個獨立的檔案系統。磁碟的 0 號盤扇區稱為主開機記錄(Master Boot Record, MBR),用來引導計算機。在 MBR 的結尾是分割槽表,該表給出了每個分割槽的其實和結束地址。表中的一個分割槽被標記為活動分割槽。在計算機被引導時,BIOS 讀入並執行 MBR。MBR 做的第一件事是確定活動分割槽,讀入它的第一個塊,稱為引導塊,並執行。

整個分割槽:

MBR 分割槽表 磁碟分割槽 磁碟分割槽 磁碟分割槽...

磁碟分割槽:

引導塊 超級塊 空閒空間管理 i 節點 根目錄 檔案和目錄

3.1 檔案實現

連續分配

把每個檔案作為一連串連續資料塊儲存在磁碟上。

連結串列分配

一個檔案由幾個磁碟塊組成。

在記憶體中採用表的連結串列分配

把每個磁碟塊的指標字放在記憶體的一個表中

i 節點

每個檔案賦予一個稱為 i 節點(index-node)的資料結構,列出檔案屬性和檔案快的磁碟地址。

4. 輸入/輸出

4.1 I/O 硬體原理
I/O 裝置

塊裝置 : 以塊為單位傳輸,可定址

字元裝置 : 以字元為單位收發字元流,不可定址

裝置控制器
記憶體對映 I/O
直接儲存器存取

DMA 工作原理:

  1. CPU 對 DMA 控制器進行程式設計
  2. DMA 請求磁碟傳送資料到記憶體
  3. 磁碟傳送資料到記憶體
  4. 磁碟給 DMA 控制器應答
  5. 完成中斷

5. 死鎖

5.1 資源

在程式對裝置,檔案等取得了排他性訪問許可權的時候,有可能會出現死鎖。這類需要排他性使用的物件稱為資源。

可搶佔資源

可以從擁有它的程式中搶佔而不會產生任何副作用。(儲存器)

不可搶佔資源

指在不引起相關的計算失敗的情況下,無法把他從佔有它的程式處搶佔過來。( CD 燒錄)

資源使用步驟:

  1. 請求資源
  2. 使用資源
  3. 釋放資源

5.2 死鎖概述

如果一個程式集合中的每個程式都在等待只能由該程式集合中的其他程式才能引發的事件,那麼,該程式集合就是死鎖的。

資源死鎖條件

發生資源死鎖的四個必要條件:

  1. 互斥條件 : 每個資源要麼已經分配了一個程式,要麼就是可用的。
  2. 佔有和等待條件 : 已經得到了某個資源的程式可以再請求新的資源。
  3. 不可搶佔條件 : 已經分配給一個程式的資源不能強制性地被搶佔,它只能被佔有它的程式顯示地釋放。
  4. 環路等待條件 : 死鎖發生時,系統中一定有兩個或兩個以上的程式組成的一條環路,該環路中的每個程式都在等待著下一個程式所佔有的資源。

5.3 死鎖檢測與死鎖恢復

死鎖檢測主要是判斷當前空閒資源在某種合理分配下是否能使所有程式都執行完並且最終資源都能夠釋放。

恢復方法 :

  1. 利用搶佔式恢復
  2. 利用回滾恢復
  3. 利用殺死程式恢復

5.4 死鎖避免

資源軌跡圖
安全狀態和不安全狀態
單個資源的銀行家演算法
多個資源的銀行家演算法

5.5 死鎖預防

死鎖避免從本質上來說是不可能的,因為他要獲取未來的資訊。

破壞互斥條件

如果資源不被一個程式獨佔死鎖不會發生。(假離線印表機)

破壞佔有和等待條件

開始執行前請求所有資源就不會造成等待。另一種是請求資源時先釋放自己所持有的資源,再嘗試一次請求資源。

破壞不可搶佔條件

針對某些資源進行虛擬化,實現可搶佔。

破壞環路等待條件

保證每個程式在任何時刻只能佔用一個資源如果要請求另外一個資源它必須先釋放第一個資源。另一種是將所有資源統一編號,程式可以在任何時刻提出資源請求,但是請求必須按照資源編號順序(升序)提出。

5.6 其他問題

兩階段加鎖
通訊死鎖
活鎖
飢餓

相關文章