虛擬地址、線性地址和物理地址之間的關係

 

《Linux內核完全剖析—基於0.12內核》第5章Linux內核體系結構,本章首先概要介紹了Linux內核的編制模式和體系結構,然後詳細描述了Linux 內核源代碼目錄的組織形式以及子目錄中各個代碼文件的主要功能以及基本調用的層次關係。本節爲大家介紹的是虛擬地址、線性地址和物理地址之間的關係。


5.3.6  虛擬地址、線性地址和物理地址之間的關係

前面我們根據內存分段和分頁機制詳細說明了CPU的內存管理方式。現在我們以Linux 0.12系統爲例,詳細說明內核代碼和數據以及各任務的代碼和數據在虛擬地址空間、線性地址空間和物理地址空間中的對應關係。由於任務0和任務1的生成或創建過程比較特殊,我們將對它們分別進行描述。

1.內核代碼和數據的地址

對於Linux 0.12內核代碼和數據來說,在head.s程序的初始化操作中已經把內核代碼段和數據段都設置成爲長度爲16MB的段。在線性地址空間中這兩個段的範圍重疊,都是從線性地址0開始到地址0xFFFFFF共16MB地址範圍。在該範圍中含有內核所有的代碼、內核段表(GDT、IDT、TSS)、頁目錄表和內核的二級頁表、內核局部數據以及內核臨時堆棧(將被用作第1個任務,即任務0的用戶堆棧)。其頁目錄表和二級頁表已設置成把0~16MB的線性地址空間一一對應到物理地址上,佔用了4個目錄項,即4個二級頁表。因此對於內核代碼或數據的地址來說,我們可以直接把它們看做物理內存中的地址。此時內核的虛擬地址空間、線性地址空間和物理地址空間三者之間的關係可用圖5-14來表

圖5-14  內核代碼和數據段在三種地址空間中的關係

因此,默認情況下Linux 0.12內核最多可管理16MB的物理內存,共有4096個物理頁面(頁幀),每個頁面4KB。通過上述分析可以看出:①內核代碼段和數據段區域在線性地址空間和物理地址空間中是一樣的。這樣設置可以大大簡化內核的初始化操作。②GDT和IDT在內核數據段中,因此它們的線性地址也同樣等於它們的物理地址。在實模式下的setup.s程序初始化操作中,我們曾經設置過臨時的GDT和IDT,這是進入保護模式之前必須設置的。由於這兩個表當時處於物理內存大約0x90200處,而進入保護模式後內核系統模塊處於物理內存0開始位置,並且0x90200處的空間將被挪作他用(用於高速緩衝),因此在進入保護模式後,在運行的第1個程序head.s中我們需要重新設置這兩個表。即設置GDTR和IDTR指向新的GDT和IDT,描述符也需要重新加載。但由於開啓分頁機制時這兩個表的位置沒有變動,因此無須重新建立或移動表位置。③除任務0以外,所有其他任務使用的物理內存頁面與線性地址中的頁面至少有部分不同,因此內核需要動態地在主內存區中爲它們作映射操作,動態地建立頁目錄項和頁表項。雖然任務1的代碼和數據也在內核中,但由於其需要另行分配獲得內存,因此也需要自己的映射表項。

雖然Linux 0.12默認可管理16MB物理內存,但是系統中並不是一定要有這些物理內存。機器中只要有4MB(甚至2MB)物理內存就完全可以運行Linux 0.12系統了。若機器只有4MB物理內存,那麼此時內核4MB~16MB地址範圍就會映射到不存在的物理內存地址上。但這並不妨礙系統的運行。因爲在初始化時內核內存管理程序會知道機器中所含物理內存量的確切大小,因而不會讓CPU分頁機制把線性地址頁面映射到不存在的4MB~16MB中去。內核中這樣的默認設置主要是爲了便於系統物理內存的擴展,實際並不會用到不存在的物理內存區域。如果系統有多於16MB的物理內存,由於在init/main.c程序中初始化時限制了對16MB以上內存的使用,並且這裏內核也僅映射了0~16MB的內存範圍,因此在16MB之上的物理內存將不會用到。

通過在這裏爲內核增加一些頁表,並且對init/main.c程序稍作修改,我們可以對此限制進行擴展。例如在系統中有32MB物理內存的情況下,我們就需要爲內核代碼和數據段建立8個二級頁表項來把32MB的線性地址範圍映射到物理內存上。

2.任務0的地址對應關係

任務0是系統中人工啓動的第1個任務。它的代碼段和數據段長度被設置爲640KB。該任務的代碼和數據直接包含在內核代碼和數據中,是從線性地址0開始的640KB內容,因此它可以直接使用內核代碼已經設置好的頁目錄和頁表進行分頁地址變換。同樣,它的代碼和數據段在線性地址空間中也是重疊的。對應的任務狀態段TSS0也是手工預設置好的,並且位於任務0數據結構信息中,參見include/linux/sched.h第156行開始的數據。TSS0段位於內核sched.c程序的代碼中,長度爲104字節,具體位置可參見圖5-24中"任務0結構信息"一項所示。3個地址空間中的映射對應關係如圖5-15所示。

