一、內核的任務
純技術層面上,內核是硬件與軟件的之間的一個中間層。作用是將應用程序的請求傳遞給硬件,並充當底層驅動程序,對系統中的各種設備和組件進行尋址。
- 從應用程序視角上看,內核可以被認為是一台增強的計算機,將計算機抽象到一個高層次上。應用程序與硬件本沒有聯系,只與內核有聯系,內核是應用程序所知道的層次結構中的最底層。
- 當若干程序在同一系統中並發運行時,也可以將內核視為資源管理程序。內核負責將可用共享資源分配到各個系統進程,同時保證系統的完整性。
- 將內核視為庫,其提供了一組面向系統的命令。通常系統調用用於向計算機發送請求。
二、實現策略
在操作系統的實現方面,有兩種主要的泛型:微內核和宏內核。
- 微內核:只有最基本的功能直接由內核實現。所有其他的功能(譬如文件系統、內存管理等)都委托給一些獨立的進程,這些進程通過明確定義的通信接口與內核通信。這種設計方式優點包括:動態可擴展性和在運行時切換重要組件。但由於在各個組件之間支持復雜通信需要額外的CPU時間,所以盡管微內核在各研究領域早已成為活躍主題,但在使用性方面進展甚微。
- 宏內核:宏內核是構建系統內核的傳統方法。這種方法中,內核的全部代碼包括所有子系統(如內存管理、文件系統、設備驅動程序)都打包到一個文件中。內核中每個函數都可以訪問內核所有其他部分,容易導致源代碼中出現復雜的嵌套。
當前宏內核的性能仍強於微內核,Linux采取的是宏內核的設計模式。但是也進行了一定程度上的改進,系統運行中,模塊可以插入到內核代碼中,也可以移除。
三、內核的組成部分
圖1概述了組成完整Linux系統的各個層次,以及內核所包含的一些重要子系統。
圖1 Linux內核的高層次概述以及完整的Linux系統中的各個層次
1、進程、進程切換、調度
進程:傳統上UNIX操作系統下運行的應用程序、服務器及其他程序都稱為進程。每個進程都在CPU的虛擬內存中分配地址空間。各個進程的地址空間是完全獨立的。Linux是多任務系統,支持並發執行的若干進程。系統中同時真正在運行的進程數目最多不超過CPU的數目。
進程切換:進程之間的切換。內核借助CPU的幫助,負責進程切換的技術細節。通過在撤銷進程的CPU資源之前保存進程所有與狀態相關的要素,並將進程置於空閑狀態。重新激活進程時,將保存的狀態原樣恢復。
調度:內核必須確定如何在現存進程之間共享CPU時間。重要進程得到的CPU時間多一點,次要進程少一點,確定哪個進程運行多長時間的過程稱為調度。
2、UNIX進程
Linux對進程采用了一種層次系統,每個進程都依賴於一個父進程。內核啟動init程序作為第一個進程,負責進一步系統初始化工作,init是進程樹的根,所有進程都直接或間接起源於該進程(在linux系統終端輸入pstree查看進程樹)。
樹型結構的擴展方式與新進程創建方式密切相關。UNIX操作系統中創建新進程的機制有兩個,分別是fork和exec。fork可以創建當前進程的一個副本,除了PID,子進程完全復制父進程的內存內容。在Linux中,采用寫時復制(copy on write)技術,將內存復制操作延遲到父進程或子進程向某內存頁面寫入數據之前,在只讀訪問的情況下父進程和子進程共用一個內存頁,提高了執行效率。exec將一個新程序加載到當前進程的內存中並執行,舊程序的內存頁被刷出,其內容替換為新數據,開始執行新程序。
線程:有時也稱為輕量級進程。本質上一個進程可能由若干線程組成,這些線程共享同樣的數據和資源,但可能執行程序中不同的代碼路徑。Linux用clone方法創建線程,工作方式類似於fork,但是會檢查確認哪些資源與父進程共享,哪些資源為線程獨立創建。細粒度的資源分配擴展了一般線程的概念,在一定程度上允許線程與進程之間的連續轉換。
命名空間:傳統的Linux使用了許多全局量,啟用命名空間后,以前的全局資源具有了不同的分組。每個命名空間可以包含一個特定的PID集合,或可以提供文件系統的不同視圖,在某個命名空間中掛載的卷不會傳播到其他命名空間中。(並非內核的所有部分都完全支持命名空間)命名空間的經典作用之一:可以通過稱為容器的命名空間來建立系統的多個視圖,一台物理機中可以運行多個虛擬機。與完全的虛擬解決方案(如KVM)相比,計算機上多了一個內核來管理所有的容器。
3、地址空間與特權級別
由於內存區域是通過指針尋址,因此CPU的字長決定了所能管理的地址空間的最大長度。以32位系統為例,可以管理232B=4GB的內存。
地址空間的最大長度與實際可用的物理內存數量無關,因此被稱為虛擬地址空間。從系統中每個進程的角度看,地址空間中只有自身一個進程,無法感知到其他進程的存在。Linux將虛擬地址空間划分為兩個部分,分別為內核空間和用戶空間,如圖2所示。
圖2 虛擬地址空間的划分
圖3特權級別的環狀系統
系統中每個進程都有自身的虛擬地址范圍,從0到TASK_SIZE,用戶空間之上的區域保留給內核專用。以IA-32為例,地址空間在3GB處划分,每個進程虛擬地址空間都是3GB,內核空間由1GB可用。此划分與內存數量無關。由於地址空間虛擬化的結果,每個用戶進程都認為自身有3GB內存。各個系統進程的用戶空間彼此完全分離。(64位計算機可管理巨大的理論虛擬地址空間,操作系統中傾向於使用小於64的位數,實際使用的如42為或47位等,這樣做可以節省一些CPU的工作量)。
特權級別:內核把虛擬地址空間划分為兩個部分,因此能夠保護各個系統進程,使之彼此分離。所有現代的CPU都提供了幾種特權級別,進程可以駐留在某一個特權級別。IA-32體系結構使用4種特權級別構成的系統,各級別可以看作是環,如圖3所示。Intel處理器有4種特權級別,Linux只使用兩種不同的狀態:核心態和用戶狀態。兩種狀態的關鍵差別在於用戶狀態禁止訪問內存空間。從用戶狀態到核心態的切換通過系統調用的特定轉換手段完成,且系統調用的執行因具體系統而不同。圖4概述了不同的執行上下文(詳細討論見下一篇博客)。此外,在多處理器系統中,線程啟動時可以指定CPU,並限制只能在特定CPU上運行。
圖4 在核心態和用戶態執行(CPU大多時間在執行用戶空間中代碼,當應用程序執行系統調用時切換到核心態,此時,內核可以訪問虛擬地址空間用戶部分。系統調用結束后CPU回到用戶狀態。硬件中斷也可以使CPU切換到核心態,這種情況下內核不能訪問用戶空間)
大多情況下,單個虛擬地址空間就比系統中可用的物理內存大。此外,每個進程也都有自身的虛擬地址空間,因此,內核和CPU必須考慮如何將實際可用的物理內存映射到虛擬地址空間的區域。Linux內核中用頁表來為物理地址分配虛擬地址。虛擬地址關系到進程的用戶空間和內核空間,而物理地址則用來尋址實際可用的內存。原理如圖5所示。兩個進程的虛擬地址空間都被划分為很多等長的部分(頁),物理內存同樣進行划分。
圖5 虛擬地址和物理地址
物理內存頁經常稱為頁幀,頁則指虛擬地址空間中的划分單位。虛擬地址空間和物理內存之間的映射也使得進程之間的隔離有一點點松動(內核負責將虛擬地址空間映射到物理地址空間,決定哪些內存區域在進程之間共享哪些不共享)。圖5表明並非虛擬地址空間的所有頁都映射到某個頁幀(可能是因為頁沒有使用,或者數據尚不需要使用而沒有載入)。
4、頁表
用來將虛擬地址空間映射到物理地址空間的數據結構稱為頁表,對於頁表的管理采用多級分頁模型。如圖6所示,將虛擬地址划分成4部分,這樣需要一個三級頁表。(當前Linux內核采用了四級頁表)此處以三級頁表為例,虛擬地址的第一部分稱為全局頁目錄(Page Global Directory, PGD),用於索引進程中的一個數組(每個進程有且僅有一個);虛擬地址的第二個部分稱為PMD(Page Middle Directory),並通過PGD中的數組項找到對應的PMD之后,使用PMD來索引PMD;虛擬地址的第三部分稱為PTE(Page Table Entry),用作頁表的索引,頁表的數組項指向頁幀,虛擬內存頁和頁幀之間的映射由此完成;虛擬內存的最后一部分稱為偏移量,它指定了頁內部的一個字節的位置。每個地址都指向地址空間中唯一定義的某個字節。
圖6 分配虛擬地址
頁表對虛擬地址空間中不需要的區域,不必創建中間頁目錄或頁表,節省了大量的內存。但是每次訪問內存時,必須逐級訪問多個數組才能將虛擬地址轉化為物理地址。
CPU試圖使用MMU(Memory Management Unit)優化內存訪問操作;同時對於地址轉換中出現最高頻的那些地址,保存到地址轉換后備緩沖器(Translation Lookaside Buffer)的CPU高速緩存中。在許多體系結構中高速緩存的運轉是透明的,但某些體系結構則需要內核專門處理。
與CPU交互:IA-32體系結構在將虛擬地址映射到物理地址時,只用了兩級頁表,64位體系結構中需要三級或四級頁表,內核與體系結構無關的部分總是假定使用四級頁表。對於只支持二級或三級頁表的CPU來說,內核中體系結構相關代碼必須通過空頁表對缺少的頁表進行仿真。
內存映射:內存映射是一種重要的抽象手段。映射方法可以將任意來源的數據傳輸到虛擬地址空間中,作為映射目標的地址空間區域,可以像普通內存那也用通常的方法訪問。內核在時限設備驅動程序時,直接使用了內存映射,外設的輸入/輸出可以映射到虛擬地址空間的區域中,對相關內存區域的讀寫會由系統重定向到設備,簡化了驅動程序的實現。
5、物理內存的分配
內核分配內存時,會記錄頁幀已分配或空閑狀態,以免兩個進程使用同樣的內存區域。內核只分配完整的頁幀,將內存划分為更小的部分工作則委托給用戶空間中的標准庫。
內核采用伙伴系統進行快速檢測內存中的連續區域。系統中的內存塊總是兩兩分組,每組中兩個內存塊稱為伙伴。若兩個伙伴都空閑,則將其合並為一個更大內存塊,作為下一層次上的某個內存塊的伙伴。圖7師范了伙伴系統,初始大小為8頁。從上到下,如果系統需要8個頁幀,則將16個頁幀組成的塊分為兩個伙伴,往下類似...。
圖7 伙伴系統
內核本身經常需要比完整頁幀小得多的內存塊,由於內核無法使用標准庫函數,因此在伙伴系統的基礎上,設置了內存管理層,將伙伴系統提供的頁划分為更小的部分,此外還為頻繁使用的小對象設置了slab緩存。slab緩存自動維護與伙伴系統的交互,在緩存用盡時會請求信的頁幀。圖8綜述了伙伴系統、slab分配器以及內核其他方面之間的關聯。
圖8 頁幀的分配由伙伴系統進行,而slab分配器則負責分配小內存以及提供一般性的內核緩存
頁面交換通過利用磁盤空間作為擴展內存,增大了可用內存。內核需要更多內存時,不經常使用的頁可用寫入硬盤,再需要訪問的時候通過缺頁異常機制,將相應的頁切換回內存。
頁面回收用於將內存映射被修改的內容與底層的塊設備同步。有時簡稱數據回寫。
6、計時
全局變量jiffies_64和jiffies(分別是64位和32位)為內核的時間坐標,會按恆定的時間間隔遞增。(對其的更新操作可使用底層體系結構提供的各種定時器機制執行)
基於jiffies的計時相對粒度較粗,在底層硬件能力允許的前提下,內核可使用高分辨率的定時器提供額外的計時手段,能夠以納秒級的精確度和分辨率計量時間。
計時的周期可以動態改變,動態改變計時周期對於供電受限的系統(比如筆記本電腦和嵌入式系統)是很有用的。
7、系統調用
系統調用是用戶進程與內核交互的經典方法。POSIX標准定義了許多系統調用,以及這些系統調用在所有遵從POSIX的系統包括Linux上的語義。傳統的系統調用按不同類別分組,為:
- 進程管理:創建新進程,查詢信息,調試。
- 信號:發送信號,定時器以及相關處理機制。
- 文件:創建、打開和關閉文件,從文件讀取和向文件寫入,查詢信息和狀態。
- 目錄和文件系統:創建、刪除和重命名目錄,查詢信息,鏈接,變更目錄。
- 保護機制:讀取和變更UID/GID,命名空間的處理。
- 定時器函數:定時器函數和統計信息。
用戶進程要從用戶狀態切換到核心態,並將系統關鍵任務委派給內核執行,系統調用是必由之路。(不同的是不同的硬件平台提供的切換機制不盡相同)
8、設備驅動程序、塊設備和字符設備
設備驅動程序用於與系統鏈接的輸入/輸出裝置通信,如硬盤、軟驅、各種借口、聲卡等。
外設可以分為塊設備和字符設備。
塊設備:應用程序可以隨機訪問設備數據,程序可自行確定讀取數據的位置。數據的讀寫只能以塊的倍數(通常512B)進行,不支持基於字符的尋址。(應用:硬盤)
字符設備:提供連續的數據流,應用程序可以順序讀取,通常不支持隨機存取。相反,此類設備支持按字節/字符來讀寫數據。(應用:調制解調器)
由於內核為提高系統性能,廣泛使用了緩存機制,塊設備驅動程序比字符設備復雜。
9、網絡
由於在網絡通信期間,數據打包到了各種協議層中。接收到數據時,內核必須針對各協議層的處理,對數據進行拆包與分析,然后將有效數據傳遞給應用程序;發送數據時,內核必須首先根據各協議層的要求打包數據,才能發送。Linux使用了源於BSD的套接字抽象,以支持通過文件接口處理網絡連接。套接字可以看作為應用程序、文件接口、內核的網絡實現之間的代理。
10、文件系統
Linux系統由大量文件組成,其數據存儲在硬盤或其他塊設備。存儲使用了層次式文件系統。Linux支持許多不同的文件系統:標准的Ext2、Ext3和Ext4文件系統、ReiserFS、XFS、VFAT(為兼容DOS)等等。不同的文件系統基於不同的概念抽象。此外內核必須提供一個VFS(Virtual Filesystem或Virtual Filesystem Switch),將各種底層文件系統的具體特性與應用層隔離。如圖9所示,VFS既是向下的接口(所有文件系統都必須實現該接口),同時也是向上的接口(用戶進程通過系統調用最終能夠訪問文件系統功能)。
圖9 虛擬文件系統層、文件系統實現和塊設備層之間的互操作
11、模塊和熱插拔
模塊用於在運行時動態向內核添加功能(如設備驅動程序、文件系統、網絡協議等),消除了宏內核與微內核相比一個重要的不利之處。模塊也可以在運行時從內核寫在,方便了開發新內核組件。
模塊本質上也是普通程序,它必須提供某些代碼段在模塊初始化和終止時執行,以便向內核注冊和注銷模塊。模塊代碼可以像編譯到內核中的代碼一樣,訪問內核所有函數和數據。
對支持熱插拔而言,模塊本質上是必須的。某些總線(比如USB)允許在系統運行時連接設備,無需重啟,系統檢測到設備時,通過加載對應的模塊,將驅動添加到內核中。(某些模塊開不開源有爭論)
12、緩存
內核使用緩存來改進性能。從低速的塊設備讀取的數據會暫時保持在內存中,應用程序下次訪問數據時,可以繞過低速的塊設備。由於內核通過基於頁的內存映射來實現對塊設備的訪問,因此緩存也按頁組織,稱為頁緩存。塊緩存用於緩存沒有組織成頁的數據,如今已被頁緩存取代。
13、鏈表處理
C程序中重復出現的一項任務是對雙向鏈表的處理,內核同樣需要處理這樣的鏈表。內核提供了一個標准鏈表,可用於將任何類型的數據結構彼此連接起來(非類型安全)。加入鏈表的數據結構必須包含一個類型為list_head的成員,其中包含了正向和反向指針。鏈表的起點是list_head的實例,通常用LIST_HEAD(list_name)宏來聲明初始化。圖10為內核建立的標准雙鏈表示意圖。對鏈表進行操作時,內核定義了一些API。
圖10 標准雙鏈表
- list_add(new,head):在緊接head之后插入new元素。
- list_add_tail(new,head):在head之前(即鏈表末尾)插入new元素。
- list_del(entry):從鏈表中刪除一項。
- list_empty(head):檢測鏈表是否為空。
- list_splice(list,head):在head后插入list鏈表,合並兩個鏈表。
- list_entry(ptr,type,member):查找鏈表元素。(ptr為指向數據結構list_head的指針,type是該數據結構的類型,member是數據結構中表示鏈表元素的成員名)
- list_for_each(pos,head):遍歷鏈表所有元素。
14、對象管理和引用計數
內核中許多地方需要跟蹤記錄C語言中結構的實例,這會導致代碼復制。因此,在內核2.5開發期間,采用一般性的方法來管理內核對象,它不止是為了防止代碼復制,同時也為內核不同部分管理的對象提供了一致的視圖,在許多部分可以有效地使用相關信息。一般性的內核對象機制可用於執行:引用計數;管理對象鏈表;集合加鎖;將對象屬性導出到用戶空間(通過sysfs文件系統)。
(1)一般性的內核對象
一般性的內核對象抽象成了一個結構體kobject,用作內核對象的基礎。
1 struct kobject{ 2 const char * k_name; //對象的文本名稱 3 struct kref kref; //用於簡化引用計數的管理 4 struct list_head entry; //標准鏈表元素 5 struct kobject * parent; //一個指向父對象的指針 6 struct kset * kset; //將對象與其他對象放置到一個集合時需要 7 struct kobj_type *ktype; //提供了包含kobject數據結構更多詳細信息 8 struct sysfs_dirent * sd; 9 }
kobject與面向對象語言(C++/Java)中的對象概念的性質相似。kobject抽象提供了在內核使用面向對象技術的可能性。
(2)對象集合
很多情況下,需要將不同的內核對象歸類到集合中(比如所有字符設備集合,所有基於PCI的設備集合等)。
1 struct kset{ 2 struct kobj_type * ktype; //指向kset中各內核對象的公用kobj_type結構,提供了與sysfs文件系統的接口 3 struct list_head list;//當前集合的內核對象鏈表 4 ... 5 struct kobject kobj; 6 struct kset_uevent_ops * uevent_ops; //提供了若干函數指針,將集合狀態傳遞給用戶層 7 }
kset是內核對象應用的第一個例子,它對kobject的管理是在kset中嵌入了一個kobject的實例kobj,它與集合中包含的各個kobject無關,只是用來管理kset對象本身。
(3)引用計數
引用計數用於檢測內核中有多少地方使用了某個對象。每當內核的一個部分需要某個對象所包含的信息時,該對象引用計數加1,如果不再需要,則引用計數減1。當計數為0時,內核知道不再需要該對象,便從內存中將其釋放。(對引用計數的操作為原子操作)
15、數據類型
(1)類型定義
內核使用typedef定義各種數據類型,避免依賴於體系結構相關的特性。(比如sector_t用於指定塊設備扇區編號,pid_t表示進程ID,_s8(8位有符號數),_u8(8位無符號數)等)
(2)字節序
現代計算機采用大端序(big endian)或小端序(little endian)格式。大端序中,高位在低字節;小端序中低位在低字節。內核提供了各種函數和宏,可以在CPU使用的格式與特定表示法之間轉換。
(3)per_cpu變量
per_cpu變量是通過DEFINE_PER_CPU(name,type)聲明,在有若干CPU的SMP系統上,會為每個CPU分別創建變量的一個實例。用於某個特定CPU的實例可以通過get_cpu(name,cpu)獲得,其中smp_processor_id()可以返回當前活動處理器ID。采用per_cpu變量好處 :所需數據很可能存在於處理器緩存中,因此可以快速訪問;繞過了多處理器系統中使用可能被所有CPU同時訪問的變量的通信問題。
(4)訪問用戶空間
源代碼中多處指針標記為_user,表示對用戶空間程序設計未知,在沒有進一步預防措施時,不能輕易訪問這些指針指向的區域。因為內存是通過頁表映射到虛擬地址空間的用戶空間部分的,不是由物理內存直接映射的,內核需要確保指針所指的頁幀確實存在於物理內存中。
四、內核特別之處
- 調試內核通常比調試用戶層程序困難。
- 內核提供了許多輔助函數,類似於用戶空間的C語言庫,但內核領域中的東西總是朴素得多。
- 用戶層程序錯誤可能會導致segmentation fault或core dump,但內核錯誤會導致整個系統故障。
- 必須考慮到內核運行的許多體系結構上根本不支持非對齊的內存訪問。
- 所有內核代碼都必須是並發安全的。對於多處理器計算機的支持,Linux內核代碼必須是可沖入和線程安全的。
- 內核代碼必須在小端序和大端序計算機上都能夠工作。
- 大多數的體系結構根本不允許在內核中執行浮點計算,因此計算需要想辦法用整型來替代。