1. 程式人生 > >Linux使用者程序記憶體分配及二級頁表PTE的二三事

Linux使用者程序記憶體分配及二級頁表PTE的二三事

我們在用偵錯程式看Linux使用者程序程式碼時,發現了一件很有意思的事情,在一段記憶體空間中,有一整頁(4K)都是data abort,如下:

第一頁4011c000資料正常

... ...

4011cfec [0xe28dd014]   add      r13,r13,#0x14

4011cff0 [0xe8bd40f0]   ldmfd    r13!,{r4-r7,r14}

4011cff4 [0xe12fff1e]   bx       r14

4011cff8 [0xe92d41f0]   stmfd    r13!,{r4-r8,r14}

4011cffc [0xe59f4064]   ldr      r4,0x4011d068


第二頁4011d000 都是Data abort

4011d000       *** Data abort ***

4011d004       *** Data abort ***

4011d008       *** Data abort ***

4011d00c       *** Data abort ***

... ...


第三頁 4011e000 資料正常

4011e000        資料正常


由於當時並不知道Linux是如何處理使用者程序的記憶體分配,所以認為這是一個“錯誤”。既然有錯誤,我們決定找到這個問題發生的根源。

在追蹤這個問題的過程中,leeming同學做了一個很BT的實驗。我簡單說兩句,詳細大家可以去看他的文章(
http://blog.chinaunix.net/u3/99423/showart_2096904.html
)。大概的方法就是將實體記憶體全部dump出來,通過第一頁的程式碼,比如0xe59f4064,查詢其在記憶體中的實體地址,再通過提取實體地址的前20位,就可以查詢Linux系統的二級頁表(對應ARM的TLB,Linux中叫PTE)。這個實驗雖然看上去很不可思議,但實現起來並不複雜。最終得到了Linux存放在記憶體中的TLB資料。

每個表項是32bit

虛擬頁地址    對應表項的內容

4011c000              3156caae

4011d000              00000000

4011e000        3156faae


       很有意思,Data abort的資料段,對應的PTE也是空的,這難道是一個系統錯誤?

       NO!經過進一步的學習後發現,在Linux系統中,這是一個很正常的現象。Linux在使用者程序執行時並沒有建立所有記憶體頁面的對映,而是需要用到的時候再建立對映關係。當Linux使用者程序訪問到沒有建立對映的頁表(此時PTE指標為0),會呼叫相應的函式進行處理,或建立、或換出,具體執行這個操作的函式叫handle_pte_fault(),位於核心的mm/memory.c中。

       但是,Linux是如何進入缺頁處理的呢?

有兩種情況,都是利用了ARM處理器的異常中斷進行相應的處理。

       第一種是程式順序執行,正常頁面的最後一條指令執行完後進入空頁面,當空頁面的第一條指令進入ARM處理器流水線的執行週期時,ARM處理器會報告一個指令預取異常中斷,並跳轉到地址0x0c,在Linux系統中由於使用了高地址向量表,所以會跳轉到0xffff000c。此時ARM處理器進入ABORT狀態,執行一系列程式碼儲存現場(程式碼位於/arch/arm/kernel/entry-armv.s),然後進入SVC狀態執行arch/arm/mm/fault.c中的do_PrefetchAbort(),最後會呼叫handle_pte_fault()處理缺頁異常。

       第二種情況,頁面中的程式執行時需要使用未分配頁面的資料,比如“ldr r0,未分配頁地址”。遇到這種情況,就不是指令預取異常了,而是資料訪問異常(Data abort)。此時處理器依然會進入ABORT狀態,跳轉到0xffff0010執行相應的vector_dabt程式碼(entry_armv.s)儲存狀態,進入SVC態,執行do_DataAbort()函式,最後同樣呼叫handle_pte_fault()處理缺頁異常。

       因此,最開始遇到的情況:三個PTE,中間是空的,這是一個很正常的情況。因為第三頁很可能由於前面的呼叫而已經建立,第二頁卻還沒有建立。

       至於handle_pte_fault()如何處理缺頁異常,我還沒有看完,就不在本文討論了。已知至少有do_no_page()、do_swap_page()、do_wp_page等多種方式,此為後話。

       通過跟蹤使用者程式,發現Linux使用者程序基本所有的頁面都是這樣處理,因此處理器會很頻繁的進出Abort狀態,執行頁面處理函式,這是會不會效率有點低了呢?待研究。

handle_pte_fault()

上文最後提到了handle_pte_fault()這個函式,用來處理頁錯,分配PTE。為了更清楚的瞭解PTE是如何申請到的,還是有必要深究一下。

handle_pte_fault()有幾個函式用來檢查當前pte的狀態:

pte_present() 檢測頁面是否在記憶體中

pte_none() 檢測頁表項是否為空

pte_file() 同一地址多對映(此函式不重要)

vm->ops->fault標記位

核心用likely對其做了標記,說明這個標記一般滿足,適用於已經建立好虛擬記憶體和檔案的對映關係的情況。


1)針對滿足pte_present()函式,即PTE不在記憶體中,會在以4個下函式中選擇一個進行處理:

(1)do_linear_fault();

       最常見的情況,PTE表項為空,但滿足vm->ops->fault,說明已經在記憶體中建立虛擬記憶體和檔案的對映關係。

(2)do_anonymous_page();

       PTE表項為空,但是沒有建立和檔案的對映關係,說明是第一次demanding page。

(3)do_nonlinear_fault();

       PTE表項非空,滿足pte_file()檢查,對同一個實體地址做多個虛擬對映。

(4)do_swap_page();

       PTE表項非空,不滿足pte_file()檢查,此頁將會被換出。


2)如果不滿足pte_present(),即PTE在記憶體中,則會執行下面的COW操作:

       COW的全稱叫做“copy on write”,即寫時複製。這一塊是涉及到兩個程序共享操作的,簡單的說兩個程序可以共享頁面(特別是fork出的程序),只有當一個程序需要寫入檔案時,才從同一頁面複製一份副本。下面的函式呼叫do_wp_page(),將生成的複製頁賦值給寫程序。由於我僅僅跟了系統的“/sbin/init”,所以這裡根本沒有呼叫到。

      

附:

do_anonymous_page()函式的跟蹤

       ——>mk_pte() 構建對映表

              ——>ptn_pte()

                     ——>__pte((ptn << 12 | pgprot_val)

最終生成的PTE為32bit,其中20bit實體地址,12bit控制資訊