一、背景
在之前的内存管理的这篇 内存管理相关——malloc,mmap,mlock与unevictable列表-CSDN博客 博客里,我们已经讲到了锁内存相关mlock和SetPageReserved两个函数,里面也提到了get_user_pages和pin_user_pages,但是没有详细展开,另外,在非gdb方式观察应用程序的运行时的变量状态-CSDN博客 博客里,我们讲到了 pin_user_pages_remote 的使用(可以配合其他函数读取任何一个进程里的数据且不影响进程的运行),从实现上来说pin_user_pages_remote和get_user_pages/pin_user_pages是类似的。
这篇博客里会展开get_user_pages和pin_user_pages里的细节,在描述这两个函数时,会接触到内存管理的关键函数follow_page_mask函数和缺页异常有关的handle_mm_fault函数,对于这两个函数,我们也会展开分析一下。
二、get_user_pages和pin_user_pages
先说一下这两个函数的用途,get_user_pages和pin_user_pages可以锁定对应的page(物理页),不让系统移动它或者回收它。从这个描述可以看出,它的锁要比mlock的锁更加彻底,因为mlock的锁内存并不能确保对应内存不被迁移,只能确保对应内存不被swap,换句话说,mlock后,相应的物理内存还可以是发生变化的,它保证的是有对应物理内存,并不保证对应物理内存不发生变更。而get_user_pages/pin_user_pages则能保证既不会被swap掉,也不会发生物理地址的变更。
我们来温故一下mlock的内核实现部分的调用链(详细细节见之前的博客 内存管理相关——malloc,mmap,mlock与unevictable列表-CSDN博客)这里列得简化一点:
SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
do_mlock(start, len, VM_LOCKED)
apply_vma_lock_flags
__mm_populate
populate_vma_page_range
__get_user_pages
可以看到mlock调用到了__get_user_pages
而get_user_pages和pin_user_pages也最终调用到了__get_user_pages,我们以get_user_pages来举例:
get_user_pages
__gup_longterm_locked
__get_user_pages_locked
__get_user_pages
这就带来两个问题:
1)get_user_pages/pin_user_pages是怎么做到不让回收也不让移动的?
2)get_user_pages和pin_user_pages从调用链上来说和mlock内核里的实现是相似的,都是最终调用的__get_user_pages,为什么mlock做不到不让移动?
2.1 migrate时的引用计数检查
我们先看一下,在做page的移动时,是如何检查的?
在migrate_page->folio_migrate_mapping的如下逻辑进行检查,根据当前page状态page属性算出期望的引用计数,和实际引用计数进行比较,如果不一致则返回错误值:
上图中的folio_ref_freeze函数调用了page_ref_freeze,这里面有检查引用计数:
2.2 get_user_pages和pin_user_pages如何增加引用计数
get_user_pages和pin_user_pages最终还是调用到了__get_user_pages接口
我们以pin_user_pages为例:
pin_user_pages->__gup_longterm_locked->__get_user_pages_locked->__get_user_pages
在 内存管理相关——malloc,mmap,mlock与unevictable列表-CSDN博客 里的3.1 mlock系统调用的内核实现 一节里,有列过__get_user_pages的一些关键函数调用
下图中的两个红框是本博客需要去涉及的两个重要函数follow_page_mask和handle_mm_fault:
follow_page_mask和handle_mm_fault会依次在后面第三第四章去讲,这里我们还是回到这一节的主题,也就是如何增加引用计数的。
引用计数的增加是在handle_mm_fault里吗?并不是!是在follow_page_mask里!
可能有同学就会问了,不是follow_page_mask失败以后才执行主动缺页异常逻辑吗?那不是如果缺页那么就引用计数增加不了了吗?
事实上,它有个retry的label:
__get_user_pages里,它是先执行的follow_page_mask,但是对于没有分配物理内存的场景的话,它会失败,继而走faultin_page的逻辑,faultin_page成功以后会goto retry,重新follow_page_mask,这时候就可以增加到引用计数了:
get_user_pages和pin_user_pages引用计数的调用链的是:
__get_user_pages->follow_page_mask->follow_p4d_mask->follow_pud_mask->follow_pmd_mask->follow_page_pte->try_grab_page
如下逻辑:
只有在传入FOLL_GET/FOLL_PIN时才会增加该引用计数
get_user_pages是FOLL_GET,增加引用计数1
pin_user_pages是FOLL_PIN,增加引用计数1024
另外,要注意的是,FOLL_GET和FOLL_PIN这两个标志位是不允许同时带上的,在上上图的注释里可以看到,很显然同时带上会引起逻辑混乱且也没有必要。
上面已经阐述了,增加引用计数的逻辑是在follow_page_mask函数里,在第三章里我们详细展开一下这个函数。
2.4 get_user_pages和pin_user_pages的使用场景区别
2.2里已经说明了在引用计数上get_user_pages和pin_user_pages有差别,pin_user_pages是后加的机制,为的是能更方便内核察觉是被pin住的,已经方便在使用场景上做一些推荐的区分。
关于get_user_pages和pin_user_pages的使用场景区别大体上可以分成:
DMA的操作用pin_user_pages,其他的用get_user_pages
详细而具体的分类见内核里的Documentation/core-api/pin_user_pages.rst
另外,FOLL_LONGTERM如果要置上必须和FOLL_PIN一起置上,FOLL_LONGTERM标志简单来说就是禁用了一些特殊同步操作,如page_mkclean或munmap
三、follow_page_mask的详细讲解
再重复一下从__get_user_pages到最终增加引用计数的try_grab_page的调用链
__get_user_pages->follow_page_mask->follow_p4d_mask->follow_pud_mask->follow_pmd_mask->follow_page_pte->try_grab_page
follow_page_mask
->follow_huge_addr处理巨页
->follow_p4d_mask遍历P4D
->p4d_offset找到P4D页表项
->follow_pud_mask遍历PUD
->pud_offset找到PUD页表项
->follow_pmd_mask遍历PMD
->pmd_offset找到PMD页表项
->follow_page_pte遍历PTE
详细看一下follow_page_pte的实现:
follow_page_pte
->检查FOLL_PIN和FOLL_GET没有同时传入进来
->pmd_bad检查传入的PMD是否有效
->pte_offset_map_lock通过PMD和地址获取PTE,同时还获取了自旋锁
->通过pte_preset判断该页面是否在内存中
->如果不在内存中
->如果分配掩码没有FOLL_MIGRATION,说明这个页面没有在页面合并中出现,goto no_page
->如果PTE为空,goto no_page
->如果PTE是正在合并的swap页面,那么调用migration_entry_wait等待该页面合并完成后再尝试
->vm_normal_page根据PTE返回普通映射页面的page 与之相对的,是特殊页面
->如果分配掩码支持可写属性 FOLL_WRITE,但是PTE只具有只读属性,goto no_page
->pte_devmap与get_dev_pagemap处理设备映射页面 device mapping page
->vm_normal_page没返回有效页面,说明可能是特殊页面
->is_zero_pfn判断该页面是否为零页,如果是系统零页,则返回page
->try_grab_page函数会根据FOLL_PIN或FOLL_GET来增加引用计数
->当flag有设置FOLL_TOUCH时,则需要标记页面可访问,调用mark_page_accessed设置页面是活跃的
->pte_unmap_unlock释放pte_offset_map_lock里获取的自旋锁
上面高亮的vm_normal_page我们详细展开一下
3.1 vm_normal_page函数返回普通映射页面的page
先看一下vm_normal_page的实现:
CONFIG_ARCH_HAS_PTE_SPECIAL是一个配置选项,x64上是有的,arm64上也有的
arm64上定义了PTE_SPECIAL位,利用的是硬件上的空闲的位。
arm64上定义了PTE_SPECIAL位,利用的是硬件上的空闲的位。
x86上也是有的:
内核通常使用pte_mkspecial宏来设置软件定义的PTE_SPECIAL位
这些特殊页面分为:
1)内核的零页
2)驱动程序使用remap_pfn_range函数来映射内核页面到用户空间。这些VMA通常设置了VM_IO | WM_PFNMAP | VM_NOTEXPAND | VM_DONTDUMP属性
3)vm_insert_page/vm_insert_pfn映射内核页面到用户空间
vm_normal_page函数把页面分为两类:
一类普通页面,normal mapping,如匿名页面、page cache、共享内存页面
另一类特殊页面,special mapping,这些页面不希望参与内存管理的回收或者合并,如映射如下属性的页面
VM_IO:为I/O设备映射内存
VM_PFN_MAP:纯PFN映射
VM_MIXEDMAP:混合映射,表示映射混合使用页帧号pfn和页描述符page
在remap_pfn_range的实现里,有做VM_IO | WM_PFNMAP | VM_NOTEXPAND | VM_DONTDUMP的设置:
在vm_insert_pfn的实现里,有做VM_MIXEDMAP的设置,因为page_fault的逻辑并不是一次映射所有的,是有可能混在一起的
上图中可以从BUG_ON反推两个要求:
VM_MIXEDMAP不能和VM_PFNMAP同时存在,page_fault的逻辑会使用mmap_read_lock 读锁
vm_normal_page函数实现的详细解释如下:
vm_normal_page
->如果定义了CONFIG_ARCH_HAS_PTE_SPECIAL (x86工控机上目前是开的)
->如果PTE的PTE_SPECIAL没有置位 !pte_special goto check_pfn
->如果自定义了find_special_page函数则执行这个自定义的vma->vm_ops的函数
->如果vm_flags设置了VM_PFNMAP或VM_MIXEDMAP,那么这是特殊页面,返回NULL
->is_zero_pfn判断是否是系统零页,如果是,返回NULL
->没有定义CONFIG_ARCH_HAS_PTE_SPECIAL的情况
->如果vm_flags设置了VM_PFNMAP或VM_MIXEDMAP
->因为没有PTE_SPECIAL的辅佐判断,所以还需要进一步做检查
->如果设置的是VM_MIXEDMAP 表示pfn直接映射和page映射混用
->因为没有PTE_SPECIAL,得用pfn_valid来参与整套机制,就得在__vm_insert_mixed用insert_page代替insert_pfn,就需要判断pfn_valid,如果pfn不是valid的话,才表示是特殊页面,这个PTE_SPECIAL架构不支持时的逻辑没有完全理顺,参考__vm_insert_mixed的注释
->如果没有设置的是VM_PFNMAP的话,则用公式来判断是否是特殊页面
->如果是写时复制,页面也是普通页面
->如果是系统零页,返回特殊页面
->label check_pfn 如果pfn大于memory上限,则返回NULL
->通过pfn_to_page返回普通页面page数据结构实例
3.1.1 关于零页
关于零页,zero page是一个特殊的物理页,里面值全部为0,zero page是针对匿名场景专门进行优化,主要是节省内存并对性能进行了一定优化。当malloc或者map一段虚拟内存后,第一次对该内存访问为读操作,将会发生匿名页的page fault。
do_anonymous_page处理,由于第一次是读操作,还未发生写操作,可为其申请一个特殊物理页zero page。ARM64中empty_zero_page是一个全局数组。
3.1.2 vm_insert_page和remap_pfn_range的用法
关于vm_insert_page和remap_pfn_range的用法,使用自定义mmap或者DMA-BUF机制进行内存映射,我们后面会单独写一篇博客来介绍用法
四、handle_mm_fault函数——缺页异常相关
handle_mm_fault函数有两种被调用到的场景,一种是被动缺页异常,一种是主动触发缺页异常
主动触发缺页异常的调用链比如:
以pin_user_pages为例:
pin_user_pages->__gup_longterm_locked->__get_user_pages_locked->__get_user_pages->faultin_page->handle_mm_fault->__handle_mm_fault->handle_pte_fault
handle_mm_fault是缺页异常的arch相关的处理函数__do_page_fault(比如arm64:arch/arm64/mm/fault.c)会调用到的核心的非arch相关的函数
这句话有点抽象,意思就是handle_mm_fault是各个arch实现的缺页异常逻辑里的公共逻辑部分(抽象出共性的部分,放到了这个memory.c里的handle_mm_fault里)
handle_mm_fault->__handle_mm_fault也在mm/memory.c里
__handle_mm_fault->pgd_offset计算PGD页表项(page global directory)
->p4d_alloc计算出p4d页表项(这是5级页表用到的页表项,当前i9是开的5级页表)
->pud_alloc计算出pud页表项(u表示upper)
->pmd_alloc计算出pmd页表项(m表示middle)
->不考虑巨页的情况的话,就运行handle_pte_fault函数
->判断页表项是否为空 情况一:页表项还没建立,情况二:页表项的内容被清空了 ptep_get_and_clear
->如果页表项为空,则判断是否是匿名映射(判断vma->vm_ops即可)
->是匿名映射,则do_anonymous_page
->不是匿名映射,则调用vm_ops函数中定义的fault函数 do_fault(vmf) vmf是vm_fault结构体
->如果页表项不为空,但是pte_present为0,则说明被swap了,do_swap_page从交换分区中读回
->如果页面在内存中,还有可能是设置为NUMA调度的页面,do_numa_page
->如果处理器是因为写内存而触发缺页异常,并且PTE是只读属性
->就会触发一个写时复制的缺页中断,调用do_wp_page,如父子进程之间共享的内存
缺页中断流程图: