今天同事偶然问我一个问题,说程序中有一处malloc了很大的内存块,结果失败了,虚地址空间应该足够大呀,为什么会失败呢?
模糊印象中确实可能会失败,貌似跟overcommit设置有关,具体原因记不清了,于是追查了一把,简单记录下。
在机器上编译运行
对于由上层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的代码:
如果我们是基于匿名页的映射请求且mmap包含了PROT_WRITE,则条件accountable_mapping判断是满足的,所以会调用security_vm_enough_memory_mm,里面最终调用的是mmap.c里的__vm_enough_memory函数,这个函数我不贴了,主要逻辑是针对系统对sysctl vm.overcommit_memory的不同设置判断是否允许这次mmap虚地址空间的申请,好吧这里非常值得怀疑了,我们写段kprobe代码验证下,代码如下:
运行./test_malloc
查看dmesg,结果如下:
另外代码中通过加入一个jprobe探针,证实了__vm_enough_memory返回-ENOMEM的原因是因为请求的内存页超过了空闲的内存页。这段代码就是摘抄的__vm_enough_memory内的一段判断代码,如果pages > free则返回-ENOMEM。
模糊印象中确实可能会失败,貌似跟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。