内存管理之——get_user_pages和pin_user_pages及缺页异常

一、背景

在之前的内存管理的这篇 内存管理相关——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,如父子进程之间共享的内存

 

缺页中断流程图:

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰克崔

打赏后可回答相关技术问题

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值