chibi-scheme源代码分析之二--内存管理与垃圾回收

时间:2021-01-08 03:54:42

chibi-scheme的内存管理,维护着一些受控堆区.所有的scheme对象都是在受控堆中分配的,垃圾回收也是局限于此受控堆区.

保守的垃圾回收算法会作用于整个堆区,而使性能受影响.而这里只有scheme语言的堆区和sexp对象是垃圾回收的,因此嵌入到C中时宿主程序不会受影响.

第一部分:堆结构

typedef struct sexp_free_list_t *sexp_free_list;
struct sexp_free_list_t {
sexp_uint_t size;
sexp_free_list next;
};

typedef struct sexp_heap_t *sexp_heap;
struct sexp_heap_t {
sexp_uint_t size, max_size;
sexp_free_list free_list;
sexp_heap next;
/* note this must be aligned on a proper heap boundary, */
/* so we can't just use char data[] */
char *data;
};

每个heap通过next字段链接在一起,size记录当前块的空间大小.当空间不够时,堆的增长方式是每一块是前一块的大小的2倍。free_list是一个带头结点的*链。

通过上述结构体,堆的实现已经很清晰了,代码比较简单,不做具体分析。可以看gc.c中的sexp_make_heap()函数。

需要注意的是对齐的存在。对齐会使得后面垃圾回收时的标记清除方便许多,因此使用对齐。

第二部分:垃圾回收

默认使用的垃圾回收是采用标记-清除法。函数sexp_gc会调用sexp_mark和sexp_sweep。由于有些资源需要析构,所以清除之前还调用了一个sexp_finalize()。这个函数调用sexp对象的"析构函数"finalizer以释放资源,比如对于打开的文件描述符。

void sexp_mark (sexp ctx, sexp x)

函数sexp_mark以x作为根,在ctx的堆中进行游走,遇到没有标记过的则进行标记,并递归地进行这一标记过程。

sexp sexp_sweep (sexp ctx, size_t *sum_freed_ptr)

函数sexp_sweep对ctx的堆进行扫描,遇到没有标记的则回收空间.其中释放空间就是将此部分空间放到free_list中,可能涉及合并操作.
垃圾回收时,需要一个根结点作为起点.这个根结点选取的正是上下文对象ctx.所有的对象都封装在了上下文对象中,因此从上下文对象作为根出发才可以访问到所有的有用的对象,避免其被作为垃圾清除了.值得注意的是,上下文对象本身的存储空间也是在堆中分配的,也就是上下文对象中有个堆成员,而上下文对象的自身存储空间也是在堆中分配的.

这看似一个鸡生蛋蛋生鸡的问题,那么第一个上下文对象是怎么来的呢?这个可以查看文件sexp.c中的sexp_bootstrap_context函数.

第三部分:临时变量保护

假设我想使用两个浮点型的临时变量,使用如下代码:

sexp f1 = sexp_make_flonum(ctx,1.0);
sexp f2 = sexp_make_flonum(ctx,2.0);

这样使用会隐含着一个bug:在创建f2时调用链会调用到sexp_alloc,该函数会先调用sexp_try_alloc,如果失败,则调用sexp_gc之后再尝试.由于f1没有加以保护,在gc的过程中它可能被当作垃圾清理了,因此这种时候临时变量必须保护起来.

所谓保护,就是使得上下文对象能够跟踪到它,因此上下文对象中有一个saves链,作用就是保护这种临时变量的.把临时变量的地址放放到saves链中,垃圾回收的mark阶段临时变量就会被加上标记,sweep阶段不会被清除.

    struct {
sexp_heap heap;
struct sexp_gc_var_t *saves;
  ...
} context;

因为是临时变量,使用完之后又要注意在ctx的saves链中清除它,否则会永远占据内存空间不被释放.为此chibi-scheme定义了一组宏,以方便处理:

struct sexp_gc_var_t {
sexp *var;
struct sexp_gc_var_t *next;
};

#define sexp_gc_var(ctx, x, y) \
sexp x = SEXP_VOID; \
struct sexp_gc_var_t y = {NULL, NULL};

#define sexp_gc_preserve(ctx, x, y) \
do { \
sexp_gc_preserve_name(ctx, x, y); \
(y).var = &(x); \
(y).next = sexp_context_saves(ctx); \
sexp_context_saves(ctx) = &(y); \
} while (0)

#define sexp_gc_release(ctx, x, y) (sexp_context_saves(ctx) = y.next)

这段代码写得是相当的精妙啊! sexp_gc_var_t y是放在c函数的栈中的,因此出了函数栈保护就失效了.但正是由于sexp_gc_var声明的目的也是作为临时变量使用,因此这种处理洽到好处.chibi-scheme还定义了sexp_gc_var1到sexp_gc_var6的宏,分别是一次声明1到6个临时变量.

如果想保存永久变量该如何呢?chibi-scheme提供了类似的sexp_preverse_object和sexp_release_object两个宏.实现的机制与上面类似.具体在下一部分讲解.

 第四部分:上下文结构初窥

struct {
sexp_heap heap;
struct sexp_gc_var_t *saves;
#if SEXP_USE_GREEN_THREADS
sexp_sint_t refuel;
unsigned char* ip;
struct timeval tval;
#endif
char tailp, tracep, timeoutp, waitp;
sexp_uint_t pos, depth, last_fp;
sexp bc, lambda, stack, env, fv, parent, child,
globals, dk, params, proc, name, specific, event;
#if SEXP_USE_DL
sexp dl;
#endif
} context;

由于上下文在chibi-scheme中是如此重要的一个结构,这里进行部分分析.更详细的在eval中还会讲到.

上下文是垃圾回收算法的标记的根.上下文还是编译器执行一个s表达式所必须的结构.当一个s表达式要执行时,它需要一个上下文,这个上下文里包含了堆,栈,环境,等等.

可以看上面有heap,stack,env等字段.parent和child字段用于将各个上下文结构联系起来.bc的意思是bytecode,s表达式会被编译成字节码,在虚拟机中执行.

fv的意思是free value*变量,这和闭包等是lisp/scheme语言中的概念.

tailp字段是用于尾调用优化的,scheme语言标准要求必须是严格尾递归的.

saves前面刚讲过,它是为了保护临时变量在gc时不被回收掉.

env环境也是scheme中的一个重要概念,环境中,包含了name到value的绑定,在编译过程中,编译器会通过环境查找变量的真实值.

现在重点看globals.它的真实类型是一个vector对象,大小是SEXP_G_NUM_GLOBALS.存的是一些全局信息.每一个槽位都有各自的作用.

enum sexp_context_globals {
SEXP_G_TYPES,
SEXP_G_NUM_TYPES,
SEXP_G_OOM_ERROR, /* out of memory exception object */
...
SEXP_G_PRESERVATIVES,
SEXP_G_NUM_GLOBALS
};

其中SEXP_G_PRESERVATIVES槽是一个保护链,放置需要保护的非临时变量的,原理与saves链类似.

#define sexp_global(ctx,x)      (sexp_vector_data(sexp_context_globals(ctx))[x])
void sexp_preserve_object(sexp ctx, sexp x) {
sexp_global(ctx, SEXP_G_PRESERVATIVES) = sexp_cons(ctx, x, sexp_global(ctx, SEXP_G_PRESERVATIVES));
}

还记得上一篇的类型对象表,我们当时为了便于理解叫table,这个表正是存于globals的SEXP_G_TYPES槽的,即sexp_global(ctx, SEXP_G_TYPES). 

总之,上下文是一个非常复杂的结构,chibi-scheme使用这个结构来对s表达式进行求值.

 小结

首先给出了用于内存管理的堆的结构,这个比较简单,代码没有展开讲.熟悉C语言的写过内存池或自己的内存管理系统的很容易就能理解.

垃圾回收使用的标记清除算法,原理网上有很多,知道原理了代码很容易就能看懂.值得注意的是标记的根是使用的上下文对象,这是一个很巧妙的设计.

不加保护地使用临时变量可能遇到的陷阱.要使用API中提供的sexp_gc_var1至n,sexp_gc_preverse加以保护,使用完毕之后注意用sexp_gc_release释放.

上下文在整个chibi-scheme中的地位非常重要,这里进行了一些说明.后面具体讲eval时还会提到.看似与本篇的题目无关,但无论是作为垃圾回收的根,还是globals的保存链都有所涉及.并且给出中下文对象的结构也是为后面几篇做个铺垫.

 这些都是基础,只有对类型系统和内存管理游刃有余之后,读后面的代码才不至于不停的回头查看前面的函数定义.