《Linux原理與結(jié)構(gòu)》課件第8章_第1頁(yè)
《Linux原理與結(jié)構(gòu)》課件第8章_第2頁(yè)
《Linux原理與結(jié)構(gòu)》課件第8章_第3頁(yè)
《Linux原理與結(jié)構(gòu)》課件第8章_第4頁(yè)
《Linux原理與結(jié)構(gòu)》課件第8章_第5頁(yè)
已閱讀5頁(yè),還剩178頁(yè)未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

第八章虛擬內(nèi)存管理8.1虛擬內(nèi)存管理結(jié)構(gòu)8.2虛擬內(nèi)存區(qū)域管理8.3虛擬地址空間建立8.4頁(yè)故障處理8.5頁(yè)面回收在內(nèi)核看來,一個(gè)進(jìn)程就是一個(gè)以task_struct為代表的管理結(jié)構(gòu)。內(nèi)核通過管理結(jié)構(gòu)創(chuàng)建、回收、調(diào)度系統(tǒng)中的進(jìn)程,并不關(guān)心進(jìn)程所執(zhí)行的程序。然而在用戶看來,一個(gè)進(jìn)程就是一臺(tái)虛擬的計(jì)算機(jī),它有自己的虛擬內(nèi)存用以存放程序和數(shù)據(jù),有自己的虛擬處理器用以執(zhí)行程序,并有自己的虛擬文件系統(tǒng)和虛擬外部設(shè)備等用于輸入輸出。內(nèi)核的核心工作就是為每一個(gè)進(jìn)程都虛擬出一臺(tái)這樣的虛擬機(jī),其中處理器的虛擬化工作由調(diào)度器完成,內(nèi)存的虛擬化工作由虛擬內(nèi)存管理器完成。虛擬內(nèi)存管理器的主要設(shè)計(jì)目標(biāo)是為每個(gè)進(jìn)程都提供一塊從0開始編址的、連續(xù)的、不受系統(tǒng)物理內(nèi)存大小限制的大地址空間。進(jìn)程的大地址空間是虛擬內(nèi)存管理器利用物理內(nèi)存和外部存儲(chǔ)設(shè)備模擬出來的,雖在功能上等價(jià)于物理內(nèi)存,但并不實(shí)際存在,所以被稱為虛擬地址空間或虛擬內(nèi)存。由于每個(gè)進(jìn)程都有自己獨(dú)立的虛擬地址空間,因而在系統(tǒng)中可以同時(shí)存在多個(gè)進(jìn)程,每個(gè)進(jìn)程都可執(zhí)行任意大小的程序且互不干涉。一般情況下,一個(gè)進(jìn)程不能執(zhí)行另一個(gè)進(jìn)程的程序,也不能訪問另一個(gè)進(jìn)程的數(shù)據(jù),一個(gè)進(jìn)程的問題不會(huì)影響其它進(jìn)程的運(yùn)行。也就是說,各虛擬地址空間是相互隔離的。與物理內(nèi)存管理器不同,虛擬內(nèi)存管理器提供的主要功能包括動(dòng)態(tài)地創(chuàng)建、回收和調(diào)整進(jìn)程的虛擬地址空間,協(xié)助內(nèi)存管理單元(MMU)實(shí)現(xiàn)虛擬地址到物理地址的轉(zhuǎn)換,實(shí)現(xiàn)虛擬頁(yè)面的動(dòng)態(tài)建立和淘汰,實(shí)現(xiàn)虛擬地址空間的隔離、保護(hù)與共享,提供文件映射、動(dòng)態(tài)鏈接、負(fù)載統(tǒng)計(jì)等其它服務(wù)。進(jìn)程的虛擬地址空間隨進(jìn)程的創(chuàng)建而創(chuàng)建,隨進(jìn)程的終止而撤銷。

進(jìn)程的核心工作是執(zhí)行程序,被執(zhí)行的程序及其數(shù)據(jù)、堆棧等必須被預(yù)先裝入內(nèi)存。在沒有虛擬內(nèi)存管理器的情況下,只能將程序、數(shù)據(jù)等一次性地全部裝入物理內(nèi)存(如分區(qū)法),這種執(zhí)行程序的方法雖然簡(jiǎn)單但卻存在一些問題,如無法裝入超過可用物理內(nèi)存量的程序、難以實(shí)現(xiàn)進(jìn)程之間的隔離與保護(hù)、并發(fā)的進(jìn)程數(shù)受物理內(nèi)存大小的限制、進(jìn)程間切換的開銷過大、程序的設(shè)計(jì)過分依賴于物理內(nèi)存的配置、編程困難、移植困難等。8.1虛擬內(nèi)存管理結(jié)構(gòu)為解決這些問題,人們提出了多種改進(jìn)辦法,如覆蓋(通過覆蓋那些暫時(shí)不用的程序或數(shù)據(jù)來復(fù)用內(nèi)存)、交換(在內(nèi)外存之間以進(jìn)程為單位換入換出)等,但這些辦法都不夠理想。1978年,Unix操作系統(tǒng)第一次在VAX機(jī)上實(shí)現(xiàn)了基于分頁(yè)機(jī)制的虛擬內(nèi)存管理器,徹底解決了上述問題。虛擬內(nèi)存的引入是操作系統(tǒng)發(fā)展史上一個(gè)里程碑,它改變了軟件設(shè)計(jì)、運(yùn)行和管理的方式,甚至改變了計(jì)算機(jī)系統(tǒng)的使用方式。虛擬內(nèi)存管理的基本思路是虛擬或制造假象,它使每個(gè)進(jìn)程都認(rèn)為系統(tǒng)中有足夠大的內(nèi)存,而且自己在獨(dú)占該內(nèi)存。虛擬或造假的工作由虛擬內(nèi)存管理器負(fù)責(zé),它的設(shè)計(jì)依據(jù)是程序運(yùn)行的局部性規(guī)律,實(shí)現(xiàn)的基本方法是:

(1)隔離,不讓進(jìn)程看到實(shí)際的物理內(nèi)存,只給它看到一個(gè)美好的假象。

(2)借用,借用容量更大的外存來存儲(chǔ)暫時(shí)不用的程序和數(shù)據(jù)以擴(kuò)充內(nèi)存容量。

(3)延遲,將鏈接、加載、復(fù)制、擴(kuò)充等工作向后推遲,僅裝入進(jìn)程真正執(zhí)行的程序和真正使用的數(shù)據(jù),僅復(fù)制實(shí)際修改的虛擬頁(yè),僅擴(kuò)充實(shí)際使用到的匿名頁(yè)。

(4)換入/換出,以頁(yè)為單位,快速地在內(nèi)、外存之間復(fù)制頁(yè)面,保證進(jìn)程當(dāng)前使用的代碼和數(shù)據(jù)都在物理內(nèi)存中,暫時(shí)不用的數(shù)據(jù)都在外存中。

Linux虛擬內(nèi)存管理器的實(shí)現(xiàn)思路如圖8.1所示:

(1)將系統(tǒng)的物理地址空間和進(jìn)程的虛擬地址空間都分成固定大小的頁(yè),為每個(gè)進(jìn)程建立一個(gè)頁(yè)表,以實(shí)現(xiàn)兩個(gè)地址空間的隔離,使進(jìn)程僅能看到自己的虛擬地址空間。

(2)由虛擬內(nèi)存管理器維護(hù)進(jìn)程的頁(yè)表,建立進(jìn)程虛擬頁(yè)與系統(tǒng)物理頁(yè)之間的對(duì)應(yīng)關(guān)系,利用系統(tǒng)硬件自動(dòng)實(shí)現(xiàn)進(jìn)程虛擬地址到物理地址的轉(zhuǎn)換。

(3)由虛擬內(nèi)存管理器負(fù)責(zé)在內(nèi)外存之間交換頁(yè)面,將進(jìn)程使用的虛擬頁(yè)換入物理內(nèi)存,將暫時(shí)不用的虛擬頁(yè)換出物理內(nèi)存,盡可能提高物理內(nèi)存的利用率,從而用有限的物理內(nèi)存為進(jìn)程模擬出幾乎無限的虛擬內(nèi)存,給進(jìn)程造成擁有大內(nèi)存的假象。圖8.1Linux虛擬內(nèi)存管理器的實(shí)現(xiàn)思路在虛擬內(nèi)存管理結(jié)構(gòu)中,頁(yè)表是最關(guān)鍵、最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),它記錄著進(jìn)程虛擬頁(yè)與物理頁(yè)之間的對(duì)應(yīng)關(guān)系。在Intel的32位處理器中,頁(yè)表分為兩級(jí),分別稱為頁(yè)目錄和頁(yè)表。每個(gè)進(jìn)程都有自己的頁(yè)目錄和頁(yè)表。虛擬內(nèi)存管理器通過不斷地調(diào)整進(jìn)程的頁(yè)目錄和頁(yè)表來管理進(jìn)程的虛擬地址空間。頁(yè)目錄和頁(yè)表雖僅是簡(jiǎn)單的數(shù)組,卻具有強(qiáng)大的功能和極大的靈活性,概括如下:

(1)頁(yè)目錄、頁(yè)表的內(nèi)容只能由虛擬內(nèi)存管理器維護(hù),進(jìn)程無法看到、更無法修改。虛擬內(nèi)存管理器保證不同進(jìn)程的頁(yè)表間不會(huì)出現(xiàn)重疊,從而保證進(jìn)程的虛擬地址空間是獨(dú)立的,相互之間是隔離的。

(2)當(dāng)虛擬頁(yè)在內(nèi)存時(shí),頁(yè)表項(xiàng)記錄著映射關(guān)系。當(dāng)虛擬頁(yè)不在內(nèi)存時(shí),頁(yè)表項(xiàng)記錄著頁(yè)面在外存的位置。頁(yè)表項(xiàng)中還包含有豐富的控制信息,用以實(shí)現(xiàn)頁(yè)的保護(hù)。

(3)頁(yè)表項(xiàng)甚至頁(yè)表本身都是動(dòng)態(tài)創(chuàng)建的,也可以動(dòng)態(tài)刪除。頁(yè)表項(xiàng)可以為空,表示虛擬頁(yè)不在內(nèi)存。頁(yè)目錄項(xiàng)也可以為空,表示整個(gè)頁(yè)表都不存在。頁(yè)表的內(nèi)容可以動(dòng)態(tài)修改,頁(yè)表所反映的映射關(guān)系可動(dòng)態(tài)變化。虛擬頁(yè)與物理頁(yè)的映射關(guān)系不要求連續(xù),也不要求有序。如果需要,還可以使頁(yè)表的內(nèi)容重疊,即將多個(gè)進(jìn)程的虛擬頁(yè)映射到同一個(gè)物理頁(yè),從而實(shí)現(xiàn)內(nèi)存共享。進(jìn)程切換時(shí),頁(yè)目錄、頁(yè)表也要隨著切換。頁(yè)目錄、頁(yè)表描述了進(jìn)程完整的虛擬地址空間。進(jìn)程在用戶態(tài)運(yùn)行需要的所有信息都保存在它的虛擬地址空間中,進(jìn)程用虛擬地址訪問自己的程序和數(shù)據(jù)。然而,由于屬性的不同,進(jìn)程對(duì)自己虛擬地址空間的不同區(qū)域有不同的使用與管理方法,如程序代碼可以執(zhí)行但不能修改、數(shù)據(jù)可以讀寫但不能執(zhí)行、堆??梢詣?dòng)態(tài)增長(zhǎng)等,因而不應(yīng)將進(jìn)程的虛擬地址空間看成完整的一塊,而應(yīng)將其劃分成不同的區(qū)域。Linux用vm_area_struct結(jié)構(gòu)描述進(jìn)程的虛擬內(nèi)存區(qū)域,其中的主要內(nèi)容如下:

