09.16 Linux 內核系統架構

描述Linux內核的文章已經有上億字了

但是對於初學者,還是應該多學習多看,畢竟上億字不能一下子就明白的。

即使看了所有的Linux 內核文章,估計也還不是很明白,這時候,還是需要fucking the code.

28年前(1991年8月26日)Linus公開Linux的代碼,開啟了一個偉大的時代。這篇文章從進程調度,內存管理,設備驅動,文件系統,網絡等方面講解Linux內核系統架構。Linux的系統架構是一個經典的設計,它優秀的分層和模塊化,融合了數量繁多的設備和不同的物理架構,讓世界各地的內核開發者能夠高效並行工作。先來看看Linus在多年前公開Linux的郵件。

"Hello everybody out there using minix - I’m doing a (free) operating system (just a hobby, won’t be big and professional like gnu) for 386(486) AT clones. This has been brewing since april, and is starting to get ready. I’d like any feedback on things people like/dislike in minix, as my OS resembles it somewhat (same physical layout of the file-system (due to practical reasons) among other things).

I’ve currently ported bash(1.08) and gcc(1.40), and things seem to work. This implies that I’ll get something practical within a few months, and I’d like to know what features most people would want. Any suggestions are welcome, but I won’t promise I’ll implement them :-)

Linus ([email protected][1].fi)"

事實上,從那一天開始,Linux便是博採眾長,融合了非常多的優秀設計。在瞭解操作系統的時候,我們至少需要知道:

1.操作系統是如何管理各種資源的?

2.軟硬件如何協同工作?3.如何通過抽象化屏蔽差異4.軟硬件如何分工?

這篇文章通過對內核主要模塊的介紹,希望能為大家尋找這些問題的答案起一個拋磚引玉的作用。實際上,建議每一個希望成為技術專家的人都讀一遍Linux的源代碼。

先來看看Linux內核一個高階架構圖:

Linux 内核系统架构

Linux系統架構圖

架構非常清晰,從硬件層,硬件抽象層,內核基礎模塊(進程調度,內存管理,網絡協議棧等)到應用層,這個基本上也是各類軟硬件結合的系統架構的基礎設計,例如物聯網系統(從單片機,MCU等小型嵌入式系統,到智能家居,智慧社區甚至智慧城市)在接入端設備的可參考架構模型。

Linux最初是運行在PC機上的,使用的x86架構處理器相對來說比較強大,各類指令和模式也比較齊全。例如我們看到的用戶態和內核態,在一般的小型嵌入式處理器上是沒有的,它的好處是通過將代碼和數據段(segment)給予不同的權限,保護內核態的代碼和數據(包括硬件資源)必須通過類似系統調用(SysCall)的方式才能訪問,確保內核的穩定。

想象一下,如果需要你寫一個操作系統,有哪些因素需要考慮?

進程管理:如何在多任務系統中按照調度算法分配CPU的時間片。

內存管理:如何實現虛擬內存和物理內存的映射,分配和回收內存。

文件系統:如何將硬盤的扇區組織成文件系統,實現文件的讀寫等操作。

設備管理:如何尋址,訪問,讀,寫設備配置信息和數據。

這些概念是操作系統的核心概念,由於篇幅原因,本文章主要從高階的角度來講,更多細節不在本文覆蓋。

進程管理

進程在不同的操作系統中有些稱為process,有些稱為task。操作系統中進程數據結構包含了很多元素,往往用鏈表連接。

進程相關的內容主要包括:虛擬地址空間,優先級,生命週期(阻塞,就緒,運行等),佔有的資源(例如信號量,文件等)。

CPU在每個系統滴答(Tick)中斷產生的時候檢查就緒隊列裡面的進程(遍歷鏈表中的進程結構體),如有符合調度算法的新進程需要切換,保存當前運行的進程的信息(包括棧信息等)後掛起當前進程,選擇新的進程運行,這就是進程調度。

進程的優先級差異是CPU調度的基本依據,調度的終極目標是讓高優先級的活動能夠即時得到CPU的計算資源(即時響應),低優先級的任務也能公平分配到CPU資源。因為需要保存進程運行的上下文(process context)等,進程的切換本身是有成本的,調度算法在進程切換頻率上也需要考慮效率。

在早期的Linux操作系統中,主要採用的是時間片輪轉算法(Round-Robin),內核在就緒的進程隊列中選擇高優先級的進程運行,每次運行相等的時間。該算法簡單直觀,但仍然會導致某些低優先級的進程長時間無法得到調度。為了提高調度的公平性,在Linux 2.6.23之後,引入了稱為完全公平調度器CFS(Completely Fair Scheduler)。

