天天看點

Linux核心剖析 之 程序位址空間(三)缺頁異常處理程式建立和删除程序的位址空間堆的管理

本節主要講述缺頁異常處理程式和堆的管理等内容。

觸發缺頁異常程式的兩種情況:

1. 由程式設計錯誤引起的異常(如通路越界,位址不屬于程序位址空間)。

2. 位址屬于線性位址空間,但核心還未配置設定相應的實體頁,導緻缺頁異常。

缺頁異常處理程式總體方案:

Linux核心剖析 之 程式位址空間(三)缺頁異常處理程式建立和删除程式的位址空間堆的管理

線性區描述符可以讓缺頁異常處理程式非常有效的完成它的工作。

do_page_fault()函數是80x86上的缺頁中斷服務程式,它把引起缺頁的線性位址和目前程序的線性區相比較,進而根據具體方案選擇适當的方法處理此異常。

辨別符vmalloc_fault、good_area、do_sigbus、bad_area、no_context、survive、out_of_memory和bad_area_nosemaphore對應的代碼段對不同的缺頁異常進行不同的處理操作。

接收參數:

pt_regs結構的位址regs,該結構包含異常發生時的微處理器寄存器的值。

3位的error_code:

===>>error_code:

*第0位被清零,通路一個不存在的頁——異常;第0位被置位,無效的通路權限——異常。

*第1位被清零,讀通路或者執行通路——異常;第1位被置位,寫通路——異常。

*第2位被清零,處理器核心态——異常;第2位被置位,處理器使用者态——異常。

執行步驟:

*讀取引起缺頁的線性位址。當異常發生時,cpu控制單元将此線性位址存放在cr2控制寄存器中。

pt_regs結構指針regs指向異常發生前cpu中各寄存器内容的一份副本,這是由核心的中斷響應機制儲存下來的“現場”,而error_code則進一步指明映射失敗的具體原因。如果缺頁發生之前或cpu運作在80x86模式時就打開了本地中斷,則使能local_irq_enable(),并将指向current程序描述符的指針儲存在tsk局部變量中。

*接下來:

Linux核心剖析 之 程式位址空間(三)缺頁異常處理程式建立和删除程式的位址空間堆的管理

對此圖進行說明:

do_page_fault()首先檢查引起缺頁的線性位址是否在核心位址空間:

如果是,則當核心試圖通路不存在的頁,跳轉執行非連續記憶體區位址通路代碼,即vmalloc_fault标記處後的代碼,否則,執行bad_area_ nosemaphore标記處後的代碼。

如果不是,則引起缺頁的線性位址在使用者位址空間。此時,判斷缺頁是否發生中斷處理程式、可延遲函數、臨界區或核心線程中:

如果是,由于中斷處理程式等不使用小于task_size的位址,故轉而執行bad_area_nosemaphore辨別處代碼。

如果沒有,即卻也沒有發生在中斷處理程式、可延遲函數、臨界區或者核心線程中,則函數檢查程序所擁有的線性區以決定引起缺頁的線性位址是否包含在程序的位址空間中,為此必須獲得程序的mmap_sem讀寫信号量。

當函數擷取了mmap_sem信号量,do_page_fault()開始搜尋錯誤線性位址所在的線性區,并根據vma的值,跳轉到相應标志代碼段。

如果address(引起缺頁的線性位址)不屬于程序的位址空間,則do_page_fault()函數執行bad_area标記處的語句。

如果異常發生在使用者态,則發送一個sigsegv信号給current程序并結束函數;

其中,force_sig_info()确信程序不忽略或阻塞sigsegv信号,并通過info局部變量傳遞附加資訊的同時把該信号發送給使用者态程序。

如果異常發生在核心态(error_code的第二位被清零),有兩種可選的情況(在no_context代碼段實作):

*異常的引起是由于把某個線性位址作為系統調用的參數傳給核心;

*異常是因一個真正的核心缺陷引起的。

對于第一種情況,代碼跳轉到一段“修正代碼”處,這段代碼的典型操作就是向目前程序發送sigsegv信号,或用一個适當的出錯碼終止系統調用處理程式。

對于第二種情況,函數把cpu寄存器和核心态堆棧的全部轉儲列印到控制台,并輸出到系統消息緩沖區,然後調用do_exit()殺死目前程序。——核心漏洞“kernel oops”。

如果address位址屬于程序的位址空間,則do_page_fault()轉到good_area标記處執行程式:

對error_code&3===>>>

case 2:如果異常由寫通路引起,函數檢查這個線性區是否可寫。如果不可寫(!(vma->vm_flags & vm_write)),跳到bad_area代碼處;如果可寫,把write局部變量置為1.

case 1 and case 0:如果異常是由讀或執行通路引起,函數檢查這一頁是否已經存在于ram中。在存在的情況下,異常發生是由于程序試圖通路使用者态下的一個有特權的頁框,是以函數跳轉到bad_area代碼處。在不存在的情況下,函數還将檢查這個線性區是否可讀或可執行。

