内存管理实战案例分析3(为何分配不出一个页面())

知识养成了思想,思想同时又在融化知识。这篇文章主要讲述内存管理实战案例分析3:为何分配不出一个页面?相关的知识,希望能为你提供帮助。

微信公众号:奔跑吧linux社区
本文节选自《奔跑吧Linux内核》第二版卷1第6.3.3章
1.问题描述
下面是有问题的OOM Killer内核日志,其中空闲页面为86048KB,最低警戒水位为22528KB,低水位为28160KB。读者可能会感到疑惑,为什么即使空闲页面远远大于最低警戒水位也无法分配出一个物理页面?
< OOM Killer的问题内核日志>

[150.257731] insmod invoked oom-killer: gfp_mask=0x6000c0(GFP_KERNEL),
order=0, oom_score_adj=0
...
[150.272821] Node 0 DMA32 free:86048KB min:22528KB low:28160KB high:33792KB
active_anon:16384KB inactive_anon:6316KB active_file:896KB inactive_file:
808KB unevictable:0KB writepending:0KB present:1048576KB managed:999784KB mlocked:
0KB kernel_stack:2848KB pagetables:812KB bounce:0KB free_pcp:1864KB local_pcp:
756KB free_cma:64280KB
[150.335591] lowmem_reserve[]: 0 0 0
...
Oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,
mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/user@0.service,
task=(sd-pam),pid=512,uid=0
[150.297054] Out of memory: Kill process 512 ((sd-pam)) score 2 or sacrifice child
[150.299368] Killed process 512 ((sd-pam)) total-vm:166912KB, anon-rss:2616KB,
file-rss:0KB, shmem-rss:0KB
[150.357941] oom_reaper: reaped process 512 ((sd-pam)), now anon-rss:0KB,
file-rss:0KB, shmem-rss:0KB

2.问题分析
现在的服务器或者手机等设备都配备了大量的内存。虽然配置了大量的内存,当服务器业务量越来越大时,系统内存会处于承压状态,可能系统想分配一个页面都分配不出来,从而触发OOM Killer机制。
我们先来分析一个OOM Killer的正常内核日志。
< OOM Killer的正常内核日志>

[296.106260] systemd invoked oom-killer:
gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0

...

[296.134445] Node 0 DMA32 free:23592KB min:24576KB low:30208KB high:35840KB active_
anon:40680KB inactive_anon:5000KB active_file:72KB inactive_file:112KB unevictable:
0KB writepending:0KB present:1048576KB managed:738068KB mlocked:0KB kernel_stack:
2432KB pagetables:1268KB bounce:0KB free_pcp:32KB local_pcp:32KB free_cma:0KB
[296.137154] lowmem_reserve[]: 0 0 0
[296.137980] Node 0 DMA32: 1322*4KB (UME) 834*8KB (UME) 378*16KB (UME) 119*32KB
(UME) 28*64KB (UM) 0*128KB 0*256KB 0*512KB 0*1024KB 0*2048KB 0*4096KB = 23608KB

从内核日志中可以看出以下两点。
  1. 系统只有一个内存节点,并且只有一个内存管理区ZONE_DMA32,因此这台计算机可能配置了少于4GB内存的嵌入式系统(如ARM64的系统)。
  2. systemd线程想分配一个物理页面,但是失败了,分配掩码为GFP_HIGHUSER_ MOVABLE。
    继续看内核日志,发现空闲页面(free)为23592KB,最低警戒水位(min)为24576KB,这种情景下分配不出一个物理页面,是正常的。对于GFP_HIGHUSER_MOVABLE分配掩码来说,这属于正常的分配优先级,在内存短缺的情况下,它没有权限去使用系统保留的内存。
    接下来分析OOM Killer的问题内核日志。从OOM Killer的问题内核日志可以看出以下两点。
  3. 系统只有一个内存节点和一个内存管理区ZONE_DMA32。
  4. insmod进程想分配一个物理页面,但是失败了,分配掩码为GFP_KERNEL。
    当前系统的空闲内存有86048KB,而系统的最高水位是33792KB,说明系统空闲的内存远远大于最高水位,那为什么还是分配不出一个物理页面呢?
    另外,从日志中可以看出,这次分配使用掩码GFP_KERNEL。GFP_KERNEL是内核最常见的分配掩码,它分配的内存只给内核本身使用,分配优先级为普通级别,因此它不能使用最低警戒水位以下的内容,它分配的内存的迁移类型是不可迁移类型MIGRATE_UNMOVABLE。
    判断是否可以在某个内存管理区中分配出内存的函数是zone_watermark_fast(),它实现在mm/page_alloc.c文件中,下面是该函数的代码片段。
< mm/page_alloc.c>

static inline bool zone_watermark_fast()
long free_pages = zone_page_state(z, NR_FREE_PAGES);