CPU在任何時間點只能運行一個程序,用戶在使用優酷APP看視頻時,同時在微信中打字聊天,優酷和微信是兩個不同的程序,為什麼看起來像是在同時運行?CFS的目標就是讓所有的程序看起來都是以相同的速度在多個並行的CPU上運行,即nr_running 個運行的進程,每個進程以1/nr_running的速度併發執行,例如如有2個可運行的任務,那麼每個以50%的CPU物理能力併發執行。

CFS引入了"虛擬運行時間"的概念,虛擬運行時間用p->se.vruntime (nanosec-unit) 表示,通過它記錄和度量任務應該獲得的"CPU時間"。在理想的調度情況下,任何時候所有的任務都應該有相同的p->se.vruntime值(上面提到的以相同的速度運行)。因為每個任務都是併發執行的,沒有任務會超過理想狀態下應該佔有的CPU時間。CFS選擇需要運行的任務的邏輯基於p->se.vruntime值,非常簡單:它總是挑選p->se.vruntime值最小的任務運行(最少被調度到的任務)。

CFS使用了基於時間排序的紅黑樹來為將來任務的執行排時間線。所有的任務按p->se.vruntime關鍵字排序。CFS從樹中選擇最左邊的任務執行。隨著系統運行,執行過的任務會被放到樹的右邊,逐步地地讓每個任務都有機會成為最左邊的任務,從而在一個可確定的時間內獲得CPU資源。

總結來說,CFS首先運行一個任務,當任務切換(或者Tick中斷髮生的時候)時,該任務使用的CPU時間會加到p->se.vruntime裡,當p->se.vruntime的值逐漸增大到別的任務變成了紅黑樹最左邊的任務時(同時在該任務和最左邊任務間增加一個小的粒度距離,防止過度切換任務,影響性能),最左邊的任務被選中執行,當前的任務被搶佔。

Linux 内核系统架构

CFS紅黑樹

一般來說,調度器處理單個任務,且儘可能為每個任務提供公平的CPU時間。某些時候,可能需要將任務分組,併為每個組提供公平的CPU時間。例如,系統可以為每個用戶分配平均的CPU時間後,再為每個用戶的每個任務分配平均的CPU時間。

內存管理

內存本身是一個外部存儲設備,系統需要對內存區域尋址,找到對應的內存單元(memory cell),讀寫其中的數據。

內存區域通過指針尋址,CPU的字節長度(32bit機器,64bit機器)決定了最大的可尋址地址空間。在32位機器上最大的尋址空間是4GBtyes。在64位機器上理論上有2^64Bytes。

最大的地址空間和實際系統有多少物理內存無關,所以稱為虛擬地址空間。對系統中所有的進程來說,看起來每個進程都獨立佔有這個地址空間,且它無法感知其它進程的內存空間。事實上操作系統讓應用程序無需關注

其它應用程序,看起來每個任務都是這個電腦上運行的唯一進程。

Linux將虛擬地址空間分為內核空間和用戶空間。每個用戶進程的虛擬空間範圍從0到TASK_SIZE。從TASK_SIZE到2^32或2^64的區域保留給內核,不能被用戶進程訪問。TASK_SIZE可以配置,Linux系統默認配置3:1,應用程序使用3GB的空間,內核使用1GB的空間,這個劃分並不依賴實際RAM的大小。在64位機器上,虛擬地址空間的範圍可以非常大,但實際上只使用其中42位或47位(2^42 或 2^47)。

Linux 内核系统架构

虛擬地址空間

絕大多數情況下,虛擬地址空間比實際系統可用的物理內存(RAM)大,內核和CPU必須考慮如何將實際可用的物理內存映射到虛擬地址空間。

一個方法是通過頁表(Page Table)將虛擬地址映射到物理地址。虛擬地址與進程使用的用戶&內核地址相關,物理地址用來尋址實際使用的RAM。

如下圖所示,進程A和B的虛擬地址空間被分為大小相等的部分,稱為頁(page)。物理內存同樣被分割為大小相等的頁(page frame)。

Linux 内核系统架构

虛擬和物理地址空間映射

進程A第1個內存頁映射到物理內存(RAM)的第4頁;進程B第1個內存頁映射到物理內存第5頁。進程A第5個內存頁和進程B第1個內存頁都映射到物理內存的第5頁(內核可決定哪些內存空間被不同進程共享)。

