《LINUX3.0内核源代码分析》第一章:内存寻址
https://blog.csdn.net/ekenlinbing/article/details/7613334
摘要:本章主要介紹了LINUX3.0內(nèi)存尋址方面的內(nèi)容,重點(diǎn)對follow_page函數(shù)進(jìn)行注釋,以幫助讀者大致了解ARM A9的頁表組織。 讀者需要理解一些基本概念:虛擬地址、物理地址、MPU、MMU、ARM中的二級頁表、cache、TLB。
法律聲明:《LINUX3.0內(nèi)核源代碼分析》系列文章由謝寶友(scxby@163.com)發(fā)表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代碼遵循GPL協(xié)議。除此以外,文檔中的其他內(nèi)容由作者保留所有版權(quán)。謝絕轉(zhuǎn)載。
本連載文章并不是為了形成一本適合出版的書籍,而是為了向有一定內(nèi)核基本的讀者提供一些linux3.0源碼分析。因此,請讀者結(jié)合《深入理解LINUX內(nèi)核》第三版閱讀本連載。
本系列文章分析ARM A9的linux3.0代碼實現(xiàn)。因此,需要讀者有一定的ARM體系硬件知識。推薦閱讀《ARM嵌入式系統(tǒng)開發(fā)-軟件設(shè)計與優(yōu)化》。另外,讀者最好對內(nèi)核有所了解,推薦閱讀《深入理解LINUX內(nèi)核》第三版。
讀者需要理解一些基本概念:虛擬地址、物理地址、MPU、MMU、ARM中的二級頁表、cache、TLB。
1.1 基本函數(shù)
Linux3.0將分頁抽象為四級:
| 名稱 | 數(shù)據(jù)結(jié)構(gòu) | 備注 |
|---|---|---|
| 頁全局目錄 | Pgd_t | |
| 頁上級目錄 | Pud_t | A9未用 |
| 頁中間目錄 | Pmd_t | A9未用 |
| 頁表 | Pte_t |
/*** 對A9來說,只支持4K大小的頁,因此PAGE_SHIFT定義為12.它表示一個虛擬地址的頁內(nèi)偏移量的位數(shù)。* 根據(jù)它計算出來的頁大小PAGE_SIZE為4K,PAGE_MASK為0xffff000。*/#define PAGE_SHIFT 12#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)#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/*** 分別代表一個頁表、頁中間目錄、頁全局目錄表中表項的個數(shù)。*/#define PTRS_PER_PTE 512#define PTRS_PER_PMD 1#define PTRS_PER_PGD 2048/*** 將pte\pmd\pud\pgd\pgprot轉(zhuǎn)換為整型值*/#define pte_val(x) (x)#define pmd_val(x) (x)#define pgd_val(x) ((x)[0])#define pgprot_val(x) (x)/*** 將整型值轉(zhuǎn)換為pte\pmd\pud\pgd\pgprot*/#define __pte(x) (x)#define __pmd(x) (x)#define __pgprot(x) (x)
1.1.1 判斷頁表項標(biāo)志的函數(shù)
/*** 頁表項是否為0*/#define pte_none(pte) (!pte_val(pte))/*** 頁表項是否可用。當(dāng)頁在內(nèi)存中但是不可讀寫時置此標(biāo)志。典型的用途是寫時復(fù)制。*/#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)/*** 頁表項是否有可寫標(biāo)志*/#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)/*** 頁表項是否有可執(zhí)行標(biāo)志*/#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)/*** 判斷兩個頁表項是否指向相同的頁并且有相同的訪問權(quán)限*/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)/*** 頁表項是否可用。當(dāng)頁在內(nèi)存中但是不可讀寫時置此標(biāo)志。典型的用途是寫時復(fù)制。*/#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)
1.1.2 頁表項操作函數(shù)
/*** 虛擬地址在頁全局目錄中索引*/#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)/*** 計算一個進(jìn)程用戶態(tài)地址對應(yīng)的頁全局目錄項地址。* 計算內(nèi)核態(tài)地址的頁全局目錄項地址應(yīng)當(dāng)使用pgd_offset_k*/#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))/* to find an entry in a kernel page-table-directory *//*** 計算一個內(nèi)核態(tài)地址的頁全局目錄項地址。*/#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 }))/*** 在頁全局目錄表中,查找一個虛擬地址對應(yīng)的頁上級目錄位置。* 對二級頁表來說,頁上級目錄就是頁全局目錄,因此直接返回頁全局目錄。*/#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)))/*** 獲得一個線性地址對應(yīng)的頁表項在頁表中的索引*/#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))/*** 在主內(nèi)核頁表中定位內(nèi)核地址對應(yīng)的頁表項的虛擬地址。*/#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))/*** 在進(jìn)程頁表中定位線性地址對應(yīng)的頁表項的地址。如果頁表保存在高端內(nèi)存中,那么還為頁表建立一個臨時內(nèi)核映射。*/#define pte_offset_map(pmd,addr) (__pte_map(pmd) + pte_index(addr))/*** 如果頁表在高端內(nèi)存中,不解除由pte_offset_map建立的臨時內(nèi)核映射。*/#define pte_unmap(pte) __pte_unmap(pte)/*** 獲取頁表項中的頁幀號。*/#define pte_pfn(pte) (pte_val(pte) >> PAGE_SHIFT)/*** 根據(jù)頁幀號和頁面屬性,合成頁表項。*/#define pfn_pte(pfn,prot) __pte(__pfn_to_phys(pfn) | pgprot_val(prot))/*** 從頁表項中提取頁幀號,并定位該頁幀號對應(yīng)的頁框。*/#define pte_page(pte) pfn_to_page(pte_pfn(pte))/*** 根據(jù)頁框和頁面屬性,合成頁表項。*/#define mk_pte(page,prot) pfn_pte(page_to_pfn(page), prot)/*** 當(dāng)頁表項映射到文件,并且沒有裝載進(jìn)內(nèi)存時,從頁表項中提取文件頁號。*/#define pte_to_pgoff(x) (pte_val(x) >> 3)/*** 將頁面映射的頁號存放到頁表項中*/#define pgoff_to_pte(x) __pte(((x) << 3) | L_PTE_FILE)
1.1.3 頁表分配相關(guān)的函數(shù)
/*** 為頁全局目錄分配內(nèi)存*/pgd_t *pgd_alloc(struct mm_struct *mm)/*** 釋放頁全局目錄項*/void pgd_free(struct mm_struct *mm, pgd_t *pgd_base)/*** 分配頁上級目錄,在二級頁表中,此函數(shù)什么也不做。*/#define pud_alloc(mm, pgd, address) (pgd)/*** 釋放頁上級目錄,在二級頁表中,這個函數(shù)什么也不做*/#define pud_free(mm, x) do { } while (0)Pmd_alloc、pmd_free、pte_alloc_map、pte_free等宏或函數(shù)與此類似。
1.2 刷新cache和TLB
Cache是CPU與內(nèi)存之間的緩存,而TLB是CPU與MMU之間緩存。
當(dāng)外部硬件通過DMA修改了內(nèi)存中的數(shù)據(jù)時,需要使cache中的數(shù)據(jù)失效,強(qiáng)制CPU從內(nèi)存中裝載數(shù)據(jù)。當(dāng)CPU向緩存中寫入數(shù)據(jù)后,為了通過DMA將數(shù)據(jù)傳送到外部硬件,則需要將緩存中的數(shù)據(jù)強(qiáng)制寫入內(nèi)存。
當(dāng)頁表項映射的頁面發(fā)生變化后,也需要將頁面緩存的內(nèi)容寫入內(nèi)存。
同理,當(dāng)修改了頁表項后,為了避免TLB中緩存的項進(jìn)行錯誤的MMU轉(zhuǎn)換,也需要使TLB中緩存的項失效。
1.3 follow_page函數(shù)
follow_page函數(shù)是從進(jìn)程的頁表中搜索特定地址對應(yīng)的頁面對象。這個函數(shù)對于理解LINUX內(nèi)核頁表管理有幫助。
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;/*** 在一級目錄項中,查找地址對應(yīng)的一級目錄索引項。*/pgd = pgd_offset(mm, address);/*** 該地址對應(yīng)的一級目錄項無效。對ARM來說,pgd_none總返回0,真正的判斷是在pmd_none。*/if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))goto no_page_table;/*** 查找地址對應(yīng)的頁上級目錄項。這對4級目錄的分組體系來說才有效。ARM不存在頁上級目錄和頁中間目錄。* pud總是返回pgd。*/pud = pud_offset(pgd, address);/*** pud_none總是返回0,因此下面的判斷是無用。真正有用的判斷在后面的pmd_none*/if (pud_none(*pud))goto no_page_table;if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {BUG_ON(flags & FOLL_GET);page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);goto out;}if (unlikely(pud_bad(*pud)))goto no_page_table;/*** 取頁中間目錄,對ARM來說,pmd直接返回pud,即pgd。*/pmd = pmd_offset(pud, address);/*** 判斷pmd是否為0,即ARM一級目錄是否有效。對pgd,pud的判斷都是無用的,真正的判斷在這里。*/if (pmd_none(*pmd))goto no_page_table;/*** 判斷pmd是否是一個巨頁,以及用戶虛擬地址空間段是否是一個巨頁段,略過。*/if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {BUG_ON(flags & FOLL_GET);/*** 查找巨頁地址映射的物理頁面。*/page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);goto out;}/*** 透明巨頁處理,對某些體系結(jié)構(gòu),如mips來說,這個功能是有效的。但是雖然ARM硬件支持巨頁(1M頁)* 目前的內(nèi)核還不支持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);spin_unlock(&mm->page_table_lock);goto out;}} elsespin_unlock(&mm->page_table_lock);/* fall through */}split_fallthrough:/*** 判斷pmd是否有效。*/if (unlikely(pmd_bad(*pmd)))goto no_page_table;/*** 在二級頁表中找到地址對應(yīng)的pte。并將pte指針返回。* 注意,這里獲取了進(jìn)程的內(nèi)存頁表鎖。以防止內(nèi)核其他路徑修改進(jìn)程頁表,使得ptep指向的pte產(chǎn)生變化。* ptl是內(nèi)存頁表鎖。* 如果內(nèi)核支持將pte表放到高端內(nèi)存,那么還需要調(diào)用kmap_atomic將頁表到內(nèi)核地址空間中。*/ptep = pte_offset_map_lock(mm, pmd, address, &ptl);pte = *ptep;/*** 這里判斷頁表項是否有效。* 有時,頁面在內(nèi)存中,但是不允許訪問。比如寫時復(fù)制。* 當(dāng)頁完全不在內(nèi)存中時,頁表項也沒有效。*/if (!pte_present(pte))goto no_page;/*** 希望搜索一個可寫的頁面,但是頁表項沒有寫權(quán)限。*/if ((flags & FOLL_WRITE) && !pte_write(pte))goto unlock;/*** 根據(jù)pte中保存的頁幀號,找到該頁幀號對應(yīng)的page結(jié)構(gòu)。*/page = vm_normal_page(vma, address, pte);if (unlikely(!page)) {/* 根據(jù)頁幀號無法找到page結(jié)構(gòu),可能是一些特殊情況。如驅(qū)動自行管理的pte出了問題。 */if ((flags & FOLL_DUMP) || /* 不允許返回0頁 */!is_zero_pfn(pte_pfn(pte))) /* 不是0頁 */goto bad_page;page = pte_page(pte);/* 向上層返回0頁 */}/*** 調(diào)用者要求獲取頁面引用,則增加頁面引用計數(shù)。*/if (flags & FOLL_GET)get_page(page);if (flags & FOLL_TOUCH) {/* 調(diào)用者希望設(shè)置訪問標(biāo)志,可能是隨后會寫頁面 */if ((flags & FOLL_WRITE) &&/* 獲取寫引用 */!pte_dirty(pte) && !PageDirty(page))/* 頁面和pte的臟標(biāo)志都還沒有設(shè)置,則強(qiáng)制設(shè)置臟標(biāo)志 */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().*//*** 標(biāo)記頁面訪問標(biāo)志。*/mark_page_accessed(page);}/*** 調(diào)用者想將頁面鎖在內(nèi)存中。*/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:/*** 釋放進(jìn)程頁面鎖,同時,如果支持將頁表放到高端內(nèi)存,就解除對頁表的映射。*/pte_unmap_unlock(ptep, ptl);out:return page;bad_page:pte_unmap_unlock(ptep, ptl);return ERR_PTR(-EFAULT);no_page:pte_unmap_unlock(ptep, ptl);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);return page;
總結(jié)
以上是生活随笔為你收集整理的《LINUX3.0内核源代码分析》第一章:内存寻址的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求一个关于错过的个性签名。
- 下一篇: ARM32页表-虚拟地址到物理地址的转换