探究Windows內核你知多少

如上所述,現代操作系統的一個明顯特徵就是用戶空間和系統空間的劃分,從UNIX時代以來,人們一直把存在於系統空間的代碼和數據的集合稱爲“內核(Kernel)”,因此內核是有明確邊界的。空間的不同,或者說CPU運行模式(系統態和用戶態)的不同,是不會被混淆的本質區別。可是,在Windows的術語中卻不同,微軟並不把系統空間的所有代碼和數據的集合稱爲內核,而是把這裏面的一部分,即比較低層、與硬件靠得最近因而最爲核心的一部分稱爲“內核”,即Kernel。實際上,這也反映了當初微軟在決策上的舉棋不定,因爲微軟稱爲Kernel的那一部分大致上相當於一個微內核。有些資料甚至據此而認定Windows內核爲“微內核”,殊不知現今的Windows內核恐怕是最宏的“宏內核”,因爲連圖形界面和視窗機制的實現也在內核裏面了。微軟的文獻把同在系統空間但在所謂“Kernel”以上的部分稱爲Executive。國內有些資料把Executive譯爲“執行體”,其實是不妥當的。試想,有“執行體”,莫非還有“不執行體”嗎?實際上,在英語中,特別是在企業管理的語境中,Executive是“管理層”、“企業高管”的意思,所以微軟其實是把內核分成了兩大層,其中的低層或者說核心部分稱爲“內核”,而高層則稱爲“管理層”。管理層中有些什麼呢?讀者在後面將看到,裏面有“對象管理”、“內存管理”、“進程/線程管理”、“I/O管理”、“安全管理”、“進程(線程)間通信”等模塊。但是,這麼一來,所謂“內核”的邊界就變得不很清晰了。
      相比之下,還是像UNIX/Linux那樣,以系統空間與用戶空間的劃分爲界,把存在於系統空間的所有成分的集合統稱爲內核比較清晰,也更爲科學(因爲有明確的判定方法)。所以,在本書中,只要沒有特別加以說明,“內核”就是“系統空間”的同義詞,而不是特指微軟所稱的那個內核;而微軟所稱的內核,則在本書中稱爲內核中的“核心層”。內核中從高到低在邏輯上分成若干層次,這一點任何操作系統的內核都是如此。事實上,研究操作系統的幾種觀點(視野)之一就是分層模擬、分層提供服務的觀點。即便是微內核的內部,也還是分出層次,其中最底層的就是“硬件抽象層(Hardware Abstraction Layer)”HAL。
    不過操作系統的概念有狹義和廣義之分。狹義的操作系統就是指內核,而廣義的操作系統則並不只是一個內核,也包括一些用戶空間的軟件。例如,一些工具性的軟件、實現人機界面的軟件(例如Linux上的Shell,Windows上的“資源管理器”),就一般都認爲屬於操作系統的範疇。Windows操作系統原先的設計目標是支持三種“子系統”。其中用來實現視窗子系統的服務程序,即服務進程Csrss所執行的軟件,當然也應該屬於操作系統。此外,應用軟件在運行時需要用到一些函數庫,特別是“動態連接庫”即DLL,其中靠近內核的“系統DLL”,也應屬於操作系統。這樣,包括應用軟件在內的整個Windows系統的結構。
    圖中畫出了系統的結構層次,愈往上愈接近應用軟件,愈往下愈接近硬件。而包括內核在內的所有中間層次的作用,則是幫助應用軟件更好、更安全、更方便、更有效地利用包括CPU在內的硬件資源。
    位於最高層的是應用軟件,就是一般文件擴展名爲.exe的可執行程序。除.exe可執行程序之外,應用軟件的開發者和提供者可以(但並非必須)選擇把其中的一部分基礎性的功能放在若干“動態連接庫”DLL裏面,這就是文件擴展名爲.dll的可執行程序。DLL也是分層次的,相對高層的DLL依賴於相對低層的DLL,調用由低層DLL所提供的服務。
    在Windows系統中,應用軟件是看不見操作系統內核的,應用軟件通過一個“應用程序(設計)界面”Win32 API獲得Windows操作系統的支持。微軟通過一系列的DLL來實現其API,其中低層、比較靠近內核的幾個DLL稱爲“系統DLL”,意思是這些DLL已經屬於操作系統的範疇。其中最靠近內核、最基本的DLL是ntdll.dll,此外還有kernel32.dll、user32.dll。這樣,所謂Windows操作系統,至少應該是內核加上系統DLL還有子系統服務進程的總和。當然,其中最重要、最複雜的是內核。
    圖中的水平粗線表示用戶空間和系統空間的分界。在Windows操作系統中,整個4GB的虛存地址空間被對半分成兩塊,從地址0x80000000開始向上是“系統空間”,就是內核所在的地方,下面則是“用戶空間”,是應用程序所在的地方。後面將講到,系統空間是全局的,而用戶空間只屬於具體的進程,每個(用戶)進程都有自己的用戶空間。
    除表示用戶空間和系統空間的分界外,圖中的水平粗線還有另一層意思,就是表示Windows的“應用程序二進制界面”,即ABI。ABI一方面是系統調用的界面,定義了所有系統調用函數界面的集合,並規定了系統調用如何進行;另一方面也規定了可執行程序文件的結構和格式。Windows的系統調用界面是不公開的,現在人們所知道的有關信息基本上來自有關的研究和實驗,特別是來自逆向工程的研究。
    CPU必須進入“系統態”才能執行存放在系統空間的程序,訪問存放在系統空間的數據。而對用戶空間的數據則不論處於“系統態”或“用戶態”都能訪問。但是CPU怎樣才能進入“系統態”,即進入內核呢?只有三種途徑。第一種是“系統調用”,第二種是“中斷”,第三種是“異常”。所以圖中內核的最上層是“系統服務”,即系統調用的界面。圖中還畫上了中斷和異常的入口,這是因爲通過中斷或異常進入內核時的一些系統開銷性質的操作與系統調用相似。但是,從邏輯的角度講,中斷和異常的入口應該是在內核的底部,因爲中斷和異常都來自(包括CPU在內的)硬件。
    到了內核中,系統調用界面的下面就是Executive,即內核的管理層,管理層的下面又具體分爲對象管理、內存管理、進程管理、安全管理、I/O管理等模塊。在微軟的術語中,這些管理模塊稱爲Manager,即“管理者”或“主管”,例如對象管理模塊就稱爲“Object Manager”,意爲“高管(Executive)”領導下的部門主管。在特定的語境下,也有把這些模塊稱爲“子系統”的,讀者需要注意分辨,不要跟“Windows子系統”、“POSIX子系統”和“OS/2子系統”相混淆。值得注意的是,這些模塊雖然在管理層,但是它們所管理的目標和操作並不侷限在管理層內部,就好像具體的管理部門雖然在公司總部,但是所管理的具體操作卻可能在基層的營業所或辦事處。例如內存管理,其上層在管理層中,其底層的操作卻可能在HAL層中。所以,層次是橫向的概念,而模塊是縱向的概念。
    再往下就是微軟所稱的“內核(Kernel)”了。所以微軟所稱的“內核”其實是內核中較爲核心的、比較接近底層的一層。這一層中包含了跟設備驅動底層中斷處理、異常處理等有關的功能。這下面就是“硬件抽象層”HAL了。
    當CPU運行於內核中時,儘管都是在內核中,有些操作只允許在特定的層次上進行,或者只允許針對特定的層次進行。例如在中斷處理的內部就不允許線程切換,線程切換只有在完成了中斷處理以後才能進行。所以,Windows內核規定只有在從所謂的“內核”層(而不是整個內核)退出來時纔可以進行線程切換。
在線程調度/切換的問題上,有兩種不同的方式:一種稱爲“剝奪式(Preemptive)”,或稱“搶佔式”,意思是只要有優先級更高的線程就緒,哪怕CPU已經在爲別的優先級較低的線程所用而且無意主動讓路,也要立即把它奪過來;另一種是“非剝奪式(Non-Preemptive)”,或稱“不搶佔式”,如果CPU已經在執行別的線程,就先忍一忍,等待適當的機會。什麼機會呢?就是等正在系統空間運行的線程返回用戶空間的時候。可是,優先級較高的線程之所以變成就緒,其觸發的條件也必爲系統調用、中斷或異常其中之一,總之是在CPU運行於系統空間的時候變成就緒的。所以,所謂“剝奪式”和“非剝奪式”,其實不是剝奪不剝奪的問題(那取決於調度策略),而是什麼時候剝奪的問題。2.6版以前的Linux主要面對分時應用,所以是不剝奪的,其特徵就是要到CPU從內核退回用戶空間時才進行調度和切換。現在的Linux則更多地面向桌面應用,所以也是剝奪式的了。但是,即使是剝奪式的調度和切換,也不表示在內核中的任何一個角落都可以切換線程,例如在中斷服務程序中就不允許,所以這裏也有個時機的問題。如上所述,Windows內核規定只有在從所謂的“內核”退出來時纔可以進行線程切換。那麼Windows的線程調度/切換是剝奪式的還是不剝奪式的呢?微軟一直聲稱是剝奪式的(後面讀者將看到確實是),但是又說只有在從“內核”退出來時纔可以進行線程切換,這就引起了質疑和困惑,因爲當時很多人都把從“內核”退出來理解成從整個內核退出來而返回用戶空間的時候。可見,一方面不公開技術細節,另一方面對術語的使用又與人不同,這就很容易引起困惑。
    與Linux相比,Windows的內核還有個明顯的不同,就是其內核的相當一部分頁面是可倒換的。後面讀者將看到,內存管理即虛存技術的重要內容之一是物理頁面的倒換,就是可以將已經有映射但是暫時不受到訪問的頁面倒換到外存中去,到實際受到訪問時再倒換進來,使外存成爲內存的擴充。但是,在Linux中,屬於內核的頁面無論是用於代碼還是數據(用於文件內容緩存的除外),是不受倒換的。這一方面是爲了簡化內核的設計和實現,一方面也是因爲覺得價值不大,因爲內核畢竟是全局的,所有進程都公用同一個系統空間,即使要倒換也油水不大。但是,Windows的內核卻不同,其相當一部分頁面是可倒換的。究其原因,另一方面可能是來自VMS的影響,另一方面也可以從Windows內核的體積得到一些解釋。如前所述,微軟把圖形操作/視窗服務的實現也搬到了內核中,這就使內核的體積增加了很多。再說每個線程的系統空間堆棧也因此而增大了許多,當線程數量很大時也確實不可忽視。所以,在這樣的情況下,使內核的部分頁面成爲可倒換的確實有其合理的一面。不過當然不是所有頁面都可倒換,有些頁面註定是不可倒換的。例如,與中斷和異常有關的代碼和數據所在的頁面,以及與頁面倒換有關的代碼和數據所在的頁面,就顯然是不能被倒換出去的,否則就無法把這些頁面倒換回來了。這樣,我們就大致可以推斷,Windows內核中“內核”層及其以下應該是不允許倒換的。
    與此相關,Windows爲CPU的運行狀態定義了許多“IRQ級別”,即IRQL。在任一時間中,CPU總是運行於其中的某一個級別,這個級別就表明了什麼事情可以做、什麼事情不可以做。下面是這些級別的定義:
#define PASSIVE_LEVEL                          0
#define LOW_LEVEL                             0
#define APC_LEVEL                              1
#define DISPATCH_LEVEL                        2
#define PROFILE_LEVEL                         27
#define CLOCK1_LEVEL                          28
#define CLOCK2_LEVEL                          28
#define IPI_LEVEL                               29
#define POWER_LEVEL                           30
#define HIGH_LEVEL                             31
其基本的意圖是,如果CPU從而一個線程已經處於某個級別,其操作就不能受同級或更低級別的操作所幹擾。
    這裏的PASSIVE_LEVEL是級別最低的,但是卻對應着系統結構中較高的層次。當CPU運行於用戶空間,或者雖然進入了內核但還只是運行於管理層的時候,其運行級別就是PASSIVE_LEVEL。比其略高的是APC_LEVEL,那是在(內核中)爲APC函數(見本書“進程與線程”一章)的執行進行準備時的運行級別,APC請求相當於對用戶空間程序的(軟件)中斷。注意IRQL在x86系統結構中並沒有硬件的支持(CPU中並沒有這麼一個寄存器)而只是一個變量。與CPU只能通過特殊的指令或中斷/異常才能進入系統態不同,IRQL是CPU可以自由設置的,每當CPU進入更底層、更核心的層次時就提高IRQL,反之則降低IRQL。不過,表明IRQL的變量在內核中,運行於用戶空間時是無法改變IRQL的。