如圖所示,並不是每個虛擬地址空間的頁都與某個page frame關聯,該頁可能並未使用或者數據還沒有被加載到物理內存(暫時不需要),也可能因為物理內存頁被置換到了硬盤上,後續實際再需要的時候再被置換回內存。

頁表(page table)將虛擬地址空間映射到物理地址空間。最簡單的做法是用一個數組將虛擬頁和物理頁一一對應,但是這樣做可能需要消耗整個RAM本身來保存這個頁表,假設每個頁大小為4KB,虛擬地址空間大小為4GB,需要一個1百萬個元素的數組來保存頁表。

因為虛擬地址空間的絕大多數區域實際並沒有使用,這些頁實際並沒有和page frame關聯,引入多級頁表(multilevel paging)能極大降低頁表使用的內存,提高查詢效率。關於多級也表的細節描述可以參考xxx。

內存映射(memory mapping)是一個重要的抽象方法,被運用在內核和用戶應用程序等多個地方。映射是將來自某個數據源的數據(也可以是某個設備的I/O端口等)轉移到某個進程的虛擬內存空間。對映射的地址空間的操作可以使用處理普通內存的方法(對地址內容直接進行讀寫)。任何對內存的改動會自動轉移到原數據源,例如將某個文件的內容映射到內存中,只需要通過讀該內存來獲取文件的內容,通過將改動寫到該內存來修改文件的內容,內核確保任何改動都會自動體現到文件裡。

另外,在內核中,實現設備驅動時,外設(外部設備)的輸入和輸出區域可以被映射到虛擬地址空間,讀寫這些空間會被系統重定向到設備,從而對設備進行操作,極大地簡化了驅動的實現。

內核必須跟蹤哪些物理頁已經被分配了,哪些還是空閒的,避免兩個進程使用RAM中的同一個區域。內存的分配和釋放是非常頻繁的任務,內核必須確保完成的速度儘量快,內核只能分配整個page frame,它將內存分為更小的部分的任務交給了用戶空間,用戶空間的程序庫可以將從內核收到的page frame分成更小的區域後分配給進程。

虛擬文件系統

Unix系統是建立在一些有見地的理念上的,一個非常重要的隱喻是:

Everything is a file.

即系統幾乎所有的資源都可以看成是文件。為了支持不同的本地文件系統,內核在用戶進程和文件系統實現間包含了一層虛擬文件系統(Virtual File System)。大多數的內核提供的函數都能通過VFS(Virtual File System)定義的文件接口訪問。例如內核子系統:字符和塊設備,管道,網絡Socket,交互輸入輸出終端等。

另外用於操作字符和塊設備的設備文件是在/dev目錄下的真實文件,當讀寫操作執行的時候,其的內容會被對應的設備驅動動態創建。

Linux 内核系统架构

VFS系統

在虛擬文件系統中,inode用來表示文件和文件目錄(對於系統來說,目錄是一種特殊的文件)。inode的元素包含兩類:1. Metadata用於描述文件的狀態,例如讀寫權限。2. 用於保存文件內容的數據段。

每個inode都有一個特別的號碼用於唯一識別,文件名和inode的關聯建立在該編號基礎上。以內核查找/usr/bin/emacs為例,講解inodes如何組成文件系統的目錄結構。從根inode開始查找(即根目錄‘/’),該目錄使用一個inode表示,inode的數據段沒有普通的數據,只包含了根目錄存的一些文件/目錄項,這些項可以表示文件或其它目錄,每項包含兩個部分:1. 下一個數據項所在的inode編號 2. 文件或目錄名

首先掃描根inode的數據區域直到找到一個名為‘usr’的項,查找子目錄usr的inode。通過‘usr’ inode編號找到關聯的inode。重複以上步驟,查找名為‘bin’的數據項,然後在其數據項的‘bin’對應的inode中搜索名字‘emacs’的數據項,最後返回的inode表示一個文件而不是一個目錄。最後一個inode的文件內容不同於之前,前三個每個都表示了一個目錄,包含了它的子目錄和文件清單,和emacs文件關聯的inode在它的數據段保存了文件的實際內容。

儘管在VFS查找某個文件的步驟和上面的描述一樣,但細節上還是有些差別。例如因為頻繁打開文件是一個很慢的操作,引入緩存加速查找。

Linux 内核系统架构

通過inode機制查找某個文件

設備驅動

