本打算尽快完善一下mlua的gc,按照我学习的思路,如果对一块东西底层的原理或者他的来龙去脉都不清楚的话,是很难透彻理解相关东西的。对于gc我们就要搞清楚程序的内存是如何被分配,又是如何被释放的,对应的底层又是怎么实现的。说实话,在这之前我对这些都是很模糊的,动态分配内存用C++只会new delete,用C语言只会malloc和free,根本没有关心他们底层的原理和实现,趁这个机会好好的学习一下内存分配和管理方面的知识。
先从C语言的malloc说起。大多数的程序员可能都用过这个函数,但是他的底层原理和实现,相信研究的人不多。malloc函数是用来在堆上分配内存的,关于程序的内存布局可以看如下的图片:
malloc分配的内存在图中的Heap区域,Heap区域有一个指针来标识堆顶的位置,叫program break brk。分配或释放内存的时候,实际上是改变了该指针的指向。malloc底层是调用brk/sbrk系统调用来实现的,也就是说brk/sbrk改变了堆顶指针的指向。brk函数是改变了堆顶指针绝对的指向,而sbrk改变的是相对指向(相对堆顶的偏移)。free函数也是通过brk/sbrk函数回退堆顶指针的。
注意,brk、sbrk只是分配了进程的虚拟地址,他还没有映射物理地址的。linux采取延迟映射的策略,也就是真正操作(set/get)该虚拟地址时才映射物理地址,这样能最大程度的节省内存的分配。而且映射物理内存的时候也是有讲究的,他绝不会分配一点,然后映射一点,而是将堆顶所在的页面全部被映射到物理内存上。所谓页面,是操作系统为了方便内存管理而设立的一个单位,一般在32机器上对应的大小是4Kb,即4096字节。最终用到的虚拟地址中的每一个字节都会对应在某一个物理页面上。
所以,调用brk/sbrk函数时,也是分配的整个虚拟地址页。就是说sbrk(1)和sbrk(100)都会分配一个页面的虚拟地址,只有分配的大小大于页面的大小时才会翻页。例如sbrk(4097),一个页面不够,则另起一个页面来分配,当然他们的地址是相邻的。注意,堆顶的地址还是函数调用时的参数或偏移后的地址。这样有个有趣的现象,堆顶之上(堆地址上向上增长的)的地址仍然可以被操作,也就是在分配内存的页面内(即使在堆顶之外,不包括堆顶这个字节)set/get数据都是合法的。
那么问题来了,分配内存之后,再调用brk/sbrk将堆顶退回,之前的数据会怎么样。如果在当前页面退回指针是不会释放内存的,只有当堆顶指针退回至前一页时,原来那一页内存将会被释放,对数据的set/get都将出错。
可以通过如下代码的实验来验证我上面的结论:
/* sbrk and brk example */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
void *curr_brk, *tmp_brk = NULL;
printf("Welcome to sbrk example:%d\n", getpid());
/* sbrk(0) gives current program break location */
tmp_brk = curr_brk = sbrk(0);
printf("Program Break Location1:%p\n", curr_brk);
getchar();
sbrk(4097); //将分配两个页面大小的内存,将堆顶k指针后移4097字节
curr_brk = sbrk(0);
char* p1 = curr_brk;
*(p1 + 4094) = 123; //指向了第二个页面的最后一个字节,然后是可以赋值的,再往后一个字节就错误了
return 0;
}
通过命令cat /proc/pid/maps可以查看进程内存的布局。
说malloc调用brk/sbrk来分配内存,不完全正确。当分配大于128k的内存时,将会调用另一个系统调用mmap来分配内存。关于mmap函数具体的就不再多说,这里只探讨大致的原理。
经测试,在我的64位centos机器上,第一次malloc一个小的内存会使堆顶向高处偏移0x2100个字节,多次测试的结果一样。连续两次malloc小的内存块,返回给进程的内存地址不是连续的,首先它会按照一定的规则对齐,所以可能有padding。其次就算正好是对齐的,每次调用malloc时也分配内存给其头结点,头结点之后的地址才是返回给用户的。经多次测试头结点的大小应该为8字节。malloc分配内存的可能示意图如下:
调用free释放小的内存块时,也不会让堆顶指针回退,所以虽然内存块被释放了,但是该区域还是可以被set/get的,只是get的数据不再是之前的数据,可能是0了。
有了上面的分析,我们可以初步写出我们自己的malloc和free函数了。这个我们下篇再说。
参考: