Linux深入理解内存管理33

Linux深入理解内存管理33(基于Linux6.6)---匿名映射介绍


一、概念

匿名映射 是指将虚拟内存区域映射到一个未命名的、无文件关联的内存区域。与文件映射不同,匿名映射不涉及任何文件,而是直接在进程的虚拟地址空间中分配内存。

在 Linux 和其他类 Unix 操作系统中,匿名映射通常通过 mmap() 系统调用来实现。当你使用 mmap() 创建匿名映射时,通常不会指定文件名或文件描述符,而是使用特殊的标志来表示这是一个匿名映射。

匿名映射的特点

  1. 没有文件关联

    • 匿名映射不与磁盘文件关联,表示该内存区域并不是从某个文件中加载的,而是由操作系统直接分配的。
  2. 虚拟内存分配

    • 内存区域是由操作系统通过虚拟内存管理机制分配的。这些内存区域通常会在进程退出时自动释放。
  3. 支持共享与私有映射

    • 匿名映射可以选择是 共享MAP_SHARED)还是 私有MAP_PRIVATE)。共享映射的内存区域在多个进程间共享,而私有映射的内存区域在各个进程中是独立的。
  4. 页面交换

    • 匿名映射的内存区域通常可以被交换到磁盘上(通过交换空间),以便腾出物理内存供其他进程使用。内存中的页面在不使用时会被换出(swap out),当再次需要时会被换入(swap in)。
  5. 可用于动态内存分配

    • 通过匿名映射,程序可以动态地分配内存区域,而无需依赖堆(heap)或栈(stack)。这对于需要大块内存的应用程序(如数据库、大型数据处理等)非常有用。
  6. 默认初始化为零

    • 通常,使用 mmap() 创建的匿名映射内存会被初始化为零。程序可以通过访问这些内存来读取和写入数据。

由于应用程序申请内存后,内核分配了虚拟内存,当访问所需要的的内存时候,才会分配实际的物理内存,才能节省实际内存的使用。来看看匿名映射和文件映射的区别

  • 匿名页面,是指那些没有关联到文件页,如进程堆、栈、数据段和任务已修改的共享库等,不是以文件形式存在,因此无法和磁盘文件交换
  • 文件页面,也就是映射文件的页,对于文件映射背景的页面,程序可以通过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

  • 其主要作用就是只要用户引用一个只读的匿名页面,并没有进行写操作,缺页中断处理中,内核就不会给用户进程分配新的页面,都会映射到这个页面中,从而节约内存空间

二、代码流程

在缺页中断处理中,匿名页面的触发条件为下面的两个条件,当满足这两个条件的时候就会调用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是否为空。

include/linux/mm.h

static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
    return !vma->vm_ops;
}

mm/memory.c

/*
 * 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)
		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))
		return VM_FAULT_OOM;

	/* See comment in handle_pte_fault() */
	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)) {
		entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
						vma->vm_page_prot));
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				vmf->address, &vmf->ptl);
		if (!pte_none(*vmf->pte)) {
			update_mmu_tlb(vma, vmf->address, vmf->pte);
			goto unlock;
		}
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			goto unlock;
		/* Deliver the page fault to userland, check inside PT lock */
		if (userfaultfd_missing(vma)) {
			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)))
		goto oom;
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	if (mem_cgroup_charge(page_folio(page), vma->vm_mm, GFP_KERNEL))
		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);

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

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

	ret = check_stable_address_space(vma->vm_mm);
	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);
	page_add_new_anon_rmap(page, vma, vmf->address);
	lru_cache_add_inactive_or_unevictable(page, vma);
setpte:
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);
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;
}

对于do_anonymous_page其处理流程如下图所示:

  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标签处。
  1. 处理VMA属性是可写的情况,首先,anon_vma_prepare为建立RMAP做准备,使用alloc_zeroed_user_highpage_movable函数来分配一个可移动的匿名页面,其分配页面的最终会调用伙伴系统的核心函数alloc_pages,这里的页面分配会优先使用高端内存,但是对于64位的系统已经没有高端内存的概念了。
  2. 通过mk_pte、pte_mkdirty、pte_mkwrite等生成一个新的PTE,这个新的PTE是基于刚分配的物理页面的帧号建立起来的。
  3. pte_offset_map_lock获取address对应的PTE,同时会申请一个自旋锁。
  4. 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、第一次读匿名页

对于第一次读匿名映射,内核的处理代码如下所示:

mm/memory.c