(點擊查看大圖)圖5-15  任務0在3個地址空間中的相互關係

由於任務0直接被包含在內核代碼中,因此不需要爲其再另外分配內存頁。它運行時所需要的內核態堆棧和用戶態堆棧空間也都在內核代碼區中,並且由於在內核初始化時(head.s)這些內核頁面在頁表項中的屬性都已經被設置成了0b111,即對應頁面用戶可讀寫並且存在,因此用戶堆棧user_stack[]空間雖然在內核空間中,但任務0仍然能對其進行讀寫操作。

3.任務1的地址對應關係

與任務0類似,任務1也是一個特殊的任務。它的代碼也在內核代碼區域中。與任務0不同的是在線性地址空間中,系統在使用fork()創建任務1(init進程)時爲存放任務1的二級頁表而在主內存區申請了一頁內存來存放,並複製了父進程(任務0)的頁目錄和二級頁表項。因此任務1有自己的頁目錄和頁表表項,它把任務1佔用的線性空間範圍64~128MB(實際上是64MB~64MB+640KB)也同樣映射到了物理地址0~640KB處。此時任務1的長度也是640KB,並且其代碼段和數據段相重疊,只佔用一個頁目錄項和一個二級頁表。另外,系統還會爲任務1在主內存區域中申請一頁內存用來存放它的任務數據結構和用作任務1的內核堆棧空間。任務數據結構(也稱進程控制塊PCB)信息中包括任務1的TSS段結構信息,如圖5-16所示。

(點擊查看大圖)圖5-16  任務1在3種地址空間中的關係

任務1的用戶態堆棧空間將直接共享使用處於內核代碼和數據區域(線性地址0~640KB)中任務0的用戶態堆棧空間user_stack[](參見kernel/sched.c,第82~87行),因此這個堆棧需要在任務1實際使用之前保持"乾淨",以確保被複制用於任務1的堆棧不含有無用數據。在剛開始創建任務1時,任務0的用戶態堆棧user_stack[]與任務1共享使用,但當任務1開始運行時,由於任務1映射到user_stack[]處的頁表項被設置成只讀,使得任務1在執行堆棧操作時將會引起寫頁面異常,從而由內核另行分配主內存區頁面作爲堆棧空間使用。

4.其他任務的地址對應關係

對於被創建的從任務2開始的其他任務,它們的父進程都是init(任務1)進程。我們已經知道,在Linux 0.12系統中共可以有64個進程同時存在。下面我們以任務2爲例來說明其他任何任務對地址空間的使用情況。

從任務2開始,如果任務號以nr來表示,那麼任務nr在線性地址空間中的起始位置將被設定在nr×64MB處。例如任務2的開始位置= nr×64MB = 2×64MB = 128MB。任務代碼段和數據段的最大長度被設置爲64MB,因此任務2佔有的線性地址空間範圍是128MB~192MB,共佔用64MB/4MB = 16個頁目錄項。虛擬空間中任務代碼段和數據段都被映射到線性地址空間相同的範圍,因此它們也完全重疊。圖顯示出了任務2的代碼段和數據段在3種地址空間中的對應關係。

在任務2被創建出來之後,將在其中運行execve()函數來執行shell程序。當內核通過複製任務1剛創建任務2時,除了佔用線性地址空間範圍不同外(128MB~128MB+640KB),此時任務2的代碼和數據在3種地址空間中的關係與任務1的類似。當任務2的代碼(init())調用execve()系統調用開始加載並執行shell程序時,該系統調用會釋放掉從任務1複製的頁目錄和頁表表項及相應內存頁面,然後爲新的執行程序shell重新設置相關頁目錄和頁表表項。圖給出的是任務2中開始執行shell程序時的情況,即任務2原先複製任務1的代碼和數據被shell程序的代碼段和數據段替換後的情況。圖中顯示出已經映射了一頁物理內存頁面的情況。這裏請注意,在執行execve()函數時,系統雖然在線性地址空間爲任務2分配了64MB的空間範圍,但是內核並不會立刻爲其分配和映射物理內存頁面。只有當任務2開始執行時由於發生缺頁而引起異常時纔會由內存管理程序爲其在主內存區中分配並映射一頁物理內存到其線性地址空間中。這種分配和映射物理內存頁面的方法稱爲需求加載(load on demand),請參見第13章中的相關描述。

(點擊查看大圖)圖5-17  其他任務地址空間中的對應關係
從Linux內核0.99版以後,對內存空間的使用方式發生了變化。每個進程可以單獨享用整個4GB的地址空間範圍。如果我們能理解本節說描述的內存管理概念,那麼對於現在所使用的Linux 2.x內核中所使用的內存管理原理也能立刻明白。由於篇幅所限,這裏對此不再說明。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章