深入理解Linux核心匿名頁的反向對映機制原理
我們知道LINUX的記憶體管理系統中有”反向對映“這一說,目的是為了快速去查找出一個特定的物理頁在哪些程序中被對映到了什麼地址,這樣如果我們想把這一頁換出(SWAP),或是遷移(Migrate)的時候,就能相應該更改所有相關程序的頁表來達到這個目的。
1、為什麼要使用反向對映
物理記憶體的分頁機制,一個PTE(Page Table Entry)對應一個物理頁,但一個物理頁可以由多個PTE與之相對應,當該頁要被回收時,Linux2。4的做法是遍歷每個程序的所有PTE判斷該PTE是否與該頁建立了對映,如果建立則取消該對映,最後無PTE與該相關聯後才回收該頁。該方法顯而易見效率極低,因為其為了查詢某個頁的關聯PTE遍歷了所有的PTE,我們不禁想:如果把每個頁關聯的PTE儲存在頁結構裡面,每次只需要訪問那些與之相關聯的PTE不很方便嗎?確實,2。4之後確實採用過此方法,為每個頁結構(Page)維護一個連結串列,這樣確實節省了時間,但此連結串列所佔用的空間及維護此連結串列的代價很大,在2。6中棄之不用,但反向對映機制的思想不過如此,所以還是有參考價值的,可以參考:
http://
blog。csdn。net/dog250/ar
ticle/details/5303581
。
2、Linux2。6中是如何實現反向對映
2。1 與RM(Reverse Mapping)相關的結構
page, address_space, vm_area_struct, mm_struct, anon_vma。
以下均顯示部分成員:
struct
page
{
struct
address_space
*
mapping
;
/* address_space型別,為對齊需要,其值為4的位數,所以最低兩位無用,為充分利用資源,所以此處利用此最低位。 * 最低位為1表示該頁為匿名頁,並且它指向anon_vma物件。 * 最低為0表對映頁,此時mapping指向檔案節點地址空間。 */
atomic_t
_mapcount
;
/* 取值-1時表示沒有指向該頁框的引用, 取值0時表示該頁框不可共享 取值大於0時表示該頁框可共享表示有幾個PTE引用 */
pgoff_t
index
;};
struct
mm_struct
{
pgd_t
*
pgd
;
}
struct
vm_area_struct
{
struct
list_head
anon_vma_node
;
/* Serialized by anon_vma->lock */
struct
anon_vma
*
anon_vma
;
/* Serialized by page_table_lock */
}
struct
anon_vma
{
spinlock_t
lock
;
/* Serialize access to vma list */
struct
list_head
head
;
/* List of private “related” vmas */
};
2。2 程序地址空間
每個程序有個程序描述符task_struct,其中有mm域指向該程序的記憶體描述符mm_struct。
每個程序都擁有一個記憶體描述符,其中有PGD域,指向該程序地址空間的全域性頁目錄;mmap域指向第一個記憶體區域描述符vm_area_strut1。
程序透過記憶體區域描述符vm_area_struct管理記憶體區域,每個記憶體區域描述符都有vm_start和vm_end域指向該記憶體區域的在虛擬記憶體中的起始位置;vm_mm域指向該程序的記憶體描述符;每個vm_area_struct都有一個anon_vma域指向該程序的anon_vma;
每個程序都有一個anon_vma,是用於連結所有vm_area_struct的頭結點,透過vm_area_struct的anon_vma_node構成雙迴圈連結串列。
最終形成了上圖。
現在假設我們要回收一個頁,我們要做的是訪問所有與該頁相關聯的PTE並修改之取消二者之間的關聯。與之相關聯的函式為:try_to_unmap。
【文章福利
】小編推薦自己的Linux核心技術交流群:【
865977150
】或者系統課程學習諮詢微信【2207032995】,備註一下(玩轉Linux核心);整理了一些個人覺得比較好的學習書籍、影片資料共享在群檔案裡面,有需要的可以自行新增哦!!!
學習直通車:
核心資料直通車:
2。3 try_to_unmap
2。3。1 try_to_unmap函式及PageOn宏 分析
int try_to_unmap(struct page *page)
{
int ret;
BUG_ON(PageReserved(page));
BUG_ON(!PageLocked(page));
/*判斷是不是匿名頁,若是的話執行try_to_unmap_anon函式,否則的話執行try_to_unmap_file函式*/
if (PageAnon(page)) // PageAnon函式分析在下面
ret = try_to_unmap_anon(page);
else
ret = try_to_unmap_file(page);
if (!page_mapped(page))
ret = SWAP_SUCCESS;
return ret;
}
static inline int PageAnon(struct page *page)
{
return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
/* #define PAGE_MAPPING_ANON 1 此函式非常easy,就是判斷page的mapping成員的末位是不是1,是的話返回1,不是的話返回0*/
}
2。3。2 try_to_unmap_anon函式及page_lock_anon_vma函式及list_for_each_entry宏 分析
還沒開始看檔案系統一節,所以try_to_unmap_file沒看懂,所以此處只分析 try_to_unmap_anon函式,等看完vfs後再來補充吧。
static
int
try_to_unmap_anon
(
struct
page
*
page
)
{
struct
anon_vma
*
anon_vma
;
struct
vm_area_struct
*
vma
;
int
ret
=
SWAP_AGAIN
;
anon_vma
=
page_lock_anon_vma
(
page
);
/* 獲取該匿名頁的anon_vma結構
* page_lock_anon_vma函式分析在下面。
*/
if
(
!
anon_vma
)
return
ret
;
list_for_each_entry
(
vma
,
&
anon_vma
->
head
,
anon_vma_node
)
{
/* 迴圈遍歷
* list_for_each_entry分析在下面
* anon_vma就是上圖中anon_vma的指標,anon_vma->head得到其head成員(是list_head)型別,
* 其next值便對應上圖中vm_area_struct1中的anon_vma_node中的head。
* vma 是vm_area_struct型別的指標,anon_vma_node為typeof(*vma)即vm_area_struct中的成員。
* 到此便可以開始雙鏈表迴圈
*/
ret
=
try_to_unmap_one
(
page
,
vma
);
// 不管是呼叫try_to_unmap_anon還是try_to_unmap_file最終還是回到try_to_unmap_one上
if
(
ret
==
SWAP_FAIL
||
!
page_mapped
(
page
))
break
;
}
spin_unlock
(
&
anon_vma
->
lock
);
return
ret
;
}
static
struct
anon_vma
*
page_lock_anon_vma
(
struct
page
*
page
)
{
struct
anon_vma
*
anon_vma
=
NULL
;
unsigned
long
anon_mapping
;
rcu_read_lock
();
anon_mapping
=
(
unsigned
long
)
page
->
mapping
;
if
(
!
(
anon_mapping
&
PAGE_MAPPING_ANON
))
goto
out
;
if
(
!
page_mapped
(
page
))
goto
out
;
// 前面已經提到,mapping最低位為1時表匿名頁,此時mapping是指向anon_vma指標,故此處-1後強制轉化為struct anon_vma指標型別,並返回該值。
anon_vma
=
(
struct
anon_vma
*
)
(
anon_mapping
-
PAGE_MAPPING_ANON
);
spin_lock
(
&
anon_vma
->
lock
);
out
:
rcu_read_unlock
();
return
anon_vma
;
}
/* 引數含義:
* head是list_head指標,無非此處需要的第一個list_head是head->next
* pos是個指向包含list_head的結構體的指標,可以用typeof(*pos)解引用來得到此結構體
* member 是list_head在typeof(*pos)中的名稱
* 這樣pos = list_entry((head)->next, typeof(*pos), member)第一次便初始化pos為指向包含head->next指向的那個結構體的指標。
* 之後便是雙迴圈連結串列遍歷了
*/
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
// list_entry分析在下面
prefetch
(
pos
->
member
。
next
),
&
pos
->
member
!=
(
head
);
\
pos
=
list_entry
(
pos
->
member
。
next
,
typeof
(
*
pos
),
member
))
/* list_entry函式其實非常簡單,其各引數的意義:
* ptr 指向list_head型別的指標
* type 包含list_head型別的結構體
* member list_head在type中的名稱
* 返回值:包含ptr指向的list_head的型別為type的指標,即由list_head指標得到包含此list_head結構體的指標,實現也很簡單,ptr減去member在type中的偏移即可
*/
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
2。3。3 try_to_unmap_one函式及vma_address函式及pdg_offset宏 分析
Linux採用三級頁表: PGD:頂級頁表,由pgd_t項組成的陣列,其中第一項指向一個二級頁表。
PMD:二級頁表,由pmd_t項組成的陣列,其中第一項指向一個三級頁表(兩級處理器沒有物理的PMD)。
PTE:是一個頁對齊的陣列,第一項稱為一個頁表項,由pte_t型別表示。一個pte_t包含了資料頁的物理地址。
static int try_to_unmap_one(struct page *page, struct vm_area_struct *vma)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long address;
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
pte_t pteval;
int ret = SWAP_AGAIN;
if (!mm->rss)
goto out;
address = vma_address(page, vma); /* 透過頁和vma得到線性地址
* vm_address函式解析在下面
*/
if (address == -EFAULT)
goto out;
/*
* We need the page_table_lock to protect us from page faults,
* munmap, fork, etc。。。
*/
spin_lock(&mm->page_table_lock); // 頁表鎖
pgd = pgd_offset(mm, address); /* 獲得pdg
* pdg_offset透過記憶體描述符和線性地址得到pgd
* 該函式解析在下面
*/
if (!pgd_present(*pgd))
goto out_unlock;
pud = pud_offset(pgd, address); /* 獲得pud
i386上應該是0吧?
*/
if (!pud_present(*pud))
goto out_unlock;
pmd = pmd_offset(pud, address); /* 獲得pmd */
if (!pmd_present(*pmd))
goto out_unlock;
pte = pte_offset_map(pmd, address); /* 獲得pte */
if (!pte_present(*pte))
goto out_unmap;
/* 有了pgd pmd pte 後我們便達到我們目的了 ===> 查詢與頁相關聯絡的頁表項,找到後便可以進行修改了(如果是要換出該頁的話則應該解除對映pte_unmap()函式)
* 但修改之前還要做些判斷和處理
*/
//
if (page_to_pfn(page) != pte_pfn(*pte))
goto out_unmap;
/*
* If the page is mlock()d, we cannot swap it out。
* If it‘s recently referenced (perhaps page_referenced
* skipped over this mm) then we should reactivate it。
*/
if ((vma->vm_flags & (VM_LOCKED|VM_RESERVED)) ||
ptep_clear_flush_young(vma, address, pte)) {
ret = SWAP_FAIL;
goto out_unmap;
}
/*
* Don’t pull an anonymous page out from under get_user_pages。
* GUP carefully breaks COW and raises page count (while holding
* page_table_lock, as we have here) to make sure that the page
* cannot be freed。 If we unmap that page here, a user write
* access to the virtual address will bring back the page, but
* its raised count will (ironically) be taken to mean it‘s not
* an exclusive swap page, do_wp_page will replace it by a copy
* page, and the user never get to see the data GUP was holding
* the original page for。
*
* This test is also useful for when swapoff (unuse_process) has
* to drop page lock: its reference to the page stops existing
* ptes from being unmapped, so swapoff can make progress。
*/
if (PageSwapCache(page) &&
page_count(page) != page_mapcount(page) + 2) {
ret = SWAP_FAIL;
goto out_unmap;
}
/* Nuke the page table entry。 */
flush_cache_page(vma, address);
pteval = ptep_clear_flush(vma, address, pte);
/* Move the dirty bit to the physical page now the pte is gone。 */
if (pte_dirty(pteval))
set_page_dirty(page);
if (PageAnon(page)) {
swp_entry_t entry = { 。val = page->private };
/*
* Store the swap location in the pte。
* See handle_pte_fault() 。。。
*/
BUG_ON(!PageSwapCache(page));
swap_duplicate(entry);
if (list_empty(&mm->mmlist)) {
spin_lock(&mmlist_lock);
list_add(&mm->mmlist, &init_mm。mmlist);
spin_unlock(&mmlist_lock);
}
set_pte(pte, swp_entry_to_pte(entry));
BUG_ON(pte_file(*pte));
mm->anon_rss——;
}
mm->rss——;
acct_update_integrals();
page_remove_rmap(page);
page_cache_release(page);
out_unmap:
pte_unmap(pte);
out_unlock:
spin_unlock(&mm->page_table_lock);
out:
return ret;
}
static inline unsigned long
vma_address(struct page *page, struct vm_area_struct *vma)
{
pgoff_t pgoff = page->index << (PAGE_CACHE_SHIFT - PAGE_SHIFT); /* PAGE_CACHE_SHIFT - PAGE_SHIFT值為0,其實就是把page->index賦給pgoff
* 至於為什麼要這樣右移一下,我也不清楚
*/
unsigned long address;
address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT); /* page->index是頁的偏移
* vma->vm_pgoff是記憶體區域首地址的偏移,都是以頁為單位
* 相減後再< * 再+vma->vma_start便得到頁在記憶體區域中的地址 */ if (unlikely(address < vma->vm_start || address >= vma->vm_end)) { /* 得到的地址應該在vm->vm_start和vm_end之間,否則報錯 */ /* page should be within any vma from prio_tree_next */ BUG_ON(!PageAnon(page)); return -EFAULT; } return address; } #define PGDIR_SHIFT 22 // 在i386機子上線性地址0-11位表PTE,12-21表PMD,22-31位表PGD,即線性地址右移22位的結果為其在全域性頁目錄的偏移 #define PTRS_PER_PGD 1024 // 因PGD共10位,所以其最多可以有2^10=1024個全域性描述符項 #define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1)) // 得到線性地址address在全域性頁目錄裡面的偏移 #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) // 再加上全域性描述符基地址(儲存在記憶體描述符mm_struct中的pdg域)後便得到其在全域性描述符中的具體位置 2022年嵌入式開發想進網際網路大廠,你技術過硬嗎? 從事十年嵌入式轉核心開發(23K到45K),給兄弟們的一些建議 騰訊T6-9首發“Linux核心原始碼嵌入式開發進階筆記”,差距不止一點點哦