先看这张图,32位系统,寻址空间是4g,在linux下0-3G是用户模式,3-4G是内核模式。
在用户模式中有这么几个段(注意内存地址从上到下是,高地址到低地址0xc0000000-0x08048000)
(1)代码段:储存程序的二进制映像
(2)数据段:存储已经初始化的全局变量和局部静态变量
(3)bss段:存储未初始化的全局变量和局部静态变量
(4)Heap:堆空间,注意节点位置有一个标记brk(),从低地址向高地址扩展
(5)内存映射:Memory Mapping segment
(6)stack:栈空间,从高地址向低地址扩展
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。
进程所面对的虚拟内存地址空间,只有按页映射到物理内存上,才能真正的使用,由于物理存储容量的限制,整个堆虚拟内存空间不可能全部映射到实际的内存物理。
获取到break的指针,也就是内存申请的初始指针了。
系统调用sbrk(位移量)能移动brk指针的位置,同时返回brk指针的位置,来达到申请内存的目的。
系统调用brk(void* addr),可以直接把brk设置为某个地址,成功返回0,不成功返回1。rlimit限制进程堆内存容量的指针
从操作系统的角度来说,分配内存有俩种方式,一个是推进brk指针,增加堆的有效区域来申请内存空间,另一种采用mmap,在进程的虚拟地址空间中(堆和栈中间,被称为文件映射区域的地方)找一块空闲的虚拟内存。这俩种都是分配虚拟内存,只有当第一次访问虚拟地址空间时,操作系统才会给分配物理内存。
malloc是采用brk的方式动态分配内存的。
malloc函数实质是将可用的内存块连接成一个列表,即空闲链表。当调用malloc函数时,寻找一个可以大到满足用户请求所需要的内存块,然后把该内存块一分为二,一部分用来分配给用户(申请大小),另一部分(如果有的话)
返回到空闲链表上,如果找不到,就会调用sbrk()推进brk指针来申请内存空间。
搜索空闲块的算法有:
(1)首次适配:第一次找到足够大的,就分配。会产生很多的内存碎片
(2)下一次适配:第二次找到的时候,分配,这样产生的比较少。
(3)最佳适配:彻底搜索,遍历所有块,找到差值最小的块分配。
调用free函数,将用户释放的内存块连接到空闲链表上,不进行合并的话, 即使是相邻的内存块还是相当于俩个内存块,形成假碎片,释放内存后,需要将进行内存整理,相邻的小空闲块合并成较大的空闲块。
内存块的大致结构:meta区+数据区,其中meta区用来记录数据块的元信息(数据区大小,空闲标志位,指针),数据区是真实分配的内存区域,数据区的第一个字节地址就是malloc返回的地址。
隐式链表和双向链表:
其中一种是隐式链表,实际上是数组,malloc分配空间必然有一个数据结构,允许它来区分边界,区分已分配和空间的空间,数据结构中包含一个头部信息和有效载荷,有效载荷的首地址就是malloc返回的地址,可能在尾部还有填充,为了保持内存对齐。头部相当于该数据结构的元数据,其中包含了块大小和是否是空闲空间的信息,这样可以根据头地址和块大小的地址推出下一个内存块的地址,这就是隐式链表。
————————————————
还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式。在之前的有效载荷中加入了之前前驱和后驱的指针,也可以称为双向链表。维护空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。
如果内存不足,函数未能成功分配储存空间,会返回一个NULL指针,测试返回值是否是NULL。