malloc何时返回失败

关键字:内存分配机制

在 [[linux是怎样工作的-内存管理]]中怀疑过malloc返回值的意义,感觉它从来不会失败。下面来分析一下应用层的内存分配机制。

应用层机制

从系统层面看,进程分配内存有两种方式,brk和mmap(暂不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存
这两种方式分配的都是虚拟内存,没有分配物理内存在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

一般程序分配内存使用的标准c库提供的malloc/free。c库本身使用上面的两种方式向内核申请一块内存,然后自己管理。
默认情况下,当malloc分配的内存小于128KB, 使用brk,分配的内存大于128KB时使用mmap

情况1 - 使用brk

因为小内存分配才调用brk, brk只是推数据段的指针,而数据段的虚拟地址限制是很长的(多长以后再来填坑)。所以brk调用不会失败,即malloc不会失败。

系统调用的实现函数:SYSCALL_DEFINE1(brk, unsigned long, brk)

情况2 - 使用mmap

mmap的内核实现很复杂,梳理出检查内存的大致路线为:

1
do_mmap --> mmap_region --> security_vm_enough_memory_mm --> __vm_enough_memory

__vm_enough_memory只是检查”剩余可用内存”,而mmap次数限制,地址空间限制在前面的流程就检查了。

那么函数__vm_enough_memory何时返回ENOMEM呢?

内核机制

应用层分配的虚拟内存大于系统的物理内存这种情况叫overcommit。
overcommit有三种策略,可通过/proc/sys/vm/overcommit_memory调整,默认值为0

1
2
3
#define OVERCOMMIT_GUESS		0   //估测可用内存,限制overcommit
#define OVERCOMMIT_ALWAYS 1 //不限制,随意overcommit
#define OVERCOMMIT_NEVER 2 //不允许overcommit

当配置为策略为OVERCOMMIT_NEVER时,如下两个参数起作用:

  • overcommit_kbytes:当不允许overcommit时,设置vm允许申请值的上限
  • overcommit_ratio:当不允许overcommit时,设置vm允许申请的百分比,默认50%

查看内存分配够不够的关键函数为__vm_enough_memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#define OVERCOMMIT_GUESS		0
#define OVERCOMMIT_ALWAYS 1
#define OVERCOMMIT_NEVER 2

/*
* Check that a process has enough memory to allocate a new virtual
* mapping. 0 means there is enough memory for the allocation to
* succeed and -ENOMEM implies there is not.
*
* cap_sys_admin is 1 if the process has admin privileges, 0 otherwise.
*/
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{
long free, allowed, reserve;
//增加vm_committed_as计数,这个全局变量统计系统当前vm申请量
//这个值也就是/proc/meminfo里Committed_AS的值
//因为最开始就增加了,因此本次申请数量也包含了
vm_acct_memory(pages);

//完全不限制虚拟内存的分配,随意overcommit,因此总是能成功
if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
return 0;
//根据一定规则限制vm的overcommit,这也是系统默认行为
//这时就要计算下当前系统free的内存了
if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
//1、NR_FREE_PAGES是系统完全free的内存,也就是free命令查到的free项
free = global_page_state(NR_FREE_PAGES);
//2、NR_FILE_PAGES是page cache使用的页面,这些页面是可以释放的,
//因此也要计入free中,但是要扣除共享内存
free += global_page_state(NR_FILE_PAGES);

//3、NR_SHMEM是共享内存,这些不能计入free中
free -= global_page_state(NR_SHMEM);
//4、获取swap的free页数
free += get_nr_swap_pages();

//5、slab里可回收的肯定是要记入free中啦
free += global_page_state(NR_SLAB_RECLAIMABLE);

//6、考虑系统运行的基本需求,也要占用一部分内存,因此free肯定不能小于该值
// totalreserve_pages大致等于high water
if (free <= totalreserve_pages)
goto error;
else
free -= totalreserve_pages;

//7、根据admin_reserve_kbytes的设置
//留一部分内存给root用户保证紧急情况下能登录系统,并恢复系统
//比如需要启动sshd/login, bash, and top/kill
if (!cap_sys_admin)
free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);
//到最后了,free大于要分配的内存,这就是真能分配了
if (free > pages)
return 0;

goto error;
}
//这里就是完全不允许overcommit的情况了
//allowed用于统计系统vm上限,这个是就是/proc/meminfo里CommitLimit的值
//计算公式:CommitLimit = (Physical RAM * vm.overcommit_ratio / 100) + Swap
allowed = vm_commit_limit();
//同上,留一部分内存给root用户
if (!cap_sys_admin)
allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

//保证单进程不要使用完所有vm空间,至少保证自己能恢复
//和admin_reserve_kbytes类似,也要给自己留点退路,不然只能让root用户来恢复系统了
if (mm) {
//给普通用户保留的空间为min(当前进程vm的32分之一,将近3%,user_reserve_kbytes)
reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
allowed -= min_t(long, mm->total_vm / 32, reserve);
}
//vm_committed_as保存当前系统中已申请(包含本次)的vm数量
//如果已分配数量小于系统允许分配上限,那就是此次内存申请ok
if (percpu_counter_read_positive(&vm_committed_as) < allowed)
return 0;
error:
vm_unacct_memory(pages);
//内存不足
return -ENOMEM;
}

/*
* Committed memory limit enforced when OVERCOMMIT_NEVER policy is used
*/
unsigned long vm_commit_limit(void)
{
unsigned long allowed;
//如果设置了overcommit_kbytes参数,那么commit就不能超过该值
if (sysctl_overcommit_kbytes)
allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10);
else
//如果没设置overcommit_kbytes参数,将读取overcommit_ratio参数的值
//既然是百分比,那么就需要有基数(总内存页面减去大页使用的内存)
allowed = ((totalram_pages - hugetlb_total_pages())
* sysctl_overcommit_ratio / 100);
//同样别忘了还有swap页面数量
allowed += total_swap_pages;

return allowed;
}

malloc失败发生过吗

在嵌入式编程里面,很少遇到malloc失败的情况。因为内存不足时,还有缓存回收机制。
当程序的代码段都需要释放掉,用时才从flash上读出来时,系统此时已经很卡了。

所以大多数情况下检查malloc返回值都没有意义。
真正的大内存(至少大于128KB)分配检查才是有意义的:比如升级时。

但上面的描述仅限于linux,可能随着内核得更新实现也有区别。所以编写可移植,长久可靠的还是检查一下比较好。

参考

vm内核参数之虚拟内存申请overcommit
malloc原理和内存碎片
知其然知其所以然,/PROC/MEMINFO之谜
[[meminfo详解]]


malloc何时返回失败
https://leon0625.github.io/2023/12/12/9f5df2999f1d/
作者
leon.liu
发布于
2023年12月12日
许可协议