面试官:动态内存分配时需要注意哪些坑?我懵懵懵了
ID:技术让梦想更伟大
作者:李肖遥
在使用C语言开发嵌入式产品的过程中,当使用到malloc函数时候都会有一个争议,“使用动态内存分配安全吗?”,就连美国军方在safety-critical的嵌入式航空电子设备代码中,也禁止动态内存分配,我们来细细分析下。
C 库函数 - malloc()
函数简介
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void* 类型返回分配的内存区域地址。
当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
函数的声明
用来分配所需的内存空间,并返回一个指向它的指针。
//参数 :size -- 内存块的大小,以字节为单位
//返回值:指针 -- 指向已分配大小的内存
// NULL -- 如果请求失败
void *malloc(size_t size)
介绍一下用法
#include
#include
#include
int main()
{
char *str;
str = (char *) malloc(15);
strcpy(str, "hello world");
printf("String = %s, Address = %u\n", str, str);
free(str);
return(0);
}
编译结果如下,大家自行体会malloc的用法。
malloc的实现机制
当我们了解了malloc的作用应用范围以及用法之后,我们先看看它是怎么实现内存分配的,在此我们需要先了解几个概念。
虚拟内存地址与物理内存地址
为了简单便捷,现代操作系统在汇编程序(或机器语言)层面,处理内存地址时,都是使用虚拟内存地址。这样每个进程可以自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位OS下,每个进程的虚拟地址空间为2*64 Byte
。
虚拟地址的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,由于在机器语言层面都是采用虚拟地址,操作系统会将虚拟内存和实际的物理内存进行映射,CPU芯片上叫做存储器管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,才能实现对真实内存数据的操作。
页与地址构成
在现代操作系统是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称。
内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
内存地址构成
上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4K,所以页内都是用低12位表示,而剩下的高地址表示页号。
MMU映射单位并不是字节,而是页,这个映射通过查一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化。下面给出一个经过简化的内存地址翻译示意图。
运行时堆
在已经映射的内存空间结尾有一个break指针,这个指针下面是映射好的内存,可以访问,上面则是未映射的访问,不能访问。可以通过系统调用sbrk(位移量)确定brk指针的位置,同时返回brk指针的位置,达到申请内存的目。brk(void *addr)
系统调用可以直接将brk设置为某个地址,成功返回0,不成功返回-1。而rlimit则是限制进程堆内存容量的指针。
malloc内存分配原理
malloc采用推进brk指针来增加堆的有效区域来申请内存空间分配内存,维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。
为什么避免使用
这其实要从malloc和free的设计上考虑,通常,它们是基于列表分配器算法将内存池组织到单个链表中的连续位置,使用分配器来管理该链表,实际上就是寻找空闲位置。
但是在极端的safety-critical
系统中,malloc常常极其不可预测,在多核系统上进行多线程开发时是个难题,具体有以下几点。
内存有限,多次申请不易管理
嵌入式的内存就只有几十K到几百K,程序在运行时向系统申请内存使用,在使用完毕后,需要显式的释放,不然后果很严重,在多次申请复杂的逻辑开发时,这就要求程序员对动态分配的内存很了解
碎片
在c语言中的malloc进行的动态内存分配和嵌入式系统中使用到堆区的内存分配会产生内存碎片,例如
char *p;
if(p=char* malloc(0)==NULL){
printf("NULL\n");
}
else{
printf("NOT NULL");
}
实际上最终出现的并不是NULL
,而是NOT NULL
这就说明了进行动态内存分配的时候产生了内存碎片
内部碎片的产生
所有的内存分配必须起始于可被 4、或8 或16 整除的地址,或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。
假设在请求一个17字节的内存块时,它可能会获得20字节、24字节等稍大一点的字节,因此由所需的大小需要四舍五入,而产生的多余空间就叫内部碎片。
外部碎片的产生
频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。
内存泄漏
分配出去的内存在使用之后没有释放掉,没有回收,长此以往,会造成没有足够的内存可以分配。一般表现为运行时间越长,占用的内存越多,最终导致系统奔溃。
所以在进行硬件内存比较小的外围开发的时候,一定要避免内存泄漏,合理的使用内存空间,才能更好的发挥硬件的作用。
怎么解决
在继续使用malloc和free的情况下
正确使用malloc函数分配内存
在实际应用中,我们可以试着把连续的大块内存按分区来管理。每个分区中包含整数个大小相同的内存块。如图所示:
利用这种机制,就可以得到和释放固定大小的内存块。这样内存的申请和释放函数的执行时间就是确定的了,但是特定的内存块在释放时,必须重新回到它原本属于的内存分区。
正确使用free函数释放内存
free函数其实就做了一件事:斩断指针变量和这块内存的对应关系,在使用free(p)
函数内存释放后,指针变量p本身保存的地址并没有改变,那我们必须需重新把p的值变为NULL:即p = NULL
自定义一套内存分配器
尽量避免使用malloc时,我们可以自定义一套本地线程内存分配器,基于栈的分配器,以及基于本地线程的分配器,通过为每个线程分配特定的内存池来避免冲突
最后
在嵌入式系统中,并不是说不使用malloc()和free()管理内存,而是说在使用时需要让我们的代码更具预测性,避免不必要的未知bug产生。
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑 关注微信公众号『技术让梦想更伟大』,后台回复“m”查看更多内容,回复“加群”加入技术交流群。 长按前往图中包含的公众号关注