天天看點

《LINUX3.0核心源代碼分析》第一章:記憶體尋址

摘要:本章主要介紹了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) &amp; L_PTE_PRESENT)

 * 頁表項是否有可寫标志

#define pte_write(pte)                (!(pte_val(pte) &amp; L_PTE_RDONLY))

 * 頁表項是否為髒

#define pte_dirty(pte)                 (pte_val(pte) &amp; L_PTE_DIRTY)

 * 頁表項是否表示最近沒有被通路過

#define pte_young(pte)               (pte_val(pte) &amp; L_PTE_YOUNG)

 * 頁表項是否有可執行标志

#define pte_exec(pte)                 (!(pte_val(pte) &amp; 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) &amp; 2)

1.1.2              頁表項操作函數

 * 虛拟位址在頁全局目錄中索引

#define pgd_index(addr)             ((addr) &gt;&gt; PGDIR_SHIFT)

 * 計算一個程序使用者态位址對應的頁全局目錄項位址。

 * 計算核心态位址的頁全局目錄項位址應當使用pgd_offset_k

#define pgd_offset(mm, addr)  ((mm)-&gt;pgd + pgd_index(addr))

/* to find an entry in a kernel page-table-directory */

 * 計算一個核心态位址的頁全局目錄項位址。

#define pgd_offset_k(addr)        pgd_offset(&amp;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) &gt;&gt; PAGE_SHIFT) &amp; (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) &gt;&gt; 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) &gt;&gt; 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-&gt;vm_mm;

        /**

         * 對ARM A9來說,沒有配置巨頁功能,follow_huge_addr實際上是空處理。

         */

        page = follow_huge_addr(mm, address, flags &amp; FOLL_WRITE);

        if (!IS_ERR(page)) {

                 BUG_ON(flags &amp; 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) &amp;&amp; vma-&gt;vm_flags &amp; VM_HUGETLB) {

                 page = follow_huge_pud(mm, address, pud, flags &amp; 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) &amp;&amp; vma-&gt;vm_flags &amp; VM_HUGETLB) {

                 /**

                  * 查找巨頁位址映射的實體頁面。

                  */

                 page = follow_huge_pmd(mm, address, pmd, flags &amp; FOLL_WRITE);

         * 透明巨頁處理,對某些體系結構,如mips來說,這個功能是有效的。但是雖然ARM硬體支援巨頁(1M頁)

         * 目前的核心還不支援ARM巨頁,略過。

        if (pmd_trans_huge(*pmd)) {

                 if (flags &amp; FOLL_SPLIT) {

                           split_huge_page_pmd(mm, pmd);

                           goto split_fallthrough;

                 }

                 spin_lock(&amp;mm-&gt;page_table_lock);

                 if (likely(pmd_trans_huge(*pmd))) {

                           if (unlikely(pmd_trans_splitting(*pmd))) {

                                    spin_unlock(&amp;mm-&gt;page_table_lock);

                                    wait_split_huge_page(vma-&gt;anon_vma, pmd);

                           } else {

                                    page = follow_trans_huge_pmd(mm, address,

                                                                     pmd, flags);

                                    goto out;

                           }

                 } else

                           spin_unlock(&amp;mm-&gt;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, &amp;ptl);

        pte = *ptep;

         * 這裡判斷頁表項是否有效。

         * 有時,頁面在記憶體中,但是不允許通路。比如寫時複制。

         * 當頁完全不在記憶體中時,頁表項也沒有效。

        if (!pte_present(pte))

                 goto no_page;

         * 希望搜尋一個可寫的頁面,但是頁表項沒有寫權限。

        if ((flags &amp; FOLL_WRITE) &amp;&amp; !pte_write(pte))

                 goto unlock;

         * 根據pte中儲存的頁幀号,找到該頁幀号對應的page結構。

        page = vm_normal_page(vma, address, pte);

        if (unlikely(!page)) {/* 根據頁幀号無法找到page結構,可能是一些特殊情況。如驅動自行管理的pte出了問題。 */

                 if ((flags &amp; FOLL_DUMP) || /* 不允許傳回0頁 */

                     !is_zero_pfn(pte_pfn(pte))) /* 不是0頁 */

                           goto bad_page;

                 page = pte_page(pte);/* 向上層傳回0頁 */

         * 調用者要求擷取頁面引用,則增加頁面引用計數。

        if (flags &amp; FOLL_GET)

                 get_page(page);

        if (flags &amp; FOLL_TOUCH) {/* 調用者希望設定通路标志,可能是随後會寫頁面 */

                 if ((flags &amp; FOLL_WRITE) &amp;&amp;/* 擷取寫引用 */

                     !pte_dirty(pte) &amp;&amp; !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 &amp; FOLL_MLOCK) &amp;&amp; (vma-&gt;vm_flags &amp; 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-&gt;mapping &amp;&amp; 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-&gt;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 &amp; FOLL_DUMP) &amp;&amp;

            (!vma-&gt;vm_ops || !vma-&gt;vm_ops-&gt;fault))

                 return ERR_PTR(-EFAULT);

繼續閱讀