良许Linux教程网 干货合集 动态内存分配时需要注意哪些坑?

动态内存分配时需要注意哪些坑?

在开发嵌入式产品时,当使用C语言来开发时,经常会遇到一个争议,即”使用动态内存分配是否安全?”。甚至美国军方在安全关键的嵌入式航空电子设备代码中都禁止使用动态内存分配。现在我们来详细分析一下这个问题。

C 库函数 – malloc()

函数简介

malloc函数的全称是memory allocation,中文叫动态内存分配,其作用是申请一块指定大小的连续内存块,并以void*类型返回分配的内存地址。

在无法预先知道内存具体位置的情况下,如果想要绑定真正的内存空间,就需要使用动态内存分配,且分配的大小要满足程序的要求。

函数的声明

malloc函数的声明如下:

void* malloc(size_t size);

该函数用于动态分配所需的内存空间,并返回一个指向该内存块的指针。

//参数  :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的用法。

image-20231007195245751
image-20231007195245751

malloc的实现机制

当我们了解了malloc的作用应用范围以及用法之后,我们先看看它是怎么实现内存分配的,在此我们需要先了解几个概念。

虚拟内存地址与物理内存地址

为了简单便捷,现代操作系统在汇编程序(或机器语言)层面,处理内存地址时,都是使用虚拟内存地址。这样每个进程可以自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位OS下,每个进程的虚拟地址空间为2*64 Byte

image-20231007195248496
image-20231007195248496

虚拟地址的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,由于在机器语言层面都是采用虚拟地址,操作系统会将虚拟内存和实际的物理内存进行映射,CPU芯片上叫做存储器管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,才能实现对真实内存数据的操作。

页与地址构成

在现代操作系统是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称。

内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:

image-20231007195251207
image-20231007195251207

内存地址构成

上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4K,所以页内都是用低12位表示,而剩下的高地址表示页号。

MMU映射单位并不是字节,而是页,这个映射通过查一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化。下面给出一个经过简化的内存地址翻译示意图。

image-20231007195253817
image-20231007195253817

运行时堆

在已经映射的内存空间结尾有一个break指针,这个指针下面是映射好的内存,可以访问,上面则是未映射的访问,不能访问。可以通过系统调用sbrk(位移量)确定brk指针的位置,同时返回brk指针的位置,达到申请内存的目。brk(void *addr)系统调用可以直接将brk设置为某个地址,成功返回0,不成功返回-1。而rlimit则是限制进程堆内存容量的指针。

image-20231007195256625
image-20231007195256625

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函数分配内存

在实际应用中,我们可以试着把连续的大块内存按分区来管理。每个分区中包含整数个大小相同的内存块。如图所示:

image-20231007195300418
image-20231007195300418

利用这种机制,就可以得到和释放固定大小的内存块。这样内存的申请和释放函数的执行时间就是确定的了,但是特定的内存块在释放时,必须重新回到它原本属于的内存分区。

正确使用free函数释放内存

free函数其实就做了一件事:斩断指针变量和这块内存的对应关系,在使用free(p) 函数内存释放后,指针变量p本身保存的地址并没有改变,那我们必须需重新把p的值变为NULL:即p = NULL

自定义一套内存分配器

尽量避免使用malloc时,我们可以自定义一套本地线程内存分配器,基于栈的分配器,以及基于本地线程的分配器,通过为每个线程分配特定的内存池来避免冲突

最后

在嵌入式系统中,并不是说不使用malloc()和free()管理内存,而是说在使用时需要让我们的代码更具预测性,避免不必要的未知bug产生。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部