本文代码基于linux内核4.19.195.
mmap系统调用是我们常用的一个系统调用,最近好奇研究了一下mmap系统调用是怎么影响页表权限位的配置的。
在mmap流程中,我们最终会走到函数mmap_region()里面完成mmap的动作,mmap_region()函数会调用vm_get_page_prot函数,将用户态传入的flag转换成权限位存放在了vma->vm_page_prot里面。然后,在必要的时候(比如通过/dev/mem做mmap映射就是在mmap的过程中,而mmap映射匿名页的话就是在后续的缺页中断中),获取vma->vm_page_prot的权限值,生成页表项并将页表项填入页表中。
下面重点分析这个权限值是怎么生成的。
pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
pgprot_t ret = __pgprot(pgprot_val(protection_map[vm_flags &
(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
pgprot_val(arch_vm_get_page_prot(vm_flags)));
return arch_filter_pgprot(ret);
}
EXPORT_SYMBOL(vm_get_page_prot);
函数非常简单,通过用户态传入的flags,与上(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)获得一个index,接着去取数组protection_map在index处的值即可。
那么,这个protection_map数组是个啥玩意?
pgprot_t protection_map[16] __ro_after_init = {
__P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
__S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};
每种处理器架构需要定义__P000 到__S111 的宏,P 代表私有(Private),S 代表共享(Shared),后面的 3 个数字分别表示可读、可写和可执行,例如__P000 表示私有、不可读、不可写和不可执行,__S111 表示共享、可读、可写和可执行。说白了,就是分成private映射和share映射两类,每类都有读、写和执行的权限位,这个和我们平时ls -l看到的文件权限位是类似的。如果是私有映射就取Pxxx,否则就取Sxxx,这个取决于是否含有VM_SHARED标志;另外几个标志亦是如此。
所以,我们可以很自然的理解如下定义的取值为什么是这样取值。
#define VM_READ 0x00000001 /* currently active flags */
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
还有一个重要的点,通过查看Pxxx的定义,我们可以发现所有Px1x的值,都是不带有_PAGE_RW权限的,所以所有作为私有映射的页,在发生写操作时,就会陷入缺页中断完成处理了。(应该都会触发cow,不知道会不会有特殊情况)。
/* xwr */
#define __P000 PAGE_NONE
#define __P001 PAGE_READONLY
#define __P010 PAGE_COPY
#define __P011 PAGE_COPY
#define __P100 PAGE_READONLY_EXEC
#define __P101 PAGE_READONLY_EXEC
#define __P110 PAGE_COPY_EXEC
#define __P111 PAGE_COPY_EXEC
总的来说,protection_map数组结合VM_READ、VM_WRITE、VM_EXEC、VM_SHARED标志,就能完成flag->权限值的转化,设计的是非常巧妙的。