纸上得来终觉浅,绝知此事要躬行
看了这么多文章,哪怕是直接看了代码,我以为我懂了,但是真动起手来还是发现有很多不知道,或者是一知半解。
好了,那咱就想办法给系统点压力,让他做个内存回收瞧瞧。
触发回收
既然大家都说内存回收在低水线触发,在高水线停止,那么我们想办法让系统可用内存达到低水位就可以了。为了更快达到低水位,我们可以抬高水位,让这个过程更容易达到。
首先我们看一下当前的水线情况。
$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 116Mi 3.5Gi 0.0Ki 244Mi 3.7Gi
$ cat /proc/sys/vm/min_free_kbytes
8000
$ grep -E "zone |min|low |high |managed|pages free" /proc/zoninfo
Node 0, zone DMA
pages free 3840
min 7
low 10
high 13
managed 3840
Node 0, zone DMA32
pages free 758226
min 1505
low 2257
high 3009
managed 758592
Node 0, zone Normal
pages free 145291
min 487
low 730
high 973
managed 243597
Node 0, zone Movable
pages free 0
min 0
low 0
high 0
managed 0
约4G的内存,留了8k作为低水位。每个zone可用内存距离低水位还都有些距离。那怎么抬高水位呢?还记得水线计算的公式么?
zone的低水线 = (当前zone的内存 * pages_min) / total
其中zone的内存和总内存没法改变,所以只能将pages_min提高才能抬高水位。看着当前还有3.5G的空闲内存,那咱就把这最少空闲内存提高到3G吧。
echo 3000000 > /proc/sys/vm/min_free_kbytes
$ grep -E "zone |min|low |high |managed|pages free" /proc/zoneinfo
Node 0, zone DMA
pages free 3840
min 2862
low 3577
high 4292
managed 3840
Node 0, zone DMA32
pages free 757824
min 565534
low 706917
high 848300
managed 758592
Node 0, zone Normal
pages free 145221
min 181602
low 227002
high 272402
managed 243597
Node 0, zone Movable
pages free 0
min 0
low 0
high 0
managed 0
怎么样,一下子水位就提高了吧。空闲内存和低水位之间的距离明显缩小。 而且有意思的是Normal Zone的空闲内存已经低于最低水位了,但是这个时候却没有发生回收。你说为啥呢?这是因为在判断节点内存是否需要回收,是按照整个节点纬度判断的。可以看到DMA的zone仍然有750M空闲内存。
另外发现DMA这个Zone的内存实在太小了,从回收的角度很容易达到低水位导致认为整个节点内存已经平衡。所以建议没有特殊需要编译时把ZONE_DMA这个选贤关掉。
# memhog 10M
.
# grep pageout /proc/vmstat
pageoutrun 0
谁唤醒了回收
kswapd作为一个内核线程,没事儿的时候他老人家是在那里睡大觉的。只有在需要的时候,才会被唤醒起来干活。 那究竟是谁在什么情况下会集齐龙珠,召唤神龙呢?我们可以看到有一个函数叫wakeup_kswapd,这个是专门用来唤醒kswapd的。 但是这个函数有两个地方被调用,为了确认唤醒kswapd的源头,小编在函数里加了dump_stack()。看看究竟是什么情况会唤醒kswapd老人家。
[ 27.185421] Call Trace:
[ 27.185750] <TASK>
[ 27.186010] dump_stack_lvl+0x33/0x42
[ 27.186453] wakeup_kswapd.cold.87+0x5/0x35
[ 27.186952] wake_all_kswapds+0x53/0xb0
[ 27.187413] __alloc_pages_slowpath.constprop.136+0xa3f/0xc40
[ 27.188120] ? get_page_from_freelist+0xe3/0xc60
[ 27.188673] __alloc_pages+0x30c/0x320
[ 27.189124] alloc_pages_vma+0x71/0x180
[ 27.189610] __handle_mm_fault+0x315/0xb60
[ 27.190103] handle_mm_fault+0xc0/0x290
[ 27.190700] do_user_addr_fault+0x1d7/0x650
[ 27.191375] exc_page_fault+0x4b/0x110
[ 27.191886] ? asm_exc_page_fault+0x8/0x30
[ 27.192447] asm_exc_page_fault+0x1e/0x30
从上面的Trace中可以看到,这是一路从缺页中断过来的。因为内存分配失败,然后唤醒了kswapd。
龙珠
刚才说了,要召唤神龙需要集齐龙珠。在召唤kswapd的过程中也需要符合多个条件。其中一条是ALLOC_KSWAPD。
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
讲真,这个ALLOC_KSWAPD的标志真的让我一顿好找。总结下来,确认分配时需不需要这个标志由以下一个原因判断:
alloc_flags从gfp_flags决定,gfp_to_alloc_flags()函数负责这个翻译
__GFP_KSWAPD_RECLAIM和ALLOC_KSWAPD的定义是一样的
gfp_flags定义中更有多个flag带了__GFP_KSWAPD_RECLAIM,其中基本的两个是__GFP_KSWAPD_RECLAIM和__GFP_RECLAIM
对于匿名页,__alloc_pages的gfp是GFP_HIGHUSER_MOVABLE, 这个定义的展开包含了__GFP_RECLAIM
所以对于普通的用户内存申请都符合这个条件。
直接回收在哪?
在[Big Picture][1]中我们看到,回收分成两种:直接回收和间接回收。上面我们看到的是间接回收,那直接回收在什么时候会发生呢?
仔细看下来,又一个重要的GFP标志:__GFP_DIRECT_RECLAIM。只有当设置了这个标志时,才会执行直接回收。
对这个标志的检测发生在两个地方:
__alloc_pages_slowpath -> can_direct_reclaim
node_reclaim() -> gfpflags_allow_blocking()