default and case2(write==1):如果這個線性區的通路權限于引起異常的通路類型相比對,調用handle_mm_fault()函數配置設定一個新的頁框(survive代碼段):

關鍵:handle_mm_fault()函數

參數:異常發生時正在cpu上運作的程序的記憶體描述符指針mm,引起異常的線性位址所線上性區的描述符指針vma,引起異常的線性位址address,write_access(如果tsk試圖向address寫則置位,如果tsk試圖向address讀或執行則清零)。

步驟:

*函數首先檢查發生異常的原因,然後檢查用來映射address的頁目錄和頁表是否存在,再執行配置設定頁目錄和頁表的任務;

*handle_pte_fault()函數檢查address位址對應的頁表項,并決定如何為程序配置設定一個新頁框:

===>>>

#如果通路的頁在記憶體中不存在,也就是說,這個頁還沒有被存放在任何一個頁框中,則核心配置設定一個新的頁框并适當初始化。這種技術稱為請求調頁(demand paging);

#如果被通路的頁存在但是标記為隻讀,也就是說,它已經被存放在一個頁框中,則核心配置設定一個新的頁框,并把舊的頁框的資料拷貝到新頁框來初始化它的内容。這種技術稱為寫時複制(copy on write,cow)。

*如果線性區的通路權限與引起異常的通路類型相比對,handle_mm_fault()函數配置設定一個新的頁框:

當handle_mm_fault()成功地給程序配置設定一個頁框,則傳回vm_fault_minor或vm_fault_major。值vm_fault_minor表示在沒有阻塞目前程序的情況下處理了缺頁,這種缺頁叫做次缺頁(minor fault)。值vm_fault_major表示缺頁迫使目前程序睡眠,阻塞目前程序的缺頁叫做主缺頁(major fault)。

當沒有足夠記憶體時,函數傳回vm_fault_oom,此時函數不配置設定新的頁框,核心通常殺死目前程序。不過,如果目前程序是init程序,則隻是把它放在運作隊列的末尾并調用排程程式,一旦init恢複執行,則handle_mm_fault又執行:

out_of_memory标記處代碼(過程如上所述):

請求調頁指的是一種動态記憶體配置設定技術,它把頁框的配置設定推遲到不能再推遲為止,也就是說,一直推遲到程序要通路的頁不再記憶體中時為止,由此引起缺頁異常。

請求調頁技術的動機是:請求調頁能增加系統中的空閑頁框的平均數,進而更好地利用空閑記憶體,從總體上能使系統有更大的吞吐量。

付出的代價是系統額外的開銷:由請求調頁所引發的每個“缺頁”異常必須由核心處理。

有關請求調頁的代碼:

pte_present()宏指明entry頁是否在主存中。如果entry頁不在主存中,其原因或是程序從未通路過該頁,或是核心已經回收了相應的頁框。

在這兩種情況下,缺頁處理程式必須為程序配置設定新的頁框。不過,如何初始化這個頁框有三種特殊情況:

*entry頁從未被程序通路到且沒有映射到磁盤檔案:

pte_none()宏==>do_no_page()函數;

*entry頁屬于非線性磁盤檔案的映射:

pte_file()宏==>do_file_page()函數;

*entry頁已經被程序通路過,但是其内容被臨時儲存在磁盤中(present=dirty=0):

==> do_swap_page()函數。

handle_pte_fault()函數通過檢查address對應頁表項的标志能夠區分這三種情況,并根據不同标志來進行不同的函數處理。

===>>>匿名頁和映射頁:

在linux虛拟記憶體中,如果頁對應的vma映射的是檔案,則稱為映射頁;如果不是映射的檔案,則稱為匿名頁。兩者最大的差別展現在頁和vma的組織上,因為在頁框回收處理時要通過頁來逆向搜尋映射了該頁的vma。對于匿名頁的逆映射,vma都是通過vma結構體中的vma_anon_node(連結清單節點)和anon_vma(連結清單頭)組織起來,再把該連結清單頭的資訊儲存在頁描述符中;而映射頁和 vma的組織是通過vma中的優先樹節點和頁描述符中的mapping->i_mmap優先樹樹根進行組織的。

原始的程序建立:

當發出fork()系統調用時,核心原樣将父程序的整個位址空間複制一份給子程序。這種方式非常耗時:

1. 為子程序的位址空間配置設定頁框

2. 為子程序的頁表配置設定頁框

3. 初始化子程序的頁表

4. 将父程序的頁複制到子程序相應的頁

缺點:耗費cpu周期。

現在linux系統采用一種寫時複制的技術;

原理:父子程序共享頁框而不是複制;共享意味着不能被修改,父子程序無論何時試圖寫頁框,就會産生異常;這時核心将這個實體頁複制到一個新的頁框,标記為可寫。

頁描述符的_count字段用于跟蹤共享相應頁框的程序數目。隻要程序釋放一個頁框或者在它上面執行寫時複制,它的_count字段就減小;隻有當_count變為-1時,此頁框才被釋放。

寫時複制相關代碼:

核心函數:do_wp_page()函數

該函數首先擷取與缺頁異常相關的頁框描述符。接下來,函數确定頁的複制是否真正必要。具體說來,函數讀取頁描述符的_count字段,如果它等于0(隻有一個所有者),寫時複制就不必進行。如果多個程序通過寫時複制共享頁框,那麼函數就把舊頁框的内容複制到新配置設定的頁框中(copy_page()宏)。然後,新頁框的實體位址最終被寫進頁表項,且使對應的tlb寄存器無效。同時,lru_cache_add_active()函數把新頁框插入到與交換相關的資料結構中。最後,do_wp_page()把old_page的使用計數器減少兩次(pte_unmap()函數),第一次減少是取消複制頁框内容之前進行的安全性增加;第二次減少是反映目前程序不再擁有該頁框的事實。

異常發生在核心态且産生缺頁的線性位址大于task_size。此時,do_page_fault()檢查相應的主核心頁全表項:

do_page_fault()把存放在cr3寄存器中的目前程序頁全局目錄的實體位址賦給局部變量pgd_paddr,把與pgd_paddr對應的線性位址賦給局部變量pgd,并且把主核心頁全局目錄的線性位址賦給pgd_k局部變量。

如果産生缺頁的線性位址所對應的主核心頁全局目錄項為空,則函數跳到标号為no_context的代碼處。否則,函數檢查與錯誤線性位址相對應的主核心頁上級目錄項和主核心頁中間目錄項。如果它們中間有一個為空,就再次跳轉到no_context處。否則,就把主目錄項複制到程序頁中間目錄的相應項中。随後對首頁表項重複上述操作。

程序獲得一個新線性區的六種典型情況:

    程式執行

    exec()函數

    缺頁異常處理程式

    記憶體映射

    ipc共享記憶體

    malloc()函數

fork()系統調用要求為子程序建立一個完整的新位址空間。相反,當程序結束是,核心撤銷它的位址空間。

vfork()/fork()/clone()系統調用=====>>>:

copy_mm()函數:

如果flag參數的clone_vm标志被置位,copy_mm()函數把父程序(current)位址空間給子程序。

如果沒有設定clone_vm标志,copy_mm()函數建立新的位址空間,配置設定一個新的記憶體描述符,複制父程序的mm内容到新的程序描述符中。

然後調用函數init_new_context()和init_mm()函數進行初始化工作;

最後調用dup_mmap()函數複制父程序的線性區和頁表。

當程序結束時,核心調用exit_mm()釋放程序的位址空間。

mm_release()函數喚醒tsk->vfork_done補充原語上睡眠的任一程序。

如果正在被終止的程序不是核心線程,exit_mm()函數釋放記憶體描述符和所有相關的資料結構。首先,它檢查mm->core_waiters标志是否置位:如果是,程序就把記憶體的所有内容解除安裝到一個轉儲檔案中。為了避免轉儲檔案的混亂,函數利用mm->core_done和mm->core_startup_done補充原語使共享同一記憶體描述符mm的輕量級程序的執行串行化。

接下來,函數遞增記憶體描述符的主使用計數器,重新設定程序描述符的mm字段,并使處理器處于懶惰tlb模式。

最後,調用mmput()函數釋放局部描述符表、線性區描述符和頁表。由于exit_mm()函數已經遞增了主使用計數器,是以并不釋放記憶體描述符本身。當要把正在被終止的程序從本地cpu撤銷時,将由finish_task_switch()函數釋放記憶體描述符。

每個程序都有一個特殊的線性區,即堆(heap)。用于滿足程序的動态記憶體請求。記憶體描述符中的start_brk與brk字段分别表示堆的起始位址和結束位址。

操作函數

說明

malloc(size)

請求size個位元組的動态記憶體,如果成功,則傳回第一個位元組的線性位址

calloc(n, size)

請求n個大小為size的元素的記憶體,如果成功,傳回第一個元素的線性位址

realloc(ptr,size)

改變由前面malloc\calloc配置設定的記憶體大小

free(addr)

釋放由malloc、calloc配置設定的起始位址為addr的線性區

brk(addr)

直接修改堆的大小。addr參數指定current->mm->brk的新值,傳回值是線性區新的結束位址。

sbrk(incr)

類似brk(),不過其中的incr參數指定是增加還是減少以位元組為機關的堆大小

至此,程序位址空間一章節結束。

遺留問題:

1、在寫時複制時,對于父程序和子程序,其配置設定頁框的具體機制如何?

2、每個線性區是否都會劃分代碼段、資料段、堆棧等線性位址空間?

3、線性區的配置設定是動态配置設定還是靜态配置設定,即線性區的配置設定會不會随着使用記憶體的增加而動态添加配置設定?還是說程式執行的時候,系統會提前配置設定好線性區?

4、前面提到過一句在寫時複制時,do_wp_page()把old_page的使用計數器減少兩次(pte_unmap()函數),第一次減少是取消複制頁框内容之前進行的安全性增加;第二次減少是反映目前程序不再擁有該頁框的事實。這裡,為什麼需要進行安全性增加?

繼續閱讀