标头描述内存块
typedef struct header {
unsigned int size;
struct header *next;
} header_t;
动态分配的内存位于所谓的堆中,这是堆栈和BSS(未初始化的数据段—所有默认值为零的全局变量)之间的一段内存。堆从与BSS相邻的低地址开始,并在程序断点处结束,程序断点位于BSS和堆栈之间的某个地方。
程序间断点就是程序数据段的结尾。(程序间断点是为初始化数据段的起始位置)
试图访问堆栈和断点之间的任何内存都会导致访问冲突(除非您访问的内存在堆栈可以扩展的范围内,但这是一个完全不同的话题)。
为了从内核中获得更多的内存,我们只需延长断点,从而允许我们访问更多的内存。为此,我们调用Unix的sbrk系统调用,它通过参数扩展break,并在成功时返回前一个break的地址,从而为程序提供更多的内存。失败时,sbrk返回被强制转换为空指针的-1,这是一个没有人喜欢的糟糕约定。
一个可执行程序在存储时(没有加载到内存运行),至少拥有三个部分,分别是代码段(text)、数据段(data)、和BSS 段。当应用程序运行时(运行态),此时需要另外两个域:堆和栈。正在运行的程序:代码段 + 数据段 + BSS 段 + 堆 + 栈。
static header_t base; /* 零大小的块开始. */
static header_t *freep = &base; /* 指向第一个空闲内存块. ?*/
static header_t *usedp; /*指向第一个使用的内存块. */
/*
* 扫描空闲列表并寻找放置块的地方。基本上,我们要查找的是即将被释放的块可能已经被分区的任何块。
*/
static void
add_to_free_list(header_t *bp)
{
header_t *p;
//查找bp是否在freep范围内
//for循环判断条件:p < bp < p->next 在中间
//if条件:bp < p->next < p < bp 在两边(freep是循环的?)
for (p = freep; !(bp > p && bp < p->next); p = p->next)
if (p >= p->next && (bp > p || bp < p->next))
break;
//满足两个条件中其一...
//如果bp和p->next相邻(bp < p->next):
if (bp + bp->size == p->next) {
//两者的头信息合并(覆盖)
bp->size += p->next->size;
bp->next = p->next->next;
} else
//不相邻就赋next
bp->next = p->next;
//如果p和bp相邻(p < bp):
if (p + p->size == bp) {
//合并
p->size += bp->size;
p->next = bp->next;
} else
p->next = bp;
//freep可能是循环的
freep = p;
}
sbrk()参数函数中:当increment为正值时,间断点位置向后移动increment字节。同时返回移动之前的位置,相当于分配内存。当increment为负值时,位置向前移动increment字节,相当与于释放内存,其返回值没有实际意义。当increment为0时,不移动位置只返回当前位置。参数increment的符号决定了是分配还是回收内存。
#define MIN_ALLOC_SIZE 4096 /* We allocate blocks in page sized chunks. */
/*
* Request more memory from the kernel.
*/
static header_t *
morecore(size_t num_units)
{
void *vp;
header_t *up;
if (num_units > MIN_ALLOC_SIZE)
num_units = MIN_ALLOC_SIZE / sizeof(header_t);
//先除再乘sizeof(header_t)是为了将num_units化为MIN_ALLOC_SIZE的整数倍
//vp是新申请的内存的头指针
if ((vp = sbrk(num_units * sizeof(header_t))) == (void *) -1)
return NULL;
//强转成header_t,存储头信息
up = (header_t *) vp;
up->size = num_units;
//将新申请的内存加入freep
add_to_free_list (up);
return freep;
}
现在我们有了两个辅助函数,编写malloc函数就非常简单了。我们只需扫描空闲列表,并使用至少与我们要查找的块一样大的第一个块。因为我们使用我们找到的第一个块,而不是试图找到一个“更好”的块,这个算法被称为首次拟合(First Fit)。
快速说明一下:header结构体中的size字段是以header大小的块为单位测量的,而不是字节。
/*
* Find a chunk from the free list and put it in the used list.
*/
void *
GC_malloc(size_t alloc_size)
{
size_t num_units;
header_t *p, *prevp;
//这里 alloc_size + sizeof(header_t) 是整个内存块结构(字节)
//之所以-1是为了让被除数不能被sizeof(header_t)整除,从而+1,获取向上取整的内存块以header_t大小为单位的大小
num_units = (alloc_size + sizeof(header_t) - 1) / sizeof(header_t) + 1;
prevp = freep;
for (p = prevp->next;; prevp = p, p = p->next) {
if (p->size >= num_units) { /* Big enough. */
if (p->size == num_units) /* Exact size. */
prevp->next = p->next;
else {
//空闲的内存块尺寸太大了
p->size -= num_units;//内存块:小|剩下的|分配的|大
p += p->size;//p移动到分配的内存的头部
p->size = num_units;//设置大小
}
//preve保存着剩下的内存块的头信息
freep = prevp;
/* Add to p to the used list. */
if (usedp == NULL)
usedp = p->next = p;
else {
//插入usedp链
p->next = usedp->next;
usedp->next = p;
}
//p+1是为了跳过sizeof(header_t)个字节,直接获取有效内存的地址
return (void *) (p + 1);
}
if (p == freep) { /* Not enough memory. */
p = morecore(num_units);//向内核申请更多的内存
if (p == NULL) /* Request for more memory failed. */
return NULL;
}
//到这里还没有分配,但是已经调用morecore()且成功获取内存了,所以循环,看看申请来的内存是否够分配
}
}
遍历一个区间的内存,然后
//解标记,这里解标记是把低两位置0,但是标记位只有最后一位 ?
#define UNTAG(p) (((unsigned int) (p)) & 0xfffffffc)
/*
* 扫描内存区域,并在已使用列表中适当地标记任何项目。
* 两个参数都应该对齐。
*/
static void
scan_region(unsigned int *sp, unsigned int *end)
{
header_t *bp;
for (; sp < end; sp++) {
unsigned int v = *sp;
bp = usedp;
do {
//v在bp的有效内存地址内
if (bp + 1 <= v &&
bp + 1 + bp->size > v) {
//将 bp->next 的第一位置1,进行标记
bp->next = ((unsigned int) bp->next) | 1;
break;
}
//循环直到再次遇见usedp,所以可能usedp也是循环的
} while ((bp = UNTAG(bp->next)) != usedp);
}
}
现在我们可以扫描内存区域了,但是我们应该浏览哪些内存区域呢?有几个相关区域:
- BSS(未初始化的数据)和初始化的数据段:它们包含程序中的所有全局变量和静态变量。因此,它们可以引用堆中的某些内容。
- 使用的块:当然,如果用户分配一个指向另一个已分配块的指针,我们不希望释放指向的块。
- 堆栈:由于堆栈包含所有的局部变量,因此可以说这是最重要的地方。
/*
* Scan the marked blocks for references to other unmarked blocks.
*/
static void
scan_heap(void)
{
unsigned int *vp;
header_t *bp, *up;
for (bp = UNTAG(usedp->next); bp != usedp; bp = UNTAG(bp->next)) {
if (!((unsigned int)bp->next & 1))
//是否有标记,没有则continue
continue;
for (vp = (unsigned int *)(bp + 1);
vp < (bp + bp->size + 1);
vp++) {
unsigned int v = *vp;
up = UNTAG(bp->next);
do {
if (up != bp &&
up + 1 <= v &&
up + 1 + up->size > v) {
//v在up的有效内存地址内
up->next = ((unsigned int) up->next) | 1;
break;
}
} while ((up = UNTAG(up->next)) != bp);
}
}
}
与堆不同,BSS 和初始化的数据段以及堆栈都是 内存中可能包含堆中的地址。因为每个都是连续的,所以为了扫描它们,我们需要知道每个最小有效和最大有效内存地址。
初始化数据段和BSS相邻
大多数现代 Unix 链接器导出两个符号,可供用户程序访问,这些符号是 我们感兴趣的是:
- etext:etext的地址是文本段之后的最后一个地址。初始化的数据 段紧跟在文本段之后,因此 etext 的地址是初始化的数据段。
- end:end 的地址是堆的开头,或者是 BSS 末尾之后的最后一个地址。
由于 BSS 和初始化段之间没有段,因此我们不必处理它们 作为单独的实体,可以通过从 &etext 迭代到 & end 来扫描它们。
直接使用extern char end, etext;
即可获取到
堆栈顶部可用内联汇编取出esp
寄存器值来获取。堆栈底部我们将利用Linux将堆栈底部放在proc目录中进程条目的文件中的字符串中的事实
/*
* Find the absolute bottom of the stack and set stuff up.
*/
void
GC_init(void)
{
static int initted;
FILE *statfp;
if (initted) return;
initted = 1;
//通过文件获取堆栈底部
statfp = fopen("/proc/self/stat", "r");
assert(statfp != NULL);
fscanf(statfp,
"%*d %*s %*c %*d %*d %*d %*d %*d %*u "
"%*lu %*lu %*lu %*lu %*lu %*lu %*ld %*ld "
"%*ld %*ld %*ld %*ld %*llu %*lu %*ld "
"%*lu %*lu %*lu %lu", &stack_bottom);//取第28个数据,所以前27个忽略("%*lu")
fclose(statfp);
usedp = NULL;
base.next = freep = &base;
base.size = 0;
}
/*
* 标记已使用的内存块并释放未使用的内存块
*/
void
GC_collect(void)
{
header_t *p, *prevp, *tp;
unsigned long stack_top;
extern char end, etext; /* 链接器提供. */
if (usedp == NULL)
return;
/* 扫描BSS和初始化的数据段. */
scan_region(&etext, &end);
/* 扫描栈. */
asm volatile ("movl %%ebp, %0" : "=r" (stack_top));
scan_region(stack_top, stack_bottom);
/* 标记堆. */
scan_heap();
/* And now we collect! */
for (prevp = usedp, p = UNTAG(usedp->next);; prevp = p, p = UNTAG(p->next)) {
next_chunk:
if (!((unsigned int)p->next & 1)) {
/*
* 这部分还没有标记。因此,它必须被释放
*/
tp = p;
p = UNTAG(p->next);
add_to_free_list(tp);
if (usedp == tp) {
usedp = NULL;
break;
}
//先做((unsigned int) prevp->next & 1,取出prevp->next的标志位,然后加到p
prevp->next = (unsigned int)p | ((unsigned int) prevp->next & 1);
goto next_chunk;
}
//将p->next的标志位清零
p->next = ((unsigned int) p->next) & ~1;
if (p == usedp)
break;
}
}