heap堆分配在用户层面:malloc函数用于heap内存分配

void* malloc(size_t size);            

进程的虚拟内存地址布局:

对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:

  • Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
  • Data:这里存放的是初始化过的全局变量
  • BSS:这里存放的是未初始化的全局变量
  • Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
  • Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
  • Stack:这是栈区域,自高地址向低地址增长

heap内存从低地址向高地址生长:malloc函数主要是用于虚拟内存线性地址的分配

另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)

进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux维护一个break指针,这个指针指向堆空间的某个地址(线性地址空间)。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错,即是经典的segmentation fault。

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk()和mmap()(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存(不准确,系统调用会执行内核函数,分配内存),在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

这两种进程分配内存方式的区别:

1、对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放(虚拟和物理内存都释放),这个过程中间不会产生内存碎块等问题。

2、针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次进行内存申请时,glibc会将堆顶向上增长来扩展内存空间,也就是我们所说的堆地址向上增长。在对这些小块内存进行操作时,便会产生内存碎块的问题。实际上brk和sbrk系统调用,就是调整heap顶地址指针(break指针)。

(注意这里所说的内存碎片还是根据物理内存所说的)

由brk分配的heap堆内存是什么时候释放呢?

当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。

下面我要讲一个内存空洞的问题:

一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?

很遗憾,不能。

这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?

根源:这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。

A和D之间的B已经通过free(B),但是此时C的物理内存和线性内存都没有被释放,只是被标记为已经释放的空间,但是break指针没有移动,edata==break?没有回溯。在大多数malloc实现中,free函数释放的内存并不直接归还操作系统(也就是释放物理内存),而是挂接到freelist数组中。  B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢

当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求(与之前B的大小相同),那么malloc很可能就把B这块内存返回回去了。 

所以如果下次有新的虚拟内存地址分配:首先会查看freelist数组中有没有用过的但是被free的合适空间,如果有,就返还这个线性地址空间。如果没有就从break指针位置开始分配

综上:虚拟线性地址空间也有可能产生碎片(这里所说的碎片就是由于free的内存的虚拟空间没有释放,导致下次分配虚拟空间时候,不能被使用),线性空间和物理内存是一起释放的

内存碎片和内存空洞都是一个意思

问题:既然堆内内存brk和sbrk不能直接释放,为什么不全部使用 mmap 来分配,munmap直接释放呢? 
        既 然堆内碎片不能直接释放,导致疑似“内存泄露”问题,为什么 malloc 不全部使用 mmap 来实现呢(mmap分配的内存可以会通过 munmap 进行 free ,实现真正释放)?而是仅仅对于大于 128k 的大块内存才使用 mmap ?

其实,进程向 OS 申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,当munmap 后再次分配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU消耗较大。另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大。同时堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。 因此, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128k) 才使用 mmap 获得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>) 来修改这个临界值。

