【作業系統】頁表對映

学习,积累,成长發表於2024-06-10

頁表的一些術語

現在Linux核心中支援四級頁表的對映,我們先看下核心中關於頁表的一些術語:

  • 全域性目錄項,PGD(Page Global Directory)

  • 上級目錄項,PUD(Page Upper Directory)

  • 中間目錄項,PMD(Page Middle Directory)

  • 頁表項,(Page Table)

大家在看核心程式碼時會經常看的以上術語,但在ARM的晶片手冊中並沒有用到這些術語,而是使用L1,L2,L3頁表這種術語。

ARM32 虛擬地址到實體地址的轉換

虛擬地址的32個bit位可以分為3個域,最高12bit位20~31位稱為L1索引,叫做PGD,頁面目錄。中間的8個bit位叫做L2索引,在Linux核心中叫做PT,頁表。最低的12位叫做頁索引。

在ARM處理器中,TTBRx暫存器存放著頁表基地址,我們這裡的一級頁表有4096個頁表項。每個表項中存放著二級表項的基地址。我們可以透過虛擬地址的L1索引訪問一級頁表,訪問一級頁表相當於陣列訪問。

二級頁表通常是動態分配的,可以透過虛擬地址的中間8bit位L2索引訪問二級頁表,在L2索引中存放著最終實體地址的高20bit位,然後和虛擬地址的低12bit位就組成了最終的實體地址。以上就是虛擬地址轉換為實體地址的過程。

MMU訪問頁表是硬體實現的,但頁表的建立和填充需要Linux核心來填充。通常,一級頁表和二級頁表存放在主儲存器中。

ARM32 一級頁表的頁表項

下面這張圖來自ARMV7的手冊。

一級頁表項這裡有三種情況:一種是無效的,第二種是一級頁表的表項。第三種是段對映的頁表項。

  • bit 0 ~ bit 1:用來表示這個頁表項是一級頁表還是段對映的表項。

  • PXN:PL1 表示是否可以執行這段程式碼,為0表示可執行,1表示不可執行。

  • NS:none-security bit,用於安全擴充套件。

  • Domain:Domain域,指明所屬的域,Linux中只使用了3個域。

  • bit31:bit10:指向二級頁表基地址。

二級頁表的表項

  • bit0:禁止執行標誌。1表示禁止執行,0表示可執行

  • bit1:區分是大頁還是小頁

  • C/B bit:記憶體區域屬性

  • TEX[2:0]:記憶體區域屬性

  • AP[0:1] :訪問許可權

  • S:是否可共享

  • nG:用於TLB

ARM64 頁表

ARM體系結構從ARMV8-A開始就支援64bit位,最大支援48根地址線。那為什麼不支援64根地址線呢?主要原因是48根地址線時已支援最大訪問空間為256TB(核心空間和使用者空間分別256TB)滿足了大部分應用的需求。而且,64根地址線時,晶片的設計複雜度會急劇增加。ARMV8-A架構中,支援4KB,16KB和64KB的頁,支援3級或者4級對映。

下面我們以4KB大小頁+4級對映介紹下虛擬地址到實體地址的對映過程。

  • 0~11 :頁索引

  • bit 63 :頁表基地址選擇位,ARMV8架構中有2兩個頁表基地址,一個用於使用者空間,一個使用者核心空間。

  • 39~47:L0索引

  • 30~38:L1索引

  • 21~29:L2索引

  • 12~20:L3 索引

假設頁表基地址為TTBRx,訪問頁表基地址就能訪問到L0頁表的基地址,可以使用L0索引的值作為offset去訪問L0頁表。

L0的頁表項包含了下一級L1頁表的基地址,同樣的,可以使用L1索引的值作為offset去訪問L2頁表。以此類推。

最後透過L3的頁表項可以得到實體地址的bit12 ~ 47位,這個時候再將虛擬地址的頁索引位對應到實體地址的0~11就是完整的實體地址。

Linux核心關於頁表的函式

Linux核心中頁表操作的宏定義