(1)開始虛地址vm_start和終止虛地址vm_end。

(2)存取許可vm_page_prot和標(biāo)志vm_flags。

(3)映射文件vm_file和在文件中的開始頁(yè)號(hào)vm_pgoff。

(4)操作集vm_ops。

(5)紅黑樹節(jié)點(diǎn)vm_rb和通用鏈表節(jié)點(diǎn)vm_next。

(6)指向匿名域的指針anon_vma和通用鏈表節(jié)點(diǎn)anon_vma_chain。

(7)優(yōu)先樹節(jié)點(diǎn)prio_tree_node或通用鏈表節(jié)點(diǎn)list(聯(lián)合、復(fù)用域)。一個(gè)虛擬內(nèi)存區(qū)域描述的是進(jìn)程虛擬地址空間中具有相同屬性的一段連續(xù)的虛擬地址區(qū)間,即[vm_start,vm_end-1]。虛擬內(nèi)存區(qū)域的大小可變,最小為1頁(yè)。

虛擬內(nèi)存區(qū)域的屬性分別由vm_page_prot和vm_flags描述。其中vm_page_prot描述區(qū)域中各虛擬頁(yè)的存取許可特性,用于創(chuàng)建區(qū)域中各頁(yè)目錄、頁(yè)表項(xiàng)的存取控制標(biāo)志,如R/W、U/S、A、D、G位等;vm_flags描述區(qū)域的整體存取控制特性,如VM_READ(允許讀)、VM_WRITE(允許寫)、VM_EXEC(允許執(zhí)行)、VM_SHARED(允許共享)、VM_GROWSDOWN(向下增長(zhǎng),即堆棧)、VM_SEQ_READ(內(nèi)容將被順序讀)、VM_RAND_READ(內(nèi)容將被隨機(jī)讀)、VM_DONTEXPAND(禁止擴(kuò)展)等。

虛擬內(nèi)存區(qū)域中的內(nèi)容可能來源于文件(如程序、數(shù)據(jù)等),也可能是匿名的(如堆棧、堆等)。如果虛擬內(nèi)存區(qū)域是匿名的,vm_pgoff是區(qū)域的開始虛頁(yè)號(hào)。如果虛擬內(nèi)存區(qū)域中的內(nèi)容來源于一個(gè)文件,那么結(jié)構(gòu)vm_area_struct中還記錄著內(nèi)容在文件中的位置,即文件vm_file中的區(qū)間[vm_pgoff

×

4096,vm_pgoff

×

4096+vm_end-vm_start-1]。在老版本中,vm_pgoff是區(qū)間在文件中的開始偏移量,在新版本中,vm_pgoff是區(qū)間在文件中的開始頁(yè)號(hào)。

文件區(qū)間與進(jìn)程虛擬內(nèi)存區(qū)域之間的映射方式共有兩種:

(1)共享映射。文件區(qū)間可按共享方式同時(shí)映射到多個(gè)進(jìn)程的虛擬地址空間(建立的區(qū)域稱為共享區(qū)域),其內(nèi)容在內(nèi)存中只有一份拷貝,允許多個(gè)進(jìn)程直接讀寫,修改后的結(jié)果將被直接寫回到映射文件。

(2)私有映射。文件區(qū)間可按私有方式同時(shí)映射到多個(gè)進(jìn)程的虛擬地址空間(建立的區(qū)域稱為私有區(qū)域),各進(jìn)程按CopyonWrite方式共享文件內(nèi)容,進(jìn)程僅能修改自己的拷貝,修改后的結(jié)果不能直接寫回到映射文件。為了區(qū)分不同虛擬內(nèi)存區(qū)域?qū)μ囟ㄊ录?如缺頁(yè))的處理方法,Linux為每個(gè)虛擬內(nèi)存區(qū)域都定義了一個(gè)操作集vm_operations_struct,其中的open和close是虛擬內(nèi)存區(qū)域的打開和關(guān)閉操作,相當(dāng)于構(gòu)造和析構(gòu)函數(shù),fault是頁(yè)故障異常的處理操作。圖8.2是常見的一種虛擬內(nèi)存區(qū)域。

圖8.2虛擬內(nèi)存區(qū)域一個(gè)進(jìn)程的所有虛擬內(nèi)存區(qū)域構(gòu)成了它的有效虛擬地址空間,在虛擬內(nèi)存區(qū)域內(nèi)的地址是有效的,在所有虛擬內(nèi)存區(qū)域之外的地址是無效的。進(jìn)程的有效虛擬地址空間由結(jié)構(gòu)mm_struct描述,如圖8.3所示。虛擬地址空間中的內(nèi)容稱為執(zhí)行映像(image)。

圖8.3進(jìn)程虛擬內(nèi)存管理結(jié)構(gòu)結(jié)構(gòu)mm_struct的主要內(nèi)容包括如下幾部分:

(1)進(jìn)程的頁(yè)目錄pgd。每個(gè)進(jìn)程都有自己的頁(yè)目錄,一般情況下,一個(gè)進(jìn)程不應(yīng)使用其它進(jìn)程的頁(yè)目錄,除非特殊需要。

(2)

vm_area_struct結(jié)構(gòu)隊(duì)列mmap和紅黑樹mm_rb。隊(duì)列和紅黑樹都是有序的,虛擬地址小的區(qū)域在前,虛擬地址大的區(qū)域在后。建立紅黑樹的目的是為了加快對(duì)vm_area_struct結(jié)構(gòu)的查找速度。

(3)虛擬地址空間的布局。即進(jìn)程執(zhí)行映像的各部分在虛擬地址空間中的位置,包括程序(從start_code到end_code)、數(shù)據(jù)(從start_data到end_data)、堆棧(從start_stack到3GB)、堆(從start_brk到brk)、初始參數(shù)(從arg_start到arg_end)、環(huán)境變量(從env_start到env_end)等。

(4)統(tǒng)計(jì)信息。如進(jìn)程虛擬地址空間的總頁(yè)數(shù)、鎖定的頁(yè)數(shù)、共享的頁(yè)數(shù)、預(yù)留的頁(yè)數(shù)、堆棧頁(yè)數(shù)、程序代碼頁(yè)數(shù)、文件映射頁(yè)數(shù)、匿名頁(yè)數(shù)、當(dāng)前駐留在內(nèi)存中的頁(yè)數(shù)等。并不是所有進(jìn)程都需要虛擬內(nèi)存管理結(jié)構(gòu),事實(shí)上,內(nèi)核線程(包括init_task)沒有自己的mm_struct結(jié)構(gòu),當(dāng)內(nèi)核線程運(yùn)行時(shí),它們借用前一個(gè)進(jìn)程的mm_struct。在task_struct結(jié)構(gòu)中,mm指向自己的mm_struct,active_mm指向當(dāng)前使用的mm_struct,mm可以為空。

在早期的系統(tǒng)中,Linux僅用mm_struct、vm_area_struct、頁(yè)目錄、頁(yè)表等描述進(jìn)程的虛擬地址空間,這些數(shù)據(jù)結(jié)構(gòu)定義了一種正向映射關(guān)系,通過該映射關(guān)系可以方便地找到與一個(gè)虛擬頁(yè)對(duì)應(yīng)的物理頁(yè)和文件頁(yè)。然而在有些時(shí)候,如物理頁(yè)回收時(shí),還需要一種逆向映射關(guān)系,以便能同樣方便地找到使用一個(gè)物理頁(yè)或文件頁(yè)的所有進(jìn)程及該頁(yè)在各進(jìn)程中所對(duì)應(yīng)的虛擬頁(yè)。為此,新版本的Linux又引入了逆向映射關(guān)系。

1.文件頁(yè)到虛擬頁(yè)的映射關(guān)系

如果虛擬頁(yè)的內(nèi)容來源于文件頁(yè)且包含該文件頁(yè)的文件區(qū)間僅被映射到一個(gè)進(jìn)程的虛擬內(nèi)存空間中,那么虛擬內(nèi)存區(qū)域所建立的就是文件頁(yè)與虛擬頁(yè)的一一對(duì)應(yīng)關(guān)系,通過虛擬內(nèi)存區(qū)域可以方便地找到虛擬頁(yè)對(duì)應(yīng)的文件頁(yè)和文件頁(yè)對(duì)應(yīng)的虛擬頁(yè)。

如果包含某文件頁(yè)的文件區(qū)間(大小可能不同)被同時(shí)映射到多個(gè)進(jìn)程的虛擬地址空間中,那么每個(gè)進(jìn)程都會(huì)為包含該頁(yè)的文件區(qū)間建立一個(gè)虛擬內(nèi)存區(qū)域,這些虛擬內(nèi)存區(qū)域所建立的是文件頁(yè)到虛擬頁(yè)的一對(duì)多的映射關(guān)系。為了便于找到一個(gè)文件頁(yè)對(duì)應(yīng)的所有虛擬頁(yè),Linux為每個(gè)文件建立了一個(gè)文件地址空間address_space,在其中記錄著映射到該文件的所有虛擬內(nèi)存區(qū)域(被組織成優(yōu)先樹或雙向鏈表)。給定一個(gè)文件頁(yè)(頁(yè)號(hào)為pgoff),查它的address_space,可得到包含該頁(yè)的所有虛擬內(nèi)存區(qū)域vma,進(jìn)而可算出該頁(yè)在各進(jìn)程虛擬地址空間中的虛擬地址:(pgoff-vma->vm_pgoff)

×

4096+vma->vm_start。

2.物理頁(yè)到虛擬頁(yè)的映射關(guān)系

一個(gè)物理頁(yè)的內(nèi)容可能是動(dòng)態(tài)創(chuàng)建的(匿名頁(yè)),也可能來源于一個(gè)文件(映射頁(yè))。來源于文件的頁(yè)可能屬于共享區(qū)域,也可能屬于私有區(qū)域。不同來源的物理頁(yè)需要不同的逆向映射關(guān)系。

1)匿名頁(yè)

匿名頁(yè)屬于匿名的虛擬內(nèi)存區(qū)域。一般情況下,一個(gè)物理頁(yè)僅屬于一個(gè)匿名虛擬內(nèi)存區(qū)域,但有一些特殊的物理頁(yè),如CopyonWrite頁(yè)、共享內(nèi)存頁(yè)等,會(huì)同時(shí)屬于多個(gè)匿名虛擬內(nèi)存區(qū)域。值得注意的是,共享物理頁(yè)的多個(gè)匿名虛擬內(nèi)存區(qū)域具有相同的大小和屬性,就是說物理頁(yè)在各區(qū)域中的偏移量是相同的。

為了便于找到一個(gè)物理頁(yè)對(duì)應(yīng)的所有虛擬頁(yè),Linux做了如下安排:

(1)為每一類匿名虛擬內(nèi)存區(qū)域定義一個(gè)匿名域結(jié)構(gòu)anon_vma,共享同樣物理頁(yè)、具有相同屬性的虛擬內(nèi)存區(qū)域被組織在一個(gè)anon_vma結(jié)構(gòu)中(雙向鏈表)。

(2)復(fù)用page結(jié)構(gòu)中的三個(gè)域,讓_mapcount記錄映射到該頁(yè)的虛擬頁(yè)的個(gè)數(shù)(在幾個(gè)進(jìn)程的頁(yè)表中)、mapping指向anon_vma結(jié)構(gòu)、index記錄頁(yè)在虛擬內(nèi)存區(qū)域中的頁(yè)號(hào)。

給定一個(gè)物理頁(yè)的page結(jié)構(gòu),查與之關(guān)聯(lián)的anon_vma結(jié)構(gòu),可找到包含它的所有虛擬內(nèi)存區(qū)域vma,進(jìn)而可算出它在各虛擬地址空間中的虛擬地址:(index-vma->vm_pgoff)

×

4096+vma->vm_start。

2)共享映射頁(yè)

共享映射頁(yè)屬于共享區(qū)域,其內(nèi)容來源于文件,可以被多個(gè)進(jìn)程共用,包括讀、寫、執(zhí)行,因而會(huì)同時(shí)出現(xiàn)在多個(gè)進(jìn)程的頁(yè)表中。共享類型的虛擬內(nèi)存區(qū)域僅出現(xiàn)在文件的地址空間中。

為了便于找到共享頁(yè)對(duì)應(yīng)的所有虛擬頁(yè),Linux復(fù)用了page結(jié)構(gòu)中的三個(gè)域,在_mapcount中記錄映射到該物理頁(yè)的虛擬頁(yè)的個(gè)數(shù)、讓mapping指向文件的地址空間address_space、在index中記錄頁(yè)在文件中的偏移量。根據(jù)index查address_space可得到包含該頁(yè)的所有虛擬內(nèi)存區(qū)域vma,進(jìn)而可算出該頁(yè)在各虛擬地址空間中的虛擬地址:(index-vma->vm_pgoff)

×

4096+vma->vm_start。

3)私有映射頁(yè)

私有映射頁(yè)屬于私有區(qū)域,其內(nèi)容來源于文件,可以被多個(gè)進(jìn)程讀和執(zhí)行,但只能被一個(gè)進(jìn)程修改。私有類型的虛擬內(nèi)存區(qū)域出現(xiàn)在文件的地址空間中。

如果進(jìn)程未對(duì)私有映射頁(yè)實(shí)施寫操作,那么它使用的是該頁(yè)的正本。正本頁(yè)的page結(jié)構(gòu)的設(shè)置與共享映射頁(yè)相同,獲取物理頁(yè)對(duì)應(yīng)的虛擬頁(yè)的方法也相同。

如果進(jìn)程對(duì)私有頁(yè)實(shí)施了寫操作,那么它使用的就是該頁(yè)的副本。雖然副本頁(yè)的內(nèi)容來源于文件頁(yè),但它不會(huì)出現(xiàn)在包含文件頁(yè)的所有虛擬內(nèi)存區(qū)域中,因而通過文件地址空間已無法確定與副本頁(yè)對(duì)應(yīng)的所有虛擬頁(yè)。為了便于找到與副本頁(yè)對(duì)應(yīng)的所有虛擬頁(yè),Linux做了如下處理:

(1)將包含副本頁(yè)的虛擬內(nèi)存區(qū)域同時(shí)加入文件地址空間和匿名域中。

(2)復(fù)用page結(jié)構(gòu)中的三個(gè)域,讓_mapcount記錄映射到該頁(yè)的虛擬頁(yè)的個(gè)數(shù)、mapping指向anon_vma結(jié)構(gòu)、index記錄頁(yè)在文件中的頁(yè)號(hào)。給定一個(gè)副本頁(yè)的page結(jié)構(gòu),查與之關(guān)聯(lián)的anon_vma結(jié)構(gòu),可找到包含它的所有虛擬內(nèi)存區(qū)域vma,進(jìn)而可算出它在各虛擬地址空間中的虛擬地址:(index-vma->vm_pgoff)

×

4096+vma->vm_start。

顯然,一個(gè)vm_area_struct結(jié)構(gòu)會(huì)同時(shí)出現(xiàn)在多個(gè)隊(duì)列或樹中,包括進(jìn)程虛擬地址空間mm_struct中的單向隊(duì)列mmap和紅黑樹mm_rb,文件地址空間address_space中的雙向隊(duì)列i_mmap_nonlinear或優(yōu)先樹i_mmap,匿名域anon_vma中的雙向隊(duì)列head等。結(jié)構(gòu)vm_area_struct中的vm_next和vm_rb用于將自己加入進(jìn)程的虛擬地址空間,list或prio_tree_node用于將自己加入文件的地址空間,anon_vma和anon_vma_chain用于將自己加入匿名域。

在32位機(jī)器上,進(jìn)程的地址空間有4GB。缺省情況下,Linux將這4GB的地址空間劃分成兩部分,內(nèi)核使用高端的1GB,進(jìn)程使用低端的3GB。在所有進(jìn)程的地址空間中,內(nèi)核部分是完全相同的,所不同的是低端部分。進(jìn)程的虛擬地址空間指的主要是它的低端部分。8.2虛擬內(nèi)存區(qū)域管理

Linux采用動(dòng)態(tài)分區(qū)法管理進(jìn)程的低端虛擬地址空間。在進(jìn)程運(yùn)行過程中,它的低端虛擬地址空間被逐步分割成多個(gè)虛擬內(nèi)存區(qū)域,分別用于保存進(jìn)程的程序、數(shù)據(jù)、用戶堆棧、堆、動(dòng)態(tài)鏈接器、函數(shù)庫(kù)、映射文件等。在這些區(qū)域中,有些是在程序加載時(shí)建立的,有些是在運(yùn)行過程中建立的,有些大小與位置是固定的,有些是不斷變化的,因而需要預(yù)先確定進(jìn)程虛擬地址空間的布局方式和虛擬內(nèi)存區(qū)域的管理方法。

在64位機(jī)器上,進(jìn)程的地址空間有256TB,其中低端虛擬地址空間部分有128TB,比較容易分割。8.2.1虛擬地址空間布局

一般情況下,進(jìn)程的程序和數(shù)據(jù)區(qū)域是在程序加載時(shí)建立的,位于虛擬地址空間的最低端,且在運(yùn)行過程中不再改變;堆區(qū)域緊接著數(shù)據(jù)區(qū)域,會(huì)在進(jìn)程運(yùn)行過程中增長(zhǎng)或收縮,增長(zhǎng)的方向應(yīng)該是向上;初始參數(shù)和環(huán)境變量在進(jìn)程加載時(shí)確定,位于堆棧區(qū)域中;堆棧區(qū)域位于虛擬地址空間中用戶部分的最高端,會(huì)在進(jìn)程運(yùn)行過程中動(dòng)態(tài)增長(zhǎng),增長(zhǎng)的方向應(yīng)該是向下;動(dòng)態(tài)鏈接器、函數(shù)庫(kù)、數(shù)據(jù)文件等區(qū)域是動(dòng)態(tài)建立的,位于堆和堆棧區(qū)域之間,稱為文件映射區(qū),其增長(zhǎng)方向可以向上,也可以向下。

圖8.4進(jìn)程虛擬地址空間的布局

(1)如果進(jìn)程運(yùn)行的程序是靜態(tài)鏈接的,而且不需要映射數(shù)據(jù)文件,那么它就不需要建立文件映射區(qū)域,方案(a)是一種最佳的選擇。在該方案中,堆和堆棧均向中間的空閑地帶增長(zhǎng),有足夠大的擴(kuò)展余地和靈活性。

(2)如果進(jìn)程需要建立文件映射區(qū)域,它只能選擇方案(b)或(c)。在方案(b)中,文件映射區(qū)的開始位置為1GB,向上增長(zhǎng)。該方案為用戶堆棧和文件映射區(qū)留下了較大的擴(kuò)展空間,但限制了堆的大小。

(3)在方案(c)中,文件映射區(qū)的開始位置為3GB–x,向下增長(zhǎng),其中x是用戶堆棧的最大尺寸,可由用戶指定,在128MB到512MB之間。該方案為堆和文件映射區(qū)留下了較大的擴(kuò)展空間,但限制了堆棧的大小。

在虛擬地址空間中預(yù)留了多個(gè)空洞??斩?與3通常較小(大小是隨機(jī)生成的),設(shè)置它們的目的是為了增加探測(cè)的難度,提高系統(tǒng)的抗攻擊能力??斩?的大小取決于編譯器的設(shè)置,基本是固定的,且通常較大,也可以用做文件映射區(qū)。文件映射區(qū)的開始位置記錄在mm_struct結(jié)構(gòu)的mmap_base域中。由于文件映射區(qū)的開始位置和擴(kuò)展方向可能不同,因而Linux為每個(gè)虛擬地址空間提供了兩個(gè)操作,其中g(shù)et_unmapped_area用于在文件映射區(qū)中創(chuàng)建新的虛擬內(nèi)存區(qū)域,unmap_area用于釋放文件映射區(qū)中的虛擬內(nèi)存區(qū)域。8.2.2虛擬內(nèi)存區(qū)域操作

虛擬內(nèi)存區(qū)域是虛擬內(nèi)存管理所需的重要數(shù)據(jù)結(jié)構(gòu),雖不參與地址轉(zhuǎn)換,卻參與頁(yè)目錄、頁(yè)表項(xiàng)的建立,參與頁(yè)面的換入/換出,是對(duì)頁(yè)目錄、頁(yè)表的擴(kuò)充。要管理好進(jìn)程的虛擬地址空間,首要任務(wù)是管理好它的虛擬內(nèi)存區(qū)域,包括虛擬內(nèi)存區(qū)域的查找、建立、合并、拆分、擴(kuò)展、釋放等。

(1)區(qū)域查找操作find_vma。給定一個(gè)虛擬地址addr,find_vma操作會(huì)查出滿足條件addr<vm_end且地址最小的虛擬內(nèi)存區(qū)域,不要求vm_start<=addr。為了加快查找速度,find_vma先檢查上次找到的虛擬內(nèi)存區(qū)域(命中率可達(dá)35%),只有無法命中時(shí)才搜索紅黑樹。

(2)區(qū)域合并操作vma_merge。給出一個(gè)虛擬內(nèi)存區(qū)域,vma_merge操作試圖將其合并到相鄰的區(qū)域中。滿足如下條件的兩個(gè)區(qū)域可以合并:

①在虛擬地址空間中鄰接,即前一區(qū)域的vm_end等于后一區(qū)域的vm_start。

②具有相同的整體存取控制特性,即vm_flags相同。

③操作集中沒有close操作(有close操作的區(qū)域可能會(huì)被釋放)。④如果有映射文件,則映射文件必須相同且在文件中是鄰接的。

⑤如果是匿名區(qū)域,則必須屬于同一個(gè)匿名域(anon_vma相同)。

一個(gè)區(qū)域可能與它的前一個(gè)區(qū)域或后一個(gè)區(qū)域合并,也可能同時(shí)與前后兩個(gè)區(qū)域合并。合并操作會(huì)釋放被合并的區(qū)域。

(3)區(qū)域拆分操作split_vma。給定一個(gè)虛擬內(nèi)存區(qū)域[vm_start,vm_end-1]和區(qū)域中的一個(gè)虛擬地址addr,操作split_vma將該區(qū)域從addr處拆分成兩個(gè)相鄰的區(qū)域,即[vm_start,addr-1]和[addr,vm_end-1],兩個(gè)區(qū)域的屬性是相同的。

