Linux使用者程序記憶體分配及二級頁表PTE的二三事
阿新 • • 發佈:2018-12-24
我們在用偵錯程式看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控制資訊
第一頁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的實驗。我簡單說兩句,詳細大家可以去看他的文章(
每個表項是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控制資訊