與外設通信往往指的是輸入(input)和輸出(output)操作,簡稱I/O。實現外設的I/O內核必須處理三個任務:第一,必須針對不同的設備類型採用不同的方法來尋址硬件。第二,內核必須為用戶應用程序和系統工具提供操作不同設備的方法,且需要使用一個統一的機制來確保儘量有限的編程工作,和保證即使硬件方法不同應用程序也能互相交互。第三,用戶空間需要知道在內核中有哪些設備。

與外設通信的層級關係如下:

Linux 内核系统架构

設備通信層級圖

外部設備大多通過總線與CPU連接,系統往往不止一個總線,而是總線的集合。在很多PC設計中包含兩個通過一個bridge相連的PCI總線。某些總線例如USB不能當作主總線使用,需要通過一個系統總線將數據傳遞給處理器。下圖顯示不同的總線是如何連接到系統的。

Linux 内核系统架构

系統總線拓撲圖

系統與外設交互主要有以下方式:

I/O端口:使用I/O端口通信的情況下,內核通過一個I/O控制器發送數據,每個接收設備有唯一的端口號,且將數據轉發給系統附著的硬件。有一個由處理器管理的單獨的虛擬地址空間用來管理所有的I/O地址。

I/O地址空間並不總是和普通的系統內存關聯,考慮到端口能夠映射到內存中,這往往不好理解。

端口有不同的類型。一些是隻讀的,一些是隻寫的,一般情況下它們是可以雙向操作的,數據能夠在處理器和外設間雙向交換。

在IA-32架構體系中,端口的地址空間包含了2^16個不同的8位地址,這些地址可以通過從0x0到0xFFFFH間的數唯一識別。每個端口都有一個設備分配給它,或者空閒沒有使用,多個外設不能共享一個端口。很多情況下,交換數據使用8位是不夠用的,基於這個原因,可以將兩個連續的8位端口綁定為一個16位的端口。兩個連續的16位端口能夠被當作一個32位的端口,處理器可以通過組裝語句來做輸入輸出操作。

不同處理器類型在實現操作端口時有所不同,內核必須提供一個合適的抽象層,例如outb(寫一個字節),outw(寫一個字)和inb(讀一個字節)這些命令可以用來操作端口。

I/O內存映射:必須能夠像訪問RAM內存一樣尋址許多設備。因此處理器提供了將外設對應的I/O端口映射到內存中,這樣就能像操作普通內存一樣操作設備了。例如顯卡使用這樣的機制,PCI也往往通過映射的I/O地址尋址。

為了實現內存映射,I/O端口必須首先被映射到普通系統內存中(使用處理器特有的函數)。因為平臺間的實現方式差異比較大,所以內核提供了一個抽象層來映射和去映射I/O區域。

除了如何訪問外設,什麼時候系統會知道是否外設有數據可以訪問?主要通過兩種方式:輪詢和中斷。

輪詢週期性地訪問查詢設備是否有準備好的數據,如果有,便獲取數據。這種方法需要處理器在設備沒有數據的情況下也不斷去訪問設備,浪費了CPU時間片。

另一種方式是中斷,它的理念是外設把某件事情做完了後,主動通知CPU,中斷的優先級最高,會中斷CPU的當前進程運行。每個CPU都提供了中斷線(可被不同的設備共享),每個中斷由唯一的中斷號識別,內核為每個使用的中斷提供一個服務方法(ISR,Interrupt Service Routine,即中斷髮生後,CPU調用的處理函數),中斷本身也可以設置優先級。

中斷會掛起普通的系統工作。當有數據已準備好可以給內核或者間接被一個應用程序使用的時候,外設出發一箇中斷。使用中斷確保系統只有在外設需要處理器介入的時候才會通知處理器,有效提高了效率。

通過總線控制設備:不是所有的設備都是直接通過I/O語句尋址操作的,很多情況下是通過某個總線系統。

不是所有的設備類型都能直接掛接在所有的總線系統上,例如硬盤掛到SCSI接口上,但顯卡不可以(顯卡可以掛到PCI總線上)。硬盤必須通過IDE間接掛到PCI總線上。

總線類型可分為系統總線和擴展總線。硬件上的實現差別對內核來說並不重要,只有總線和它附著的外設如何被尋址才相關。對於系統總線來說,例如PCI總線,I/O語句和內存映射用來與總線通信,也用於和它附著的設備通信。內核還提供了一些命令供設備驅動來調用總線函數,例如訪問可用的設備列表,使用統一的格式讀寫配置信息。

擴展總線例如USB,SCSI通過清晰定義的總線協議與附著的設備來交換數據和命令。內核通過I/O語句或內存映射來與總線通信,通過平臺無關的函數來使總線與附著的設備通信。

