ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

内存页不足导致程序启动失败:page allocation failure

2021-12-06 17:03:02  阅读:312  来源: 互联网

标签:链表 ip allocation failure 内存 entry proc page 页表


现象

之前一直稳定运行了很久的内核ko模块突然功能失灵,通过dmesg命令查看内核信息,发现该模块提示内存页分配失败,如下图所示

当时看到 "Failed to allocate memory for ip_entry" 字样,第一反应就是内存不足,直接用命令free -h命令查看系统内存

从图中看到空闲的内存有890M,按道理,空闲内存应该是够用的,ip_entry这个数据结构怎么也不至于用掉890M以上的内存。于是再看堆栈信息,看到一个关键信息:page allocation failure,这个信息表示系统无法分配高阶内存(所谓的高阶内存,指的是大块的连续物理内存,内存分配原理可查看本文下面的“内存分配算法”),使用命令查看内存页的分配情况:cat /proc/buddyinfo

可以看到内存的碎片化情况很严重,存在大量的低阶内存页,但缺少64KB以上的高阶内存页(红框表示64KB以上的内存页数量都为0)

分析ip_entry

既然系统缺少64KB以上的内存页,那么是否说明ip_entry这个数据结构要大于64KB呢,于是写程序用sizeof函数来测试这个数据结构,因为这个数据而机构用到了内核的函数,所以要和系统的源码一起编译成ko文件,不能直接在用户态调用sizeof函数。

  • 编写Hello.c
#include <linux/rcupdate.h>
#include <linux/rbtree.h>
#include <linux/init.h>
#include <linux/module.h>
#include <asm/thread_info.h>
#include <linux/sched.h>

struct interval_tree_node {
    struct rb_node rb;
    unsigned long start;    
    unsigned long last;
    unsigned long __subtree_last;
};

struct ip_entry {
    struct rcu_head    rhead;
    struct ip_entry *next;
    struct ip_entry **pprev;
    struct interval_tree_node node;
    int type;
    __be32 saddr;
    __be32 mask;
    ktime_t timestamp;
    u64 nr_hits[NR_CPUS];
};

static int test_init(void)
{
    printk("---Insmod---");
    return 0;
}

static void test_exit(void)
{    
    struct ip_entry e;
    int c;
    printk("sizeof int: %d\n", sizeof(c));
    printk("sizeof ip_entry: %d\n", sizeof(e));
    printk("---Rmmod---");
}

module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
  • 编写Makefile
CONFIG_MODULE_SIG=n
obj-m:=Hello.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)

default:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
  • 编译:执行make命令(注意,在ubuntu20系统上能编译成功,但是在往内核插入模块时会提示错误:insmod: ERROR: could not insert module Hello.ko: Invalid module format,所以只能用ubuntu16来编译)
  • 插入内核模块:执行insmod Hello.ko,即可看到输出的内容(卸载内核模块的命令为:rmmod Hello

从上图可以看到,在64位的系统上,int的大小为4Byte,ip_entry的大小为65640Byte,折合为64.1KB,而在本系统中,刚好没有了大于等于64KB的连续内存页,所以导致了内存页分配失败。

解决方法

释放内存

  • 释放页缓存:echo 1 > /proc/sys/vm/drop_caches
  • 释放目录和索引节点缓存:echo 2 > /proc/sys/vm/drop_caches
  • 同时释放页、目录、索引节点缓存:echo 3 > /proc/sys/vm/drop_caches

    上述的操作是无害的,因为只会释放完全没有使用的内存对象,脏对象将继续被使用直到他们被写入磁盘中,所以内存中的脏对象并不会被释放。如果如果重复echo 3 > /proc/sys/vm/drop_caches不能再次释放缓存,可以先尝试echo 0 > /proc/sys/vm/drop_caches然后再执行echo 3 > /proc/sys/vm/drop_caches

内存压缩

当上面释放的内存也没有足够的高阶内存时,可以通过命令:echo 1 > /proc/sys/vm/compact_memory 进行内存压缩,但这个步骤比较消耗CPU

可以看到经过内存压缩后,释放了大量的高阶内存

Linux内存

伙伴系统

Linux系统使用了一个名为伙伴系统(buddy system)的内存分配算法,将所有的空闲页表(一个页表的大小为4K)分别链接到包含了11个元素的数组中,数组中的每个元素将大小相同的连续页表组成一个链表,页表的数量为:1,2,4,8,16,32,64,128,256,512,1024,所一次性可以分配的最大连续内存为1024个连续的4k页表,即4MB的内存。假设你想申请一个包括256个页表的内存,系统会首先查找数组中的第9个链表(即大小为256的链表),如果该链表为空,就继续查找大小为512的链表,如果找到了,就将512个页表划分为两个256,一个分配给进程,另一个就挂载到大小为256的链表上。如果大小为512的链表也是空,就会继续查找大小为1024的链表,仍然为空就返回一个错误。当一个页表被释放之后,相邻的两个页表就会合并成一个大的页框。

分配算法

当申请分配页的时候,如果无法从伙伴系统的空闲链表中获得页面,则进入慢速内存分配路径,率先使用低水位线尝试分配,若失败,则说明内存稍有不足,页分配器会唤醒 kswapd 线程异步回收页,然后再尝试使用最低水位线分配页。如果分配失败,说明剩余内存严重不足,会先执行异步的内存规整,若异步规整后仍无法分配页面,则执行直接内存回收,或回收的页面数量仍不满足需求,则进行直接内存规整,若直接内存回收一个页面都未收到,则调用 oom killer 回收内存。

内存碎片

  • 内部碎片:假设一个进程需要3KB的物理内存,但是内存页的最小颗粒度是4KB,所以就有1KB的空闲内存无法利用
  • 外部碎片:假设系统剩下的页表都不连续,此时系统就无法分配超过4KB的连续物理内存,从而导致内存溢出

参考文档

标签:链表,ip,allocation,failure,内存,entry,proc,page,页表
来源: https://www.cnblogs.com/kylinlin/p/15637910.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有