(4)空閑區(qū)間分配操作get_unmapped_area。給定一個(gè)長(zhǎng)度len和一個(gè)建議的開始位置addr,操作get_unmapped_area試圖在進(jìn)程的虛擬地址空間中找一塊足夠大的空閑區(qū)間,以便建立新的虛擬內(nèi)存區(qū)域。

如果建議位置addr處有足夠大的空閑虛擬地址區(qū)間,則選用該區(qū)間。否則,按照從高到低或從低到高的順序搜索進(jìn)程的虛擬內(nèi)存區(qū)域,找足夠大的空閑區(qū)間。為加快搜索速度,Linux在mm_struct結(jié)構(gòu)中記錄下了上次搜索的終止位置free_area_cache和曾遇到的最大空洞的尺寸cached_hole_size。如果所找區(qū)間的尺寸比cached_hole_size小,則從頭開始搜索,否則從free_area_cache處搜索即可。

(5)區(qū)域建立操作do_mmap_pgoff或do_mmap。給定參數(shù)addr、len、prot、flags、file和pgoff,區(qū)域建立操作試圖建立一個(gè)新的虛擬內(nèi)存區(qū)域[addr,addr+len-1]。如果參數(shù)中給出了映射文件file,新區(qū)域?qū)⒂成涞轿募^(qū)間[pgoff*4096,pgoff*4096+len-1]。區(qū)域建立操作的流程如下:

①根據(jù)參數(shù)對(duì)建立操作的合法性、安全性進(jìn)行檢查,對(duì)addr和len進(jìn)行規(guī)約(頁(yè)對(duì)齊),并確定區(qū)域的整體存取控制特性vm_flags,如讀、寫、執(zhí)行、共享、鎖定、增長(zhǎng)方向等。

②如果參數(shù)file不空,則根據(jù)參數(shù)flags確定文件的映射方式,如共享映射方式或私有映射方式。

③執(zhí)行操作get_unmapped_area,從進(jìn)程的虛擬地址空間中分配長(zhǎng)度為len的空閑虛擬內(nèi)存區(qū)間[addr,addr+len-1]。此處的addr可能與參數(shù)addr不同。④執(zhí)行操作vma_merge,試圖將新區(qū)域[addr,addr+len-1]合并到已有的虛擬內(nèi)存區(qū)域中。如合并成功,則不需要再建立新的區(qū)域,否則:

●申請(qǐng)一個(gè)vm_area_struct結(jié)構(gòu)并填寫其內(nèi)容。

●為新建的虛擬區(qū)域指定操作集vm_ops。文件映射區(qū)域的操作集通常為generic_file_vm_ops,匿名共享映射區(qū)域的操作集通常為shm_vm_ops,匿名私有映射區(qū)域的操作集通常為空。●將新建的虛擬內(nèi)存區(qū)域插入到合適的隊(duì)列或樹中。

⑤如果區(qū)域的內(nèi)容是鎖定的(該區(qū)域的所有頁(yè)必須總在內(nèi)存中),則通過模擬的頁(yè)故障異常將該區(qū)域的所有頁(yè)都讀入內(nèi)存。

(6)區(qū)域釋放操作do_munmap。操作do_munmap試圖釋放進(jìn)程虛擬地址空間中的一個(gè)區(qū)間[start,start+len-1]。釋放之后,進(jìn)程不能再訪問該區(qū)間中的虛擬地址。對(duì)區(qū)間的釋放可能會(huì)遇到以下幾種情況:

①區(qū)間覆蓋某個(gè)區(qū)域,將整個(gè)區(qū)域釋放即可。

②區(qū)間位于某區(qū)域的前部,將區(qū)域拆分成兩個(gè),釋放前一個(gè),留下后一個(gè)。③區(qū)間位于某區(qū)域的后部,將區(qū)域拆分成兩個(gè),釋放后一個(gè),留下前一個(gè)。

④區(qū)間位于某區(qū)域的中部,將區(qū)域拆分成三個(gè),僅釋放中間的區(qū)域。

所釋放的區(qū)間可能不在任何區(qū)域中,可能覆蓋某區(qū)域的一部分,可能覆蓋整個(gè)區(qū)域,也可能覆蓋多個(gè)區(qū)域。在區(qū)間覆蓋的多個(gè)區(qū)域中,可能包括前一個(gè)區(qū)域的后部、后一個(gè)區(qū)域的前部和中間多個(gè)完整的區(qū)域。釋放一個(gè)虛擬內(nèi)存區(qū)域包括釋放該區(qū)域的所有虛擬頁(yè),即斷開各虛擬頁(yè)與物理頁(yè)的映射關(guān)系,并釋放各物理頁(yè)。如果釋放操作導(dǎo)致某些頁(yè)表全部變空,這些頁(yè)表也要被釋放。當(dāng)然,被釋放區(qū)域的vm_area_struct結(jié)構(gòu)要從各種隊(duì)列或樹中摘除并釋放。

(7)堆棧區(qū)域擴(kuò)展操作expand_stack。一般情況下,堆棧區(qū)域允許向下擴(kuò)展。當(dāng)處理器訪問的堆棧地址addr小于堆棧區(qū)域的vm_start時(shí),應(yīng)該向下擴(kuò)展堆棧區(qū)域,使其涵蓋地址addr。當(dāng)然,addr不能離vm_start太遠(yuǎn),且用戶堆棧的大小不能超限。

(8)堆區(qū)域的調(diào)整操作sys_brk。堆(heap)是用戶虛擬地址空間的一部分。進(jìn)程在執(zhí)行過程中動(dòng)態(tài)申請(qǐng)的內(nèi)存(如malloc)都位于自己的堆中。用戶內(nèi)存管理器負(fù)責(zé)堆空間的管理,它從進(jìn)程虛擬內(nèi)存管理器申請(qǐng)一大塊空間而后再分割成小塊分配給用戶。當(dāng)發(fā)現(xiàn)堆空間緊張時(shí)用戶內(nèi)存管理器會(huì)請(qǐng)求擴(kuò)展堆的大小,當(dāng)發(fā)現(xiàn)堆空間空閑時(shí)會(huì)請(qǐng)求收縮堆的大小。堆尺寸的調(diào)整由操作sys_brk完成。

堆通常在加載執(zhí)行程序時(shí)建立,其位置記錄在mm_struct結(jié)構(gòu)中,位于start_brk和brk之間。Linux用一個(gè)獨(dú)立的虛擬內(nèi)存區(qū)域描述進(jìn)程的堆。操作sys_brk的主要作用是調(diào)整堆區(qū)域的終止位置brk。如果新位置小于老位置(收縮),則釋放新老位置之間的虛擬內(nèi)存;如果新位置大于老位置(擴(kuò)展),則為新增空間建立一個(gè)新的虛擬內(nèi)存區(qū)域,并將其與老的堆區(qū)域合并。

在創(chuàng)建之初,進(jìn)程的虛擬地址空間是從創(chuàng)建者進(jìn)程中復(fù)制的(CopyonWrite方式),甚至可能是與創(chuàng)建者進(jìn)程共用的(如vfork方式)。因而,在新進(jìn)程第一次運(yùn)行時(shí),它與創(chuàng)建者進(jìn)程執(zhí)行同樣的程序且使用同樣的數(shù)據(jù)和堆棧,唯一的區(qū)別是fork()函數(shù)的返回值。8.3虛擬地址空間建立在運(yùn)行過程中,如果希望改變自己的行為,進(jìn)程可以通過execve()類的系統(tǒng)調(diào)用加載新的可執(zhí)行程序,重建自己的虛擬地址空間,即用新的程序、數(shù)據(jù)、堆棧替換老的程序、數(shù)據(jù)、堆棧,從而讓進(jìn)程使用新的數(shù)據(jù)和堆棧,執(zhí)行新的程序。

統(tǒng)計(jì)表明,程序(尤其是大型程序)中的許多代碼(如各類錯(cuò)誤處理代碼)通常都不會(huì)被執(zhí)行到,因而一次性地將程序全部裝入內(nèi)存是一種極為低效的加載方式,它既延長(zhǎng)了加載時(shí)間,又浪費(fèi)了內(nèi)存空間。Linux采用一種較為懶惰的加載方法,它僅建立進(jìn)程虛擬地址空間與可執(zhí)行文件的映射關(guān)系,即一組虛擬內(nèi)存區(qū)域(vm_area_struct結(jié)構(gòu)),將真正的裝入工作推遲到實(shí)際使用時(shí),何時(shí)使用就何時(shí)裝入,用到多少就裝入多少。

即使采用懶惰加載方法,仍然有可能造成內(nèi)存空間的浪費(fèi)。假如多個(gè)程序都調(diào)用了同一個(gè)庫(kù)函數(shù),那么該庫(kù)函數(shù)就有可能被多個(gè)進(jìn)程多次加載,占用多份物理內(nèi)存空間。為解決這一問題,Linux引入了動(dòng)態(tài)鏈接(DynamicLink)機(jī)制,將程序的鏈接工作也推遲到真正運(yùn)行時(shí),何時(shí)調(diào)用就何時(shí)鏈接,調(diào)用哪個(gè)函數(shù)就解析并加載哪個(gè)函數(shù),且允許將一個(gè)共享庫(kù)文件同時(shí)映射到多個(gè)進(jìn)程的虛擬地址空間中。采用動(dòng)態(tài)鏈接的可執(zhí)行程序中不含庫(kù)函數(shù)的內(nèi)容,僅有庫(kù)函數(shù)的名稱和鏈接指示。

當(dāng)然,進(jìn)程也可以根據(jù)需要將數(shù)據(jù)文件映射到自己的虛擬地址空間中,以便直接訪問文件的內(nèi)容;可以在運(yùn)行過程中擴(kuò)充或收縮虛擬內(nèi)存區(qū)域。因而,進(jìn)程的虛擬地址空間是在進(jìn)程創(chuàng)建時(shí)復(fù)制的,在程序加載時(shí)重建的,在運(yùn)行過程中動(dòng)態(tài)變化的。8.3.1可執(zhí)行文件

進(jìn)程在運(yùn)行過程中加載的程序稱為可執(zhí)行程序,保存可執(zhí)行程序的文件稱為可執(zhí)行文件。Linux的可執(zhí)行文件大致可分為四類:

(1)靜態(tài)鏈接的二進(jìn)制文件。這類文件是自包含的,其中的程序已經(jīng)被編譯器轉(zhuǎn)化成機(jī)器指令,程序運(yùn)行需要的所有庫(kù)函數(shù)都已被合并到文件中,將它加載到進(jìn)程的虛擬地址空間即可開始執(zhí)行。

(2)動(dòng)態(tài)鏈接的二進(jìn)制文件。這類文件中的程序已經(jīng)被編譯器轉(zhuǎn)化成機(jī)器指令,但它所引用的庫(kù)函數(shù)并未被合并在一起,在執(zhí)行這類程序的過程中需要不斷地加載共享庫(kù)并解析對(duì)庫(kù)函數(shù)的符號(hào)引用。

(3)腳本文件。這類文件是具有特定格式的文本文件,其意義需要專門的解釋器來解釋。加載腳本文件實(shí)際上是加載它的解釋器程序,執(zhí)行腳本文件也就是執(zhí)行它的解釋器程序,腳本文件的名稱僅僅是傳遞給解釋器的參數(shù)之一。