Linux核心中封裝了很多宏來處理頁表

#define pgd_offset_k(addr) pgd_offset(&init_mm,addr) //由虛擬地址來獲取核心頁表的PGD頁表的相應的頁表項 
#define pgd_offset(mm,addr) ((mm)->pgd + pgd_index(addr)) //由虛擬地址來獲取使用者程序的頁表中相應的PGD表項
pgd_index(addr) //由虛擬地址找到PGD頁表的索引
pte_index(addr) //由虛擬地址找到PT頁表的索引
pte_offset_kernel(pmd,addr) //查詢核心頁表中對應的PT頁表的表項 

判斷頁表項的狀態

#define pte_none(pte)		(!pte_val(pte))	//pte是否存在
#define pte_present(pte)	(pte_isset((pte), L_PTE_PRESENT))	//present位元位
#define pte_valid(pte)		(pte_isset((pte), L_PTE_VALID))	//pte是否有效
#define pte_accessible(mm, pte)	(mm_tlb_flush_pending(mm) ? pte_present(pte) : pte_valid(pte))
#define pte_write(pte)		(pte_isclear((pte), L_PTE_RDONLY))	//pte是否可寫
#define pte_dirty(pte)		(pte_isset((pte), L_PTE_DIRTY))	//pte是否有髒資料
#define pte_young(pte)		(pte_isset((pte), L_PTE_YOUNG))	//
#define pte_exec(pte)		(pte_isclear((pte), L_PTE_XN))

修改頁表

mk_pte() //建立的相應的頁表項
pte_mkdirty() // 設定dirty標誌位
pte_mkold() // 清除Accessed標誌位
pte_mkclean() //清除dirty標誌位
pte_mkwrite()// 設定讀寫標誌位
pte_wrprotect() //清除讀寫標誌位
pte_mkyoung()//設定Accessed標誌位
set_pte_at()// 設定頁表項到硬體中

例子1 核心頁表的對映

前面我們介紹了很多關於核心的宏,函式,下面我們透過實際的例子學習如何使用這些宏

系統初始化時需要把kernel image區域和線性對映區建立頁表對映,這個時候依次呼叫start_kernel() --> setup_arch() --> paging_init() --> map_lowmem() --> create_mapping()去建立核心頁表。我們可以研究下核心是如何建立核心頁表的對映。

/*
 * Create the page directory entries and any necessary
 * page tables for the mapping specified by `md'.  We
 * are able to cope here with varying sizes and address
 * offsets, and we take full advantage of sections and
 * supersections.
 */
static void __init create_mapping(struct map_desc *md)
{
	if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
		pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
			(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
		return;
	}

	if (md->type == MT_DEVICE &&
	    md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START &&
	    (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
		pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
			(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
	}

	__create_mapping(&init_mm, md, early_alloc, false);
}

首先會檢查對映的虛擬地址是否在核心向量表的基址以上,並且小於使用者空間的TASK_SIZETASK_SIZE通常被定義為0xC0000000(3GB),表示使用者空間的虛擬地址範圍從0到3GB。對於64位體系結構,TASK_SIZE通常被定義為0x00007fffffffffff(128TB)。

接著會檢查對映的型別是否為裝置型別,並且虛擬地址在頁偏移以上且低於FIXADDR_START,且不在VMALLOC_STARTVMALLOC_END之間(即不在vmalloc空間中)。

最後會呼叫__create_mapping函式建立對映。傳入初始記憶體管理結構體init_mm、對映描述結構體md、早期記憶體分配函式early_alloc,以及false標誌。

/*
 * Create a mapping for the given map descriptor, md. The function
 * __create_mapping is used for both kernel and user mode mappings.
 *
 * @mm:         the mm structure where the mapping will be created
 * @md:        the map descriptor with the details of the mapping
 * @alloc:      a pointer to a function used to allocate pages for the mapping
 * @ng:         a boolean flag indicating if the mapping is non-global
 */
static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md,
				    void *(*alloc)(unsigned long sz),
				    bool ng)
{
	unsigned long addr, length, end;
	phys_addr_t phys;
	const struct mem_type *type;
	pgd_t *pgd;

	type = &mem_types[md->type];

#ifndef CONFIG_ARM_LPAE----------------------(1)
	/*
	 * Catch 36-bit addresses
	 */
	if (md->pfn >= 0x100000) {
		create_36bit_mapping(mm, md, type, ng);
		return;
	}
#endif

	addr = md->virtual & PAGE_MASK;----------------------(2)
	phys = __pfn_to_phys(md->pfn);
	length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));

	/*
	 * Check if the mapping can be made using pages.
	 * If not, print a warning and ignore the request.
	 */
	if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {----------------------(3)
		pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
			(long long)__pfn_to_phys(md->pfn), addr);
		return;
	}

	pgd = pgd_offset(mm, addr);
	end = addr + length;----------------------(4)
	do {
		unsigned long next = pgd_addr_end(addr, end);----------------------(5)

		/*
		 * Allocate a page directory entry for this range.
		 * Initialize it with the appropriate page table
		 * and make the mapping.
		 */
		alloc_init_p4d(pgd, addr, next, phys, type, alloc, ng);----------------------(6)

		/*
		 * Update the phys value with the end of the last mapped
		 * page so that the next range can be allocated properly.
		 */
		phys += next - addr;
		addr = next;----------------------(7)
	} while (pgd++, addr != end);
}

__create_mapping完成中建立對映的功能,根據給定的對映描述結構體,將虛擬地址與實體地址進行對映。

(1) 系統沒有啟用ARM LPAE(Large Physical Address Extension),並且物理頁幀號大於等於0x100000,呼叫create_36bit_mapping函式進行處理,然後返回。

在早期階段,地址匯流排也是32位的,即4G的記憶體地址空間。隨著應用程式越來越豐富,佔用的記憶體總量很容易就超過了4G。但由於程式設計模型和地址匯流排的限制,是無法使用超過4G的實體地址的。所以PAE/LPAE這種大記憶體地址方案應運而生。

PAE/LAPE方案其它很簡單,程式設計視角依然還是32位(4G)的地址空間,這層是虛擬地址空間。而計算機地址匯流排卻使用超過32位的,比如X86的就使用36位(64G)的地址匯流排,ARM使用的是48位(64G)的地址匯流排。中間是透過保護模式(X86架構)或者MMU機制(ARM架構)提供的分頁技術(paging)實現32位虛擬地址訪問超過4G的實體記憶體空間。這項技術的關鍵是分頁技術中的頁表項使用超過4位元組的對映表 (ARM在LPAE模式下,頁表項是8位元組),因為使用超過4位元組對映表,就可以指示超過4G的記憶體空間。

(2) 獲取虛擬地址的起始地址,因為地址對映的最小單位是page,因此這裡進行mapping的虛擬地址需要對齊到page size,同樣的,長度也需要對齊到page size。

(3) 首先檢查對映型別的prot_l1欄位是否為0。prot_l1表示第一級頁表(Level 1 Page Table)的保護位。如果prot_l1為0,表示無法使用頁面進行對映。如果地址、實體地址和長度與SECTION_MASK存在非零位,表示頁面對映要求地址和長度並未按頁面大小對齊。

(4)設定了頁全域性目錄(pgd)的初始偏移,並將結束地址(end)設定為起始地址(addr)加上長度(length)。

(5)然後,使用pgd_addr_end函式計算下一個地址(next),該地址是當前地址和結束地址之間的較小值。

(6)呼叫alloc_init_p4d函式,為當前範圍內的地址分配一個頁目錄項,初始化它的頁表,並進行對映。該函式使用給定的引數pgdaddrnextphystypeallocng來執行這些操作。

(7)更新phys的值,使其加上當前範圍內對映的頁面數,以便正確分配下一個範圍的地址。最後,在迴圈的末尾,遞增pgd的值,並檢查是否達到了結束地址。如果沒有達到,繼續迴圈處理下一個地址範圍。

例子2 程序頁表的對映

remap_pfn_range函式對於寫過Linux驅動的人都不陌生,很多驅動程式的mmap函式都會呼叫到該函式,該函式實現了物理空間到使用者程序的對映。

比如我們在使用者空間讀寫SOC的暫存器時,ARM中的暫存器通常都是memory map形式的,在使用者空間都要讀寫ARM空間的暫存器,通常都要操作/dev/mem裝置來實現,最後都會呼叫到remap_pfn_range來實現。

  • VMA:準備要對映的程序地址空間的VMA的資料結構

  • addr:要對映到 使用者空間的起始地址

  • pfn:準備要對映的實體記憶體的頁幀號

  • size:表示要對映的大小

  • prot:表示要對映的屬性

接下來我們從頁表的角度看下函式的實現

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
		    unsigned long pfn, unsigned long size, pgprot_t prot)
{
	pgd_t *pgd;
	unsigned long next;
	unsigned long end = addr + PAGE_ALIGN(size);
	struct mm_struct *mm = vma->vm_mm;//從VMA獲取當前程序的mm_struct結構
	unsigned long remap_pfn = pfn;
	int err;

	if (WARN_ON_ONCE(!PAGE_ALIGNED(addr)))
		return -EINVAL;

	if (is_cow_mapping(vma->vm_flags)) {
		if (addr != vma->vm_start || end != vma->vm_end)
			return -EINVAL;
		vma->vm_pgoff = pfn;
	}

	err = track_pfn_remap(vma, &prot, remap_pfn, addr, PAGE_ALIGN(size));
	if (err)
		return -EINVAL;

	vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;//設定vm_flags,remap_pfn_range直接使用實體記憶體。Linux核心對物理頁面分為兩類:normal mapping,special mapping。special mapping就是核心不希望該頁面參與到核心的頁面回收等活動中。

	BUG_ON(addr >= end);
	pfn -= addr >> PAGE_SHIFT;
	pgd = pgd_offset(mm, addr);//找到頁表項
	flush_cache_range(vma, addr, end);
    //以PGD_SIZE為步長遍歷頁表
	do {
		next = pgd_addr_end(addr, end);//獲取下一個PGD頁表項的管轄的地址範圍的起始地址
		err = remap_p4d_range(mm, pgd, addr, next,
				pfn + (addr >> PAGE_SHIFT), prot);//繼續遍歷下一級頁表
		if (err)
			break;
	} while (pgd++, addr = next, addr != end);

	if (err)
		untrack_pfn(vma, remap_pfn, PAGE_ALIGN(size));

	return err;
}

遍歷PUD頁表

static inline int remap_pud_range(struct mm_struct *mm, p4d_t *p4d,
			unsigned long addr, unsigned long end,
			unsigned long pfn, pgprot_t prot)
{
	pud_t *pud;
	unsigned long next;
	int err;

	pfn -= addr >> PAGE_SHIFT;
	pud = pud_alloc(mm, p4d, addr);//找到pud頁表項。對於二級頁表來說,PUD指向PGD
	if (!pud)
		return -ENOMEM;
    //以PUD_SIZE為步長遍歷頁表
	do {
		next = pud_addr_end(addr, end);//獲取下一個PUD頁表項的管轄的地址範圍的起始地址
		err = remap_pmd_range(mm, pud, addr, next,
				pfn + (addr >> PAGE_SHIFT), prot);//繼續遍歷下一級頁表
		if (err)
			return err;
	} while (pud++, addr = next, addr != end);
	return 0;
}

Linux核心中實現了4級頁表,對於ARM32來說,它是如何跳過中間兩級頁表的呢?大家可以看下以下兩個宏的實現

/* Find an entry in the second-level page table.. */
#ifndef pmd_offset
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
	return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
#define pmd_offset pmd_offset
#endif

接收指向頁上級目錄項的指標 pud 和線性地址 addr 作為引數。這個宏產生目錄項 addr 在頁中間目錄中的偏移地址。在兩級或三級分頁系統中,它產生 pud ,即頁全域性目錄項的地址。

