您當前的位置:首頁 > 舞蹈

可能是最簡單的作業系統書籍

作者:由 成楠Peter 發表于 舞蹈時間:2020-07-24

前言

《作業系統導論》,我是透過 starkwang( @Starkwang ) 瞭解到這本書的。之前閱讀過《深入理解計算機系統》,坡度有點大,每次下了大決心攻讀,又雙叒叕地倒下,而《作業系統導論》更適合想學作業系統的新人,有深動形象的例子,有具體的舉例說明,有可以 run 的程式碼,即使枯燥的專業名詞解釋打倒了我,但舉例說明我又不斷恢復我的信心。就這樣,一口氣,三週左右讀完了此書,還將其中的一些思想運用到了一個前端快取框架(這個之後會開源)。它真的是一本深入淺出、物超所值的書。

這本書的作者非常佛性,將原版書直接開源了,如果英語水平還不錯,可以直接看原版,中文的翻譯很多地方翻譯的都不地道。原版大概是 08 年出版,而國內的翻譯已經遲來了 10 年。

本文介紹

本文將從 4 個方面展開。虛擬化,併發,永續性,分散式系統,也對應此書的 4 大部分,我會挑一些重點內容總結。

虛擬化

程序

程式設計師肯定遇到過面試官問程序和執行緒的區別。程序是作業系統基本抽象,方便作業系統管理程式。一般程序就是執行中的程式,程序之間相互隔離,透過遠端過程呼叫(Remote Process Call,RPC) 通訊。執行緒屬於程序,一個程序下可以建立多個執行緒,為了最大化利用程序。

程序排程

首先得理解程序之間為什麼需要排程。一個作業系統有多個應用,如果沒有排程,一個程序可以惡意佔用整個作業系統。如何控制程序之間的排程。首先,作業系統需要有控制權,所以就引入

時鐘中斷

(Clock Interrupt),程序每隔一段時間,將控制權還給作業系統,作業系統決定將下次的執行許可權賦予哪個程序,這個過程,必須要排程演算法協調多方。

程序排程的策略有先進先出,短任務優先,最短完成時間優先,多級反饋,比例份額。

先進先出(FIFO)策略

,先進先出,保證每一個程序都有執行時間,完全的公平。但是越是理想情況,越是不符合現實。我們需要一種有優先順序的排程策略。

可能是最簡單的作業系統書籍

短任務優先(SJF)策略

,計算任務的完成時間,越短越優先,這和超市排隊一樣,如果你只買了口香糖,但是卻要排長長的隊,效率很低,所以有些商場會有“零散購物”通道,這樣就大大增加了效率。

可能是最簡單的作業系統書籍

最短完成時間優先(STCF)策略

,SJF 有一個問題,比如 A 在執行,A 執行總共要 10s,已經運行了 1s,但是新進了兩個任務 B、C 總執行時間只要 1s,這時候 B、C 就要等 A 執行完才能執行,還是阻塞了短任務。所以 STCF 就是用來解決這個,即使 A 在執行,也每隔一段監測是否有短任務需要插入。

可能是最簡單的作業系統書籍

多級反饋(MLFQ)策略

,將任務分優先順序,高優先的先執行,每隔一段,中低階的任務都有機會晉升為高優先順序任務,避免高優先順序任務一直佔用 CPU,低優先任務餓死。

可能是最簡單的作業系統書籍

比例份額(proportional-share)策略

,這個策略比較有意思。和彩票一樣,每個任務都有份額,高優先重要的任務份額多,低優先順序的少,每次 CPU 執行權都進行彩票抽獎,這樣達到理論上的公平。每個任務還可以將份額再細分給子任務,這樣能讓任務也有控制權。

CPU 和記憶體虛擬化

虛擬化的核心是記憶體的虛擬化,這是由作業系統實現的。作業系統透過分段和分頁技術抽象了地址空間,所以程序裡的記憶體地址是虛擬的。每個程序執行的時候,都擁有一塊非常大的虛擬地址空間,像獨佔了整個作業系統一樣。它實現了程序之間的隔離,也讓每個程序最大化利用記憶體。

分段和分頁

分段,現代作業系統的記憶體管理是由分段和分頁組成的。一般記憶體分為 4 個部分,程式程式碼,堆,棧,空閒地址。因為執行的時候程式程式碼是固定的,所以放在最上面,堆需要的內容比較大,所以向下增長。而棧所需的內容比較少,放在記憶體的最後部分,反向增長,如下圖。但是這樣空閒的部分就很大,分段就是將空閒地址,分成不同的段,提升空閒地址的利用率。

可能是最簡單的作業系統書籍

分頁,將虛擬地址轉換成物理地址,分頁的地址可以是連續的,物理地址可以不連續,這樣將物理地址分割成一個個小頁,每次置換的時候只要操作一小部分物理地址。而在分頁和物理地址之間,引入了頁表,專門管理分頁到物理地址的對映。分頁除了一級分頁,也可以設定二級分頁,三級分頁等,這還是引入中間層以達到最佳化效能的目的,中間層真是計算機一大靈丹妙藥,但同時程式的複雜度也會增高。

分頁管理如下圖。

可能是最簡單的作業系統書籍

網上也有人總結,分段是邏輯地址 -> 線性地址(虛擬地址),而分頁是虛擬地址 -> 物理空間,兩個分工明確。

TLB

如果每一次記憶體訪問,都需要執行一次虛擬地址到真實地址的查詢,那 I/O 非常高,所以引入了快速地址轉換(Translation Lookaside Buffer,TLB),

一個儲存虛擬地址到真實地址的資料結構

,也是程式中經常說的快取。快取是程式的狗皮膏藥,好用,但是大範圍使用反而適得其反。

有一句話說的好:計算機科學領域的任何問題都可以透過增加一個間接的中間層來解決,而 TLB 就是這個中間層,它很好的解決了效能問題。

可能是最簡單的作業系統書籍

併發

多處理器、多程序、多執行緒

多處理器、多程序、多執行緒其實都是模擬同一時間可以做多件事。想想你 1 分鐘能攤一個餅,一個賺 1 塊。如果 14 億個你同時攤,那中國 1 分鐘內就產生了一個億萬富翁。

併發程式

書中也給出了一個例子,下面程式,在多執行緒情況下輸出非理想的結果。

#include

#include

#include

“common。h”

#include

“common_threads。h”

static

volatile

int

counter

=

0

// mythread()

//

// Simply adds 1 to counter repeatedly, in a loop

// No, this is not how you would add 10,000,000 to

// a counter, but it shows the problem nicely。

//

void

*

mythread

void

*

arg

{

printf

“%s: begin

\n

char

*

arg

);

int

i

for

i

=

0

i

<

1e7

i

++

{

counter

=

counter

+

1

}

printf

“%s: done

\n

char

*

arg

);

return

NULL

}

// main()

//

// Just launches two threads (pthread_create)

// and then waits for them (pthread_join)

//

int

main

int

argc

char

*

argv

[])

{

pthread_t

p1

p2

printf

“main: begin (counter = %d)

\n

counter

);

Pthread_create

&

p1

NULL

mythread

“A”

);

Pthread_create

&

p2

NULL

mythread

“B”

);

// join waits for the threads to finish

Pthread_join

p1

NULL

);

Pthread_join

p2

NULL

);

printf

“main: done with both (counter = %d)

\n

counter

);

return

0

}

prompt

>

/

main

main

begin

counter

=

0

A

begin

B

begin

A

done

B

done

main

done

with

both

counter

=

19345221

作者用兩個執行緒,分別對一個數字各加

10,000,000

次,理論上最終的結果是

20,000,000

,但是結果每次都不相同。現在只是用多執行緒已經出現了不可預期的結果,如果在多處理下不知道會出現更匪夷所思的結果。

鎖,自旋鎖,條件鎖,訊號鎖

有併發就必有鎖,這兩個是相生相伴的。最典型的是寫入衝突,併發帶來了效能提升,同時帶來了不確定性。而鎖就是讓這個過程變得更確定,這也就是為什麼函數語言程式設計能成為一大程式設計正規化,對於某些場景,確定性比什麼都重要。

一般鎖的實現有自旋鎖,條件鎖,訊號鎖,無論怎麼實現,

核心都是將呼叫指令過程原子化

,直白的說將可能發生衝突的地方上鎖,只允許一個執行緒或者程序操作。

事件的併發,事件迴圈,select,poll

事件迴圈是併發的另一個解決方式,代替多執行緒。NodeJs,Nginx 都利用事件迴圈解決併發問題。考慮一個場景,假設伺服器 1s 內能處理 100 個請求,但是客戶端發起 200 個請求,服務端將所有請求加入到佇列中(不考慮佇列滿的情況,也不考慮優先佇列),再利用事件迴圈機制(一般是主執行緒有一個 for 迴圈),每次去佇列裡拿資料,保證服務端不會被流量擊垮。

select 和 poll 都是重要的 API,用來接收事件,它們之間很相似,而 Linux 裡在 2。6 核心中提出的 epoll,是 select 和 poll 的增強版,在這裡就不展開了。

用 select 接收事件

int

select

int

nfds

fd_set

*

restrict

readfds

fd_set

*

restrict

writefds

fd_set

*

restrict

errorfds

struct

timeval

*

restrict

timeout

);

永續性

這一部分說的是硬體相關的知識,作業系統離不開硬體。

磁碟持久化

網際網路上所有的資源,圖片,音訊,影片,文字這些最終都是資料,那這些資料儲存到哪?答案是磁碟。磁碟提供了資料持久化的功能,當然磁碟銷燬了,資料也就沒了,這也就是為什麼 github 前不久需要將程式碼存入交卷,儲存到冰川之下,儲存年限也不過千年。銀行也將同一份資料,分別儲存到不同的機器上,防止硬體問題導致資料丟失。所以磁碟持久化,不僅需要解決軟體問題,也要解決硬體問題。

磁碟排程,磁碟驅動器

磁碟驅動器裡有磁軌、磁碟臂、磁頭。透過找到磁頭,轉動磁碟臂,讀取和寫入磁碟資料。磁軌可能有多層,所以會有一個尋道的過程。

可能是最簡單的作業系統書籍

RAID,廉價冗餘陣列磁碟

Redundant Array of Inexpensive Disks,這是利用多個磁碟一起構建更快、更大、更可靠的磁碟系統。它有 5 個等級,分別是 RAID0、RAID1、RAID2、RAID3、RAID4、RAID5。

RAID0 透過條帶化提高了多個磁碟的讀取速度,而 RAID1-5 都是透過不同的技術,提高條帶化的速度和正確性,RAID1 用到了映象技術,RAID4 用到了奇偶校驗技術,RAID5 用到了旋轉奇偶校驗技術。

可能是最簡單的作業系統書籍

檔案系統

檔案系統是作業系統中非常重要的一環。它分為目錄和檔案,透過樹形結構組織,用 inode 資料結構記錄檔案的元資料。

歷史遺留原因,所以的檔案系統都必須實現 4 個介面,open,read,write,close。

檔案崩潰一致性

檔案崩潰一致性指當發生一些特殊情況,比如斷電,寫入的檔案並不是斷電之前的檔案。透過日誌記錄檔案的寫入內容,當檔案寫入完成之後,核對寫入檔案內容是否一致,一般透過和相加判斷內容是否一致,如果中途發生斷電,從日誌中還原檔案的資訊,重新寫入。

分散式系統

RPC

遠端過程呼叫(Remote Process Call,RPC),是程序之間通訊的協議,雖然是遠端伺服器之間的呼叫,但是 RPC 提供了像本地函式呼叫的方式,在使用者層面無感知。將複雜程度交給自己,提供最簡單的使用方式,這是計算機的一種藝術。而下面遠端檔案系統之間的通訊都是透過 RPC 完成的。

NFS 和 AFS

這是兩個分散式檔案系統管理,前者是 Sun 公司的網路檔案系統(Network File System,NFS),後者是 Andrew 檔案管理(Andrew File System,AFS)。對於分散式管理系統,主要考慮檔案的讀寫速度,以及可靠性。

NFS 讀取遠端檔案之後,將資源存在本地,當再次訪問資源即可從本地讀取。但是遠端檔案可能會更新,所以每次讀取本地檔案資源時,去遠端伺服器獲取的最後更新時間,與本地檔案的最後更新時間對比,決定是否發起檔案的訪問。這也會帶來一些問題,每次都要獲取服務端檔案的最後更新時間,增加了服務端頻寬壓力,並且很多是無效請求,遠端檔案沒更新的請求。所以 NFS 設定了本地資源的快取時間,比如 3s 內不去訪問遠端伺服器,雖然獲取到的檔案可能不一致,但是緩解了服務端壓力。

AFS 在 NFS 基礎上優化了很多內容,增加了檔案一致性,也提高了讀寫效率。第一次請求服務端,服務端記錄需要更新的客戶端地址。第二次請求檔案資源,不再是客戶端向服務端發起請求查詢檔案是否更新,而是當檔案變更的時候,服務端主動推送訊息告知客戶端檔案更新了,這樣大大增加了效率。它還有許多最佳化,比如減少檔案路徑的查詢等。不過因為 NFS 是開源協議,所以現在 NFS 吸取了 AFS 的精華之後,更為流行。

總結

我本來以為只是簡簡單單的寫一個觀後感,沒想到最後還是把重要的內容都羅列出來了,但是本文只是介紹了每個重要名詞的意思,如果感興趣,可以去深入研究。

可能是最簡單的作業系統書籍

標簽: 檔案  程序  作業系統  磁碟  counter