(4)混雜文件。這類文件中的程序已經(jīng)被編譯器轉(zhuǎn)化成了某種中間代碼,但還不能直接加載執(zhí)行,需要專門的解釋器對(duì)其進(jìn)行解釋或進(jìn)一步轉(zhuǎn)化,如Java字節(jié)碼或Windows執(zhí)行文件。加載混雜文件實(shí)際上是加載它的解釋器程序。

腳本文件和混雜文件的解釋器也是經(jīng)過編譯、鏈接的二進(jìn)制文件,因而上述四類可執(zhí)行文件的加載實(shí)質(zhì)是一樣的,即根據(jù)二進(jìn)制的可執(zhí)行文件重建進(jìn)程的虛擬地址空間。當(dāng)然,可執(zhí)行文件是有格式的,不同格式的可執(zhí)行文件有不同的加載和執(zhí)行方法。加載的首要問題是理解可執(zhí)行文件的格式。在系統(tǒng)初始化時(shí),Linux為它支持的每一種可執(zhí)行文件格式都準(zhǔn)備了一個(gè)linux_binfmt結(jié)構(gòu),并已將其注冊(cè)在formats隊(duì)列中,其中的操作load_binary用于加載特定格式的可執(zhí)行文件,load_shlib用于加載特定格式的共享庫(kù)文件。區(qū)分可執(zhí)行文件格式的方法大致有兩種,一是位于文件頭部的魔數(shù)(magicnumber),二是文件的擴(kuò)展名。

Linux支持的可執(zhí)行文件格式有ELF(ExecutableandLinkableFormat)、a.out(AssemblerOutputFormat,ELF之前的標(biāo)準(zhǔn)格式)、script(腳本)、misc(混雜格式,如Java二進(jìn)制代碼)等,其中ELF是Linux采用的標(biāo)準(zhǔn)可執(zhí)行文件格式。

ELF是一種開放的標(biāo)準(zhǔn),Linux的目標(biāo)文件(編譯后的中間文件)、可執(zhí)行文件、共享庫(kù)文件、內(nèi)核模塊文件甚至Linux內(nèi)核本身的映像文件都采用ELF格式。ELF格式的文件可用于鏈接、加載和動(dòng)態(tài)鏈接。

ELF格式的文件中包含若干個(gè)節(jié)(section),如.text、.data、.bss、.got、.plt、.dynamic等,每個(gè)節(jié)都由一個(gè)節(jié)頭結(jié)構(gòu)Elf32_Shdr描述,所有的節(jié)頭結(jié)構(gòu)組成一個(gè)數(shù)組,稱為節(jié)頭表。節(jié)頭中包含節(jié)的名稱、類型、在文件中的位置和大小、在虛擬地址空間中的位置等屬性信息。節(jié)中的信息主要用于鏈接。

用于加載的ELF格式的文件(包括可執(zhí)行文件和共享庫(kù)文件)中包含若干個(gè)加載段(segment),如.text、.data、.interp、

.dynamic等,每個(gè)段都由一個(gè)程序頭結(jié)構(gòu)elf_phdr描述,所有的程序頭結(jié)構(gòu)組成一個(gè)數(shù)組,稱為程序頭表。程序頭中包含段的類型、在文件中的位置和大小、在虛擬地址空間中的位置和大小等屬性信息,涵蓋了vm_area_struct結(jié)構(gòu)的主要內(nèi)容。32位系統(tǒng)中的程序頭由結(jié)構(gòu)elf32_phdr描述,其定義如下:

typedefstructelf32_phdr{

Elf32_Word p_type; //類型,如PT_LOAD、PT_INTERP等

Elf32_Off p_offset; //在文件中的開始位置

Elf32_Addr p_vaddr; //在虛擬地址空間中的開始位置

Elf32_Addr p_paddr; //在物理地址空間中的開始位置

Elf32_Word p_filesz; //在文件中的長(zhǎng)度

Elf32_Word p_memsz; //在虛擬地址空間中的長(zhǎng)度

Elf32_Word p_flags; //存取權(quán)限,如PF_R、PF_W、PF_X等

Elf32_Word p_align; //段在文件和內(nèi)存中的對(duì)齊方式

}Elf32_Phdr;

ELF格式文件的頭部是一個(gè)文件頭elfhdr,用于描述整個(gè)文件的組織結(jié)構(gòu),包括文件的類型、版本、適用的機(jī)型、程序頭表的位置和大小、節(jié)頭表的位置和大小、程序入口的虛擬地址等。通過文件頭可以找到它的程序頭表和節(jié)頭表,進(jìn)而找到所有的加載段和節(jié)。通常情況下,一個(gè)ELF格式的可執(zhí)行文件被分成三大部分,前部是只讀的,中部是可寫的,后部是不需要加載的,如圖8.5所示。

圖8.5ELF文件的格式

ELF格式可執(zhí)行文件的前部由文件頭、程序頭表和若干個(gè)只讀的節(jié)組成,中部由若干個(gè)可讀、可寫的節(jié)組成,后部由注釋、串表、符號(hào)表等節(jié)組成。前、中兩部分各由一個(gè)PT_LOAD類型的程序頭描述,都將在加載時(shí)被映射到進(jìn)程的虛擬地址空間。其余的程序頭用于描述加載段中的特殊區(qū)間,如類型為PT_INTERP的程序頭所描述的段中記錄著動(dòng)態(tài)鏈接器文件的路徑名,類型為PT_DYNAMIC的程序頭所描述的段中記錄著程序的動(dòng)態(tài)鏈接信息。8.3.2加載函數(shù)

Linux的標(biāo)準(zhǔn)函數(shù)庫(kù)中提供了多個(gè)用于加載的函數(shù),如execl()、execle()、execlp()、execv()、execve()、execvp()等,這些函數(shù)接受參數(shù)的格式不同,但處理的方式大致相同,即將參數(shù)轉(zhuǎn)化成統(tǒng)一的格式,而后進(jìn)入內(nèi)核(系統(tǒng)調(diào)用),執(zhí)行函數(shù)sys_execve()。

函數(shù)sys_execve()僅接受三個(gè)參數(shù),分別是可執(zhí)行程序的文件名filename、傳遞給新程序的初始參數(shù)argv和環(huán)境變量envp,其處理過程如下:

(1)為當(dāng)前進(jìn)程選擇一個(gè)負(fù)載最輕的處理器。若所選的不是當(dāng)前處理器,則將當(dāng)前進(jìn)程遷移到新處理器上。由于當(dāng)前進(jìn)程正重建虛擬地址空間,它在當(dāng)前處理器上的緩存即將失效,此時(shí)進(jìn)行負(fù)載平衡對(duì)它的性能影響最小。

(2)申請(qǐng)1頁(yè)內(nèi)存,將可執(zhí)行文件名拷貝到內(nèi)核中。

(3)如果當(dāng)前進(jìn)程與其它進(jìn)程共享同一個(gè)files_struct結(jié)構(gòu),則為其新建一個(gè),內(nèi)容從老結(jié)構(gòu)中復(fù)制,包括文件描述符表。

(4)為進(jìn)程準(zhǔn)備一個(gè)新的證書結(jié)構(gòu)cred,根據(jù)可執(zhí)行文件的屬性(如SETUID等)確定它的euid、egid、cap_permitted等安全標(biāo)識(shí)。

(5)創(chuàng)建一個(gè)新的mm_struct結(jié)構(gòu)用以描述進(jìn)程的新虛擬地址空間,為其創(chuàng)建一個(gè)新的頁(yè)目錄(內(nèi)核部分從swapper_pg_dir中拷貝)和一個(gè)匿名的虛擬內(nèi)存區(qū)域(位置在3GB以下,用于描述新的用戶堆棧)。

(6)在新虛擬地址空間的[3G-x,3G)處創(chuàng)建新的用戶堆棧,為其分配物理頁(yè),將可執(zhí)行文件名、環(huán)境變量、初始參數(shù)等拷貝到內(nèi)核并按順序壓入新建的用戶堆棧。其中x是可執(zhí)行文件名、環(huán)境變量、初始參數(shù)等的長(zhǎng)度。

(7)按只讀方式打開可執(zhí)行文件,讀出它的前128個(gè)字節(jié)(文件頭)。

(8)遍歷系統(tǒng)中的可執(zhí)行文件格式隊(duì)列formats,順序執(zhí)行各結(jié)構(gòu)中的load_binary操作,讓它們識(shí)別已讀入的文件頭。

①如果某個(gè)load_binary操作能識(shí)別文件頭,它將根據(jù)可執(zhí)行文件建立進(jìn)程的虛擬地址空間,完成可執(zhí)行文件的加載工作。

②如果所有的load_binary操作都不能識(shí)別文件頭,則加載失敗。

顯然,真正的加載工作是由可執(zhí)行文件格式中的load_binary操作完成的。8.3.3ELF文件加載

Linux為ELF格式的可執(zhí)行文件注冊(cè)的二進(jìn)制格式是elf_format,其中的可執(zhí)行文件加載操作是load_elf_binary。ELF文件的加載思路較為直觀,即為每一個(gè)PT_LOAD類型的加載段創(chuàng)建一個(gè)虛擬內(nèi)存區(qū)域。

對(duì)動(dòng)態(tài)鏈接的可執(zhí)行程序,除自身的加載段之外,還需要額外加載一個(gè)動(dòng)態(tài)鏈接器,用于共享庫(kù)的加載和全局符號(hào)的解析。動(dòng)態(tài)鏈接器也是一個(gè)ELF格式的可執(zhí)行文件,但允許重定位。如果可執(zhí)行程序采用動(dòng)態(tài)鏈接,那么它的可執(zhí)行文件中肯定有一個(gè)類型為PT_INTERP的加載段,其內(nèi)容是動(dòng)態(tài)鏈接器文件的路徑名,如/lib/ld-linux.so.2。

ELF格式的可執(zhí)行文件的加載過程如下:

(1)將函數(shù)sys_execve讀入的文件頭轉(zhuǎn)化成一個(gè)ELF格式的文件頭,對(duì)其進(jìn)行合法性檢查。如果魔數(shù)、類型、機(jī)器型號(hào)等不符,則加載失敗。

(2)申請(qǐng)一塊內(nèi)存空間,將所有的程序頭全部讀入內(nèi)存。

(3)如果程序是動(dòng)態(tài)鏈接的,則從類型為PT_INTERP的加載段中獲取動(dòng)態(tài)鏈接器的路徑名。打開動(dòng)態(tài)鏈接器文件,將它的文件頭也讀入內(nèi)存,并對(duì)其進(jìn)行合法性檢查,如魔數(shù)、類型、機(jī)器型號(hào)、長(zhǎng)度、權(quán)限等不符,則加載失敗。

(4)如果當(dāng)前進(jìn)程在一個(gè)線程組中,那么它與該組中的其它進(jìn)程在共享當(dāng)前的虛擬地址空間,加載新的可執(zhí)行程序必然導(dǎo)致同組中的其它進(jìn)程無法正常運(yùn)行,因而應(yīng)向它們發(fā)送SIGKILL信號(hào)(將其全部殺死),并等待它們終止。如當(dāng)前進(jìn)程不是線程組的領(lǐng)頭進(jìn)程,還要將它轉(zhuǎn)化成領(lǐng)頭進(jìn)程。

(5)關(guān)閉進(jìn)程上的所有時(shí)間間隔定時(shí)器。