再高一級是DISPATCH_LEVEL,這大致相當於CPU運行於Windows內核中的核心層,即“內核”層。線程的切換隻能發生於CPU行將從DISPATCH_LEVEL級別下降的時候。
    IRQL級別3及以上用於硬件中斷。顯然,設計者的意圖是採用中斷優先級,即優先級較高的中斷源可以中斷優先級較低的中斷服務。但是x86的系統結構並不支持中斷優先級,所以這實際上是來自VMS的遺蹟,因爲VAX和PDP的系統結構都是支持中斷優先級的。
    回到頁面換出的問題上,只要CPU的IRQL級別不高於APC_LEVEL的層次,其代碼都是允許倒換的,但是從DISPATCH_LEVEL開始就不允許了。顯然,如果在這一點上搞錯了,後果是很嚴重的。所以在管理層的代碼中幾乎每個函數的開頭都要放上一個宏操作PAGED_CODE(),說明代碼作者的意圖是讓這個函數所佔的頁面可以被倒換出去。這個宏操作的定義如下:
#ifdef DBG
#define PAGED_CODE() { /
  if (KeGetCurrentIrql() > APC_LEVEL) { /
    KdPrint( ("NTDDK: Pageable code called at IRQL > APC_LEVEL (%d)/n",
                                                  KeGetCurrentIrql() )); /
    ASSERT(FALSE); /
  } /
}
#else
#define PAGED_CODE()
#endif
    在Debug模式下,這個宏操作檢查CPU當前的運行級別,如果發現高於APC_LEVEL就說明這個函數有可能在DISPATCH_LEVEL或更高的級別上受到調用,因而是不應該被倒換出去的,所以就發出警告。至於在正式運行的版本中,則這個宏操作定義爲空。
當然,光是在程序中引用宏操作PAGED_CODE()不會使一個函數所在的頁面可倒換,真正使其可倒換的是編譯指示“#pragma alloc_text()”。例如NtQueryObject()中的第一行就是PAGED_CODE(),與此相應,這個函數所在的源文件中就有這麼一行:
#pragma alloc_text(PAGE, NtQueryObject)
    正是這一行編譯指示讓編譯工具將爲此函數生成的可執行代碼放在可被倒換的區間。
    在本書所引的代碼中,許多函數的開頭都有對PAGED_CODE()的引用,但是爲了壓縮篇幅而將其省略了。此外,原來的代碼中爲增加可讀性而插有一些空行,爲壓縮篇幅也把它們刪去了。還有,在一些if語句中,也是爲增加可讀性,許多人主張哪怕只有一行代碼也要加上前後花括號。筆者很贊同這些主張,但是那樣一來就又得多佔兩行,所以書中有些地方把前後花括號刪去了,這只是爲壓縮篇幅,而不表示筆者認爲不應該使用這些花括號。
    書中所引代碼中凡是黑體字的部分都是筆者覺得需要提醒讀者注意的,但是爲不同目的而閱讀同一段代碼時的側重面可能不同,所以這只是就一般的閱讀、就代碼的“主旋律”而言,並不表示別的代碼就不重要。

發佈了1 篇原創文章 · 獲贊 2 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章