一、背景
mmap接口是很常用的用户态内存映射接口,大部分时候用于映射一个文件,也可以映射一块内存。
#include <sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void *addr, size_t len);
二、linux内核实现机制
1、mmap接口
对应的内核实现接口是do_mmap接口,这个接口内部主要实现了两部分:
1)从进程地址空间找到一个可用的地址;分配一个vma负责该地址区间(有相邻地址,可以合并到原有vma)
2)初始化配置vma,重点关注vma->vm_ops:
如果是文件映射,则指向相应file的vm_ops;
如果是匿名映射,vm_ops为空。
vm_ops包含了很多回调函数,常用的就是几种,典型的就是page fault接口。以fuse文件系统的vm_ops为例说明:
static const struct vm_operations_struct fuse_file_vm_ops = {
.close = fuse_vma_close,
.fault = filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = fuse_page_mkwrite,
};
mmap里只是分配了地址空间和相应的地址管理单元vma,并不涉及真实的物理页面分配,创建页表映射等。根据按需分配的准则,真实的页面分配、文件数据准备、页表建立都是在缺页异常中处理的。
2、缺页异常处理
缺页异常是X86架构的第14号中断(异常),在内核初始化阶段,初始化IDT表时配置缺页异常处理接口。
static const __initconst struct idt_data early_pf_idts[] = {
INTG(X86_TRAP_PF, asm_exc_page_fault), //缺页异常中断
};
void __init idt_setup_early_pf(void)
{
idt_setup_from_table(idt_table, early_pf_idts,
ARRAY_SIZE(early_pf_idts), true);
}
异常处理入口函数asm_exec_page_fault如下。
DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
unsigned long address = read_cr2();
irqentry_state_t state;
prefetchw(¤t->mm->mmap_lock);
/*
* Entry handling for valid #PF from kernel mode is slightly
* different: RCU is already watching and rcu_irq_enter() must not
* be invoked because a kernel fault on a user space address might
* sleep.
*
* In case the fault hit a RCU idle region the conditional entry
* code reenabled RCU to avoid subsequent wreckage which helps
* debuggability.
*/
state = irqentry_enter(regs);
instrumentation_begin();
handle_page_fault(regs, error_code, address); //page fault处理接口
instrumentation_end();
irqentry_exit(regs, state);
}
handle_page_fault接口实现如下。
static __always_inline void
handle_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
trace_page_fault_entries(regs, error_code, address);
if (unlikely(kmmio_fault(regs, address)))
return;
/* Was the fault on kernel-controlled part of the address space? */
if (unlikely(fault_in_kernel_space(address))) {
do_kern_addr_fault(regs, error_code, address); //内核地址缺页处理
} else {
do_user_addr_fault(regs, error_code, address); //用户态地址缺页处理
/*
* User address page fault handling might have reenabled
* interrupts. Fixing up all potential exit points of
* do_user_addr_fault() and its leaf functions is just not
* doable w/o creating an unholy mess or turning the code
* upside down.
*/
local_irq_disable();
}
}
因为mmap流程就是对应用户态的地址映射,所以我们看一下用户态地址的缺页处理接口do_user_addr_fault().这个接口里的处理路径很长,最终如果判断是有效的访问,则会执行handle_mm_fault接口,这个接口看起来比较熟悉。
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
//遍历4级页表项,如果页表缺页,分配和填充每一级页表项
//最后处理内存地址缺页
return handle_pte_fault(&vmf);
}
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
if (!vmf->pte) { //pte为空
if (vma_is_anonymous(vmf->vma)) //vma->vm_ops为空,判定匿名映射
return do_anonymous_page(vmf); //匿名映射缺页处理
else
return do_fault(vmf); //文件映射缺页处理
}
}
2.2.1 PTE为空
如果是PTE页表项没有建立,执行handle_pte_fault路径,会调用到do_fault,就会调用vma->vm_ops->fault接口。
1、文件系统,以fuse文件系统为例,filemap_fault
1)pagecache_get_page,从page cache中分配一个页面或返回已有的页面;
2)filemap_read_page,读取文件映射页面;
2、设备驱动
如果是映射MMIO地址空间,就会做ioremap操作,比如vfio_pci_mmap_fault。
2.2.2 PTE不为空
需要进一步区分几种情况,如页面是否已经换出、是否是访问权限不匹配等分别作不同处理。
(1)do_numa_page
由于各个节点的物理页不平衡,并且判断vma结构体是可以操作的,说明物理页再其他节点中,需要把物理页从其他节点中国移回来到目前的节点内。
(2)do_swap_page
由于匿名物理页面被回收了,所以进程再次访问一块虚拟地址时,就会产生缺页中断,最终进入到 do_swap_page,在这个函数中会重新分配新的页面,然后再从swap分区读回这块虚拟地址对应的数据。
(3)do_wp_page
写时复制一般发生在父子进程之间,fork时copy_mm时会将父进程和子进程的页表项都设置为只读,无论是父进程或子进程执行写入都会触发page fault,通过此函数执行写时复制,分配新的page,拷贝旧page到新page, 并修改相应的页表项为读写。参数vmf->vma保存了线性区原有的读写权限。