(6)如果當(dāng)前進(jìn)程與其它進(jìn)程共用一個(gè)sighand_struct結(jié)構(gòu),則要為其復(fù)制一個(gè)新的,并將其中所有不是SIG_IGN(忽略)的處理程序都換成SIG_DFL(缺省)。

(7)如果當(dāng)前進(jìn)程是按vfork方式創(chuàng)建的,那么創(chuàng)建者進(jìn)程肯定還在當(dāng)前進(jìn)程的vfork_done隊(duì)列上睡眠等待,需將其喚醒。

(8)將當(dāng)前進(jìn)程的虛擬內(nèi)存管理結(jié)構(gòu)換成在sys_execve()中新建的mm_struct,將當(dāng)前處理器的CR3換成新的頁(yè)目錄,而后遞減老的mm_struct結(jié)構(gòu)上的引用計(jì)數(shù)。如果老mm_struct結(jié)構(gòu)的引用計(jì)數(shù)被減到0,則逐個(gè)釋放它的所有虛擬內(nèi)存區(qū)域及區(qū)域中的虛擬頁(yè),并釋放它的頁(yè)表、頁(yè)目錄、LDT等,最后釋放老的mm_struct結(jié)構(gòu),從而釋放進(jìn)程的老虛擬地址空間。

(9)確定進(jìn)程新虛擬地址空間的布局方案,主要是確定其中的文件映射區(qū)開始位置mmap_base及操作get_unmapped_area與unmap_area的實(shí)現(xiàn)函數(shù)。

(10)將新程序的名稱記錄在當(dāng)前進(jìn)程task_struct結(jié)構(gòu)中的comm域中。

(11)清除當(dāng)前進(jìn)程中thread_struct結(jié)構(gòu)(現(xiàn)場(chǎng))中的調(diào)試信息和FPU信息。

(12)關(guān)閉當(dāng)前進(jìn)程中所有應(yīng)該在加載時(shí)關(guān)閉的文件,這些文件記錄在進(jìn)程文件描述符表的位圖close_on_exec中。

(13)將用戶堆棧下移若干頁(yè),在棧底留出一個(gè)隨機(jī)大小的空洞。根據(jù)新堆棧的位置、大小、權(quán)限等,調(diào)整它的vm_area_struct結(jié)構(gòu)及各堆棧頁(yè)所對(duì)應(yīng)的頁(yè)目錄、頁(yè)表項(xiàng),從而將新的用戶堆棧加入到進(jìn)程的新虛擬地址空間中。一般情況下,用戶堆棧區(qū)域是向下增長(zhǎng)的、匿名的私有區(qū)域,可讀、可寫但不可執(zhí)行。再將用戶堆棧區(qū)域向下擴(kuò)展20頁(yè),為堆棧的增長(zhǎng)留出余地。

(14)順序搜索程序頭表,根據(jù)其中的參數(shù)(p_offset、p_filesz、p_vaddr、p_flags等)為每一個(gè)PT_LOAD類型的加載段創(chuàng)建一個(gè)虛擬內(nèi)存區(qū)域。注意:①區(qū)域的大小以加載段在文件中的長(zhǎng)度為準(zhǔn),且被規(guī)約成頁(yè)的倍數(shù),因而可能比p_filesz大。

②區(qū)域在文件中的開始位置必須是頁(yè)的邊界,因而可能比p_offset小。

③區(qū)域在虛擬地址空間中的開始地址必須是p_vaddr&0xFFFFF000,因?yàn)榭蓤?zhí)行程序中的地址都是絕對(duì)的,不能重定位。

④區(qū)域的映射方式為私有的,進(jìn)程對(duì)它們的修改不能直接寫回文件。⑤區(qū)域的讀、寫、執(zhí)行權(quán)限由加載段的p_flags決定。

⑥由于頁(yè)對(duì)齊的原因,前一個(gè)區(qū)域的尾部通常包含著后一個(gè)區(qū)域的頭部,后一個(gè)區(qū)域的頭部通常包含著前一個(gè)區(qū)域的尾部,如圖8.5所示。

(15)如果可執(zhí)行文件中有.bss節(jié),它肯定位于最后一個(gè)加載段的尾部。.bss節(jié)占用虛擬地址空間,大小是(p_memsz-p_filesz),但不占用文件空間。如果最后一個(gè)區(qū)域不能完全涵蓋.bss節(jié),則為它的剩余部分建立一個(gè)匿名的虛擬內(nèi)存區(qū)域(可讀、可寫但沒有操作集),稱為初始堆區(qū)域,如圖8.6所示。將.bss節(jié)所用內(nèi)存全部清0。

(16)如果可執(zhí)行程序是動(dòng)態(tài)鏈接的,則需要加載鏈接器。常用的鏈接器文件是/lib/ld-linux.so.2,類型為ET_DYN,是一個(gè)ELF格式的可重定位文件。

①申請(qǐng)一塊內(nèi)存空間,將鏈接器文件的所有程序頭全部讀入內(nèi)存。

②順序搜索程序頭表,根據(jù)其中的參數(shù)(p_offset、p_filesz、p_vaddr、p_flags等)為每一個(gè)PT_LOAD類型的加載段創(chuàng)建一個(gè)虛擬內(nèi)存區(qū)域。注意:

●由于鏈接器程序中的地址是可重定位的,因而程序頭中的p_vaddr僅是一個(gè)建議地址,真正的開始地址vaddr位于進(jìn)程的文件映射區(qū)中,是臨時(shí)分配的。但必須保證各區(qū)域的偏移量vaddr-p_vaddr是相同的?!駞^(qū)域的映射方式為私有的,進(jìn)程對(duì)它們的修改不能直接寫回文件。

●如果鏈接器程序中有.bss節(jié),且最后一個(gè)區(qū)域無法將其完全涵蓋,則為剩余的.bss部分建立一個(gè)匿名區(qū)域(初始堆)。

③釋放鏈接器文件的程序頭,關(guān)閉動(dòng)態(tài)鏈接器文件,釋放其文件頭。

圖8.6可執(zhí)行文件與虛擬內(nèi)存區(qū)域的映射

(17)為共用的vsyscall頁(yè)創(chuàng)建一個(gè)專門的虛擬內(nèi)存區(qū)域,其大小為1頁(yè),可讀、可執(zhí)行,但不許修改,區(qū)域的操作集是special_mapping_vmops。在新版本中,vsyscall頁(yè)位于用戶虛擬地址空間中,其位置是動(dòng)態(tài)分配的。

(18)將進(jìn)程的安全證書換成在函數(shù)sys_execve()中新建的cred結(jié)構(gòu)。

(19)將ELF格式可執(zhí)行文件的特殊信息保存在當(dāng)前進(jìn)程的mm_struct結(jié)構(gòu)中(saved_auxv數(shù)組),包括AT_SYSINFO中的vsyscall入口地址、AT_BASE中的鏈接器程序的入口地址、AT_ENTRY中的新程序入口地址、AT_PHDR中的程序頭表位置、AT_PHNUM中的程序頭個(gè)數(shù)等。

(20)在用戶堆棧的棧頂壓入處理器型號(hào)、16字節(jié)的PRNG種子、saved_auxv數(shù)組、指向各環(huán)境變量的指針env、指向各參數(shù)的指針argv、argc等。真正的環(huán)境變量和參數(shù)在用戶堆棧的棧底處,此處構(gòu)造的是指針數(shù)組env和argv。

(21)在mm_struct結(jié)構(gòu)中記錄新程序的位置信息,如代碼的開始與終止位置、數(shù)據(jù)的開始與終止位置、用戶堆棧的開始位置、堆的終止位置、參數(shù)的開始和終止位置、環(huán)境變量的開始和終止位置等。

(22)將系統(tǒng)堆棧棧頂中的bx、cx、dx、si、di、bp、ax、gs、fs清0,并將其中的ds、es、ss設(shè)成用戶數(shù)據(jù)段、將cs設(shè)成用戶代碼段,sp設(shè)成新用戶堆棧的棧頂。如果可執(zhí)行程序是靜態(tài)鏈接的,將棧頂中的ip設(shè)為可執(zhí)行程序的入口;如果可執(zhí)行程序是動(dòng)態(tài)鏈接的,將棧頂中的ip設(shè)為鏈接器的入口。

(23)釋放可執(zhí)行程序的程序頭、文件頭等,返回。

如果加載成功,函數(shù)load_elf_binary的返回將導(dǎo)致函數(shù)sys_execve()的返回,并進(jìn)而導(dǎo)致進(jìn)程彈出系統(tǒng)堆棧的棧頂,從核心態(tài)返回用戶態(tài)。返回用戶態(tài)的進(jìn)程將面對(duì)一個(gè)全新的虛擬內(nèi)存空間,使用新的用戶堆棧,開始新程序的執(zhí)行。8.3.4動(dòng)態(tài)鏈接器初始化

靜態(tài)鏈接的可執(zhí)行程序被成功加載之后會(huì)直接執(zhí)行,但動(dòng)態(tài)鏈接的可執(zhí)行程序還不能直接執(zhí)行,原因是程序?qū)?kù)函數(shù)等的引用還未被解析。為解析可執(zhí)行程序中的符號(hào)引用,需要先執(zhí)行動(dòng)態(tài)鏈接器程序。

雖然動(dòng)態(tài)鏈接器可以一次性地解析可執(zhí)行程序中的所有符號(hào)引用(將符號(hào)替換成虛擬地址),實(shí)現(xiàn)可執(zhí)行程序的加載時(shí)鏈接。但較好的做法是將解析工作推遲到真正引用時(shí),即在程序第一次引用符號(hào)時(shí)才對(duì)其進(jìn)行解析。當(dāng)然,為了實(shí)現(xiàn)符號(hào)的動(dòng)態(tài)解析(或動(dòng)態(tài)鏈接),需要?jiǎng)討B(tài)鏈接器完成一些前期的準(zhǔn)備工作(預(yù)先運(yùn)行的原因),包括:

(1)完成動(dòng)態(tài)鏈接器自身的重定位。動(dòng)態(tài)鏈接器的重定位表.rel.dyn和.rel.plt中記錄著所有需重定位的位置及重定位的方法,所謂重定位就是將這些位置的地址轉(zhuǎn)換成實(shí)際加載位置的虛擬地址。

(2)從棧頂取出參數(shù)argc、argv、envp及新加載程序的程序頭、入口地址、堆等位置信息。分析環(huán)境變量,確定新程序的鏈接方式(加載時(shí)鏈接還是執(zhí)行時(shí)鏈接)、共享庫(kù)的搜索路徑等。

(3)加載新程序需要的共享庫(kù)。新程序需要的共享庫(kù)記錄在它的.dynamic節(jié)中,其中的每個(gè)DT_NEEDED項(xiàng)描述一個(gè)共享庫(kù)。一個(gè)可執(zhí)行程序可能需要引用多個(gè)共享庫(kù),因而可能有多個(gè)DT_NEEDED項(xiàng)。當(dāng)然,被加載的共享庫(kù)可能還需要引用其它的共享庫(kù),因而共享庫(kù)的加載過程是遞歸的。在加載共享庫(kù)的過程中,會(huì)按私有映射方式創(chuàng)建多個(gè)虛擬內(nèi)存區(qū)域,以記錄共享庫(kù)文件與進(jìn)程虛擬內(nèi)存之間的映射關(guān)系。