static vm_fault_t 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));
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				vmf->address, &vmf->ptl);
		if (!pte_none(*vmf->pte)) {
			update_mmu_tlb(vma, vmf->address, vmf->pte);
			goto unlock;
		}
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			goto unlock;
		/* Deliver the page fault to userland, check inside PT lock */
		if (userfaultfd_missing(vma)) {
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return handle_userfault(vmf, 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来转换为保护位组合,继续往下看:

static pgprot_t protection_map[16] __ro_after_init = {
	[VM_NONE]					= __PAGE_NONE,
	[VM_READ]					= __PAGE_READONLY,
	[VM_WRITE]					= __PAGE_COPY,
	[VM_WRITE | VM_READ]				= __PAGE_COPY,
	[VM_EXEC]					= __PAGE_READONLY_EXEC,
	[VM_EXEC | VM_READ]				= __PAGE_READONLY_EXEC,
	[VM_EXEC | VM_WRITE]				= __PAGE_COPY_EXEC,
	[VM_EXEC | VM_WRITE | VM_READ]			= __PAGE_COPY_EXEC,
	[VM_SHARED]					= __PAGE_NONE,
	[VM_SHARED | VM_READ]				= __PAGE_READONLY,
	[VM_SHARED | VM_WRITE]				= __PAGE_SHARED,
	[VM_SHARED | VM_WRITE | VM_READ]		= __PAGE_SHARED,
	[VM_SHARED | VM_EXEC]				= __PAGE_READONLY_EXEC,
	[VM_SHARED | VM_EXEC | VM_READ]			= __PAGE_READONLY_EXEC,
	[VM_SHARED | VM_EXEC | VM_WRITE]		= __PAGE_SHARED_EXEC,
	[VM_SHARED | VM_EXEC | VM_WRITE | VM_READ]	= __PAGE_SHARED_EXEC
};
DECLARE_VM_GET_PAGE_PROT

对于私有匿名映射的页,假设设置的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写时复制缺页,其处理流程如下:

三、总结

1. 匿名映射的缺页异常说明

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

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

以下是匿名映射缺页异常的简要流程:

  1. 进程访问虚拟内存:进程尝试读取或写入匿名映射的虚拟内存区域。

  2. 缺页检测:由于该虚拟页面尚未被映射到物理内存,硬件会触发一个缺页异常(Page Fault)。此时,操作系统会通过页表判断该页面是否已被加载到内存中。

  3. 检查映射类型:操作系统会检查这个虚拟页面的映射类型:

    • 如果是匿名映射(MAP_ANONYMOUS),并且没有内容预先填充,则该页面通常会被初始化为零,或者根据映射的权限,进行必要的操作(如私有映射的写入保护)。
    • 如果是共享映射(MAP_SHARED),多个进程可能会共享这个页面,操作系统需要确保这些共享页面的一致性。
  4. 加载或分配页面:操作系统通过以下方式来处理缺页:

    • 对于匿名映射,操作系统通常会为该页面分配物理内存。如果页面是私有的(如 MAP_PRIVATE),操作系统会分配一个新的物理页面,并将其清零(初始化为零)。
    • 对于共享映射,操作系统可能需要同步其他进程对该页面的访问。
  5. 更新页表:操作系统会更新进程的页表,将新的物理页面映射到虚拟地址上。

  6. 返回到进程执行:一旦缺页异常处理完成,进程继续执行,访问的虚拟页面被成功加载到物理内存中。

3. 匿名映射的缺页异常处理

在匿名映射中,缺页异常的处理有一些特殊的注意事项,主要包括以下几点:

  • 初始化为零:当进程首次访问匿名映射的页面时,操作系统会通常将该页面初始化为零(除非设置了其他初始化选项)。这意味着进程访问的匿名映射页面可能包含空白内容,直到进程实际写入数据。

  • 按需加载(Demand Paging):在大多数现代操作系统中,匿名映射会使用按需分页的方式。当进程访问一个未加载的页面时,操作系统仅在此时才将页面加载到内存中,而不是提前加载整个映射区域。这可以减少内存的浪费,尤其是在内存较大的情况下。

  • 私有映射与共享映射的区别:对于 MAP_PRIVATE 类型的匿名映射,缺页异常通常会导致操作系统为该进程分配一页新的物理内存,并将其内容清零。而对于 MAP_SHARED 类型的匿名映射,缺页异常可能会影响到其他共享该页面的进程,因此操作系统需要确保所有进程能够看到相同的内存内容。

4. 缺页异常的性能考虑

缺页异常的处理通常涉及以下几个步骤,这会引入一定的性能开销:

  • 磁盘I/O:如果缺页的页面不在物理内存中,操作系统可能需要从磁盘或交换空间中加载页面。这一过程可能会显著影响性能,特别是在内存不足或页面很大时。

  • 内存分配和映射:为处理缺页异常,操作系统需要进行内存分配和页表更新,这会占用 CPU 时间,并且可能引起缓存失效。

  • 页面交换(Swapping):如果物理内存资源紧张,操作系统可能会将其他不常用的页面交换到磁盘(swap),然后再加载所需的页面,这可能导致更多的延迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值