内存映射(mmap)是 Linux 内核的一个重要机制,它为程序提供了一种将文件内容直接映射到进程虚拟地址空间的方式。同时内存映射也是虚拟内存管理和文件 IO 的重要组成部分。
在 Linux 中,虚拟内存管理是基于内存映射来实现的。在调用 mmap 函数时,会创建一个 vm_area_struct
结构体,该结构体代表了一段连续的虚拟地址空间,它们会相应地映射到一个后备文件或者一个匿名文件的虚拟页。
一个 vm_area_struct
结构体映射到一组连续的页表项,这些页表项指向物理内存中的一页。这样就把一个文件和物理内存页相映射起来。当进程试图访问映射到 vm_area_struct
的虚拟地址空间时,如果该空间没有在内存中,则会发生缺页异常,内核会通过文件系统将文件中对应的数据读入内存。
内存映射的优点是可以有效地减少文件 IO 的次数,提高文件读写性能。同时内存映射还支持多进程共享同一个映射,可以节省内存空间,并且方便不同的进程之间进行通信。
虚拟地址映射的过程涉及到对 vm_area_struct
的匹配以及对页表项的查找和操作。具体来说,当程序访问一个虚拟地址时,系统会根据已有的 vm_area_struct
结构来确定这个虚拟地址是否属于某个区域。
如果没有匹配到相应的 vm_area_struct
,就会触发段错误,因为访问了一个未分配的虚拟地址,这表示程序在访问一个未经内核分配的内存区域,是非法的操作。
如果匹配到了相应的 vm_area_struct
,系统会根据虚拟地址和页表的映射关系找到对应的页表项PTE。如果 PTE 没有分配,就会触发缺页异常,此时系统会将相应的文件数据加载到物理内存中;如果 PTE 已经分配,就可以直接从对应的物理页的偏移位置读取数据。
在这个过程中,虚拟页有三种状态:
- 未分配虚拟页:指的是没有使用 mmap 建立相应的
vm_area_struct
,因此也就没有对应到具体的页表项。 - 已分配虚拟页,未映射到物理页:表示已经使用了 mmap 建立的
vm_area_struct
,虚拟页可以映射到对应的页表项,但页表项尚未指向具体的物理页。 - 已分配虚拟页,已映射到物理页:表示已经使用了 mmap 建立的
vm_area_struct
,虚拟页可以映射到对应的页表项,并且页表项已经指向具体的物理页。
mmap 函数可以将虚拟地址映射到一个后备文件或者一个匿名文件。当操作系统分配物理内存时,实际上会利用匿名文件的 mmap 来完成内存分配。
mmap和虚拟内存管理
用户进程的虚拟内存管理是通过Linux内核中的mm_struct
结构来表示一个用户进程的虚拟内存地址空间。该结构包含了几个重要的字段来描述不同区域的地址范围和属性。
-
start_code
和end_code
:指定了进程的代码段的起始地址和结束地址,用于表示可执行代码的边界。一旦ELF二进制文件映射到虚拟内存后,这些地址就不会再改变。 -
start_data
和end_data
:指定了进程数据段的起始地址和结束地址,用于表示已初始化数据的边界。类似于代码段,这些地址在映射后也不再改变。 -
start_brk
和brk
:指定了堆的起始地址和结束地址,用于表示动态分配内存的边界。start_brk
表示堆的初始位置,在进程的整个生命周期中保持不变,而brk表示堆的结束位置,会随着堆的长度动态改变。 -
stack_top
:指定了栈的起始位置,一般位于用户进程地址空间的顶部,用于存放函数调用的栈帧。 -
task_size
:指定了用户进程地址空间的长度,即用户空间的顶部边界。 -
mmap_base
:指定了用户进程虚拟地址空间中用作内存映射部分的基地址。通常情况下,它位于用户地址空间的1/3处,即TASK_SIZE / 3
位置。
这些字段中的地址都是用户进程的虚拟地址,通过虚拟地址和页表结构,用户进程可以访问内存。当用户进程访问一个虚拟地址时,会将该地址转换成对应的页表项索引,然后查找页表项中保存的物理内存页的页号,并加上虚拟地址低12位的偏移量,从而确定一个唯一的物理内存地址。
如果物理内存地址所在的页已经存在,就可以返回该物理地址存放的内容。如果不存在,则会触发缺页异常。虚拟内存管理采用按需分配和缺页异常机制来管理页表项并分配对应的物理内存页。当一个虚拟地址对应的页表项不存在时,会先创建页表结构,然后分配物理内存页,并最后修改页表。
除了mm_struct
结构,进程的虚拟内存管理还涉及到虚拟内存区域的管理,即通过vm_area_struct
结构来管理用户进程的不同虚拟内存区域,如数据段、文本段和共享库等。这些区域通过vm_area_struct
结构进行管理和映射。
vm_area_struct
struct vm_area_struct {struct mm_struct *vm_mm; /* 所属进程的内存描述符 */unsigned long vm_start; /* 区域起始地址 */unsigned long vm_end; /* 区域结束地址 */struct vm_area_struct *vm_next, *vm_prev; /* 双向链表指针 */pgprot_t vm_page_prot; /* 页保护标志 */unsigned long vm_flags; /* VMA标志位,如映射类型等 */struct rb_node rb;union {struct {unsigned long shared_vm; /* 共享区域大小 */} anon_vma;struct vm_userfaultfd_ctx *userfaultfd_ctx;spinlock_t lock; /* 文件锁 */struct list_head list; /* 指向共享VMA列表项 */ };#ifdef CONFIG_MMU_NOTIFIERstruct mmu_notifier_mm *mmu_notifier_mm;
#endif#ifdef CONFIG_NUMA_BALANCING/** Virtual memory areas in a shared-memory area. Protected by* mmap_sem and guarded by mm->mmap_lock.** WARNING: Once you add a new member to this group you MUST update* dup_mmap() function!!!** You also have to modify arch_dup_mmap() if your architecture is one* of the architectures which implement that function.** Also, __split_vma() must be taught about how to copy the information.*/unsigned long shared_dirty_pages;unsigned long private_dirty_pages;unsigned long shared_clean_pages;unsigned long private_clean_pages;
#endif /* CONFIG_NUMA_BALANCING */
};
vm_area_struct
是一种用于管理用户进程虚拟内存区域的数据结构,它可以以两种不同的组织形式存在。
首先是单链表形式,包含了所有已创建的vm_area_struct
实例。这种形式使得可以按照顺序遍历和访问所有的虚拟内存区域。
其次是红黑树形式,用于加速对虚拟内存区域的查找。这种组织形式可以通过快速的二分查找来定位特定的虚拟内存区域,提高了访问效率。
需要注意的是,这两种组织形式都是针对同一份vm_area_struct实例而言的,只是在不同的数据结构中进行组织和管理。
在考虑vm_area_struct
和页表之间的关系时,我们可以看到,vm_area_struct
本质上表示了用户进程的一段虚拟地址空间。而虚拟地址和页表数组的索引是一一对应的关系。页表数组的最后一级PTE数组的数组项存放着物理内存页的页号,从而建立了虚拟内存地址到物理内存地址的对应关系。
有一种情况是当先有虚拟地址时,通过访问虚拟地址触发缺页异常,然后加载相应的物理内存页,并更新页表,以建立起虚拟地址、页表和物理内存之间的联系。
另一种情况是在进行内存映射(mmap)时,首先从设备加载文件数据,建立address_space
和页缓存(物理内存),然后创建vm_area_struct
结构,更新页表,并返回相应的虚拟地址。这样就实现了从设备加载文件到建立虚拟地址、页表和物理内存之间联系的过程。
vm_area_struct
是用于描述用户进程虚拟内存区域的结构体:
-
vm_start
和vm_end
:表示区域的起始位置和结束位置,用于确定区域的边界。这两个字段确保了不同的vm_area_struct
之间不会出现交叉的情况,从而清晰地划分了各个虚拟内存区域。 -
vm_page_prot
:表示了该区域的页的访问权限,包括读、写、执行等。这些权限信息将影响到用户进程对该区域的访问行为。 -
shared
:处理具有后备文件的内存映射。它将该区域与后备文件的address_space
地址空间进行关联,以便在需要时能够正确地读取和写入数据。 -
anon_vma_node
和anon_vma
:处理匿名文件共享内存映射的情况。当多个虚拟内存区域映射到同一物理内存页时,这些映射将保存在一个链表中,并由anon_vma_node
进行管理,确保它们之间的正确关联。 -
vm_pgoff
和vm_file
:处理具有后备文件的内存映射的情况。vm_pgoff
表示了该映射在文件中的页偏移量,而vm_file
则包含了打开文件file实例的相关信息,以便在需要时能够正确地定位和操作对应的文件数据。
这些字段的信息使得vm_area_struct
能够全面描述用户进程的虚拟内存区域,包括区域的边界、访问权限、关联文件信息以及共享情况,为内核提供了管理和操作虚拟内存的重要依据。
对于有后备文件的映射,内核利用优先查找树结构来加速确定一个文件和所有映射到这个文件的虚拟内存区域vm_area_struct
实例的关系,从而可以方便地获取所有映射到这个文件的进程信息。这种优先查找树结构能够高效地管理文件和映射关系,提高查找效率和操作性能。
同时,内核提供了一系列函数用于对虚拟内存区域vm_area_struct
进行操作,包括创建、删除、合并、查找等功能。这些函数可以帮助内核有效地管理用户进程的虚拟内存区域,确保内存映射的正确性和一致性。
另外,mmap是C标准库提供给用户程序的函数,用于通过内存映射建立文件地址空间和虚拟内存区域的映射关系。通过mmap函数,用户程序可以将文件映射到自身的虚拟内存空间中,实现了方便的文件访问和操作。这种映射关系的建立是通过内核提供的相关功能实现的,确保了对文件数据的高效管理和访问。
mmap的4种类型
mmap函数在Linux系统中用于创建内存映射,可以分为有后备文件的映射和匿名文件的映射,每种映射又有私有映射和共享映射之分,因此mmap可以创建4种类型的映射。
-
有后备文件的共享映射:多个进程的
vm_area_struct
指向同一个物理内存区域,一个进程对文件内容的修改会被其他进程看到,并且这些修改会被写回到后备文件中。 -
有后备文件的私有映射:多个进程的
vm_area_struct
指向同一个物理内存区域,但采用写时拷贝的方式。当一个进程对文件内容做修改,不会被其他进程看到,并且对文件的修改也不会被写回到后备文件。当内存不足时,私有映射的页被交换到交换区。这种映射常用于加载共享代码库。 -
匿名文件的共享映射:内核创建一个初始为0的物理内存区域,然后多个进程的
vm_area_struct
指向这个共享的物理内存区域。对该区域内容的修改对所有进程可见,而且在页回收时被交换到交换区。 -
匿名文件的私有映射:内核创建一个初始为0的物理内存区域,对该区域内容的修改只对创建者进程可见。在页回收时,这种映射也会被交换到交换区。
malloc()
函数底层使用了匿名文件的私有映射来分配大块内存。
这些不同类型的映射提供了灵活的内存管理方式,使得进程可以根据需要选择适合的映射方式来处理内存数据,并且能够满足不同场景下的内存管理需求。
内核对堆空间的管理
从内核管理用户进程虚拟地址空间的角度来看,内存映射是主要的手段,通过建立vm_area_struct
结构来分配虚拟内存区域。对于堆空间的分配,主要通过brk
系统调用来实现。brk
系统调用本质上也是利用了匿名文件的私有映射机制,它分配并初始化为0的物理内存页,然后建立相应的vm_area_struct
,最后更新页表结构。
brk
系统调用分配的内存最小单位是页,需要按页对齐。在内核的视角下,每次对堆空间的分配至少是一页大小,即以页面为单位进行扩展。换句话说,更细粒度的字节级内存分配是由C语言标准库实现的,而在内核层面,堆空间的分配是以页面为单位进行管理的。这种设计确保了内核对虚拟内存的有效管理,并提供了一种简单且高效的方式来处理用户进程的内存分配需求。
参考:Linux内核源码分析(内存调优/文件系统/进程管理/设备驱动/网络协议栈)教程
Linux内核源码学习