<Linux内核深度解析>
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明内存映射是在进程的虚拟地址空间中创建一个映射,分为以下两种:
(1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件。
(2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。
通常把文件映射的物理页称为文件页,把匿名映射的物理页称为匿名页。
根据修改是否对其他进程可见和是否传递到底层文件,内存映射分为共享映射和私有映射。
(1)共享映射:修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件。
(2)私有映射:第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进程看不见,不影响数据源。
两个进程可以使用共享的文件映射实现共享内存。匿名映射通常是私有映射,共享的匿名映射只可能出现在父进程和子进程之间。
在进程的虚拟地址空间中,代码段和数据段是私有的文件映射,未初始化数据段、堆和栈是私有的匿名映射。
内存映射的原理如下:
(1)创建内存映射的时候,在进程的用户虚拟地址空间中分配一个虚拟内存区域。
(2)Linux内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页;如果是匿名映射,那么分配物理页,然后在页表中把虚拟页映射到物理页。
内存管理子系统提供了以下常用的系统调用:
(1)mmap()用来创建内存映射。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
(2)mremap()用来扩大或缩小已经存在的内存映射,可能同时移动。
void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ... /* void *new_address */);
(3)munmap()用来删除内存映射。
int munmap(void *addr, size_t length);
(4)brk()用来设置堆的上界。
int brk(void *addr);
(5)remap_file_pages()用来创建非线性的文件映射,即文件区间和虚拟地址空间之间的映射不是线性关系,现在被废弃了。
(6)mprotect()用来设置虚拟内存区域的访问权限。
int mprotect(void *addr, size_t len, int prot);
(7)madvise()用来向内核提出内存使用的建议,应用程序告诉内核期望怎样使用指定的虚拟内存区域,以便内核可以选择合适的预读和缓存技术。
int madvise(void *addr, size_t length, int advice);
在内核空间中可以使用以下两个函数:
(1)remap_pfn_range把内存的物理页映射到进程的虚拟地址空间,这个函数的用处是实现进程和内核共享内存。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn,unsigned long size, pgprot_t prot);
(2)io_remap_pfn_range把外设寄存器的物理地址映射到进程的虚拟地址空间,进程可以直接访问外设寄存器。
int io_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);
应用程序通常使用C标准库提供的函数malloc()申请内存。glibc库的内存分配器ptmalloc使用brk或mmap向内核以页为单位申请虚拟内存,然后把页划分成小内存块分配给应用程序。默认的阈值是128KB,如果应用程序申请的内存长度小于阈值,ptmalloc分配器使用brk向内核申请虚拟内存,否则ptmalloc分配器使用mmap向内核申请虚拟内存。
应用程序可以直接使用mmap向内核申请虚拟内存。
1. 系统调用mmap()
系统调用mmap()有以下用处:
(1)进程创建匿名的内存映射,把内存的物理页映射到进程的虚拟地址空间。
(2)进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系统调用read()和write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件的速度。
(3)两个进程针对同一个文件创建共享的内存映射,实现共享内存。
函数原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数如下:
(1)addr:起始虚拟地址。如果addr是0,内核选择虚拟地址。如果addr不是0,内核把这个参数作为提示,在附近选择虚拟地址。
(2)length:映射的长度,单位是字节。
(3)prot:保护位。
(4)flags:标志。常用的标志如下:
(5)fd:文件描述符。仅当创建文件映射的时候,这个参数才有意义。如果是匿名映射,有些实现要求参数fd是−1,可移植的应用程序应该保证参数fd是−1。
(6)offset:偏移,单位是字节,必须是页长度的整数倍。仅当创建文件映射的时候,这个参数才有意义。
返回值:
如果成功,返回起始虚拟地址,否则返回负的错误号。
2. 系统调用mprotect()
mprotect()用来设置虚拟内存区域的访问权限。
函数原型:
int mprotect(void *addr, size_t len, int prot);
参数如下:
(1)addr:起始虚拟地址,必须是页长度的整数倍。
(2)len:虚拟内存区域的长度,单位是字节。
(3)prot:保护位。
返回值:
如果成功,返回0,否则返回负的错误号。
3. 系统调用madvise()
madvise()用来向内核提出内存使用的建议,应用程序告诉内核期望怎样使用指定的虚拟内存区域,以便内核可以选择合适的预读和缓存技术。
函数原型:
int madvise(void *addr, size_t length, int advice);
参数如下:
(1)addr:起始虚拟地址,必须是页长度的整数倍。
(2)length:虚拟内存区域的长度,单位是字节。
(3)advice:建议。
POSIX标准定义的建议值如下:
Linux私有的建议值如下:
返回值:
如果成功,返回0,否则返回负的错误号。
1. 虚拟内存区域
虚拟内存区域是分配给进程的一个虚拟地址范围,内核使用结构体vm_area_struct描述虚拟内存区域,主要成员如下所示:
成员 | 说明 |
---|---|
unsigned long vm_start; | 起始地址 |
unsigned long vm_end; | 结束地址,区间是[起始地址,结束地址),不包含结束地址 |
struct vm_area_struct *vm_next, *vm_prev; | 虚拟内存区域链表,按起始地址排序 |
struct rb_node vm_rb; | 红黑树节点 |
struct mm_struct *vm_mm; | 指向内存描述符,即虚拟内存区域所属的用户虚拟地址空间 |
pgprot_t vm_page_prot; | 保护位,即访问权限 |
unsigned long vm_flags; | 标志 |
struct { struct rb_node rb; unsigned long rb_subtree_last;} shared; | 为了支持查询一个文件区间被映射到哪些虚拟内存区域,把一个文件映射到的所有虚拟内存区域加入该文件的地址空间结构体address_space的成员i_mmap指向的区间树 |
struct list_head anon_vma_chain; | 把虚拟内存区域关联的所有anon_vma实例串联起来。一个虚拟内存区域会关联到父进程的anon_vma实例和自己的anon_vma实例 |
struct anon_vma *anon_vma; | 指向一个anon_vma实例,结构体anon_vma用来组织匿名页被映射到的所有虚拟地址空间 |
const struct vm_operations_struct *vm_ops; | 虚拟内存操作集合 |
unsigned long vm_pgoff; | 文件偏移,单位是页 |
struct file *vm_file; | 文件,如果是私有的匿名映射,该成员是空指针 |
(1)成员vm_file指向文件的一个打开实例(file)。索引节点代表一个文件,描述文件的属性。
(2)成员vm_pgoff存放文件的以页为单位的偏移。
(3)成员vm_ops指向虚拟内存操作集合,创建文件映射的时候调用文件操作集合中的mmap方法(file->f_op->mmap)以注册虚拟内存操作集合。例如:假设文件属于EXT4文件系统,文件操作集合中的mmap方法是函数ext4_file_mmap,该函数把虚拟内存区域的成员vm_ops设置为ext4_file_vm_ops。
共享匿名映射的虚拟内存区域如下所示,共享匿名映射的实现原理和文件映射相同,区别是共享匿名映射关联的文件是内核创建的内部文件。在内存文件系统tmpfs中创建一个名为“/dev/zero”的文件,名字没有意义,创建两个共享匿名映射就会创建两个名为“/dev/zero”的文件,两个文件是独立的,毫无关系。
(1)成员vm_file指向文件的一个打开实例(file)。
(2)成员vm_pgoff存放文件的以页为单位的偏移。
(3)成员vm_ops指向共享内存的虚拟内存操作集合shmem_vm_ops。
私有匿名映射的虚拟内存区域如下所示:
(1)页保护位(vm_area_struct.vm_page_prot):描述虚拟内存区域的访问权限。内核定义了一个保护位映射数组,把VM_READ、VM_WRITE、VM_EXEC和VM_SHARED这4个标志转换成保护位组合。
每种处理器架构需要定义__P000到__S111的宏,P代表私有(Private),S代表共享(Shared),后面的3个数字分别表示可读、可写和可执行,例如__P000表示私有、不可读、不可写和不可执行,__S111表示共享、可读、可写和可执行。
mm/mmap.c pgprot_t protection_map[16] = { __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111, __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111 }; 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))); }
函数arch_vm_get_page_prot由每种处理器架构自定义,默认的实现如下:
include/linux/mman.h #ifndef arch_vm_get_page_prot #define arch_vm_get_page_prot(vm_flags) __pgprot(0) #endif
(2)虚拟内存区域标志:结构体vm_area_struct的成员vm_flags存放虚拟内存区域的标志,头文件“include/linux/mm.h”定义了各种标志,常用的标志如下:
1)VM_READ、VM_WRITE、VM_EXEC和VM_SHARED分别表示可读、可写、可执行和可以被多个进程共享。
2)VM_MAYREAD表示允许设置VM_READ,VM_MAYWRITE表示允许设置VM_WRITE,VM_MAYEXEC表示允许设置VM_EXEC,VM_MAYSHARE表示允许设置VM_SHARED。这4个标志用来限制系统调用mprotect可以设置的访问权限。
3)VM_GROWSDOWN表示虚拟内存区域可以向下(低的虚拟地址)扩展,VM_GROWSUP表示虚拟内存区域可以向上(高的虚拟地址)扩展。VM_STACK表示虚拟内存区域是栈,绝大多数处理器的栈是向下扩展,VM_STACK等价于VM_GROWSDOWN;少数处理器(例如PA-RISC处理器)的栈是向上扩展,VM_STACK等价于VM_GROWSUP。
4)VM_PFNMAP表示页帧号(Page Frame Number,PFN)映射,特殊映射不希望关联页描述符,直接使用页帧号,可能是因为页描述符不存在,也可能是因为不想使用页描述符。
5)VM_MIXEDMAP表示映射混合使用页帧号和页描述符。
6)VM_LOCKED表示页被锁定在内存中,不允许换出到交换区。
7)VM_SEQ_READ表示进程从头到尾按顺序读一个文件,VM_RAND_READ表示进程随机读一个文件。这两个标志用来提示文件系统,如果进程按顺序读一个文件,文件系统可以预读文件,提高性能。
8)VM_DONTCOPY表示调用fork以创建子进程时不把虚拟内存区域复制给子进程。
9)VM_DONTEXPAND表示不允许使用mremap()扩大虚拟内存区域。
10)VM_ACCOUNT表示虚拟内存区域需要记账,判断所有进程申请的虚拟内存的总和是否超过物理内存容量。
11)VM_NORESERVE表示不需要预留物理内存。
12)VM_HUGETLB表示虚拟内存区域使用标准巨型页。
13)VM_ARCH_1和VM_ARCH_2由各种处理器架构自定义。
14)VM_HUGEPAGE表示虚拟内存区域允许使用透明巨型页,VM_NOHUGEPAGE表示虚拟内存区域不允许使用透明巨型页。
15)VM_MERGEABLE表示KSM(内核相同页合并,Kernel Samepage Merging)可以合并数据相同的页。
(3)虚拟内存操作集合(vm_operations_struct):定义了虚拟内存区域的各种操作方法,其代码如下:
include/linux/mm.h struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*mremap)(struct vm_area_struct * area); int (*fault)(struct vm_fault *vmf); int (*huge_fault)(struct vm_fault *vmf, enum page_entry_size pe_size); void (*map_pages)(struct vm_fault *vmf, pgoff_t start_pgoff, pgoff_t end_pgoff); /* 通知以前的只读页即将变成可写, * 如果返回一个错误,将会发送信号SIGBUS给进程*/ int (*page_mkwrite)(struct vm_fault *vmf); /* 使用VM_PFNMAP或者VM_MIXEDMAP时调用,功能和page_mkwrite相同*/ int (*pfn_mkwrite)(struct vm_fault *vmf); … }
1)open方法:在创建虚拟内存区域时调用open方法,通常不使用,设置为空指针。
2)close方法:在删除虚拟内存区域时调用close方法,通常不使用,设置为空指针。
3)mremap方法:使用系统调用mremap移动虚拟内存区域时调用mremap方法。
4)fault方法:访问文件映射的虚拟页时,如果没有映射到物理页,生成缺页异常,异常处理程序调用fault方法来把文件的数据读到文件的页缓存中。
5)huge_fault方法:和fault方法类似,区别是huge_fault方法针对使用透明巨型页的文件映射。
6)map_pages方法:读文件映射的虚拟页时,如果没有映射到物理页,生成缺页异常,异常处理程序除了读入正在访问的文件页,还会预读后续的文件页,调用map_pages方法在文件的页缓存中分配物理页。
7)page_mkwrite方法:第一次写私有的文件映射时,生成页错误异常,异常处理程序执行写时复制,调用page_mkwrite方法以通知文件系统页即将变成可写,以便文件系统检查是否允许写,或者等待页进入合适的状态。
8)pfn_mkwrite方法:和page_mkwrite方法类似,区别是pfn_mkwrite方法针对页帧号映射和混合映射。
2. 链表和树
如下所示,进程的虚拟内存区域按两种方法排序:
(1)双向链表,mm_struct.mmap指向第一个vm_area_struct实例。
(2)红黑树,mm_struct.mm_rb指向红黑树的根。
虚拟内存区域使用起始地址和结束地址描述,链表按起始地址递增排序。红黑树是平衡的二叉查找树,按起始地址排序,使用红黑树有以下好处:
(1)在红黑树中查找一个虚拟内存区域的速度快。
(2)增加一个新的区域时,先在红黑树中找到刚好在新区域前面的区域,然后向链表和树中插入新区域,可以避免扫描链表。
C标准库封装了函数mmap用来创建内存映射,内核提供了POSIX标准定义的系统调用mmap:
asmlinkage long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t off);
Linux内核从2.3.31版本开始提供私有的系统调用mmap2:
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t off);
两个系统调用的区别是:mmap指定的偏移的单位是字节,而mmap2指定的偏移的单位是页。有的处理器架构实现了这两个系统调用,有的处理器架构只实现了其中一个系统调用,例如ARM64架构只实现了系统调用mmap。
系统调用sys_mmap的执行流程如下所示:
常见的情况是:创建内存映射的时候不分配物理页,等到进程第一次访问虚拟页的时候,生成页错误异常,页错误异常处理程序分配物理页,在页表中把虚拟页映射到物理页:
虚拟内存过量提交,是指所有进程提交的虚拟内存的总和超过物理内存的容量,内存管理子系统支持3种虚拟内存过量提交策略。
(1)OVERCOMMIT_GUESS(0):猜测,估算可用内存的数量,因为没法准确计算可用内存的数量,所以说是猜测。
(2)OVERCOMMIT_ALWAYS(1):总是允许过量提交。
(3)OVERCOMMIT_NEVER(2):不允许过量提交。
默认策略是猜测,用户可以通过文件“/proc/sys/vm/overcommit_memory”修改策略。
系统调用munmap用来删除内存映射,它有两个参数:起始地址和长度。
系统调用munmap的执行流程如下所示,它把主要工作委托给源文件“mm/mmap.c”中的函数do_munmap:
(1)根据起始地址找到要删除的第一个虚拟内存区域vma。
(2)如果只删除虚拟内存区域vma的一部分,那么分裂虚拟内存区域vma。
(3)根据结束地址找到要删除的最后一个虚拟内存区域last。
(4)如果只删除虚拟内存区域last的一部分,那么分裂虚拟内存区域last。
(5)针对所有删除目标,如果虚拟内存区域被锁定在内存中(不允许换出到交换区),那么调用函数munlock_vma_pages_all以解除锁定。
(6)调用函数detach_vmas_to_be_unmapped,把所有删除目标从进程的虚拟内存区域链表和树中删除,单独组成一条临时的链表。
(7)调用函数unmap_region,针对所有删除目标,在进程的页表中删除映射,并且从处理器的页表缓存中删除映射。
(8)调用函数arch_unmap执行处理器架构特定的处理。各种处理器架构自定义函数arch_unmap,它默认是一个空函数。
(9)调用函数remove_vma_list删除所有目标。