2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

匿名内存是用户空间的概念,不涉及内核态内存。匿名内存的概念是指一段虚拟内存映射是否与之相关联的对象,如果没有关联对象就称为匿名的。本章就主要学习缺页异常的匿名映射,其中涉及到以下内容

  1. 匿名映射的概念
  2. 匿名映射的流程

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其处理流程如下图所示:

202306111244029951.png

  1. 检查是否是共享进程,主要是为了防止共享的VMA进入匿名页面的缺页中断

  2. 使用pte_alloc来分配一个PTE,并且把PTE设置称为对应的PMD页表项中。

  3. 根据参数flags来判断是否需要可写权限,当需要分配的内存只有只读属性的时候,系统会使用一个内容填充为0的全局页面,也就是0页面empty_zero_page。

    • my_zero_pfn获取系统零页的帧号,pte_mkspecial用来设置PTE中的PET_SPECIAL位,用来表示这个特殊映射页面
    • pte_offset_map_lock获取PTE,获取成功后,就跳转到setpte标签处
  4. 处理VMA属性是可写的情况,首先,anon_vma_prepare为建立RMAP做准备,使用alloc_zeroed_user_highpage_movable函数来分配一个可移动的匿名页面,其分配页面的最终会调用伙伴系统的核心函数alloc_pages,这里的页面分配会优先使用高端内存,但是对于64位的系统已经没有高端内存的概念了

  5. 通过mk_pte、pte_mkdirty、pte_mkwrite等生成一个新的PTE,这个新的PTE是基于刚分配的物理页面的帧号建立起来的

  6. pte_offset_map_lock获取address对应的PTE,同时会申请一个自旋锁

  7. inc_mm_counter_fast增加进程匿名页面的计数,匿名页面的计数类型是MM_ANONPAGES。

    • page_add_new_anon_rmap把匿名页面添加到RMAP系统中
    • lru_cache_add_active_or_unevictable把匿名页面添加到LRU链表中,在以后的kswap内核模块中会用到LRU链表
  8. 在setpte标签处,通过set_pte_at函数将设置到硬件的页表中,通过更新cache的配置

    202306111244038942.png

在匿名页面的缺页异常中,我们使用了系统零页页面。因为对于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)不会设置为可写。对于这种匿名页读之后再去写的时候,其处理流程如下:

202306111244047953.png

2.3 第一次写匿名页

当判断不是读操作导致的缺页的时候,则是写操作造成,处理写私有的匿名页情况,请记住这依然是第一次访问这个匿名页只不过是写访问而已。首先使用anon_vma_prepare准备好逆向映射机制的数据结构,以接受一个新的匿名区域,这个后面反向映射的时候会单独学习。然后使用alloc_zeroed_user_highpage_movable分配对应物理页。其处理流程如下:

202306111244066674.png

2.4 读之后写匿名页

读之后写匿名页,其实已经很简单了,那就是发生COW写时复制缺页,其处理流程如下:

202306111244085665.png

3 总结

匿名映射缺页异常是应用程序访问内存空间常见的一种异常,对于匿名映射是用户空间通过malloc/mmap接口函数来分配内存,映射完成后,只是获取了一块虚拟内存,并没有为其分配物理内存。

  • 当第一次访问的时候,如果是读访问,将会映射到零页上去,以减小不必要的内存分配
  • 如果是写操作,则会分配的新的物理页,并用0来填充,然后映射到虚拟页
  • 如果是写读访问,然后是写访问,则会发生两次缺页异常,第一次采用匿名缺页异常的读操作处理,第二次则是按照写时复制技术处理

4. 参考文档

Linux内核虚拟内存管理之匿名映射缺页异常分析

阅读全文