linux heap堆分配的更多相关文章

  1. 栈 堆 stack heap 堆内存 栈内存 内存分配中的堆和栈 掌握堆内存的权柄就是返回的指针 栈是面向线程的而堆是面向进程的。 new/delete and malloc/ free 指针与内存模型

    小结: 1.栈内存 为什么快? Due to this nature, the process of storing and retrieving data from the stack is ver ...

  2. linux下堆溢出unlink的一个简单例子及利用

    最近认真学习了下linux下堆的管理及堆溢出利用,做下笔记:作者作为初学者,如果有什么写的不对的地方而您又碰巧看到,欢迎指正. 本文用到的例子下载链接https://github.com/ctfs/w ...

  3. Heap堆的理解以及在IAR中如何设置堆的大小

    文章首发于浩瀚先森博客 堆栈的概念在脑海里已经存在有一段时间了,今天就测试来整理下Heap堆.栈以后再说. 堆区不像全局变量和局部变量总是有指定的内存大小,它是为了在程序运行时动态分配内存而设定的一块 ...

  4. Linux C 堆内存管理函数malloc()、calloc()、realloc()、free()详解

    C 编程中,经常需要操作的内存可分为下面几个类别: 堆栈区(stack):由编译器自动分配与释放,存放函数的参数值,局部变量,临时变量等等,它们获取的方式都是由编译器自动执行的 堆区(heap):一般 ...

  5. java - Stack栈和Heap堆的区别

    首先分清楚Stack,Heap的中文翻译:Stack—栈,Heap—堆.         在中文里,Stack可以翻译为“堆栈”,所以我直接查找了计算机术语里面堆和栈开头的词语:        堆存储 ...

  6. Heap(堆)和stack(栈)有的区别是什么。

    java的内存分为两类,一类是栈内存,一类是堆内存.栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个 ...

  7. JAVA Stack栈和Heap堆的区别(转)

          首先分清楚Stack,Heap的中文翻译:Stack—栈,Heap—堆.         在中文里,Stack可以翻译为“堆栈”,所以我直接查找了计算机术语里面堆和栈开头的词语:      ...

  8. 禁止或强制使用堆分配---《C++必知必会》 条款34

    有时候,指明一些特定类的对象不应该被分配到堆(heap)上是个好主意.通常这是为了确保该对象的析构函数一定会得到调用.维护对象本身(body object)的引用计数的句柄对象(handle obje ...

  9. Stack栈 Heap堆

    Stack(栈) 栈(stack) 又名堆栈,它是一种运算受限的线性表.其限制是仅允许在表的一端进行插入和删除运算.这一端被称为栈顶,相对地,把另一端称为栈底.向一个栈插入新元素又称作进栈.入栈或压栈 ...

随机推荐

  1. 利用DNS进行传输数据

    曾经有这样一道题目,困了我数个小时,最后我尝试利用此方法时我知道真相的时候,眼泪已掉下来. 遇到的是一个流量分析题,分析DNS数据,拿到flag 流量如图所示: 进入linux,提取: [root@s ...

  2. 2016.3.7 Word2007编号设置

    1.点击下图红圈出的下拉箭头 2.点击下图新建样式按钮 3.在弹出窗口中,设置名称AAA(方便稍后的查找修改),样式类型改为列表,点击编号 4.在弹出的窗口中以此设置各级标题的编号样式,保存后设置成功 ...

  3. 玩school 学习sql server 查询的网站

    http://www.w3school.com.cn/sql/sql_like.asp

  4. UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 85

    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 85;import sys reload(sys) sys.s ...

  5. redis学习一 大体概述

    redis 命令查找:http://doc.redisfans.com/ 1,redis 技术简介以及疑问      redis是一个开源的,内存存储的数据结构服务器.可以用做数据库,高速缓存和消息队 ...

  6. C#改变LInqToSQL的引用地址,读取config的数据库字符串

    C#改变LInqToSQL的引用地址,读取config的数据库字符串修改Properties 下 Settings.Settings 下 Settings.Designer.cs 下 return ( ...

  7. OpenGL 着色器 03

    着色器(shader)是运行在GPU上小程序. 也是一种非常独立的程序,它们之间不能相互通信:它们之间唯一的沟通只有通过输入和输出. 着色器的开头总是要声明版本,接着是输入和输出变量,uniform和 ...

  8. 向vsftp服务器上传文件报“550 Permission denied”错误的解决办法

    上传文件: ftp> mput db.iso 550 Permission denied 原因:vsftp默认配置不允许上传文件. 解决:修改/etc/vsftpd.conf 将“write_e ...

  9. FileZilla Server下通过别名设置虚拟目录

    说明:FileZilla Server 的虚拟目录设置与其它 FTP 服务器软件有所不同.在 FileZilla Server 中设置虚拟目录,必须采用 FTP 根目录 + 虚拟目录名的形式来进行.比 ...

  10. SQL获取时间戳流水号

    流水号生成规则: 1:流水号总长度为22位数 2:流水号总共分三部分:标头(2位)+ 时间戳(YYYYMMDDHHmmSSsss共17位)+ 随机码(3位) 举例流水号:SN2015081210240 ...