#ifdef CONFIG_CMA
if (!(alloc_flags & ALLOC_CMA))
cma_pages = zone_page_state(z, NR_FREE_CMA_PAGES);
#endif

if (!order & & (free_pages - cma_pages) > mark + z-> lowmem_reserve[classzone_idx])
return true;
...

当内部使用的分配标志位没有设置ALLOC_CMA时,我们需要把NR_FREE_CMA_PAGES的页面考虑进去,也就是系统空闲页面减去cma_pages才是系统真正的空闲页面。那么什么时候会设置ALLOC_CMA标志位呢?
如果页面分配器在低水位,没法分配出内存时,会进入慢速路径__alloc_pages_slowpath()。在慢速路径,首先会把水位的判断条件降低到最低警戒水位,这是在gfp_to_alloc_flags()函数中实现的。另外,若系统使能了CMA机制,并且请求分配的页面属于可迁移类型的页面,那么设置ALLOC_CMA,表示可以使用CMA机制预留的内存,因为CMA机制预留的内存是属于可迁移类型的。
static inline unsigned int
gfp_to_alloc_flags(gfp_t gfp_mask)
unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
...
#ifdef CONFIG_CMA
if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
alloc_flags |= ALLOC_CMA;
#endif
return alloc_flags;

在本例中,我们看到CMA的预留内存free_cma为64280KB,而且这次请求分配的内存是不可迁移类型的,因此系统真正空闲内存的计算公式为free ? free_cma = 21768KB。它比最低警戒水位要低,导致分配内存不成功。
在zone_watermark_fast()以及__zone_watermark_ok()函数中的判断语句将返回false,代码片段如下。
< mm/page_alloc.c>

static inline bool zone_watermark_fast()
...
if (!order & & (free_pages - cma_pages) > mark + z-> lowmem_reserve[classzone_idx])
return true;
...
return __zone_watermark_ok();


bool __zone_watermark_ok()

...
if (free_pages < = min + z-> lowmem_reserve[classzone_idx])
return false;
...

3.一台x86_64服务器触发OOM killer机制的场景
我们刚才分析的场景比较简单,系统中只有一个内存节点以及一个内存管理区。下面分析一个稍微复杂一点的场景,在一台x86_64服务器上触发了OOM Killer机制。我们在这台服务器上设置了panic_on_oom。也就是说,当OOM Killer机制发生时会触发一个异常,从而触发一个故障转储事件。
< x86_64服务器触发OOM Killer机制的场景下的内核日志片段>

[14419.570538] cupsd invoked oom-killer:
gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[14419.570543] CPU: 7 PID: 926 Comm: cupsd Kdump: loaded Not tainted 5.0.0 #1
[14419.570545] Call Trace:
[14419.570552]dump_stack+0x63/0x85
[14419.570561]out_of_memory+0x356/0x4e0
[14419.570564]__alloc_pages_slowpath+0xaa6/0xed0
[14419.570573]__page_cache_alloc+0xb8/0xd0
[14419.570576]filemap_fault+0x3db/0xac0
[14419.570592]__do_fault+0x42/0x120
[14419.570601]__do_page_fault+0x2b5/0x4c0
[14419.570608]page_fault+0x1e/0x30
...
[14419.570625] Mem-Info:
[14419.570629] active_anon:1640829 inactive_anon:207463 isolated_anon:0
active_file:432 inactive_file:632 isolated_file:0
unevictable:1369 dirty:0 writeback:0 unstable:0
slab_reclaimable:13009 slab_unreclaimable:30725
mapped:284 shmem:1373 pagetables:14222 bounce:0
free:25253 free_pcp:481 free_cma:0
[14419.570633] Node 0 active_anon:6563316KB inactive_anon:829852KB active_file:
1728KB inactive_file:2528KB unevictable:5476KB isolated(anon):0KB isolated(file):
0KB mapped:1136KB dirty:0KB writeback:0KB shmem:5492KB shmem_thp: 0KB shmem_pmdmapped:
0KB anon_thp: 0KB writeback_tmp:0KB unstable:0KB all_unreclaimable? no
[14419.570635] Node 0 DMA free:15884KB min:140KB low:172KB high:204KB active_anon:
0KB inactive_anon:0KB active_file:0KB inactive_file:0KB unevictable:0KB writepending:
0KB present:15984KB managed:15900KB mlocked:0KB kernel_stack:0KB pagetables:0KB bounce:
0KB free_pcp:0KB local_pcp:0KB free_cma:0KB
[14419.570639] lowmem_reserve[]: 0 3174 7610 7610 7610
[14419.570641] Node 0 DMA32 free:44104KB min:26400KB low:33000KB high:39600KB
active_anon:3193900KB inactive_anon:12KB active_file:0KB inactive_file:0KB unevictable:
0KB writepending:0KB present:3578492KB managed:3250812KB mlocked:0KB kernel_stack:
96KB pagetables:6732KB bounce:0KB free_pcp:0KB local_pcp:0KB free_cma:0KB
[14419.570645] lowmem_reserve[]: 0 0 4435 4435 4435
[14419.570647] Node 0 Normal free:41024KB min:41036KB low:51292KB high:61548KB
active_anon:3369416KB inactive_anon:829840KB active_file:1596KB inactive_file:
2992KB unevictable:5476KB writepending:0KB present:4700160KB managed:4542320KB mlocked:
32KB kernel_stack:9008KB pagetables:50156KB bounce:0KB free_pcp:1924KB local_pcp:
172KB free_cma:0KB
[14419.570652] lowmem_reserve[]: 0 0 0 0 0
[14419.570654] Node 0 DMA: 1*4KB (U) 1*8KB (U) 0*16KB 0*32KB 2*64KB (U) 1*128KB (U)
1*256KB (U) 0*512KB 1*1024KB (U) 1*2048KB (M) 3*4096KB (M) = 15884KB
[14419.570662] Node 0 DMA32: 334*4KB (UME) 202*8KB (UME) 116*16KB (UE) 80*32KB (UME)
92*64KB (UME) 43*128KB (UE) 17*256KB (ME) 7*512KB (E) 3*1024KB (ME) 5*2048KB (UME)
1*4096KB (M) = 44104KB
[14419.570670] Node 0 Normal: 432*4KB (ME) 325*8KB (UME) 337*16KB (ME) 164*32KB (UME)
142*64KB (UME) 69*128KB (UME) 15*256KB (UE) 5*512KB (ME) 2*1024KB (E) 0*2048KB
0*4096KB = 41336KB
[14419.570693] Tasks state (memory values in pages):
[14419.571055] Kernel panic - not syncing: Out of memory: system-wide panic_on_oom
is enabled

从日志来看,cupsd线程分配一个页面(order为0),但失败了,分配掩码是GFP_HIGHUSER_MOVABLE,说明它想分配一个给用户进程使用的物理页面,并且页面迁移类型是可移动的,因此页面分配器首选的内存管理区为ZONE_NORMAL。
该系统一共有3个内存管理区,分别是ZONE_DMA、ZONE_DMA32和ZONE_NORMAL。另外,该系统还有两个虚拟的内存管理区。
cupsd线程触发OOM Killer机制的路径是在用户态访问一个文件的虚拟地址,该虚拟地址通过mmap系统调用映射,但是文件的内容并没有映射到用户空间的虚拟地址,因此触发了缺页异常。在缺页异常中调用filemap_fault()读文件的内容。这时,调用__page_cache_alloc()函数来为内容缓存分配一个物理页面。在分配物理页面过程中,出于内存压力等原因,触发了OOM Killer机制,见__alloc_pages_slowpath()函数。
接下来,分析为什么在3个内存管理区当中都不能成功分配一个页面。
首先,分析ZONE_DMA。空闲页面为15884KB,最低警戒水位为140KB,看起来空闲页面已经远远大于了最低警戒水位,那理应可以成功地从该内存管理区分配出一个页面。但是,Linux内核里有一个对低端内存管理区进行保护的机制,那就是lowmem_reserve[]数组。从日志中可以得知,lowmem_reserve[]的值为“0 3174 7610 7610 7610”。
判断一个内存管理区是否满足这次分配任务的检查函数是__zone_watermark_ok()。
< mm/page_alloc.c>

bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
int classzone_idx, unsigned int alloc_flags, long free_pages)

long min = mark;
...
if (free_pages < = min + z-> lowmem_reserve[classzone_idx])
return false;
...

在__zone_watermark_ok()中,classzone_idx是页面分配器根据分配掩码首选的内存管理区,在本场景中为ZONE_NORMAL。因此,z-> lowmem_reserve[classzone_idx]的值为7610。读者需要注意,这里的单位是页面的数量,而日志中的free和min的单位是KB。
                          140 +  7610 x 4 =  30580
空闲页面(15884KB)远远小于30580 KB ,因此,ZONE_DMA不能满足这次页面分配请求。
同理,计算在ZONE_DMA32中的判断条件。
                    26400 +  4435  x 4 =  44140
ZONE_DMA32的空闲页面为44104KB,并没有比44140KB大,因此ZONE_DMA32同样不能满足这次页面分配请求。
最后我们来分析ZONE_NORMAL,空闲页面(free)为41024KB,最低警戒水位为41036KB,说明已经低于最低警戒水位了。这次分配请求具有普通优先级,不能访问最低警戒水位以下系统保留的内存,因此分配失败,触发了OOM机制。
新书预告
《奔跑吧linux内核》第二版卷1已经上架了。

《奔跑吧linux内核》第二版卷2预计春节后上架!



金色年华,流金岁月,奔二入门篇预计春节后上架!

【内存管理实战案例分析3(为何分配不出一个页面())】

    推荐阅读