摘要:本章主要介紹了LINUX3.0記憶體尋址方面的内容,重點對follow_page函數進行注釋,以幫助讀者大緻了解ARM A9的頁表組織。 讀者需要了解一些基本概念:虛拟位址、實體位址、MPU、MMU、ARM中的二級頁表、cache、TLB。
<b></b>
本連載文章并不是為了形成一本适合出版的書籍,而是為了向有一定核心基本的讀者提供一些linux3.0源碼分析。是以,請讀者結合《深入了解LINUX核心》第三版閱讀本連載。
本系列文章分析ARM A9的linux3.0代碼實作。是以,需要讀者有一定的ARM體系硬體知識。推薦閱讀《ARM嵌入式系統開發-軟體設計與優化》。另外,讀者最好對核心有所了解,推薦閱讀《深入了解LINUX核心》第三版。
讀者需要了解一些基本概念:虛拟位址、實體位址、MPU、MMU、ARM中的二級頁表、cache、TLB。
1.1 基本函數
Linux3.0将分頁抽象為四級:
名稱
資料結構
備注
頁全局目錄
Pgd_t
頁上級目錄
Pud_t
A9未用
頁中間目錄
Pmd_t
頁表
Pte_t
/**
* 對A9來說,隻支援4K大小的頁,是以PAGE_SHIFT定義為12.它表示一個虛拟位址的頁内偏移量的位數。
* 根據它計算出來的頁大小PAGE_SIZE為4K,PAGE_MASK為0xffff000。
*/
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL)
#define PAGE_MASK (~(PAGE_SIZE-1))
* 對A9來說,沒有PMD和PUD,是以,PMD_SHIFT和PUD_SHIFT的值與PGDIR_SHIFT是一樣的,都是21.
* 21表示一個頁全局目錄項代表了2^20即1M的位址空間。
#define PMD_SHIFT 21
#define PGDIR_SHIFT 21
* 分别代表一個頁表、頁中間目錄、頁全局目錄表中表項的個數。
#define PTRS_PER_PTE 512
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 2048
* 将pte\pmd\pud\pgd\pgprot轉換為整型值
#define pte_val(x) (x)
#define pmd_val(x) (x)
#define pgd_val(x) ((x)[0])
#define pgprot_val(x) (x)
* 将整型值轉換為pte\pmd\pud\pgd\pgprot
#define __pte(x) (x)
#define __pmd(x) (x)
#define __pgprot(x) (x)
1.1.1 判斷頁表項标志的函數
* 頁表項是否為0
#define pte_none(pte) (!pte_val(pte))
* 頁表項是否可用。當頁在記憶體中但是不可讀寫時置此标志。典型的用途是寫時複制。
#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)
* 頁表項是否有可寫标志
#define pte_write(pte) (!(pte_val(pte) & L_PTE_RDONLY))
* 頁表項是否為髒
#define pte_dirty(pte) (pte_val(pte) & L_PTE_DIRTY)
* 頁表項是否表示最近沒有被通路過
#define pte_young(pte) (pte_val(pte) & L_PTE_YOUNG)
* 頁表項是否有可執行标志
#define pte_exec(pte) (!(pte_val(pte) & L_PTE_XN))
#define pte_special(pte) (0)
* 清除頁表項的值。
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)
* 向一個頁表項中寫入指定的值。
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
* 判斷兩個頁表項是否指向相同的頁并且有相同的通路權限
static inline int pte_same(pte_t pte_a, pte_t pte_b)
{
return pte_val(pte_a) == pte_val(pte_b);
}
* 檢查頁中間目錄項是否指向不可用的頁表。
#define pmd_bad(pmd) (pmd_val(pmd) & 2)
1.1.2 頁表項操作函數
* 虛拟位址在頁全局目錄中索引
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
* 計算一個程序使用者态位址對應的頁全局目錄項位址。
* 計算核心态位址的頁全局目錄項位址應當使用pgd_offset_k
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
/* to find an entry in a kernel page-table-directory */
* 計算一個核心态位址的頁全局目錄項位址。
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
* 獲得頁全局目錄項所指向的頁面。對A9來說,就是pmd_page
#define pgd_page(pgd) (pud_page((pud_t){ pgd }))
* 獲得頁全局目錄項的虛拟位址。
#define pgd_page_vaddr(pgd) (pud_page_vaddr((pud_t){ pgd }))
* 在頁全局目錄表中,查找一個虛拟位址對應的頁上級目錄位置。
* 對二級頁表來說,頁上級目錄就是頁全局目錄,是以直接傳回頁全局目錄。
#define pud_offset(pgd, start) (pgd)
* 獲得頁上級目錄頁面。
#define pud_page(pud) pgd_page(pud)
* 獲得頁上級目錄頁面的虛拟位址。
#define pud_page_vaddr(pud) pgd_page_vaddr(pud)
* 獲得一個虛拟位址的頁中間目錄中的位址。對二級頁表來說,沒有pmd,直接傳回頁全局目錄位址即可。
#define pmd_offset(dir, addr) ((pmd_t *)(dir))
* 獲得頁中間目錄指向的頁表頁面。
#define pmd_page(pmd) pfn_to_page(__phys_to_pfn(pmd_val(pmd)))
* 獲得一個線性位址對應的頁表項在頁表中的索引
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
* 在主核心頁表中定位核心位址對應的頁表項的虛拟位址。
#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))
* 在程序頁表中定位線性位址對應的頁表項的位址。如果頁表儲存在高端記憶體中,那麼還為頁表建立一個臨時核心映射。
#define pte_offset_map(pmd,addr) (__pte_map(pmd) + pte_index(addr))
* 如果頁表在高端記憶體中,不解除由pte_offset_map建立的臨時核心映射。
#define pte_unmap(pte) __pte_unmap(pte)
* 擷取頁表項中的頁幀号。
#define pte_pfn(pte) (pte_val(pte) >> PAGE_SHIFT)
* 根據頁幀号和頁面屬性,合成頁表項。
#define pfn_pte(pfn,prot) __pte(__pfn_to_phys(pfn) | pgprot_val(prot))
* 從頁表項中提取頁幀号,并定位該頁幀号對應的頁框。
#define pte_page(pte) pfn_to_page(pte_pfn(pte))
* 根據頁框和頁面屬性,合成頁表項。
#define mk_pte(page,prot) pfn_pte(page_to_pfn(page), prot)
* 當頁表項映射到檔案,并且沒有裝載進記憶體時,從頁表項中提取檔案頁号。
#define pte_to_pgoff(x) (pte_val(x) >> 3)
* 将頁面映射的頁号存放到頁表項中
#define pgoff_to_pte(x) __pte(((x)
1.1.3 頁表配置設定相關的函數
* 為頁全局目錄配置設定記憶體
pgd_t *pgd_alloc(struct mm_struct *mm)
* 釋放頁全局目錄項
void pgd_free(struct mm_struct *mm, pgd_t *pgd_base)
* 配置設定頁上級目錄,在二級頁表中,此函數什麼也不做。
#define pud_alloc(mm, pgd, address) (pgd)
* 釋放頁上級目錄,在二級頁表中,這個函數什麼也不做
#define pud_free(mm, x) do { } while (0)
Pmd_alloc、pmd_free、pte_alloc_map、pte_free等宏或函數與此類似。
1.2 重新整理cache和TLB
Cache是CPU與記憶體之間的緩存,而TLB是CPU與MMU之間緩存。
當外部硬體通過DMA修改了記憶體中的資料時,需要使cache中的資料失效,強制CPU從記憶體中裝載資料。當CPU向緩存中寫入資料後,為了通過DMA将資料傳送到外部硬體,則需要将緩存中的資料強制寫入記憶體。
當頁表項映射的頁面發生變化後,也需要将頁面緩存的内容寫入記憶體。
同理,當修改了頁表項後,為了避免TLB中緩存的項進行錯誤的MMU轉換,也需要使TLB中緩存的項失效。
1.3 follow_page函數
follow_page函數是從程序的頁表中搜尋特定位址對應的頁面對象。這個函數對于了解LINUX核心頁表管理有幫助。
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep, pte;
spinlock_t *ptl;
struct page *page;
struct mm_struct *mm = vma->vm_mm;
/**
* 對ARM A9來說,沒有配置巨頁功能,follow_huge_addr實際上是空處理。
*/
page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
if (!IS_ERR(page)) {
BUG_ON(flags & FOLL_GET);
goto out;
}
page = NULL;
* 在一級目錄項中,查找位址對應的一級目錄索引項。
pgd = pgd_offset(mm, address);
* 該位址對應的一級目錄項無效。對ARM來說,pgd_none總傳回0,真正的判斷是在pmd_none。
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto no_page_table;
* 查找位址對應的頁上級目錄項。這對4級目錄的分組體系來說才有效。ARM不存在頁上級目錄和頁中間目錄。
* pud總是傳回pgd。
pud = pud_offset(pgd, address);
* pud_none總是傳回0,是以下面的判斷是無用。真正有用的判斷在後面的pmd_none
if (pud_none(*pud))
if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
if (unlikely(pud_bad(*pud)))
* 取頁中間目錄,對ARM來說,pmd直接傳回pud,即pgd。
pmd = pmd_offset(pud, address);
* 判斷pmd是否為0,即ARM一級目錄是否有效。對pgd,pud的判斷都是無用的,真正的判斷在這裡。
if (pmd_none(*pmd))
* 判斷pmd是否是一個巨頁,以及使用者虛拟位址空間段是否是一個巨頁段,略過。
if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
/**
* 查找巨頁位址映射的實體頁面。
*/
page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
* 透明巨頁處理,對某些體系結構,如mips來說,這個功能是有效的。但是雖然ARM硬體支援巨頁(1M頁)
* 目前的核心還不支援ARM巨頁,略過。
if (pmd_trans_huge(*pmd)) {
if (flags & FOLL_SPLIT) {
split_huge_page_pmd(mm, pmd);
goto split_fallthrough;
}
spin_lock(&mm->page_table_lock);
if (likely(pmd_trans_huge(*pmd))) {
if (unlikely(pmd_trans_splitting(*pmd))) {
spin_unlock(&mm->page_table_lock);
wait_split_huge_page(vma->anon_vma, pmd);
} else {
page = follow_trans_huge_pmd(mm, address,
pmd, flags);
goto out;
}
} else
spin_unlock(&mm->page_table_lock);
/* fall through */
split_fallthrough:
* 判斷pmd是否有效。
if (unlikely(pmd_bad(*pmd)))
* 在二級頁表中找到位址對應的pte。并将pte指針傳回。
* 注意,這裡擷取了程序的記憶體頁表鎖。以防止核心其他路徑修改程序頁表,使得ptep指向的pte産生變化。
* ptl是記憶體頁表鎖。
* 如果核心支援将pte表放到高端記憶體,那麼還需要調用kmap_atomic将頁表到核心位址空間中。
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
* 這裡判斷頁表項是否有效。
* 有時,頁面在記憶體中,但是不允許通路。比如寫時複制。
* 當頁完全不在記憶體中時,頁表項也沒有效。
if (!pte_present(pte))
goto no_page;
* 希望搜尋一個可寫的頁面,但是頁表項沒有寫權限。
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
* 根據pte中儲存的頁幀号,找到該頁幀号對應的page結構。
page = vm_normal_page(vma, address, pte);
if (unlikely(!page)) {/* 根據頁幀号無法找到page結構,可能是一些特殊情況。如驅動自行管理的pte出了問題。 */
if ((flags & FOLL_DUMP) || /* 不允許傳回0頁 */
!is_zero_pfn(pte_pfn(pte))) /* 不是0頁 */
goto bad_page;
page = pte_page(pte);/* 向上層傳回0頁 */
* 調用者要求擷取頁面引用,則增加頁面引用計數。
if (flags & FOLL_GET)
get_page(page);
if (flags & FOLL_TOUCH) {/* 調用者希望設定通路标志,可能是随後會寫頁面 */
if ((flags & FOLL_WRITE) &&/* 擷取寫引用 */
!pte_dirty(pte) && !PageDirty(page))/* 頁面和pte的髒标志都還沒有設定,則強制設定髒标志 */
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
* 标記頁面通路标志。
mark_page_accessed(page);
* 調用者想将頁面鎖在記憶體中。
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
if (page->mapping && trylock_page(page)) {/* 鎖住頁面,不交換到外部存儲器中 */
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here and migration is
* blocked by the pte's page reference, we need
* only check for file-cache page truncation.
*/
if (page->mapping)
mlock_vma_page(page);
unlock_page(page);
unlock:
* 釋放程序頁面鎖,同時,如果支援将頁表放到高端記憶體,就解除對頁表的映射。
pte_unmap_unlock(ptep, ptl);
out:
return page;
bad_page:
return ERR_PTR(-EFAULT);
no_page:
if (!pte_none(pte))
return page;
no_page_table:
/*
* When core dumping an enormous anonymous area that nobody
* has touched so far, we don't want to allocate unnecessary pages or
* page tables. Return error instead of NULL to skip handle_mm_fault,
* then get_dump_page() will return NULL to leave a hole in the dump.
* But we can only make this optimization where a hole would surely
* be zero-filled if handle_mm_fault() actually did handle it.
if ((flags & FOLL_DUMP) &&
(!vma->vm_ops || !vma->vm_ops->fault))
return ERR_PTR(-EFAULT);