匿名内存是用户空间的概念,不涉及内核态内存。匿名内存的概念是指一段虚拟内存映射是否与之相关联的对象,如果没有关联对象就称为匿名的。本章就主要学习缺页异常的匿名映射,其中涉及到以下内容
- 匿名映射的概念
- 匿名映射的流程
1. 概念
由于应用程序申请内存后,内核分配了虚拟内存,当访问所需要的的内存时候,才会分配实际的物理内存,才能节省实际内存的使用。我们来看看匿名映射和文件映射的区别
- 匿名页面,是指那些没有关联到文件页,如进程堆、栈、数据段和任务已修改的共享库等,不是以文件形式存在,因此无法和磁盘文件交换
- 文件页面,也就是映射文件的页,对于文件映射背景的页面,程序可以通过read/write/mmap去读,当通过任何一种方式从磁盘读取文件时,内存都会给你申请一个pace cache来缓存硬盘内容。例如通过mmap映射文件到虚拟内存然后读文件,进程的代码段等,这些页都是由缓存
1.1 匿名映射缺页异常的触发情况
我们知道了匿名页,那么在什么情况下会触发匿名映射的缺页异常呢?我们比较常见的有
- 当我们应用程序使用malloc来申请一块内存(堆分配),在没有使用这块内存之前,仅仅是分配了虚拟内存,并没有分配物理内存,当第一次去访问的时候,才会通过缺页异常来分配物理页面,建立和虚拟页面的映射关系
- 当我们应用程序使用mmap来创建匿名的内存映射的时候,页同样只是分配了虚拟内存,并没有分配物理内存,第一次去访问的时候,才会通过触发缺页异常来分配物理页面,建立和虚拟页的映射关系
- 当函数的局部变量比较大,或者函数调用的层次比较深,导致当前的栈不够用,这个时候需要扩大栈,这个时候也会触发缺页异常来分配物理页面
1.2 零页面时什么?
在系统初始化过程中分配了一个页面的内存,这段内存在初始化的时候全部填充为0,内核是如何分配零页面的
/*
* empty_zero_page is a special page that is used for
* zero-initialized data and COW.
*/
struct page *empty_zero_page;
EXPORT_SYMBOL(empty_zero_page);
并在paging_init中对这个零页进行了初始化,可以看到定义了一个全局变量empty_zero_page,在下面的代码中为这个分配了4K的内存空间,并完成了虚拟到物理的映射
zero_page = early_alloc(PAGE_SIZE);
bootmem_init();
empty_zero_page = virt_to_page(zero_page);
__flush_dcache_page(NULL, empty_zero_page);
那么为什么需要零页面映射呢?
- 它的数据都是0填充,读的数据都是0
- 其主要作用就是只要用户引用一个只读的匿名页面,并没有进行写操作,缺页中断处理中,内核就不会给用户进程分配新的页面,都会映射到这个页面中,从而节约内存空间
2. 代码流程
在缺页中断处理中,匿名页面的触发条件为下面的两个条件,当满足这两个条件的时候就会调用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 用于当虚存页面不在物理内存而引起的“缺页异常”时所应该调用的函数
2.1 do_anonymous_page流程
内核用vma_is_anonymous()判断vma是否为匿名映射,其实就是检查vma->vm_ops是否为空:
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops;
}
对于do_anonymous_page其处理流程如下图所示:
-
检查是否是共享进程,主要是为了防止共享的VMA进入匿名页面的缺页中断
-
使用pte_alloc来分配一个PTE,并且把PTE设置称为对应的PMD页表项中。
-
根据参数flags来判断是否需要可写权限,当需要分配的内存只有只读属性的时候,系统会使用一个内容填充为0的全局页面,也就是0页面empty_zero_page。
- my_zero_pfn获取系统零页的帧号,pte_mkspecial用来设置PTE中的PET_SPECIAL位,用来表示这个特殊映射页面
- pte_offset_map_lock获取PTE,获取成功后,就跳转到setpte标签处
-
处理VMA属性是可写的情况,首先,anon_vma_prepare为建立RMAP做准备,使用alloc_zeroed_user_highpage_movable函数来分配一个可移动的匿名页面,其分配页面的最终会调用伙伴系统的核心函数alloc_pages,这里的页面分配会优先使用高端内存,但是对于64位的系统已经没有高端内存的概念了
-
通过mk_pte、pte_mkdirty、pte_mkwrite等生成一个新的PTE,这个新的PTE是基于刚分配的物理页面的帧号建立起来的
-
pte_offset_map_lock获取address对应的PTE,同时会申请一个自旋锁
-
inc_mm_counter_fast增加进程匿名页面的计数,匿名页面的计数类型是MM_ANONPAGES。
- page_add_new_anon_rmap把匿名页面添加到RMAP系统中
- lru_cache_add_active_or_unevictable把匿名页面添加到LRU链表中,在以后的kswap内核模块中会用到LRU链表
-
在setpte标签处,通过set_pte_at函数将设置到硬件的页表中,通过更新cache的配置
在匿名页面的缺页异常中,我们使用了系统零页页面。因为对于malloc函数来说,分配的内存仅仅是进程地中空间中的虚拟内存。当用户程序需要读这个malloc分配的虚拟内存,那么系统就会返回全是0的数据,因此linux内核就不必为这种情况单独分配物理内存,而使用系统零页,即使系统零页来映射malloc分配的虚拟内存。当程序需要写这个页面的时候,就会触发一个缺页异常,于是缺页异常就变成了写时复制的缺页异常。总之,应用程序使用malloc来分配虚拟内存时,有以下几种情况
- malloc分配内存后,直接读 :这种情况下,在linux内核中会进入匿名页面的缺页中断,使用系统零页进行映射,这时映射的PTE属性是只读
- malloc分配内存后,先读后写: 这种情况下,读的操作会让linux内核使用系统零页来建立页表的映射关系,这时PTE的属性是只读的。当应用程序需要往这个虚拟内存中写内容时,又触发了另外一个缺页异常,也就是后面的写时复制技术
- malloc分配内存后,直接写: 这种情况下,在Linux内核中会进入缺页匿名的缺页异常中,使用alloc_zeroed_user_highpage_movable函数分配一个新的页面,并且使用该页面来设置PTE,这时候这个PTE的属性是可写的。
2.2 第一次读匿名页情况
对于第一次读匿名映射,内核的处理代码如下所示
/* Use the zero-page for reads */
if (!(fe->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(fe->address),
vma->vm_page_prot));
fe->pte = pte_offset_map_lock(vma->vm_mm, fe->pmd, fe->address,
&fe->ptl);
if (!pte_none(*fe->pte))
goto unlock;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(fe->pte, fe->ptl);
return handle_userfault(fe, VM_UFFD_MISSING);
}
goto setpte;
}
pfn_pte用来将页帧号和页表属性拼接为页表项值,是将pfn左移PAGE_SHIFT位(一般为12bit),或上pgprot_val(prot)
#define pfn_pte(pfn,prot) (__pte(((phys_addr_t)(pfn) << PAGE_SHIFT) | pgprot_val(prot)))
而对于pfn,则使用内核初始化设置的empty_zero_page这个零页得到的页帧号,这个之前已经介绍过。
static inline unsigned long my_zero_pfn(unsigned long addr)
{
extern unsigned long zero_pfn;
return zero_pfn;
}
而第二个参数port是vma->vm_page_port,这个是vma的访问权限,在做内存映射的mmap的时候被设置,那么代码中如何设置的呢?mm/mmap.c中do_brk会通过以下配置这个参数
vma->vm_page_prot = vm_get_page_prot(flags);
pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
return __pgprot(pgprot_val(protection_map[vm_flags &
(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
pgprot_val(arch_vm_get_page_prot(vm_flags)));
}
vm_get_page_prot函数会根据传递来的vmflags是否为VMREAD|VMWRITE|VMEXEC|VMSHARED来转换为保护位组合,继续往下看
/* description of effects of mapping type and prot in current implementation.
* this is due to the limited x86 page protection hardware. The expected
* behavior is in parens:
*
* map_type prot
* PROT_NONE PROT_READ PROT_WRITE PROT_EXEC
* MAP_SHARED r: (no) no r: (yes) yes r: (no) yes r: (no) yes
* w: (no) no w: (no) no w: (yes) yes w: (no) no
* x: (no) no x: (no) yes x: (no) yes x: (yes) yes
*
* MAP_PRIVATE r: (no) no r: (yes) yes r: (no) yes r: (no) yes
* w: (no) no w: (no) no w: (copy) copy w: (no) no
* x: (no) no x: (no) yes x: (no) yes x: (yes) yes
*
* On arm64, PROT_EXEC has the following behaviour for both MAP_SHARED and
* MAP_PRIVATE:
* r: (no) no
* w: (no) no
* x: (yes) yes
*/
pgprot_t protection_map[16] = {
__P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
__S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};
protection_map数组定义了从P000到S111一共16种组合,P表示私有(PRIVATE),S表示共享(SHARED),后面三个数字依次为可读、可写、可执行,如:_S010表示共享、不可读、可写、不可执行。
对于私有匿名映射的页,假设设置的vmflags为VMREAD|VMWRITE则对应的保护位组合为:P110即为PAGE_READONLY_EXEC=pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PT_ENG | PTE_PXN)不会设置为可写。对于这种匿名页读之后再去写的时候,其处理流程如下:
2.3 第一次写匿名页
当判断不是读操作导致的缺页的时候,则是写操作造成,处理写私有的匿名页情况,请记住这依然是第一次访问这个匿名页只不过是写访问而已。首先使用anon_vma_prepare准备好逆向映射机制的数据结构,以接受一个新的匿名区域,这个后面反向映射的时候会单独学习。然后使用alloc_zeroed_user_highpage_movable分配对应物理页。其处理流程如下:
2.4 读之后写匿名页
读之后写匿名页,其实已经很简单了,那就是发生COW写时复制缺页,其处理流程如下:
3 总结
匿名映射缺页异常是应用程序访问内存空间常见的一种异常,对于匿名映射是用户空间通过malloc/mmap接口函数来分配内存,映射完成后,只是获取了一块虚拟内存,并没有为其分配物理内存。
- 当第一次访问的时候,如果是读访问,将会映射到零页上去,以减小不必要的内存分配
- 如果是写操作,则会分配的新的物理页,并用0来填充,然后映射到虚拟页
- 如果是写读访问,然后是写访问,则会发生两次缺页异常,第一次采用匿名缺页异常的读操作处理,第二次则是按照写时复制技术处理
4. 参考文档
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。