vmlck大于rss的问题分析

一、背景

在现今越来越重视性能,越来越注重实时性的环境下,我们会使用锁内存并关注锁内存所带来的潜在收益。锁内存其实是一个很大的话题,因为内存的种类很多,细分的类型也很多,锁内存的方法也很多,不同的锁内存的方式所带来的影响也不一样。

我讨论通常意义下的锁内存,从锁内存的内存的类型分类来说一般是指两种:

1)锁的是匿名页,也就是没有对应文件的内存,为了简化,我们先不考虑共享内存场景和因为调用因为调用madvise MADV_FREE后的场景(madvise MADV_FREE对文件页统计的影响及原理_madvise 函数-CSDN博客),我们就说是普通的如malloc分配出来的匿名页的场景。通过mlock把这些如malloc分配出来的匿名页的空间锁住,关于mlock及mlock逻辑有关的unevictable列表的介绍见之前的博客 内存管理相关——malloc,mmap,mlock与unevictable列表-CSDN博客。针对匿名页和文件页以及active anon/inactive anon/active file/inactive file是有大量的细节需要说明的,我们会在后面的博客里说明。

2)调用mlock把进程有关的代码段(一般有对应文件)或是其他文件锁进内存

一般来说,程序运行的代码段都是属于一个具体的如.bin,.so这样的具体的文件里的内容,程序在运行起来之后,会根据这些文件(换句话说是elf文件)进行解析,解析出里面的数据段部分和代码段部分,数据段部分会根据是不是常量还是变量进行可读或可读写的私有映射,映射到进程地址空间里,代码段部分则进行可读可执行的私有映射,也映射到进程地址空间里。这些信息都是可以通过/proc/<pid>/maps里来获取到的。我们可以遍历/proc/<pid>/maps里的条目来找到对应的虚拟地址段,再对该地址段执行mlock。要说明的是,这里说的这些有对应文件的其实就是page cache,其实,我们还有别的方法来把这些pagecache来锁进内存,参考之前的 借助内核逻辑锁pagecache到内存_brief lock memory to ram to avoid delays-CSDN博客 博客里借助内核逻辑锁pagecache到内存的方法。

这篇博客我们说的是在进行上面描述的第二种进程的代码段执行mlock锁内存操作后,发现的一处奇怪的现象,如下图看到,vmlck的大小竟然比vmrss的大小还大:

照理来说,在我们的例程里,我们并没有进行上面说的第一种匿名页的mlock,仅仅是锁了代码段也就是pagecache。但是看到vmlck竟然都比vmrss都大,不符合常理。

我们在下面第二章里描述原因及相关原理,在第三章里,我们给出完整的整理后的图示。

二、内存统计的相关原理及问题原因

跟踪vmlck和vmrss的数值的来源逻辑我们就可以跟踪到时mm_rss的进程统计相关的逻辑,这块逻辑在分析后发现,它随着版本迭代发生过大的变化,内核在最最新的版本里是不会出现上面截图里的明显的异常的情况,但是在相对老一些的5.19或者6.1的版本还是会出现进程统计的vmrss小于vmlck的情况。我们在下面 2.1 完整的顺藤摸瓜的跟踪一下。然后在 2.2 里总结一下问题原因。

2.1 内存统计的相关原理

在出现奇怪的VmRSS小于VmLck的情况时通过cat /proc/<pid>/maps就可以发现,VmLck的数值是符合预期的,出问题的是VmRSS的数值。

我们跟一下VmRSS是怎么读到的,以及是如何进行数值的更新和统计的。

2.1.1 VmRSS的数值的获取逻辑task_mem函数

搜索VmRSS就可以搜到进程的VmRSS数值的获取是在task_mmu.c里的task_mem函数,如下的逻辑:

可以从上图里看到,进程的VmRSS的数值是通过get_mm_counter函数根据MM_ANONPAGES和MM_FILEPAGES和MM_SHMEMPAGES三个类型来得到统计值,相加得到的VmRSS的值。

get_mm_counter的实现如下图还是比较简单的,是读取的mm_struct里的rss_stat里的count数组里的对应索引的数值:

而mm_struct其实就是通过task_struct里拿到的,下图是上面说到的task_mem函数被调用的地方:

可以看到mm_struct是通过task_struct得到的,用的get_task_mm函数:

上图里task->flags & PF_KTHREAD是用来判断当前的task是否是内核线程,内核线程因为用的地址空间是系统全局的,只需要用内核的公用的那张页表,不像用户进程需要有各自私有的页表。

有关如何判断cpu上运行的线程是内核线程还是用户线程,以及当前线程运行的状态处于内核态还是用户态,可以参考之前的博客 监测各个核上cpu上的线程是内核线程还是用户线程,处于内核态还是用户态的方法_如何知道当前是内核态还是用户态-CSDN博客

2.1.2 mm_struct的rss_stat的更新逻辑及与current->rss_stat的关系

在上一节 2.1.1 里我们讲到VmRSS的值是通过get_mm_counter函数通过获取三个类型MM_ANONPAGES、MM_FILEPAGES、MM_SHMEMPAGES的内存数值相加之后得到的,也讲到了下图里传入的第一个参数mm就是task_struct里的mm成员。

这一节我们看一下get_mm_counter函数里上图里的这个task_struct里的mm成员,mm->rss_stat.count的数值是怎么更新的。

搜索内核代码里的rss_stat看有哪些地方:

如上图可以看到,有两处都叫rss_stat,一个是mm->rss_stat,一个是current->rss_stat。

get_mm_counter函数是获取的mm->rss_stat,那么current->rss_stat又是干嘛的呢,他们俩之间的关系是什么?

事实上,current->rss_stat是mm->rss_stat的一个缓冲,即高频的变更会反映在current->rss_stat里,在合适的时间会更新到mm->rss_stat里,我们获取进程的vmrss时,刚也分析到了是用的mm->rss_stat里的信息,并不是用的current->rss_stat里的信息。

另外,要特别注意的是current->rss_stat缓冲的只是delta值,就是差值,系统会在合适的时机把这个delta值加到mm->rss_stat里去。

2.1.3 哪些情况下会把current->rss_stat里的信息同步到mm->rss_stat里去

接下来我们就需要看一下,这个缓冲的current->rss_stat的信息什么时候会被同步到mm->rss_stat里去。这里我们的分析以linux 5.19为例(其实linux 6.1这块逻辑也是差不多的,直到linux 6.5版本,这块逻辑才有变更)。

搜索代码可以发现,更新到mm->rss_stat里去必须经过函数inc_mm_counter/dec_mm_counter/add_mm_counter这三个函数之一,inc和dec只是增减1,相对来说add_mm_counter使用的地方更多,我们就看add_mm_counter在哪些情况下会调用到:

如上图,还是有不少地方使用到了add_mm_counter,回到这篇博客的主题,分析的是“vmlck大于rss的问题”,所以我们先不管huge_memory.c和khugepaged.c和madvise.c,看一下memory.c里有哪些地方有使用,可以看到与标题vmlock大于rss的问题相关的调用的地方就三处:

1)通过sync_mm_rss来同步

如上图,这个sync_mm_rss函数是将current->rss_stat里缓冲的各个类型的delta(差值)加到mm->rss_stat里去。

另外,这个sync_mm_rss的上图的实现是在SPLIT_RSS_COUNTING被定义时用的,默认情况下,这个是被定义的。如果SPLIT_RSS_COUNTING不被定义,sync_mm_rss的实现是空的:

2)add_mm_counter_fast里在判断到传入的mm并不是current->mm时会进行add_mm_counter的更新

意思就是,如果传入的mm就是当前进程的mm,那么只缓冲到current->rss_stat里,并不直接更新到current->mm->rss_stat里。

3)add_mm_rss_vec判断出传入的mm是current->mm时会先进行一次current->rss_stat到mm->rss_stat的同步,然后再把传入的rss的各个类别的增加的delta值通过add_mm_counter加到current->mm里

接下里,我们看一下具体什么情况下会分别调用sync_mm_rss、add_mm_counter_fast和add_mm_rss_vec。不过我们关注的场景还是聚焦在当前讨论的vmlock大于rss也就是调用mlock后导致问题现象的场景。

2.1.4 sync_mm_rss在什么情况下会调用

除了刚才说到的在add_mm_rss_vec里在判断出传入的mm是current->mm时会调用sync_mm_rss进行current->rss_stat到mm->rss_stat的同步以外,在很多内存解映射的场景里也是会进行sync_mm_rss的操作的。还有一个比较重要的调用sync_mm_rss的地方就是在check_sync_rss_stat里调用,而check_sync_rss_stat在什么情况下调用,会在下面 2.1.7 里讲到。

这里其实也还是可以理解的,因为从内存分配到真正的占用物理内存它往往有一个过程,也就是缺页异常的过程,不触发缺页异常,内存就算进行了mmap也不占用物理内存。但是,对于解映射而言,它就是瞬间完成大量的内存释放的动作的,这时候,也是需要进行立马的内存统计的同步的。

在如下图exec_mmap里有old_mm的内存统计的同步:

在exit_mm里有如下sync_mm_rss的同步:

在madvise_free_pte_range里也有如下sync_mm_rss的同步:

2.1.5 add_mm_counter_fast在什么情况下会调用及相关性能开销

接下来,我们看一下add_mm_counter_fast的调用的地方,目前使用它的地方是通过如下两个宏来使用:

搜索inc_mm_counter_fast可以发现,调用它的位置基本都是在缺页异常的处理流程里,有关缺页异常之前的 内存管理之——get_user_pages和pin_user_pages及缺页异常_get user page-CSDN博客 博客里有详细介绍。

我们列一下有哪些函数会使用inc_mm_counter_fast宏:

insert_page_into_pte_locked

wp_page_copy

do_swap_page

do_anonymous_page

do_set_pte

这是因为缺页异常是一个小颗粒度的行为,默认是4k为一个单位进行触发,而如此相对还是比较高频的触发,不使用add_mm_counter_fast,而使用inc_mm_counter进行增加的话,会让inc_mm_counter函数里的atomic_long_inc_return操作频繁调用,而cas add操作相比普通的加是有一定的总线上的额外开销的,另外设计上cas add也是针对并发场景,所以也有缓冲一致性的性能损耗,所以频繁的进行同步是会影响一定的性能,所有这么一个current->rss_stat的缓冲设计,让在current上下文里的内存的频繁变更的逻辑进行多次事件上的合并,防止频繁的引起cas add或者缓冲一致性的开销。

2.1.6 add_mm_rss_vec在什么情况下会调用

注意,add_mm_rss_vec在current->mm等于mm的情况下,是会执行sync_mm_rss的,所以,add_mm_rss_vec在current->mm等于mm的情况下是会同步current->rss_stat信息到mm->rss_stat里去的,重要的是,它还会把传入add_mm_rss_vec的第二个参数也就是rss的数组也更新到mm->rss_stat里去,所以,在一些内存统计变更比较复杂的情况下,也就是同时会进行多个类别的内存统计值更新时,是需要用这个add_mm_rss_vec接口来进行同步工作的。

直接调用add_mm_rss_vec的地方并不多:

就copy_pte_range和zap_pte_range两处,copy_pte_range在start_kernel、kernel_clone、fork_idle这些需要进行dump_mm的地方进行的copy_pte_range的操作。

zap_pte_range呢?从zap这个英文的含义来说,表示的就是remove或者erase的意思,也就是在移除一些pte的表项所附带需要执行的这个add_mm_rss_vec的内存统计项的同步动作的。

我们跟踪zap_pte_range的调用的链路就可以跟踪到源头是brk系统调用、mmap系统调用等一些需要在执行操作过程中会进行解映射操作的地方调用到了__do_munmap,继而调用到了unmap_single_vma,继而最终调用到了zap_pte_range。

除了brk/mmap/mremap这几个系统调用完,还有madvise系统调用及其他泛madvise系统调用会调用到madvise_dontneed_free,继而也调用到unmap_single_vma,继而最终调用到了zap_pte_range。

在上面提到的__do_munmap调用到unmap_single_vma的过程中,调用到了unmap_vmas这个接口,这个接口除了会被__do_munmap间接调用到,也会被exit_mmap调用到,调用exit_mmap的地方有mmput和binder_alloc_free_page两处。

2.1.7 check_sync_rss_stat的调用场景

调用check_sync_rss_stat函数的地方就一处,是在handle_mm_fault里的开头,还为走到缺页异常核心逻辑__handle_mm_fault前进行的这个check_sync_rss_stat的检查和更新。

我们看一下这个check_sync_rss_stat函数的实现:

如上图,这个函数只在current等于传入的task时才工作,它并不会增加内存统计值,它只会累加current->rss_stat里的events值,并检查当前的current->rss_stat里记录的events超过或者达到阈值64时会进行sync_mm_rss的内存统计值的同步。这是因为内存的减少是可能瞬间发生的,但是内存的增加从缺页异常角度而言,是一次次缺页异常累积起来的。要注意,内存统计值的增加并不是在这里check_sync_rss_stat函数里完成的,刚也说到,它是只做检查和同步,不做数值上的更新,增加current->rss_stat里的delta统计值的动作只在add_mm_counter_fast里做的。

另外一方面,add_mm_counter_fast是不会更新rss_stat里的这个events值的,所以那些调用inc_mm_counter_fast的地方,如果一直只是调用inc_mm_counter_fast而不调用check_sync_rss_stat或者sync_mm_rss的话,在current里的rss_stat里的数值是不会更新到mm_struct里的。

2.2 问题原因

总结一下问题原因:

由于低版本内核上(其实也并不低6.1版本也有此问题)rss_stat的统计分为mm_struct里的累积值,和current->rss_stat里的本线程范围内的delta值,两个部分。

而在handle_mm_fault里是在进入handle_mm_fault之后的头部会进行check_sync_rss_stat的操作,是在check_sync_rss_stat里进行的内存统计delta值增加,这里增加的值是增加到current->rss_stat里,而不是在mm_struct里,只在进入check_sync_rss_stat了64次后才会把current->rss_stat里的内存统计delta值更新到mm_struct里去,导致统计到current->rss_stat里的数值没有及时更新,导致看到的vmrss的数值其实是滞后的。

在后面的博客里我们也会讲到这里面的另外一个细节,就是进入handle_mm_fault的节奏并不是每个4k页进入一次的,这会导致check_sync_rss_stat增加的events的数值并不是PAGE SIZE粒度一次的,是会比它还大。

2.3 高版本的rss_stat的相关机制及低版本上可以采用的解决办法

这个问题在高版本上来说是不存在的,这是因为高版本上,rss_stat的这块逻辑进行了整改,不再有add_mm_counter_fast接口,都是用add_mm_counter进行内存统计值的增加,且高版本内核里获取进程的vmrss时是用的准确数值的接口。

2.3.1 高版本上的rss_stat的相关机制

高版本里对于rss_stat的统计删除了current->rss_stat这个缓冲的delta值,只在mm_struct里进行rss_stat的统计,对于rss_stat的统计,使用了percpu_counter的这套机制,这套机制的详细介绍在后面的博客里会展开。简单理解的话,就是通过percpu的机制和一个滞后的sum统计值来一样的缓解缓存一致性有关的性能问题。

高版本内核里,在获取进程的rss_stat时,调用的是percpu_counter里获取准确统计值的接口get_mm_counter_sum,所以并不会存在数值失真的情况。

2.3.2 低版本上可以采用的解决办法

针对这个问题所在的内核版本上,我们可以通过牺牲理论上的一点点的性能来换取准确的数值,也就是剔除add_mm_counter_fast接口只留add_mm_counter进行内存统计值的更新,直接更新到mm_struct里的rss_stat里去,另外,也不用在handle_mm_fault里再去调用check_sync_rss_stat接口去累加events值并进行sync_mm_rss了,因为通过add_mm_counter进行的内存统计值更新是直接更新到mm_struct里的rss_stat里去的,和获取时用的是一样的变量。

三、完整的图示

其实在上面第二章里,我们已经对问题相关的概念和调用链进行了一定的分析,相关的调用链的图如下。

如果要看更完整的图示内容,可下载 https://download.csdn.net/download/weixin_42766184/91680157

拆分成三张图后如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

杰克崔

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

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

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

打赏作者

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

抵扣说明:

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

余额充值