Linux 内存管理:匿名内存映射简析
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 匿名内存映射的典型场景
本文以 ARMv7 + Linux 4.14.x 为上下文,分析匿名内存映射的典型场景下,代码实现的概要细节。
2.1 只读内存匿名映射过程
用户空间分配内存,而后进行读操作,映射为 zero page:
size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
char c = ptr[0]; // 读操作触发 page fault
munmap(ptr, size);
语句 char c = ptr[0]; 会触发硬件 page fault,从而进入读操作匿名内存页的映射过程。ARMv7 架构的处理流程如下:
do_DataAbort()
...
do_page_fault()
__do_page_fault()
handle_mm_fault()
__handle_mm_fault()
handle_pte_fault()
static int handle_pte_fault(struct vm_fault *vmf)
{
...
if (unlikely(pmd_none(*vmf->pmd))) {
/*
* Leave __pte_alloc() until later: because vm_ops->fault may
* want to allocate huge page, and if we expose page table
* for an instant, it will be difficult to retract from
* concurrent faults and from rmap lookups.
*/
vmf->pte = NULL;
} else {
...
}
if (!vmf->pte) { /* @vmf 的 PTE 页表项 还未建立,即还未分配物理页面 */
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); /* 匿名映射 */
else
return do_fault(vmf);
}
...
}
static int do_anonymous_page(struct vm_fault *vmf)
{
...
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
...
goto setpte;
}
...
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
...
}
上面的代码中,my_zero_pfn() 选择将读地址映射到 zero page。
从硬件 MMU 可以得知,当前的 page fault 是由读还是写引起的,从而去决定是否设置 FAULT_FLAG_WRITE 标志位。如 ARMv7 架构下,发生 page fault 时,从寄存器 DFSR(Data Fault Status Register) 获知是读还是写:


