位域 内存 字节序_malloc 的实现原理 内存池 mmap sbrk 链表

参考:《深入理解计算机系统》 本文需要比较多的前言基础,在前面的文章中都有体现

malloc实现原理 这位大佬写得也很不错,部分内容参考这里。

要看这个malloc,得先了解静态内存分配和动态内存分配。下面是这两者的区别:(静态内存分配在书中一直没找到。。)

内存的静态分配和动态分配的区别主要是两个:

一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。

二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现

对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

---------------------

作者:liuchao1986105

来源:CSDN

原文:https://blog.csdn.net/liuchao1986105/article/details/6724392

版权声明:本文为博主原创文章,转载请附上博文链接!

总结一下就是,动态内存分配可以在程序运行的时候动态地获取和使用内存,然后是动态内存分配的区间不同。

动态内存分配器更方便,也有更好的移植性。它维护者一个进程的虚拟内存区域,称为堆(heap)。它在未初始化数据的上边,然后向上生长(即向更高的地址),对于每个进程来说,内核维护着一个brk(break)变量,它指向堆的顶部,如下图:

736f62cb68002dcfe534662409abd539.png

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行。(忽然很庆幸自己学习了一波多级页表,所以对里面这个模型比较容易理解 可参考 何柄融:linux 多级页表 cpu取虚拟地址 TLB 特别是里面的 chunk )

然后动态的分配器有两种基本风格:显式和隐式。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已经分配的块。

