頁表的一些術語
現在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_SIZE
。TASK_SIZE
通常被定義為0xC0000000(3GB),表示使用者空間的虛擬地址範圍從0到3GB。對於64位體系結構,TASK_SIZE
通常被定義為0x00007fffffffffff(128TB)。
接著會檢查對映的型別是否為裝置型別,並且虛擬地址在頁偏移以上且低於FIXADDR_START
,且不在VMALLOC_START
和VMALLOC_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
函式,為當前範圍內的地址分配一個頁目錄項,初始化它的頁表,並進行對映。該函式使用給定的引數pgd
、addr
、next
、phys
、type
、alloc
和ng
來執行這些操作。
(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;
}
- 如果是共享則意味著之前以及透過mmap方式在其他程序申請過實體記憶體,vma應該存在對應實體記憶體對映,不應該再發生page fault
- 呼叫pte_alloc函式來為頁面表表項(PTE)分配記憶體,並傳遞vma->vm_mm和vmf->pmd作為引數
- 如果頁面錯誤不是寫操作且記憶體管理子系統允許使用零頁,則對映到零頁面
- 生成一個特殊頁表項,對映到專有的0頁,一頁大小
- 據pmd,address找到pte表對應的一個表項,並且lock住
- 如果頁表項不為空,則呼叫
update_mmu_tlb
函式更新記憶體管理單元(MMU)的轉換查詢緩衝(TLB)並且跳unlock。 - 檢查地址空間的穩定性。
- 如果發現
userfaultfd
缺失,則解除對映並解鎖頁面表項(PTE) - 對vma進行預處理,主要是建立anon_vma和anon_vma_chain,為後續反向對映做準備
- 從高階記憶體區的夥伴系統中獲取一個頁,這個頁會清0
- 申請記憶體成功之後,將新申請的page加入到mcgroup管理
- 設定此頁的PG_uptodate標誌,表示此頁是最新的
- 將頁面和頁面保護位(
vma->vm_page_prot
)組合成一個 PTE 條目。 - 如果vma區是可寫的,則給頁表項新增允許寫標誌。將 PTE 條目的
Dirty
位和Young
位設定為1。 - 鎖定
pte
條目,防止同時更新和更多虛擬記憶體對實體記憶體對映 - pte條目存在的話,讓mmu更新頁表項,應該會清除tlb
- 檢查給定的記憶體是否從使用者複製過來的。如果從使用者複製過來的記憶體不穩定,不用處理。
- 增加
mm_struct
中匿名頁的統計計數 - 對這個新頁進行反向對映,主要工作是:設定此頁的
_mapcount
= 0,說明此頁正在使用,但是是非共享的(>0是共享)。設定page->mapping
最低位為1,page->mapping
指向此vma->anon_vma
,page->index
存放此page在vma中的第幾頁。 - 透過判斷,將頁加入到活動lru快取或者不能換出頁的lru連結串列
- 將上面配置好的頁表項寫入頁表
- 更新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