與總線附著的設備通信不一定需要通過在內核空間的驅動進行,在某些情況下也可以通過用戶空間實現。一個主要的例子是SCSI Writer,通過cdrecord工具來尋址。這個工具產生所需要的SCSI命令,在內核的幫助下通過SCSI總線將命令發送到對應的設備,處理和回覆設備產生或返回的信息。

塊設備(block)和字符設備(character)在3個方面顯著不同:

塊設備中的數據能夠在任何點操作,而字符設備不能也沒這個要求。

塊設備數據傳輸的時候總是使用固定大小的塊。即使只請求一個字節的情況下,設備驅動也總是從設備獲取一個完整的塊。相反,字符設備能夠返回單個字節。

讀寫塊設備會使用緩存。讀操作方面,數據緩存在內存中,能夠在需要的時候重新訪問。寫操作方面,也會被緩存,延時寫入設備。使用緩存對於字符設備(例如鍵盤)來說不合理,每個讀請求都必須被可靠地交互到設備。

塊和扇區的概念:塊是一個指定大小的字節序列,用於保存在內核和設備間傳輸的數據,塊的大小可以被設置。扇區是固定大小的,能被設備傳輸的最小的數據量。塊是一段連續的扇區,塊大小是扇區的整數倍。

網絡

Linux的網絡子系統為互聯網的發展提供了堅實的基礎。網絡模型基於ISO的OSI模型,如下圖右半部分。但在具體應用中,往往會把相應層級結合以簡化模型,下圖左半部分為Linux運用的TCP/IP參考模型。(由於介紹Linux網絡部分的資料比較多,在本文中只對大的層級簡單介紹,不展開說明。)

Linux 内核系统架构

網絡模型

Host-to-host層(Physical Layer和Data link layer,即物理層和數據鏈路層)負責將數據從一個計算機傳輸到另一臺計算機。這一層處理物理傳輸介質的電氣和編解碼屬性,也將數據流拆分成固定大小的數據幀用於傳輸。如多個電腦共享一個傳輸路線,網絡適配器(網卡等)必須有一個唯一的ID(即MAC地址)來區分。從內核的角度,這一層是通過網卡的設備驅動實現的。

OSI模型的網絡層在TCP/IP模型中稱為網絡層,網絡層使網絡中的計算機之間能交換數據,而這些計算機不一定是直接相連的。

如下圖,A和B之間物理上並沒有直接相連,所以也沒有直接的數據交換。網絡層的任務是為網絡中各機器之間通信找到路由。

Linux 内核系统架构

網絡連接的電腦

網絡層也負責將要傳輸的包分成指定的大小,因為包在傳輸路徑上每個電腦支持的最大的數據包大小可能不一樣,在傳輸時,數據流被分割成不同的包,在接收端再被組合。

網絡層為網絡中的電腦分配了唯一的網絡地址以便他們能互相通信(不同於硬件的MAC地址,因為網絡往往由子網絡組成)。在互聯網中,網絡層由IP網絡組成,有V4和V6版本。

傳輸層的任務是規範在兩個連接的電腦上運行的應用程序之間的數據傳輸。例如兩臺電腦上的客戶端和服務端程序,包括TCP或UDP連接,通過端口號來識別通信的應用程序。例如端口號80用於web server,瀏覽器的客戶端必須將請求發送到這個端口來獲取需要的數據。而客戶端也需要有一個唯一的端口號以便web server能將回復發送給它。

這一層還負責為數據的傳輸提供一個可靠的連接(TCP情況下)。

TCP/IP模型中的應用層在OSI模型中包含(session層,展現層,應用層)。當通信連接在兩個應用之間建立起來後,這一層負責實際內容的傳輸。例如web server與它的客戶端傳輸時的協議和數據,不同與mail server與它的客戶端之間。

大多數的網絡協議在RFC(Request for Comments)中定義。

網絡實現分層模型:內核對網絡層的實現類似TCP/IP參考模型。它是通過C代碼實現的,每個層只能和它的上下層通信,這樣的好處是可以將不同的協議和傳輸機制結合。如下圖所示:

Linux 内核系统架构

網絡實現分層圖

本文先介紹到這,對技術感興趣的朋友可以關注 "從零開始學架構",後續也會繼續推出對各類架構設計的介紹,希望和大家多多交流,也歡迎大家留言。(The End)

《Professional Linux Kernel Architecture》

《Understanding Linux Kernel》

《Architecture of the Linux Kernel》

References

<code>[1]/<code>[email protected]:mailto:[email protected]


分享到:


相關文章: