使用kprobe追踪内核内存分配失败现象

今天同事偶然问我一个问题,说程序中有一处malloc了很大的内存块,结果失败了,虚地址空间应该足够大呀,为什么会失败呢?

模糊印象中确实可能会失败,貌似跟overcommit设置有关,具体原因记不清了,于是追查了一把,简单记录下。

首先简单写段malloc很大空间的程序,然后使用strace确认是哪里返回的错误,程序如下:

#include <stdlib.h>
#include <unistd.h>
#include <assert.h>

int main(int argc, char **argv)
{
        long long size;
        char *a = NULL;

        size = 1024L * 1024L * 1024L *14L;
        
        a = malloc(size);

        assert(a);
}

在机器上编译运行

#gcc -o test_malloc test_malloc.c
#./test_malloc
test_malloc: test_malloc.c:14: main: Assertion `a' failed.
Aborted (core dumped)
使用strace查看确认是mmap系统调用返回了-ENOMEM:

#strace ./test_malloc
...
mmap(NULL, 15032389632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
...
OK,那么这里我们确认是内核mmap系统调用返回的错误,于是简单查看了下源码,找到了几处可能返回-ENOMEM的地方,可以通过kretprobe来捕捉相关函数的返回值,判断具体是哪个相关的函数返回的-ENOMEM

对于由上层malloc内存分配发起的mmap的系统调用,内核主要会依次进入do_mmap_pgoff,mmap_region这两个函数,对于这种private匿名页的分配请求,主要就是进行各种检查和分配vma的工作。大体上检查工作中可能返回-ENOMEM的条件罗列如下:

1. do_mmap_pgoff函数中的PAGE_ALIGN(len)的检查,即检查mmap请求的长度是否是整页对齐的,这个glibc提交mmap系统调用的时候已经对齐过了,可以通过上面strace的结果看到,所以肯定不是这里。

2. do_mmap_pgoff函数中mm->map_count > sysctl_max_map_count的条件,这个判断在我们系统环境中是肯定不会超过的,所以不是这里。

3. do_mmap_pgoff函数中通过get_unmapped_area获得未映射的一段虚拟空间的起始地址,里面会检查请求长度len是否大于TASK_SIZE以及获得的起始虚拟地址addr+mmap请求长度是否大于TASK_SIZE,由于是64位系统,且该程序本身运行起来没有其它大块虚拟地址的使用请求,所以这两个条件也理应是不会满足的。

4. mmap_region中may_expand_vm函数对address space limit限制的检查即当前使用+新申请的vm空间是否大于rlimit(RLIMIT_AS),查看了ulimit的设置该项是unlimited,继续排除。

5. mmap_region中一段memory accounting的代码:

       if (accountable_mapping(file, vm_flags)) {
                charged = len >> PAGE_SHIFT;
                if (security_vm_enough_memory_mm(mm, charged))
                        return -ENOMEM;
                vm_flags |= VM_ACCOUNT;
        }

如果我们是基于匿名页的映射请求且mmap包含了PROT_WRITE,则条件accountable_mapping判断是满足的,所以会调用security_vm_enough_memory_mm,里面最终调用的是mmap.c里的__vm_enough_memory函数,这个函数我不贴了,主要逻辑是针对系统对sysctl vm.overcommit_memory的不同设置判断是否允许这次mmap虚地址空间的申请,好吧这里非常值得怀疑了,我们写段kprobe代码验证下,代码如下:

文件mmap_test.c

#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/swap.h>
#include <linux/module.h>
#include <linux/kprobes.h>

static int vm_enough_memory_ret(struct kretprobe_instance *ri,
                                                                struct pt_regs *regs)
{
        int retval = regs_return_value(regs);
        
        if (strstr(current->comm, "test_malloc") && retval == -ENOMEM) {
                printk("[kprobe]__vm_enough_memory check failed, not enough memory!!\n");
        }

        return 0;
}

int vm_enough_memory_entry(struct mm_struct *mm, long pages,
                                                   int cap_sys_admin)
{
        unsigned long free;

        if (strstr(current->comm, "test_malloc")) {
                free = global_page_state(NR_FREE_PAGES);
                free += global_page_state(NR_FILE_PAGES);
                free -= global_page_state(NR_SHMEM);

                free += global_page_state(NR_SLAB_RECLAIMABLE);

                printk("[kprobe]vm_enough_memory free pages = %lu, request pages = %ld\n",
                           free, pages);
        }
        
        jprobe_return();

        return 0;
}

static struct kretprobe mmap_test_kretprobe = {
        .handler = vm_enough_memory_ret,
        .entry_handler = NULL,
        .data_size = 0,
        .maxactive = 100,
        .kp = {
                .symbol_name = "__vm_enough_memory",
  },
};

static struct jprobe mmap_test_jprobe = {
        .entry = vm_enough_memory_entry,
        .kp = {
                .symbol_name = "__vm_enough_memory",
        },
};

static int __init mmap_test_init(void)
{
        int ret;

        ret = register_kretprobe(&mmap_test_kretprobe);

        if (ret < 0) {
                printk(KERN_INFO "register_kretprobe failed, returned %d\n",
                                ret);

                return -1;
        }

        ret = register_jprobe(&mmap_test_jprobe);

        if (ret < 0) {
                printk(KERN_INFO "register_jprobe failed, returned %d\n",
                                ret);

                return -1;
        }
        
        return 0;
}

static void __exit mmap_test_exit(void)
{
          unregister_kretprobe(&mmap_test_kretprobe);
        unregister_jprobe(&mmap_test_jprobe);
}

module_init(mmap_test_init)
module_exit(mmap_test_exit)
MODULE_LICENSE("GPL");
Makefile:


TARGET = mmap_test
obj-m := $(TARGET).o
KERNELDIR=/lib/modules/`uname -r`/build
PWD=`pwd`

default :
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

install :
        insmod $(TARGET).ko
uninstall :
        rmmod $(TARGET).ko

clean :
        rm -rf *.o *.mod.c *.ko
编译运行 make & make install
运行./test_malloc
查看dmesg,结果如下:

..
[kprobe]vm_enough_memory free pages = 512843, request pages = 3670017
[kprobe]__vm_enough_memory check failed, not enough memory!!
...
确认问题确实出在这里,我们通过kretprobe在__vm_enough_memory中加入了一个函数返回的探针,并判断是否返回了-ENOMEM结果证实确实由这个函数返回的错误。

另外代码中通过加入一个jprobe探针,证实了__vm_enough_memory返回-ENOMEM的原因是因为请求的内存页超过了空闲的内存页。这段代码就是摘抄的__vm_enough_memory内的一段判断代码,如果pages > free则返回-ENOMEM。

结论:

内核在mmap系统调用中,会根据sysctl vm.overcommit_memory的取值在__vm_enough_memory中进行判断,该取值有3个,定义于mman.h中:

#define OVERCOMMIT_GUESS 0 默认值
#define OVERCOMMIT_ALWAYS 1
#define OVERCOMMIT_NEVER 2

设置为1时,该函数直接返回0,不做任何判断,直接通过

设置为0时(默认值), 系统会判断当前剩余可用内存+swap空间是否可以满足mmap申请的大小,如果不满足就会返回-ENOMEM

设置为2时,则会判断当前物理内存 * 系统sysctl_overcommit_ratio 的比率值 + swap空间是否满足mmap申请的大小,不满足返回-ENOMEM。

所以我们最终通过jprobe打印出了空闲内存和mmap申请内存的大小,可以看到确实是远远超过了空闲内存,所以这里返回了-ENOMEM。

其实mmap内核虽然只是创建地址空间,但是也是会考虑如果后续touch了这些空间,导致的物理内存页分配会不会产生物理内存不足的情况。也就是提前做的判断。

本来可以用systemtap更方便点,不过我的内核版本比较新,配合systemtap还有各种问题,所以这里直接用了kprobe。

http://www.douban.com/note/359581786/


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值