(4)當(dāng)把程序、動(dòng)態(tài)鏈接器、共享庫(kù)全部加載進(jìn)來之后,按依賴關(guān)系的逆序?qū)蚕韼?kù)和新程序進(jìn)行重定位,主要內(nèi)容包括:①解析.got表中各全局?jǐn)?shù)據(jù)符號(hào)的目的地址(仍記錄在.got表中);

②將.got.plt的第0項(xiàng)設(shè)為自己的.dynamic節(jié)的開始地址;

③將.got.plt的第1項(xiàng)設(shè)為自己的鏈接映射結(jié)構(gòu)link_map的開始地址;

④將.got.plt的第2項(xiàng)設(shè)為動(dòng)態(tài)鏈接器中符號(hào)解析函數(shù)_dl_runtime_resolve()的入口地址;

⑤將.got.plt的其余各項(xiàng)設(shè)為它們?cè)谧约?plt中的入口地址+6。在新程序中,需要重定位的位置都在節(jié).got和.got.plt中,重定位過程不需要搜索和修改全部虛擬地址空間,也不需要將新程序真正讀入內(nèi)存。

(5)跳轉(zhuǎn)到新程序的入口_start,再完成一些初始化工作,如在數(shù)組_

_exit_funcs中注冊(cè)退出程序等,而后執(zhí)行下列程序:

result=main(argc,argv,_

_environMAIN_AUXVEC_PARAM);

exit(result);

其中的main是新加載程序的真正入口,因而新程序?qū)膍ain開始執(zhí)行。在新程序的執(zhí)行過程中,如遇到了exit()函數(shù),進(jìn)程將在該函數(shù)處終止,如未遇到exit()函數(shù),進(jìn)程將在main()函數(shù)正常返回后執(zhí)行函數(shù)exit()以便終止整個(gè)進(jìn)程。8.3.5ELF格式動(dòng)態(tài)鏈接

動(dòng)態(tài)鏈接器在做完前期的準(zhǔn)備工作之后,開始執(zhí)行新加載的應(yīng)用程序,但并未解析新程序?qū)蚕韼?kù)中各函數(shù)名的引用。事實(shí)上,解析工作被推遲到了真正引用時(shí)。

應(yīng)用程序所引用的外界符號(hào)可大致分為兩類,一是對(duì)外地全局?jǐn)?shù)據(jù)(如全局變量)的引用,二是對(duì)外地全局函數(shù)的調(diào)用。引用或調(diào)用的位置分布在整個(gè)可執(zhí)行程序中。為了便于管理,ELF格式的可執(zhí)行文件中定義了兩個(gè)節(jié).got和.got.plt,合稱全局偏移表GOT(GlobalOffsetTable),專門用于集中存放可執(zhí)行程序引用的各全局符號(hào)的目的地址。全局?jǐn)?shù)據(jù)符號(hào)的目的地址記錄在.got節(jié)中,全局函數(shù)符號(hào)的目的地址記錄在.got.plt節(jié)中。編譯器已將對(duì)全局?jǐn)?shù)據(jù)符號(hào)的直接引用轉(zhuǎn)化為對(duì)GOT表中相應(yīng)項(xiàng)的間接引用。在動(dòng)態(tài)鏈接器初始化時(shí),已解析出各全局?jǐn)?shù)據(jù)符號(hào)的目的地址,并已將其填入GOT表中,使對(duì)全局?jǐn)?shù)據(jù)符號(hào)的間接引用可訪問到真正的數(shù)據(jù)存儲(chǔ)位置。

為了動(dòng)態(tài)解析對(duì)外地全局函數(shù)的調(diào)用,在ELF格式的可執(zhí)行文件中另外定義了一個(gè).plt節(jié),稱為過程鏈接表PLT(procedurelinkagetable)。每個(gè)外地全局函數(shù)符號(hào)在PLT中都有一個(gè)對(duì)應(yīng)項(xiàng),其中包含由三條指令構(gòu)成的程序片段,如下(寄存器ebx中的內(nèi)容是.got.plt節(jié)的開始地址):

PLT0:pushl 4(%ebx) //將link_map結(jié)構(gòu)的地址壓入堆棧

jmp *8(%ebx) //跳轉(zhuǎn)到函數(shù)_dl_runtime_resolve()

PLTn:jmp *fn@GOT(%ebx) //fn在.got.plt節(jié)中的地址

pushl $offset //fn在.rel.plt節(jié)中的偏移量

jmp PLT0@PC //跳轉(zhuǎn)到PLT0

編譯器已將對(duì)全局函數(shù)fn的調(diào)用轉(zhuǎn)化成對(duì)PLTn的調(diào)用。

初始情況下,.got.plt節(jié)的第0項(xiàng)是它的.dynamic節(jié)的地址,第1項(xiàng)是自身的鏈接映射結(jié)構(gòu)link_map的地址,第2項(xiàng)是符號(hào)解析函數(shù)_dl_runtime_resolve()的入口地址。函數(shù)fn在.got.plt節(jié)中的地址是PLTn+6,即緊接在PLTn之后的指令pushl$offset的地址。當(dāng)應(yīng)用程序首次調(diào)用函數(shù)fn時(shí),PLT中的程序片段將fn在.rel.plt節(jié)中的偏移量offset((offset/8)*4+12是fn在.got.plt節(jié)中的真正偏移量)和應(yīng)用程序自身的link_map結(jié)構(gòu)的地址壓入堆棧,而后跳轉(zhuǎn)到_dl_runtime_resolve(),進(jìn)行符號(hào)解析。函數(shù)_dl_runtime_resolve()搜索各共享庫(kù)中的符號(hào)表(.symtab節(jié)),獲得符號(hào)fn的虛擬地址fnaddr,用該地址替換函數(shù)fn在.got.plt節(jié)中的地址,而后修改堆棧,返回到地址fnaddr處,從而執(zhí)行函數(shù)fn。此后,當(dāng)應(yīng)用程序再次調(diào)用函數(shù)fn時(shí),PLTn會(huì)將其直接轉(zhuǎn)到fnaddr,不需要再次進(jìn)行符號(hào)解析。如圖8.7所示。圖8.7符號(hào)的動(dòng)態(tài)解析

新建進(jìn)程的虛擬地址空間是按CopyonWrite方式從創(chuàng)建者進(jìn)程中復(fù)制的,屬于共享的虛擬地址空間,試圖向其中寫數(shù)據(jù)的操作必然會(huì)違反頁(yè)表的保護(hù)約定,導(dǎo)致處理器產(chǎn)生頁(yè)故障(Pagefault)異常。

在進(jìn)程剛加載完新程序之后,它的虛擬地址空間幾乎是空的。加載操作僅在虛擬地址空間與可執(zhí)行文件(包括共享庫(kù)文件)之間建立了映射關(guān)系,并未真正將文件的內(nèi)容讀入內(nèi)存。進(jìn)程對(duì)虛擬地址空間的讀、寫、執(zhí)行操作都會(huì)引起頁(yè)故障異常。8.4頁(yè)?故?障?處?理當(dāng)然,進(jìn)程執(zhí)行過程中的非法內(nèi)存訪問,如試圖存取內(nèi)核數(shù)據(jù)或訪問無效虛擬地址等,也會(huì)引起頁(yè)故障異常。

頁(yè)故障異常是處理器提供給操作系統(tǒng)的重要管理機(jī)制,是虛擬內(nèi)存得以實(shí)現(xiàn)的基礎(chǔ)。如果沒有頁(yè)故障異常,諸如寫時(shí)復(fù)制、按需加載、內(nèi)存保護(hù)等機(jī)制都將無法實(shí)現(xiàn)。8.4.1頁(yè)故障異常處理流程

當(dāng)頁(yè)故障異常發(fā)生時(shí),處理器必須離開當(dāng)前的工作,轉(zhuǎn)去處理異常。頁(yè)故障異常處理程序需要知道下列信息:

(1)異常發(fā)生時(shí)處理器所在的地址空間或特權(quán)級(jí)。

(2)引起頁(yè)故障異常的虛擬地址,可能是下一條指令的地址也可能是當(dāng)前指令欲訪問的數(shù)據(jù)地址。

(3)引起頁(yè)故障異常的原因,如虛擬頁(yè)不在內(nèi)存(缺頁(yè))、虛擬頁(yè)不許寫等。由于引起頁(yè)故障異常的原因大多是缺頁(yè),因而頁(yè)故障異常又稱為缺頁(yè)異常。處理器為頁(yè)故障異常的處理提供了上述信息。當(dāng)頁(yè)故障異常發(fā)生時(shí),處理器會(huì)自動(dòng)在當(dāng)前進(jìn)程的系統(tǒng)堆棧上壓入EFLAGS、CS、EIP和錯(cuò)誤代碼,用于指示異常產(chǎn)生的環(huán)境和異常產(chǎn)生的原因,與此同時(shí),處理器還在CR2寄存器中存入了引起頁(yè)故障異常的虛擬地址。目前的Intel處理器提供的錯(cuò)誤代碼共有5位,其意義如表8.1所示。

表8.1頁(yè)故障異常的錯(cuò)誤代碼頁(yè)故障異常必須由操作系統(tǒng)內(nèi)核處理。系統(tǒng)初始化程序已在IDT表中為頁(yè)故障異常創(chuàng)建了中斷門,入口程序是page_fault,真正的處理程序是do_page_fault。每當(dāng)頁(yè)故障異常發(fā)生時(shí),處理器都會(huì)離開當(dāng)前的工作,轉(zhuǎn)去執(zhí)行do_page_fault,流程如圖8.8所示。

圖8.8頁(yè)故障處理流程由于在處理頁(yè)故障異常時(shí)還有可能出現(xiàn)新的頁(yè)故障異常,為了避免CR2的內(nèi)容被新的頁(yè)故障異常覆蓋,應(yīng)首先將其中的故障地址暫存在變量addr中以備后用。

判斷頁(yè)故障異常是否合法的依據(jù)是:addr所在地址空間(內(nèi)核或用戶空間)、異常發(fā)生時(shí)處理器所在空間(處理器的特權(quán)級(jí))、引起頁(yè)故障的錯(cuò)誤代碼、addr所在的虛擬內(nèi)存區(qū)域、當(dāng)前進(jìn)程的頁(yè)目錄/頁(yè)表等。如果addr位于用戶空間,搜索當(dāng)前進(jìn)程的虛擬內(nèi)存區(qū)域樹,可確定它所在的虛擬內(nèi)存區(qū)域vma,結(jié)果有下列幾個(gè):

(1)

addr在所有區(qū)域之上。

(2)

addr恰在某個(gè)區(qū)域內(nèi)部。

(3)

addr在某個(gè)區(qū)域之下,但該區(qū)域允許向下擴(kuò)展(用戶堆棧),且擴(kuò)展后的區(qū)域能包含地址addr。

(4)

addr在某個(gè)區(qū)域之下,且該區(qū)域不能向下擴(kuò)展。

結(jié)果(1)和(4)說明進(jìn)程訪問了虛擬內(nèi)存區(qū)域之外的無效地址,結(jié)果(2)和(3)表明進(jìn)程訪問的是有效虛擬地址。

如果頁(yè)故障異常能被成功處理,那么程序do_page_fault會(huì)正常返回,引起異常的指令會(huì)被重新執(zhí)行,此次應(yīng)該能夠成功。如果頁(yè)故障異常不能被成功處理,如無法分配新的物理內(nèi)存、異常頁(yè)對(duì)應(yīng)的文件頁(yè)不存在、異常頁(yè)對(duì)應(yīng)的交換頁(yè)不存在等,說明系統(tǒng)發(fā)生了嚴(yán)重錯(cuò)誤,可以向當(dāng)前進(jìn)程發(fā)送SIGBUS信號(hào)將其殺死,也可以通過殺死其它進(jìn)程來回收物理內(nèi)存。8.4.2非法訪問頁(yè)故障處理

頁(yè)故障異??赡馨l(fā)生在取下一條指令的過程中,如EIP所指的虛擬代碼頁(yè)不在物理內(nèi)存;也可能發(fā)生在當(dāng)前指令的執(zhí)行過程中,如指令所訪問的虛擬數(shù)據(jù)頁(yè)不在物理內(nèi)存或不能按指令要求的方式訪問等。頁(yè)故障異常發(fā)生時(shí),處理器可能正在用戶空間也可能正在內(nèi)核空間,引起頁(yè)故障的虛擬地址可能在用戶空間也可能在內(nèi)核空間。雖然引起頁(yè)故障異常的原因多種多樣,但產(chǎn)生頁(yè)故障異常的場(chǎng)景只有四種,分別是內(nèi)核程序訪問內(nèi)核空間、內(nèi)核程序訪問用戶空間、用戶程序訪問內(nèi)核空間、用戶程序訪問用戶空間。頁(yè)故障異常發(fā)生時(shí),下列情況屬于非法內(nèi)存訪問:

(1)故障地址對(duì)應(yīng)的頁(yè)目錄或頁(yè)表項(xiàng)中的保留位被置1。正常情況下,頁(yè)目錄或頁(yè)表項(xiàng)中的保留位(如頁(yè)目錄項(xiàng)中的第6位、頁(yè)表項(xiàng)中的第7位)應(yīng)保持為0,這些位為1表明當(dāng)前進(jìn)程的頁(yè)表已被破壞。

(2)故障地址在用戶空間、處理器在執(zhí)行中斷處理程序。由于中斷的異步性,無法預(yù)知被中斷的當(dāng)前進(jìn)程,因而在中斷處理程序(包括硬處理和軟處理程序)中不應(yīng)訪問進(jìn)程的用戶虛擬地址。

(3)故障地址在用戶空間、處理器在執(zhí)行內(nèi)核線程。內(nèi)核線程永遠(yuǎn)運(yùn)行在內(nèi)核中,沒有自己的虛擬地址空間,它對(duì)用戶虛擬地址的訪問肯定是非法的。

(4)故障地址在內(nèi)核空間、處理器在執(zhí)行用戶程序。按照Linux的約定,用戶程序的特權(quán)級(jí)都是3,內(nèi)核程序和數(shù)據(jù)的特權(quán)級(jí)都是0,用戶程序直接調(diào)用內(nèi)核程序或訪問內(nèi)核數(shù)據(jù)都是非法的。

(5)故障地址在用戶空間且是無效的。進(jìn)程的有效虛擬地址都位于它的虛擬內(nèi)存區(qū)域中,在所有區(qū)域之外的用戶虛擬地址都是無效的。

(6)進(jìn)程對(duì)故障地址的訪問方式不符合約定。進(jìn)程對(duì)各虛擬地址的訪問約定記錄在它的虛擬內(nèi)存區(qū)域中,試圖向不可寫區(qū)域中寫數(shù)據(jù)的操作是非法的,試圖訪問不可讀、寫、執(zhí)行區(qū)域的任何操作都是非法的。

對(duì)非法內(nèi)存訪問的處理方法如下:

(1)如果頁(yè)故障發(fā)生時(shí)處理器正在用戶空間執(zhí)行程序,則向當(dāng)前進(jìn)程發(fā)送信號(hào)SIGSEGV,而后返回。在返回用戶態(tài)之前,進(jìn)程會(huì)處理該信號(hào)。如果進(jìn)程注冊(cè)了SIGSEGV信號(hào)的處理程序,該程序?qū)⑾缺粓?zhí)行;如果進(jìn)程未注冊(cè)SIGSEGV信號(hào)的處理程序,內(nèi)核將把它殺死。

(2)如果頁(yè)故障發(fā)生時(shí)處理器正在內(nèi)核空間執(zhí)行程序,則搜索異常列表(在節(jié)_

_ex_table中),看有沒有為該異常預(yù)定處理程序(見4.2.2節(jié)):

①如有,則重置棧頂?shù)膇p,而后返回。返回操作會(huì)跳轉(zhuǎn)到預(yù)定的處理程序,由該程序處理此次的頁(yè)故障異常。

②如無,則顯示與故障相關(guān)的現(xiàn)場(chǎng)信息,如指令地址、故障地址、頁(yè)表項(xiàng)、堆棧等,而后終止進(jìn)程。8.4.3有效用戶頁(yè)故障處理

如果引起頁(yè)故障異常的是用戶態(tài)的有效虛擬地址,且程序?qū)λ脑L問方式是合法的,則應(yīng)該設(shè)法處理該異常,使虛擬地址生效,以便程序能夠正常執(zhí)行下去。為此需要對(duì)該類合法頁(yè)故障異常做進(jìn)一步地分析,以便明確原因,確定處理方法。

確定處理方法的依據(jù)是頁(yè)目錄/頁(yè)表項(xiàng)、錯(cuò)誤代碼和虛擬內(nèi)存區(qū)域(尤其是其操作集中的fault操作)。根據(jù)頁(yè)故障地址addr可以算出它所在的虛擬頁(yè),查當(dāng)前進(jìn)程的頁(yè)目錄、頁(yè)表可以找到該虛擬頁(yè)對(duì)應(yīng)的頁(yè)表項(xiàng)。如果在查找過程中發(fā)現(xiàn)頁(yè)表不存在,則要臨時(shí)為其創(chuàng)建一個(gè)新的頁(yè)表。事實(shí)上,進(jìn)程的頁(yè)表都是動(dòng)態(tài)創(chuàng)建的,新建頁(yè)表中的所有頁(yè)表項(xiàng)都是0。頁(yè)故障原因的判斷條件及處理方法如表8.2所示。

表8.2頁(yè)故障產(chǎn)生原因及處理方法

1.線性文件映射頁(yè)的故障處理

在為進(jìn)程加載可執(zhí)行程序或映射數(shù)據(jù)文件時(shí),已建立了虛擬內(nèi)存區(qū)域與文件區(qū)間之間的映射關(guān)系,但未將文件的內(nèi)容讀入物理內(nèi)存,或者說還未建立起進(jìn)程虛擬頁(yè)與物理內(nèi)存頁(yè)之間的映射關(guān)系(頁(yè)表項(xiàng)為0)。當(dāng)進(jìn)程訪問這些虛擬頁(yè)時(shí),自然會(huì)產(chǎn)生頁(yè)故障異常。對(duì)這類頁(yè)故障的處理思路十分明確:找到虛擬頁(yè)對(duì)應(yīng)的文件頁(yè),將其讀入物理內(nèi)存,而后修改頁(yè)表項(xiàng),將虛擬頁(yè)映射到新讀入的文件頁(yè)即可。此后,當(dāng)進(jìn)程再次訪問故障地址addr時(shí),處理器便能順利地將其轉(zhuǎn)換成正確的物理地址,訪問到正確的文件內(nèi)容。如果引起頁(yè)故障異常的地址addr在虛擬內(nèi)存區(qū)域vma的內(nèi)部,那么該區(qū)域?qū)?yīng)的文件是vma->vm_file,包含addr的虛擬頁(yè)所對(duì)應(yīng)的文件頁(yè)的頁(yè)號(hào)是:

pgoff=(((addr&PAGE_MASK)-vma->vm_start)>>PAGE_SHIFT)+vma->vm_pgoff

其中PAGE_MASK是0xFFFFF000,PAGE_SHIFT是12。頁(yè)故障處理的核心操作是將文件vma->vm_file中的第pgoff頁(yè)讀入內(nèi)存。由于映射文件可能位于不同的文件系統(tǒng),因而將其文件頁(yè)讀入內(nèi)存的方法可能有差異。虛擬內(nèi)存區(qū)域操作集中的fault操作記錄著將文件vma->vm_file中的頁(yè)讀入內(nèi)存的方法,目前常用的是filemap_fault。

對(duì)文件的讀寫操作以頁(yè)為單位。為了加快文件操作的速度,減少讀寫磁盤的次數(shù),Linux為每個(gè)文件都準(zhǔn)備了一個(gè)文件頁(yè)緩存。來自同一個(gè)文件的所有頁(yè),不管是被哪個(gè)進(jìn)程讀入的,都被記錄在它的頁(yè)緩存中。當(dāng)需要讀入某個(gè)文件頁(yè)時(shí),Linux首先查該文件的頁(yè)緩存。如果文件頁(yè)已在其中且內(nèi)容是最新的,則可直接使用。只有當(dāng)文件頁(yè)不在緩存中或其內(nèi)容已陳舊時(shí),才需要將其從文件中讀入。當(dāng)然,從文件中新讀入的頁(yè)也要加入頁(yè)緩存。頁(yè)緩存記錄在文件的address_space結(jié)構(gòu)中,被組織成一棵基數(shù)樹,見11.5節(jié)。

為了減少頁(yè)故障異常的次數(shù),還應(yīng)對(duì)獲得的文件頁(yè)做進(jìn)一步檢查。如果區(qū)域vma是私有的且引起頁(yè)故障的動(dòng)作是寫,按照約定,應(yīng)為該文件頁(yè)創(chuàng)建一個(gè)副本,以免再次出現(xiàn)頁(yè)故障。進(jìn)程只能修改私有映射頁(yè)的副本,且對(duì)它的修改不能再直接寫回映射文件,因而需要將vma加入特定的匿名域中(可能已在匿名域中),并在副本頁(yè)的page結(jié)構(gòu)上加入PG_swapbacked標(biāo)志。

頁(yè)的page結(jié)構(gòu)也需要調(diào)整,如增加它的引用計(jì)數(shù)、讓它的mapping指向文件的地址空間或匿名域結(jié)構(gòu)、在index中記錄頁(yè)在文件中的頁(yè)號(hào)等。

根據(jù)頁(yè)的物理地址和虛擬內(nèi)存區(qū)域的保護(hù)權(quán)限,可創(chuàng)建一個(gè)頁(yè)表項(xiàng),將其填入當(dāng)前進(jìn)程的頁(yè)表之后,addr即變成了有效虛擬地址。

2.非線性文件映射頁(yè)的故障處理

正常情況下,虛擬內(nèi)存區(qū)域與文件區(qū)間之間的映射關(guān)系是線性的,區(qū)域中的第i個(gè)虛擬頁(yè)對(duì)應(yīng)區(qū)間中的第i個(gè)文件頁(yè)。但從2.5版之后,Linux允許建立非線性映射關(guān)系(通過系統(tǒng)調(diào)用remap_file_pages()),即允許將區(qū)域中的某些虛擬頁(yè)映射到文件的其它位置(區(qū)間內(nèi)的其它頁(yè)或區(qū)間外的某頁(yè))。為了記錄這些非線性虛擬頁(yè)的映射位置,Linux對(duì)這些頁(yè)的頁(yè)表項(xiàng)進(jìn)行了特殊設(shè)置:

(1)頁(yè)表項(xiàng)的P位被清0;

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論