純技術層面上,內核是硬件與軟件的之間的一箇中間層。做用是將應用程序的請求傳遞給硬件,並充當底層驅動程序,對系統中的各類設備和組件進行尋址。linux
在操做系統的實現方面,有兩種主要的泛型:微內核和宏內核。設計模式
當前宏內核的性能仍強於微內核,Linux採起的是宏內核的設計模式。可是也進行了必定程度上的改進,系統運行中,模塊能夠插入到內核代碼中,也能夠移除。數組
圖1概述了組成完整Linux系統的各個層次,以及內核所包含的一些重要子系統。緩存
圖1 Linux內核的高層次概述以及完整的Linux系統中的各個層次安全
進程:傳統上UNIX操做系統下運行的應用程序、服務器及其餘程序都稱爲進程。每一個進程都在CPU的虛擬內存中分配地址空間。各個進程的地址空間是徹底獨立的。Linux是多任務系統,支持併發執行的若干進程。系統中同時真正在運行的進程數目最多不超過CPU的數目。服務器
進程切換:進程之間的切換。內核藉助CPU的幫助,負責進程切換的技術細節。經過在撤銷進程的CPU資源以前保存進程全部與狀態相關的要素,並將進程置於空閒狀態。從新激活進程時,將保存的狀態原樣恢復。網絡
調度:內核必須肯定如何在現存進程之間共享CPU時間。重要進程獲得的CPU時間多一點,次要進程少一點,肯定哪一個進程運行多長時間的過程稱爲調度。數據結構
Linux對進程採用了一種層次系統,每一個進程都依賴於一個父進程。內核啓動init程序做爲第一個進程,負責進一步系統初始化工做,init是進程樹的根,全部進程都直接或間接起源於該進程(在linux系統終端輸入pstree查看進程樹)。併發
樹型結構的擴展方式與新進程建立方式密切相關。UNIX操做系統中建立新進程的機制有兩個,分別是fork和exec。fork能夠建立當前進程的一個副本,除了PID,子進程徹底複製父進程的內存內容。在Linux中,採用寫時複製(copy on write)技術,將內存複製操做延遲到父進程或子進程向某內存頁面寫入數據以前,在只讀訪問的狀況下父進程和子進程共用一個內存頁,提升了執行效率。exec將一個新程序加載到當前進程的內存中並執行,舊程序的內存頁被刷出,其內容替換爲新數據,開始執行新程序。ide
線程:有時也稱爲輕量級進程。本質上一個進程可能由若干線程組成,這些線程共享一樣的數據和資源,但可能執行程序中不一樣的代碼路徑。Linux用clone方法建立線程,工做方式相似於fork,可是會檢查確認哪些資源與父進程共享,哪些資源爲線程獨立建立。細粒度的資源分配擴展了通常線程的概念,在必定程度上容許線程與進程之間的連續轉換。
命名空間:傳統的Linux使用了許多全局量,啓用命名空間後,之前的全局資源具備了不一樣的分組。每一個命名空間能夠包含一個特定的PID集合,或能夠提供文件系統的不一樣視圖,在某個命名空間中掛載的卷不會傳播到其餘命名空間中。(並不是內核的全部部分都徹底支持命名空間)命名空間的經典做用之一:能夠經過稱爲容器的命名空間來創建系統的多個視圖,一臺物理機中能夠運行多個虛擬機。與徹底的虛擬解決方案(如KVM)相比,計算機上多了一個內核來管理全部的容器。
因爲內存區域是經過指針尋址,所以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代表並不是虛擬地址空間的全部頁都映射到某個頁幀(多是由於頁沒有使用,或者數據尚不須要使用而沒有載入)。
用來將虛擬地址空間映射到物理地址空間的數據結構稱爲頁表,對於頁表的管理採用多級分頁模型。如圖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來講,內核中體系結構相關代碼必須經過空頁表對缺乏的頁表進行仿真。
內存映射:內存映射是一種重要的抽象手段。映射方法能夠將任意來源的數據傳輸到虛擬地址空間中,做爲映射目標的地址空間區域,能夠像普通內存那也用一般的方法訪問。內核在時限設備驅動程序時,直接使用了內存映射,外設的輸入/輸出能夠映射到虛擬地址空間的區域中,對相關內存區域的讀寫會由系統重定向到設備,簡化了驅動程序的實現。
內核分配內存時,會記錄頁幀已分配或空閒狀態,以避免兩個進程使用一樣的內存區域。內核只分配完整的頁幀,將內存劃分爲更小的部分工做則委託給用戶空間中的標準庫。
內核採用夥伴系統進行快速檢測內存中的連續區域。系統中的內存塊老是兩兩分組,每組中兩個內存塊稱爲夥伴。若兩個夥伴都空閒,則將其合併爲一個更大內存塊,做爲下一層次上的某個內存塊的夥伴。圖7師範了夥伴系統,初始大小爲8頁。從上到下,若是系統須要8個頁幀,則將16個頁幀組成的塊分爲兩個夥伴,往下相似...。
圖7 夥伴系統
內核自己常常須要比完整頁幀小得多的內存塊,因爲內核沒法使用標準庫函數,所以在夥伴系統的基礎上,設置了內存管理層,將夥伴系統提供的頁劃分爲更小的部分,此外還爲頻繁使用的小對象設置了slab緩存。slab緩存自動維護與夥伴系統的交互,在緩存用盡時會請求信的頁幀。圖8綜述了夥伴系統、slab分配器以及內核其餘方面之間的關聯。
圖8 頁幀的分配由夥伴系統進行,而slab分配器則負責分配小內存以及提供通常性的內核緩存
頁面交換經過利用磁盤空間做爲擴展內存,增大了可用內存。內核須要更多內存時,不常用的頁可用寫入硬盤,再須要訪問的時候經過缺頁異常機制,將相應的頁切換回內存。
頁面回收用於將內存映射被修改的內容與底層的塊設備同步。有時簡稱數據回寫。
全局變量jiffies_64和jiffies(分別是64位和32位)爲內核的時間座標,會按恆定的時間間隔遞增。(對其的更新操做可以使用底層體系結構提供的各類定時器機制執行)
基於jiffies的計時相對粒度較粗,在底層硬件能力容許的前提下,內核可以使用高分辨率的定時器提供額外的計時手段,可以以納秒級的精確度和分辨率計量時間。
計時的週期能夠動態改變,動態改變計時週期對於供電受限的系統(好比筆記本電腦和嵌入式系統)是頗有用的。
系統調用是用戶進程與內核交互的經典方法。POSIX標準定義了許多系統調用,以及這些系統調用在全部聽從POSIX的系統包括Linux上的語義。傳統的系統調用按不一樣類別分組,爲:
用戶進程要從用戶狀態切換到核心態,並將系統關鍵任務委派給內核執行,系統調用是必由之路。(不一樣的是不一樣的硬件平臺提供的切換機制不盡相同)
設備驅動程序用於與系統連接的輸入/輸出裝置通訊,如硬盤、軟驅、各類藉口、聲卡等。
外設能夠分爲塊設備和字符設備。
塊設備:應用程序能夠隨機訪問設備數據,程序可自行肯定讀取數據的位置。數據的讀寫只能以塊的倍數(一般512B)進行,不支持基於字符的尋址。(應用:硬盤)
字符設備:提供連續的數據流,應用程序能夠順序讀取,一般不支持隨機存取。相反,此類設備支持按字節/字符來讀寫數據。(應用:調制解調器)
因爲內核爲提升系統性能,普遍使用了緩存機制,塊設備驅動程序比字符設備複雜。
因爲在網絡通訊期間,數據打包到了各類協議層中。接收到數據時,內核必須針對各協議層的處理,對數據進行拆包與分析,而後將有效數據傳遞給應用程序;發送數據時,內核必須首先根據各協議層的要求打包數據,才能發送。Linux使用了源於BSD的套接字抽象,以支持經過文件接口處理網絡鏈接。套接字能夠看做爲應用程序、文件接口、內核的網絡實現之間的代理。
Linux系統由大量文件組成,其數據存儲在硬盤或其餘塊設備。存儲使用了層次式文件系統。Linux支持許多不一樣的文件系統:標準的Ext二、Ext3和Ext4文件系統、ReiserFS、XFS、VFAT(爲兼容DOS)等等。不一樣的文件系統基於不一樣的概念抽象。此外內核必須提供一個VFS(Virtual Filesystem或Virtual Filesystem Switch),將各類底層文件系統的具體特性與應用層隔離。如圖9所示,VFS既是向下的接口(全部文件系統都必須實現該接口),同時也是向上的接口(用戶進程經過系統調用最終可以訪問文件系統功能)。
圖9 虛擬文件系統層、文件系統實現和塊設備層之間的互操做
模塊用於在運行時動態向內核添加功能(如設備驅動程序、文件系統、網絡協議等),消除了宏內核與微內核相比一個重要的不利之處。模塊也能夠在運行時從內核寫在,方便了開發新內核組件。
模塊本質上也是普通程序,它必須提供某些代碼段在模塊初始化和終止時執行,以便向內核註冊和註銷模塊。模塊代碼能夠像編譯到內核中的代碼同樣,訪問內核全部函數和數據。
對支持熱插拔而言,模塊本質上是必須的。某些總線(好比USB)容許在系統運行時鏈接設備,無需重啓,系統檢測到設備時,經過加載對應的模塊,將驅動添加到內核中。(某些模塊開不開源有爭論)
內核使用緩存來改進性能。從低速的塊設備讀取的數據會暫時保持在內存中,應用程序下次訪問數據時,能夠繞太低速的塊設備。因爲內核經過基於頁的內存映射來實現對塊設備的訪問,所以緩存也按頁組織,稱爲頁緩存。塊緩存用於緩存沒有組織成頁的數據,現在已被頁緩存取代。
C程序中重複出現的一項任務是對雙向鏈表的處理,內核一樣須要處理這樣的鏈表。內核提供了一個標準鏈表,可用於將任何類型的數據結構彼此鏈接起來(非類型安全)。加入鏈表的數據結構必須包含一個類型爲list_head的成員,其中包含了正向和反向指針。鏈表的起點是list_head的實例,一般用LIST_HEAD(list_name)宏來聲明初始化。圖10爲內核創建的標準雙鏈表示意圖。對鏈表進行操做時,內核定義了一些API。
圖10 標準雙鏈表
內核中許多地方須要跟蹤記錄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時,內核知道再也不須要該對象,便從內存中將其釋放。(對引用計數的操做爲原子操做)
(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,表示對用戶空間程序設計未知,在沒有進一步預防措施時,不能輕易訪問這些指針指向的區域。由於內存是經過頁表映射到虛擬地址空間的用戶空間部分的,不是由物理內存直接映射的,內核須要確保指針所指的頁幀確實存在於物理內存中。