显式分配器:要求应用显式地释放任何已经分配的块。例如c标准库中的malloc. c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符和c中搞得malloc和free相当。(就是自己手动释放内存

隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如java,ML,Lisp之类的高级语言就依赖垃圾收集来释放已分配的块。(就是不用程序猿自己手动释放内存

例如:图形处理密集的应用程序就经常使用标准分配器来要求获得一大块的虚拟内存,然后使用与应用相关的分配器来管理内存,在该快中创建和销毁图形的节点。(总不能自己手动释放这些内存吧,那多难受啊)

然后我们先讨论显式内存分配器:

c标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从堆中分配块。

void *malloc(size_t size)  ;
返回:若成功则为已分配块的指针,若出错则为null.

malloc函数返回一个指针,指向大小至少为size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。(这个会根据系统位数返回相应规格的地址)

如果malloc遇到问题(例如,程序要求的内存块比可用的虚拟内存还要大),那么它就返回null.并设置errno。malloc不初始化它返回的内存。

下面很大一部分参考自文首链接:malloc实现原理

然后来深入了解malloc:

先了解:brk()和sbrk()函数

int brk( const void *addr )
void* sbrk ( intptr_t incr );

这两个函数的作用主要是扩展heap的上界brk。第一个函数的参数为设置的新的brk上界地址,如果成功返回0,失败返回-1。第二个函数的参数为需要申请的内存的大小,然后返回heap新的上界brk地址。如果sbrk的参数为0,则返回的为原来的brk地址。

然后来了解:mmap

mmap函数第一种用法是映射磁盘文件到内存中(前面讲进程通信的时候讲过);而malloc使用的mmap函数的第二种用法,即匿名映射,匿名映射不映射磁盘文件,而是向映射区申请一块内存

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

这里不重复啰嗦了,对mmap不熟悉的同学可以参考何柄融:linux 进程通信方式 pipe无名管道 fifo有名管道 共享内存映射 socket 消息队列 或者文章最前面的连接(感觉我写得比较好,哈哈)

这里要注意的是fd参数,fd为映射的文件描述符,如果是匿名映射,可以设为-1

当申请小内存的时,malloc使用sbrk分配内存;当申请大内存时,使用mmap函数申请内存;但是这只是分配了虚拟内存,还没有映射到物理内存,当访问申请的内存时,才会因为缺页异常,内核分配物理内存

然后接着深入:

由于brk/sbrk/mmap属于系统调用,如果每次申请内存,都调用这三个函数中的一个,那么每次都要产生系统调用开销(即cpu从用户态切换到内核态的上下文切换,这里要保存用户态数据,等会还要切换回用户态),这是非常影响性能的;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果低地址的内存没有被释放,高地址的内存就不能被回收。

鉴于此,malloc采用的是内存池的实现方式,malloc内存池实现方式更类似于STL分配器和memcached的内存池,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块即可

由于文首的链接里没图,所以从 glibc内存管理那些事儿 这个链接里盗来几张图,而且这个链接讲的内存池也很不错。

先看内存池的整体结构:(感觉下面的都很核心!)

92a1554c42d4d370eb06cca8503478ed.png
(第一眼看上去有没有很像哈希表?哈哈。)

malloc将内存分成了大小不同的chunk,然后通过bins来组织起来。malloc将相似大小的chunk(图中可以看出同一链表上的chunk大小差不多)用双向链表链接起来,这样一个链表被称为一个bin。malloc一共维护了128个bin,并使用一个数组来存储这些bin。数组中第一个为unsorted bin,数组从2开始编号,前64个bin为small bins,同一个small bin中的chunk具有相同的大小,两个相邻的small bin中的chunk大小相差8bytes。small bins后面的bin被称作large bins。large bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小序排列。large bin的每个bin相差64字节。(这里我本来想计算一下加深理解的,8x64=512b, 64x64=4096b 然后加起来发现和图片里的不太匹配,所以不知道自己哪里理解错了?可是前面的88又对啊,怎么后面就12k那么大呢?)

malloc除了有unsorted bin,small bin,large bin三个bin之外,还有一个fast bin。一般的情况是,程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,malloc 中在分配过程中引入了 fast bins,不大于 max_fast(默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins中,fast bins 中的 chunk 并不改变它的使用标志 P。这样也就无法将它们合并,当需要给用户分配的 chunk 小于或等于 max_fast 时,malloc 首先会在 fast bins 中查找相应的空闲块,然后才会去查找 bins 中的空闲 chunk。在某个特定的时候,malloc 会遍历 fast bins 中的 chunk,将相邻的空闲 chunk 进行合并,并将合并后的 chunk 加入 unsorted bin 中,然后再将 usorted bin 里的 chunk 加入 bins 中

unsorted bin 的队列使用 bins 数组的第一个,如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中,在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则malloc 会先在 unsorted bin 中查找合适的空闲 chunk,然后才查找 bins。如果 unsorted bin 不能满足分配要求。 malloc便会将 unsorted bin 中的 chunk 加入 bins 中。然后再从 bins 中继续进行查找和分配过程。从这个过程可以看出来,unsorted bin 可以看做是 bins 的一个缓冲区,增加它只是为了加快分配的速度。(其实感觉在这里还利用了局部性原理,常用的内存块大小差不多,从unsorted bin这里取就行了,这个和TLB之类的都是异曲同工之妙啊!)

除了上述四种bins之外,malloc还有三种内存区。

  1. 当fast bin和bins都不能满足内存需求时,malloc会设法在top chunk中分配一块内存给用户;top chunk为在mmap区域分配一块较大的空闲内存模拟sub-heap。(比较大的时候)
  2. 当chunk足够大,fast bin和bins都不能满足要求,甚至top chunk都不能满足时,malloc会从mmap来直接使用内存映射来将页映射到进程空间,这样的chunk释放时,直接解除映射,归还给操作系统。(极限大的时候)
  3. Last remainder是另外一种特殊的chunk,就像top chunk和mmaped chunk一样,不会在任何bins中找到这种chunk。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chunk。(这个应该是fast bins中也找不到合适的时候,用于极限小的)

malloc内存分配(下面算是正常一般的情况了)

一开始时,brk和start_brk是相等的,这时实际heap大小为0;如果第一次用户请求的内存大小小于mmap分配阈值,则malloc会申请(chunk_size+128kb) align 4kb大小的空间作为初始的heap。初始化heap之后,第二次申请的内存如果还是小于mmap分配阈值时,malloc会先查找fast bins,如果不能找到匹配的chunk,则查找small bins。若还是不行,合并fast bins,把chunk 加入到unsorted bin,在unsorted bin中查找,若还是不行,把unsorted bin中的chunk全加入large bins中,并查找large bins。在fast bins和small bins中查找都需要精确匹配,而在large bins中查找时,则遵循"smalest-first,best-fit"的原则,不需要精确匹配。

若以上都失败了,malloc则会考虑使用top chunk。若top chunk也不能满足分配,且所需的chunk大小大于mmap分配阈值,则使用mmap进行分配。否则增加heap,增加top chunk,以满足分配要求。

现在来不上chunk的结构图:

malloc利用chunk结构来管理内存块,malloc就是由不同大小的chunk链表组成的。一个使用中的chunk的结构如下图:

8d62cffe27043e847a66df177242b3ec.png

malloc会给用户分配的空间的前后加上一些控制信息,用这样的方法来记录分配的信息,以便完成分配和释放工作。chunk指针指向chunk开始的地方,图中的mem指针才是真正返回给用户的内存指针。

  1. chunk 的第二个域的最低一位为 P,它表示前一个块是否在使用中,P 为 0 则表示前一个 chunk 为空闲,这时chunk的第一个域 prev_size 才有效,prev_size 表示前一个 chunk 的 size,程序可以使用这个值来找到前一个 chunk 的开始地址。当 P 为 1 时,表示前一个 chunk 正在使用中,prev_size程序也就不可以得到前一个 chunk 的大小。不能对前一个 chunk 进行任何操作malloc分配的第一个块总是将 P 设为 1,以防止程序引用到不存在的区域。(这里就很细!)
  2. Chunk 的第二个域的倒数第二个位为 M,他表示当前 chunk 是从哪个内存区域获得的虚拟内存M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的
  3. Chunk 的第二个域倒数第三个位为 A,表示该 chunk 属于主分配区或者非主分配区,如果属于非主分配区,将该位置为 1,否则置为 0。

右边图是空闲的chunk:

当chunk空闲时,其M状态是不存在的,只有AP状态,原本是用户数据区的地方存储了四个指针,指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunk,malloc通过这两个指针将大小相近的chunk连成一个双向链表。在large bin中的空闲chunk,还有两个指针,fd_nextsize和bk_nextsize,用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的。(这里就很符合网上大多数人说的链表理论了)

以上内容大部分参考阿里华庭写的Glibc内存管理,Ptmalloc2源代码分析。

感觉从这个人这里学到了很多东西,是个大佬。

然后结合网上的一般回答:

链接:https://www.nowcoder.com/questionTerminal/5aae63b290c542f0ab0582d293e6c791
来源:牛客网

malloc可以分别由伙伴系统或基于链表的实现;

1、它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表;

2、 调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。

3、 调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

虽然这个答案比较粗糙,但是也有大概的思想了。

malloc和free的实现原理 这位大哥讲得也不错,可以参考一下。

最后可以结合 腾讯云技术社区:十问 Linux 虚拟内存管理 (glibc) (一) 腾讯的这个实践来理论联系实践。下面参考于这个腾讯的链接(建议可以去点个赞):

malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free 进行释放,否则导致内存泄露。

关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是:

  1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。
  2. 若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。
  3. 这里讨论的是简单情况,如果涉及并发可能会复杂一些,不过先不讨论。

其中 sbrk 就是修改栈顶指针位置,而 mmap 可用于生成文件的映射以及匿名页面的内存,这里指的是匿名页面。而这个 128k ,是 glibc 的默认配置,可通过函数 mallopt 来设置

接着: VSZ为虚拟内存 RSS为物理内存

  1. VSZ 并不是每次 malloc 后都增长,是与上一节说的堆顶没发生变化有关,因为可重用堆顶内剩余的空间,这样的 malloc 是很轻量快速的。
  2. 但如果 VSZ 发生变化,基本与分配内存量相当,因为 VSZ 是计算虚拟地址空间总大小。
  3. RSS 的增量很少,是因为 malloc 分配的内存并不就马上分配实际存储空间,只有第一次使用,如第一次 memset 后才会分配。
  4. 由于每个物理内存页面大小是 4k ,不管 memset 其中的 1k 还是 5k 、 7k ,实际占用物理内存总是 4k 的倍数。所以 RSS 的增量总是 4k 的倍数
  5. 因此,不是 malloc 后就马上占用实际内存,而是第一次使用时发现虚存对应的物理页面未分配,产生缺页中断,才真正分配物理页面,同时更新进程页面的映射关系。这也是 Linux 虚拟内存管理的核心概念之一

相信到这里大家应该对malloc有比较深入的理解了。还是慢慢啃书先吧。。。

嗯,有点晚了,睡了

欢迎交流讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值