2.2 只写内存匿名映射过程
用户空间分配内存,而后进行写操作,新分配物理内存并映射:
size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
ptr[0] = 0xAA; // 写操作触发 page fault
munmap(ptr, size);
语句 ptr[0] = 0xAA; 会触发硬件 page fault,从而进入写操作匿名内存页的映射过程。ARMv7 架构的处理流程如下:
do_DataAbort()
...
do_page_fault()
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
...
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
...
/* 从 DFSR 寄存器得知是写操作引发的 page fault,则设置 FAULT_FLAG_WRITE 标志位 */
if (fsr & FSR_WRITE)
flags |= FAULT_FLAG_WRITE;
...
fault = __do_page_fault(mm, addr, fsr, flags, tsk);
...
}
__do_page_fault()
handle_mm_fault()
__handle_mm_fault()
handle_pte_fault()
do_anonymous_page()
static int do_anonymous_page(struct vm_fault *vmf)
{
...
struct page *page;
...
...
/* 分配新内存页面 */
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page)
goto oom;
...
__SetPageUptodate(page);
entry = mk_pte(page, vma->vm_page_prot); /* 构建 pte */
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry)); /* 设置页面可写 */
...
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); /* 填充 PTE 页表项 */
...
}
2.3 COW 匿名映射过程
2.3.1 先读后写内存匿名映射过程
用户空间分配内存,先读而后进行写操作,将引发写时拷贝(COW):
size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
char c = ptr[0]; // 读操作触发 page fault,过程见 2.1 只读内存匿名映射过程
ptr[0] = 0xAA; // 对只读内存进行写操作,触发 COW
munmap(ptr, size);
语句 char c = ptr[0]; 触发 page fault,将虚拟地址映射到只读的 zero page,过程见 2.1 只读内存匿名映射过程,这里不再赘述。语句 ptr[0] = 0xAA; 写只读内存,将触发写时拷贝(COW):
do_DataAbort()
...
do_page_fault()
__do_page_fault()
handle_mm_fault()
__handle_mm_fault()
handle_pte_fault()
static int handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
if (unlikely(pmd_none(*vmf->pmd))) {
...
} else {
...
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
vmf->orig_pte = *vmf->pte;
...
}
...
vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
spin_lock(vmf->ptl);
entry = vmf->orig_pte;
if (unlikely(!pte_same(*vmf->pte, entry)))
goto unlock;
if (vmf->flags & FAULT_FLAG_WRITE) { /* 请求发起写操作 */
if (!pte_write(entry)) /* 但页面不允许写 */
return do_wp_page(vmf);/* 做写时拷贝(COW: Copy-On-Write) */
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
...
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
static int do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
...
return wp_page_copy(vmf);
}
2.3.2 父子进程写 COW 匿名映射过程
size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
// (1) 写操作触发 page fault,分配内存页面
ptr[0] = 'A';
fork();
// (2.1) 父进程写触发 page fault,
// (2.2)
ptr[0] = 'B';
- 父进程写触发 COW
假定父进程先执行 ptr[0] = 'B';,在写入过程中,会触发 page fault 并进行 COW:
static int do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
...
/*
* Ok, we need to copy. Oh, well..
*/
get_page(vmf->page);
pte_unmap_unlock(vmf->pte, vmf->ptl);
return wp_page_copy(vmf);
}
static int wp_page_copy(struct vm_fault *vmf)
{
...
if (is_zero_pfn(pte_pfn(vmf->orig_pte))) { /* 旧页面 是 zero page, 直接分配清 0 的新页面即可, 无需拷贝 */
...
} else { /* 旧页面 非 zero page, 分配新页面, 并拷贝旧页面内容 */
/* 为(父进程)分配新页面 */
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
vmf->address);
if (!new_page)
goto oom;
/* 拷贝 (父进程) 旧页面内容 到 新分配的页面 */
cow_user_page(new_page, old_page, vmf->address, vma);
}
...
/*
* Re-check the pte - we dropped the lock
*/
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {
...
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
/* 为(父进程)构建新的 PTE 页表项内容 */
entry = mk_pte(new_page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
...
/* 用新构建的 PTE 填充 PTE 页表项 */
set_pte_at_notify(mm, vmf->address, vmf->pte, entry)
...
/* Free the old page.. */
new_page = old_page;
page_copied = 1;
} else {
...
}
...
return page_copied ? VM_FAULT_WRITE : 0;
...
}
从上面的分析看到,为父进程创建了新的页面(假定父进程先执行ptr[0] = 'B';),然后拷贝旧页面内容,并填充了 PTE 页表项。
- 子进程写触发 COW
假定子进程后执行 ptr[0] = 'B';,在写入过程中,会触发 page fault 并进行 COW:
static int do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
...
/*
* Take out anonymous pages first, anonymous shared vmas are
* not dirty accountable.
*/
if (PageAnon(vmf->page) && !PageKsm(vmf->page)) {
...
if (!trylock_page(vmf->page)) {
...
}
if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
...
/* 子进程 重用 fork 时 父进程 的 旧页面 */
wp_page_reuse(vmf);
return VM_FAULT_WRITE;
}
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
...
}
}
static inline void wp_page_reuse(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page = vmf->page;
pte_t entry;
/*
* Clear the pages cpupid information as the existing
* information potentially belongs to a now completely
* unrelated process.
*/
if (page)
page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
entry = pte_mkyoung(vmf->orig_pte);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
update_mmu_cache(vma, vmf->address, vmf->pte);
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
上面分析假定父进程先执行语句 ptr[0] = 'B';,此时父进程新建页面,而子进程通过 wp_page_reuse() 重用旧页面。对于子进程先执行语句 ptr[0] = 'B'; 的情形,不过父子进程互换一下角色,即为 fork 后先写的子进程分配新页面,拷贝旧页面内容,而父进程重用旧页面。还有一点值得注意的是,如果没有前面的 ptr[0] = 'A'; 语句,则在创建新页面时无需进行拷贝操作,只需要创建清 0 的新页面即可,详见代码 is_zero_pfn() 判定处。

浙公网安备 33010602011771号