创建内存映射
C标准库封装了函数mmap用来创建内存映射,内核提供了POSIX标准定义的系统调用mmap:
1-sys_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.13所示。
- (1)检查偏移是不是页的整数倍,如果偏移不是页的整数倍,返回“-EINVAL”。
- (2)如果偏移是页的整数倍,那么把偏移转换成以页为单位的偏移,然后调用函数sys_mmap_pgoff。
2-sys_mmap_pgoff
函数sys_mmap_pgoff的执行流程如下。
-
(1)如果是创建文件映射,根据文件描述符在进程的打开文件表中找到file实例。
-
(2)如果是创建匿名巨型页映射,在hugetlbfs文件系统中创建文件“anon_hugepage”,并且创建该文件的一个打开实例file。注意:文件名没有实际意义,创建匿名巨型页映射两次,就会在hugetlbfs文件系统中创建两个名为“anon_hugepage”的文件,这两个文件没有关联。
-
(3)调用函数vm_mmap_pgoff进行处理。
3-vm_mmap_pgoff
函数vm_mmap_pgoff的执行流程如下。
- (1)以写者身份申请读写信号量mm->mmap_sem。
- (2)把创建内存映射的主要工作委托给函数do_mmap。
- (3)释放读写信号量mm->mmap_sem。
- (4)如果调用者要求把页锁定在内存中,或者要求填充页表并且允许阻塞,那么调用函数mm_populate,分配物理页,并且在页表中把虚拟页映射到物理页。
常见的情况是:创建内存映射的时候不分配物理页,等到进程第一次访问虚拟页的时候,生成页错误异常,页错误异常处理程序分配物理页,在页表中把虚拟页映射到物理页。
4-do_mmap
函数do_mmap实现创建内存映射的主要工作,执行流程如图所示。
-
(1)调用函数get_unmapped_area,从进程的虚拟地址空间分配一个虚拟地址范围。函数get_unmapped_area根据情况调用特定函数以分配虚拟地址范围。
- 1)如果是创建文件映射或匿名巨型页映射,那么调用file->f_op->get_unmapped_area以分配虚拟地址范围。
- 2)如果是创建共享的匿名映射,那么调用shmem_get_unmapped_area以分配虚拟地址范围。
- 3)如果是创建私有的匿名映射,那么调用mm->get_unmapped_area以分配虚拟地址范围。ARM64架构的内核在装载程序时,如果选择传统布局,函数arch_pick_mmap_layout把mm->get_unmapped_area设置为函数arch_get_unmapped_area。
-
(2)计算虚拟内存标志。
vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
把系统调用中指定的保护位和标志合并到一个标志集合中,函数calc_vm_prot_bits把以“PROT_”开头的保护位转换成以“VM_”开头的标志,函数calc_vm_flag_bits把以“MAP_”开头的标志转换成以“VM_”开头的标志。
mm->def_flags是默认的虚拟内存标志:进程默认的虚拟内存标志是VM_NOHUGEPAGE,即不使用透明巨型页;内核线程默认的虚拟内存标志是0。
VM_MAYREAD表示允许设置标志VM_READ, VM_MAYWRITE表示允许设置标志VM_WRITE, VM_MAYEXEC表示允许设置标志VM_EXEC。这3个标志是系统调用mprotect所需要的。
- (3)调用函数mmap_region以创建虚拟内存区域。
5-mmap_region
-
(1)调用函数may_expand_vm以检查进程申请的虚拟内存是否超过限制。
首先检查(进程的虚拟内存总数 + 申请的页数)是否超过地址空间限制:mm->total_vm +npages > rlimit(RLIMIT_AS)>> PAGE_SHIFT。
如果是私有的可写映射,并且不是栈,那么检查(进程数据的虚拟内存总数 + 申请的页数)是否超过最大数据长度:mm->data_vm + npages > rlimit(RLIMIT_DATA) >>PAGE_SHIFT。 -
(2)如果是固定映射,调用者强制指定虚拟地址范围,可能和旧的虚拟内存区域重叠,那么需要从旧的虚拟内存区域删除重叠的部分。
-
(3)如果是私有的可写映射,检查所有进程申请的虚拟内存的总和是否超过物理内存的容量。
/*
* 如果是需要记账的映射,那么检查所有进程申请的虚拟内存的总和是否超过物理内存的容量。
* 需要记账的映射具备以下3个条件。
* (1)私有的可写映射。
* (2)不是标准巨型页(因为标准巨型页单独记账)。
* (3)需要预留物理内存(即未设置VM_NORESERVE)。
*/
if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
/* 根据虚拟内存过量提交的策略,判断物理内存是否足够。*/
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
-
(4)如果可以和已有的虚拟内存区域合并,那么调用函数vma_merge,和已有的虚拟内存区域合并。
-
(5)如果不能和已有的虚拟内存区域合并,处理如下。
- 1)创建新的虚拟内存区域。
- 2)如果是文件映射,那么调用文件的文件操作集合中的mmap方法(file->f_op->mmap), mmap方法的主要功能是设置虚拟内存区域的虚拟内存操作集合(vm_area_struct.vm_ops),其中的fault方法很重要:第一次访问虚拟页的时候,触发页错误异常,异常处理程序将调用虚拟内存操作集合中的fault方法以把文件的数据读到内存。
- 文件的文件操作集合是在打开文件的时候设置的,和文件所属的文件系统相关。
- 很多文件系统把文件操作集合中的mmap方法设置为公共函数generic_file_mmap,函数generic_file_mmap的主要功能是把虚拟内存区域的虚拟内存操作集合设置为generic_file_vm_ops,其中fault方法是函数filemap_fault。
- EXT4文件系统把文件操作集合中的mmap方法设置为函数ext4_file_mmap,函数ext4_file_mmap的主要功能是把虚拟内存区域的虚拟内存操作集合设置为ext4_file_vm_ops,其中fault方法是函数ext4_filemap_fault。
- 3)如果是共享的匿名映射,那么在内存文件系统tmpfs中创建一个名为“/dev/zero”的文件,并且创建文件的一个打开实例file,虚拟内存区域的成员vm_file指向这个打开实例,把虚拟内存操作集合设置为shmem_vm_ops。如果没有开启共享内存的配置宏CONFIG_SHMEM, shmem_vm_ops等价于generic_file_vm_ops。
- 4)调用函数vma_link,把虚拟内存区域添加到链表和红黑树中。如果虚拟内存区域关联文件,那么把虚拟内存区域添加到文件的区间树中,文件的区间树用来跟踪文件被映射到哪些虚拟内存区域。
- 5)调用函数vma_set_page_prot,根据虚拟内存标志(vma->vm_flags)计算页保护位(vma-> vm_page_prot),如果共享的可写映射想要把页标记为只读,目的是跟踪写事件,那么从页保护位删除可写位。
内容来自前辈书籍:《Linux内核深度解析》