#ifndef pud_offset
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address)
{
	return (pud_t *)p4d_page_vaddr(*p4d) + pud_index(address);
}
#define pud_offset pud_offset
#endif

引數為指向頁全域性目錄項的指標 pgd 和線性地址 addr 。這個宏產生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統中,該宏產生 pgd ,即一個頁全域性目錄項的地址。

遍歷PMD頁表

remap_pmd_range函式和remap_pud_range類似。

static inline int ioremap_pmd_range(pud_t *pud, unsigned long addr,
		unsigned long end, phys_addr_t phys_addr, pgprot_t prot,
		pgtbl_mod_mask *mask)
{
	pmd_t *pmd;
	unsigned long next;

	pmd = pmd_alloc_track(&init_mm, pud, addr, mask);//找到對應的pmd頁表項,對於二級頁表來說,pmd指向pud
	if (!pmd)
		return -ENOMEM;
    //以PMD_SIZE為步長遍歷頁表
	do {
		next = pmd_addr_end(addr, end);//獲取下一個PMD頁表項的管轄的地址範圍的起始地址

		if (ioremap_try_huge_pmd(pmd, addr, next, phys_addr, prot)) {
			*mask |= PGTBL_PMD_MODIFIED;
			continue;
		}
	//繼續遍歷下一級頁表
		if (ioremap_pte_range(pmd, addr, next, phys_addr, prot, mask))
			return -ENOMEM;
	} while (pmd++, phys_addr += (next - addr), addr = next, addr != end);
	return 0;
}

遍歷PT頁表

/*
 * maps a range of physical memory into the requested pages. the old
 * mappings are removed. any references to nonexistent pages results
 * in null mappings (currently treated as "copy-on-access")
 */
static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
			unsigned long addr, unsigned long end,
			unsigned long pfn, pgprot_t prot)
{
	pte_t *pte, *mapped_pte;
	spinlock_t *ptl;
	int err = 0;

	mapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);//尋找相應的pte頁表項。注意這裡需要申請一個spinlock鎖用來保護修改pte頁表
	if (!pte)
		return -ENOMEM;
	arch_enter_lazy_mmu_mode();
    //以PAGE_SIZE為步長遍歷PT頁表
	do {
		BUG_ON(!pte_none(*pte));
		if (!pfn_modify_allowed(pfn, prot)) {
			err = -EACCES;
			break;
		}
        /*
        *pte_none()判斷這個pte是否存在
        *pfn_pte()由頁幀號pfn得到pte
        *pte_mkspecial()設定軟體的PTE_SPECIAL標誌位(三級頁表才會用該標誌位)
        *set_pte_at() 把pte設定到硬體頁表中
        */
		set_pte_at(mm, addr, pte, pte_mkspecial(pfn_pte(pfn, prot)));
		pfn++;
	} while (pte++, addr += PAGE_SIZE, addr != end);
	arch_leave_lazy_mmu_mode();
	pte_unmap_unlock(mapped_pte, ptl);//PT頁表設定完成後,需要把spinlock 釋放
	return err;
}

缺頁中斷do_anonymous_page

在缺頁中斷處理中,匿名頁面的觸發條件為下面的兩個條件,當滿足這兩個條件的時候就會呼叫do_anonymous_page函式來處理匿名對映缺頁異常,程式碼實現在mm/memory.c檔案中

  • 發生缺頁的地址所在頁表項不存在
  • 是匿名頁,即是vma->vm_ops為空,即vm_operations函式指標為空

我們知道在程序的task_struct結構中包含了一個mm_struct結構的指標,mm_struct用來描述一個程序的虛擬地址空間。程序的 mm_struct 則包含裝入的可執行映像資訊以及程序的頁目錄指標pgd。該結構還包含有指向 ~vm_area_struct ~結構的幾個指標,每個 vm_area_struct 代表程序的一個虛擬地址區間。vm_area_struct 結構含有指向vm_operations_struct 結構的一個指標,vm_operations_struct 描述了在這個區間的操作。vm_operations 結構中包含的是函式指標;其中,open、close 分別用於虛擬區間的開啟、關閉,而nopage 用於當虛存頁面不在實體記憶體而引起的“缺頁異常”時所應該呼叫的函式

/*
 * We enter with non-exclusive mmap_lock (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with mmap_lock still held, but pte unmapped and unlocked.
 */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;
	vm_fault_t ret = 0;
	pte_t entry;

	/* File mapping without ->vm_ops ? */
	if (vma->vm_flags & VM_SHARED)-----------------(1)
		return VM_FAULT_SIGBUS;

	/*
	 * Use pte_alloc() instead of pte_alloc_map().  We can't run
	 * pte_offset_map() on pmds where a huge pmd might be created
	 * from a different thread.
	 *
	 * pte_alloc_map() is safe to use under mmap_write_lock(mm) or when
	 * parallel threads are excluded by other means.
	 *
	 * Here we only have mmap_read_lock(mm).
	 */
	if (pte_alloc(vma->vm_mm, vmf->pmd))-----------------(2)
		return VM_FAULT_OOM;

	/* See the comment in pte_alloc_one_map() */
	if (unlikely(pmd_trans_unstable(vmf->pmd)))
		return 0;

	/* Use the zero-page for reads */
	if (!(vmf->flags & FAULT_FLAG_WRITE) &&
			!mm_forbids_zeropage(vma->vm_mm)) {-----------------(3)
		entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),-----------------(4)
						vma->vm_page_prot));
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,-----------------(5)
				vmf->address, &vmf->ptl);
		if (!pte_none(*vmf->pte)) {-----------------(6)
			update_mmu_tlb(vma, vmf->address, vmf->pte);
			goto unlock;
		}
		ret = check_stable_address_space(vma->vm_mm);-----------------(7)
		if (ret)
			goto unlock;
		/* Deliver the page fault to userland, check inside PT lock */
		if (userfaultfd_missing(vma)) {-----------------(8)
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return handle_userfault(vmf, VM_UFFD_MISSING);
		}
		goto setpte;
	}

	/* Allocate our own private page. */
	if (unlikely(anon_vma_prepare(vma)))-----------------(9)
		goto oom;
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);-----------------(10)
	if (!page)
		goto oom;

	if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))-----------------(11)
		goto oom_free_page;
	cgroup_throttle_swaprate(page, GFP_KERNEL);

	/*
	 * The memory barrier inside __SetPageUptodate makes sure that
	 * preceding stores to the page contents become visible before
	 * the set_pte_at() write.
	 */
	__SetPageUptodate(page);-----------------(12)

	entry = mk_pte(page, vma->vm_page_prot);-----------------(13)
	entry = pte_sw_mkyoung(entry);
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry));-----------------(14)

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);-----------------(15)
	if (!pte_none(*vmf->pte)) {
		update_mmu_cache(vma, vmf->address, vmf->pte);-----------------(16)
		goto release;
	}

	ret = check_stable_address_space(vma->vm_mm);-----------------(17)
	if (ret)
		goto release;

	/* Deliver the page fault to userland, check inside PT lock */
	if (userfaultfd_missing(vma)) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		put_page(page);
		return handle_userfault(vmf, VM_UFFD_MISSING);
	}

	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);-----------------(18)
	page_add_new_anon_rmap(page, vma, vmf->address, false);-----------------(19)
	lru_cache_add_inactive_or_unevictable(page, vma);-----------------(20)
setpte:
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);-----------------(21)

	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);-----------------(22)
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
release:
	put_page(page);
	goto unlock;
oom_free_page:
	put_page(page);
oom:
	return VM_FAULT_OOM;
}
  1. 如果是共享則意味著之前以及透過mmap方式在其他程序申請過實體記憶體,vma應該存在對應實體記憶體對映,不應該再發生page fault
  2. 呼叫pte_alloc函式來為頁面表表項(PTE)分配記憶體,並傳遞vma->vm_mm和vmf->pmd作為引數
  3. 如果頁面錯誤不是寫操作且記憶體管理子系統允許使用零頁,則對映到零頁面
  4. 生成一個特殊頁表項,對映到專有的0頁,一頁大小
  5. 據pmd,address找到pte表對應的一個表項,並且lock住
  6. 如果頁表項不為空,則呼叫update_mmu_tlb函式更新記憶體管理單元(MMU)的轉換查詢緩衝(TLB)並且跳unlock。
  7. 檢查地址空間的穩定性。
  8. 如果發現userfaultfd缺失,則解除對映並解鎖頁面表項(PTE)
  9. 對vma進行預處理,主要是建立anon_vma和anon_vma_chain,為後續反向對映做準備
  10. 從高階記憶體區的夥伴系統中獲取一個頁,這個頁會清0
  11. 申請記憶體成功之後,將新申請的page加入到mcgroup管理
  12. 設定此頁的PG_uptodate標誌,表示此頁是最新的
  13. 將頁面和頁面保護位(vma->vm_page_prot)組合成一個 PTE 條目。
  14. 如果vma區是可寫的,則給頁表項新增允許寫標誌。將 PTE 條目的 Dirty 位和 Young 位設定為1。
  15. 鎖定 pte 條目,防止同時更新和更多虛擬記憶體對實體記憶體對映
  16. pte條目存在的話,讓mmu更新頁表項,應該會清除tlb
  17. 檢查給定的記憶體是否從使用者複製過來的。如果從使用者複製過來的記憶體不穩定,不用處理。
  18. 增加mm_struct中匿名頁的統計計數
  19. 對這個新頁進行反向對映,主要工作是:設定此頁的_mapcount = 0,說明此頁正在使用,但是是非共享的(>0是共享)。設定page->mapping最低位為1,page->mapping指向此vma->anon_vmapage->index存放此page在vma中的第幾頁。
  20. 透過判斷,將頁加入到活動lru快取或者不能換出頁的lru連結串列
  21. 將上面配置好的頁表項寫入頁表
  22. 更新mmu的cache

do_anonymous_page首先判斷一下匿名頁是否是共享的,如果是共享的匿名對映,但是虛擬記憶體區域沒有提供虛擬記憶體操作集合
就返回錯誤;然後判斷一下pte頁表是否存在,如果直接頁表不存在,那麼分配頁表;

接下來判讀缺頁異常是由讀操作觸發的還是寫操作觸發的,如果是讀操作觸發的,生成特殊的頁表項,對映到專用的零頁,設定頁表項後返回;如果是寫操作觸發的,需要初始化vma中的anon_vma_chain和anon_vma,分配物理頁用於匿名對映,呼叫mk_pte函式生成頁表項,設定頁表項的髒標誌位和寫許可權,設定頁表項後返回。

小結

從以上的分析中,我們可以學習到關於常用的頁表的宏的使用方法。Linux核心就是這樣,你不光可以看到某個函式的實現,還可以看到某個函式的呼叫過程。所以,大家對某個函式有疑問的時候,可以順著這樣的思路去學習。

ARM32頁表和Linux頁表那些奇葩的地方

ARM32硬體頁表中PGD頁目錄項PGD是從20位開始的,但是為何標頭檔案定義是從21位開始?

歷史原因:Linux最初是基於x86的體系結構設計的,因此Linux核心很多的標頭檔案的定義都是基於x86的,特別是關於PTE頁表項裡面的很多位元位的定義。因此ARM在移植到Linux時只能參考x86版本的Linux核心的實現。

X86的PGD是從bit22 ~ bit31,總共10bit位,1024頁表項。PT頁表從bit12 ~ bit 21 ,總共 10 bit位,1024頁表項。

ARM的PGD是從bit20 ~ bit31,總共12bit, 4096頁表項。PT域從bit12 ~ bit 19,總共8bit,2556頁表項。

X86和ARM頁表最大的差異在於PTE頁表內容的不同。

Linux核心版本的PTE位元位的定義

/*
 * "Linux" PTE definitions for LPAE.
 *
 * These bits overlap with the hardware bits but the naming is preserved for
 * consistency with the classic page table format.
 */
#define L_PTE_VALID		(_AT(pteval_t, 1) << 0)		/* Valid */
#define L_PTE_PRESENT		(_AT(pteval_t, 3) << 0)		/* Present */
#define L_PTE_USER		(_AT(pteval_t, 1) << 6)		/* AP[1] */
#define L_PTE_SHARED		(_AT(pteval_t, 3) << 8)		/* SH[1:0], inner shareable */
#define L_PTE_YOUNG		(_AT(pteval_t, 1) << 10)	/* AF */
#define L_PTE_XN		(_AT(pteval_t, 1) << 54)	/* XN */
#define L_PTE_DIRTY		(_AT(pteval_t, 1) << 55)
#define L_PTE_SPECIAL		(_AT(pteval_t, 1) << 56)
#define L_PTE_NONE		(_AT(pteval_t, 1) << 57)	/* PROT_NONE */
#define L_PTE_RDONLY		(_AT(pteval_t, 1) << 58)	/* READ ONLY */

#define L_PMD_SECT_VALID	(_AT(pmdval_t, 1) << 0)
#define L_PMD_SECT_DIRTY	(_AT(pmdval_t, 1) << 55)
#define L_PMD_SECT_NONE		(_AT(pmdval_t, 1) << 57)
#define L_PMD_SECT_RDONLY	(_AT(pteval_t, 1) << 58)

ARM32的PTE位元位的定義

/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN		(_AT(pteval_t, 1) << 0)		/* v6 */
#define PTE_EXT_AP_MASK		(_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0		(_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1		(_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO	(_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW	(PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW	(PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW	(PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)		(_AT(pteval_t, (x)) << 6)	/* v5 */
#define PTE_EXT_APX		(_AT(pteval_t, 1) << 9)		/* v6 */
#define PTE_EXT_COHERENT	(_AT(pteval_t, 1) << 9)		/* XScale3 */
#define PTE_EXT_SHARED		(_AT(pteval_t, 1) << 10)	/* v6 */
#define PTE_EXT_NG		(_AT(pteval_t, 1) << 11)	/* v6 */

那X86和ARM的頁表差距這麼大,軟體怎麼設計呢?Linux核心的記憶體管理已經適配了X86的頁表項,我們可以透過軟體適配的辦法來解決這個問題。因此,ARM公司在移植該方案時提出了兩套頁表的方案。一套頁表是為了迎合ARM硬體的真實頁表,另一套頁表是為了迎合Linux真實的頁表。

對於PTE頁表來說,一下子就多出了一套頁表,一套頁表256表項,每個表項佔用4位元組。為了軟體實現的方便,軟體會把兩個頁表合併成一個頁表。4套頁表正好佔用256 * 4 * 4 = 4K的空間。因此,Linux實現的時候,就分配了一個page 來存放這些頁表。

這一套方案的話,相當於每個PGD頁表項有8位元組,包含指向兩套PTE頁表項的entry。每4個位元組指向一個物理的二級頁表。

本文參考

奔跑吧Linux核心

http://www.wowotech.net/memory_management/mem_init_3.html

http://blog.chinaunix.net/uid-628190-id-5821835.html

https://blog.csdn.net/zhoutaopower/article/details/88940727

https://blog.csdn.net/zhoutaopower/article/details/88940727

https://zhuanlan.zhihu.com/p/543076384

https://blog.csdn.net/huyugv_830913/article/details/5884628

https://zhuanlan.zhihu.com/p/452139283

https://www.cnblogs.com/arnoldlu/p/8335508.html

https://www.cnblogs.com/tolimit/p/5398552.html

https://blog.csdn.net/weixin_42419952/article/details/124392825

https://blog.csdn.net/sinat_22338935/article/details/128899811

https://zhuanlan.zhihu.com/p/377905409

https://www.cnblogs.com/pwl999/p/15534986.html

相關文章