1. 程式人生 > >雲風的 BLOG: Lua GC 的原始碼剖析 (1)

雲風的 BLOG: Lua GC 的原始碼剖析 (1)

/* ** Union of all Lua values */ typedef union { GCObject *gc; void *p; lua_Number n; int b; } Value; /* ** Tagged Values */ #define TValuefields Value value; int tt typedef struct lua_TValue { TValuefields; } TValue;

我們可以看到,Value 以 union 方式定義。如果是需要被 GC 管理的物件,就以 GCObject 指標形式儲存,否則直接存值。在程式碼的其它部分,並不直接使用 Value 型別,而是 TValue 型別。它比 Value 多了一個型別標識。用 int tt 記錄。通常的系統中,每個 TValue 長為 12 位元組。btw, 在

The implementation of Lua 5.0 中作者討論了,在 32 位系統下,為何不用某種 trick 把 type 壓縮到前 8 位元組內。

所有的 GCObject 都有一個相同的資料頭,叫作 CommonHeader ,在 lobject.h 裡 43 行 以巨集形式定義出來的。使用巨集是源於使用上的某種便利。C 語言不支援結構的繼承。

#define CommonHeader    GCObject *next; lu_byte tt; lu_byte marked

從這裡我們可以看到:所有的 GCObject 都用一個單向連結串列串了起來。每個物件都以 tt 來識別其型別。marked 域用於標記清除的工作。

標記清除演算法是一種簡單的 GC 演算法。每次 GC 過程,先以若干根節點開始,逐個把直接以及間接和它們相關的節點都做上標記。對於 Lua ,這個過程很容易實現。因為所有 GObject 都在同一個連結串列上,當標記完成後,遍歷這個連結串列,把未被標記的節點一一刪除即可。

Lua 在實際實現時,其實不只用一條連結串列維繫所有 GCObject 。這是因為 string 型別有其特殊性。所有的 string 放在一張大的 hash 表中。它需要保證系統中不會有值相同的 string 被建立兩份。顧 string 是被單獨管理的,而不串在 GCObject 的連結串列中。

回頭來看看 lua_State

這個型別。這是寫 C 和 Lua 互動時用的最多的資料型別。顧名思義,它表示了 lua vm 的某種狀態。從實現上來說,更接近 lua 的一個 thread 以及其間包含的相關資料(堆疊、環境等等)。事實上,一個 lua_State 也是一個型別為 thread 的 GCObject 。見其定義於 lstate.h 97 行。

/* ** `per thread' state */ struct lua_State { CommonHeader; lu_byte status; StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ global_State *l_G; CallInfo *ci; /* call info for current function */ const Instruction *savedpc; /* `savedpc' of current function */ StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ CallInfo *end_ci; /* points after end of ci array*/ CallInfo *base_ci; /* array of CallInfo's */ int stacksize; int size_ci; /* size of array `base_ci' */ unsigned short nCcalls; /* number of nested C calls */ unsigned short baseCcalls; /* nested C calls when resuming coroutine */ lu_byte hookmask; lu_byte allowhook; int basehookcount; int hookcount; lua_Hook hook; TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ GCObject *openupval; /* list of open upvalues in this stack */ GCObject *gclist; struct lua_longjmp *errorJmp; /* current error recover point */ ptrdiff_t errfunc; /* current error handling function (stack index) */ };

一個完整的 lua 虛擬機器在執行時,可有多個 lua_State ,即多個 thread 。它們會共享一些資料。這些資料放在 global_State *l_G 域中。其中自然也包括所有 GCobject 的連結串列。

所有的 string 則以 stringtable 結構儲存在 stringtable strt 域。string 的值型別為 TString ,它和其它 GCObject 一樣,擁有 CommonHeader 。但需要注意,CommonHeader 中的 next 域卻和其它型別的單向連結串列意義不同。它被掛接在 stringtable 這個 hash 表中。

除 string 外的 GCObject 連結串列頭在 rootgc ( lstate.h 75 行)域中。初始化時,這個域被初始化為主執行緒。見 lstate.c 170 行,lua_newstate 函式中:

g->rootgc = obj2gco(L);

每當一個新的 GCobject 被創建出來,都會被掛接到這個連結串列上,掛接函式有兩個,在 lgc.c 687 行的

void luaC_link (lua_State *L, GCObject *o, lu_byte tt) { global_State *g = G(L); o->gch.next = g->rootgc; g->rootgc = o; o->gch.marked = luaC_white(g); o->gch.tt = tt; } void luaC_linkupval (lua_State *L, UpVal *uv) { global_State *g = G(L); GCObject *o = obj2gco(uv); o->gch.next = g->rootgc; /* link upvalue into `rootgc' list */ g->rootgc = o; if (isgray(o)) { if (g->gcstate == GCSpropagate) { gray2black(o); /* closed upvalues need barrier */ luaC_barrier(L, uv, uv->v); } else { /* sweep phase: sweep it (turning it into white) */ makewhite(g, o); lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); } } }

upvalue 在 C 中型別為 UpVal ,也是一個 GCObject 。但這裡被特殊處理。為什麼會這樣?因為 Lua 的 GC 可以分步掃描。別的型別被新建立時,都可以直接作為一個白色節點(新節點)掛接在整個系統中。但 upvalue 卻是對已有的物件的間接引用,不是新資料。一旦 GC 在 mark 的過程中( gc 狀態為 GCSpropagate ),則需增加屏障 luaC_barrier 。對於這個問題,會在以後詳細展開。

lua 還有另一種資料型別建立時的掛接過程也被特殊處理。那就是 userdata 。見 lstring.c 的 95 行:

Udata *luaS_newudata (lua_State *L, size_t s, Table *e) { Udata *u; if (s > MAX_SIZET - sizeof(Udata)) luaM_toobig(L); u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata))); u->uv.marked = luaC_white(G(L)); /* is not finalized */ u->uv.tt = LUA_TUSERDATA; u->uv.len = s; u->uv.metatable = NULL; u->uv.env = e; /* chain it on udata list (after main thread) */ u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u); return u; }

這裡並沒有呼叫 luaC_link 來掛接新的 Udata 物件,而是直接使用的

/* chain it on udata list (after main thread) */ u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u);

把 u 掛接在 mainthread 之後。

從前面的 mainstate 建立過程可知。mainthread 一定是 GCObject 連結串列上的最後一個節點(除 Udata 外)。這是因為掛接過程都是向連結串列頭新增的。

這裡,就可以把所有 userdata 全部掛接在其它型別之後。這麼做的理由是,所有 userdata 都可能有 gc 方法(其它型別則沒有)。需要統一去呼叫這些 gc 方面,則應該有一個途徑來單獨遍歷所有的 userdata 。除此之外,userdata 和其它 GCObject 的處理方式則沒有區別,顧依舊掛接在整個 GCObject 連結串列上而不需要單獨再分出一個連結串列。

處理 userdata 的流程見 lgc.c 的 127 行

/* move `dead' udata that need finalization to list `tmudata' */ size_t luaC_separateudata (lua_State *L, int all) {

這個函式會把所有帶有 gc 方法的 userdata 挑出來,放到一個迴圈連結串列中。這個迴圈連結串列在 global_State 的 tmudata 域。需要呼叫 gc 方法的這些 userdata 在當個 gc 迴圈是不能被直接清除的。所以在 mark 環節的最後,會被重新 mark 為不可清除節點。見 lgc.c 的 545 行:

marktmu(g); /* mark `preserved' userdata */

這樣,可以保證在呼叫 gc 方法環節,這些物件的記憶體都沒有被釋放。但因為這些物件被設定了 finalized 標記。(通過 markfinalized ),下一次 gc 過程不會進入 tmudata 連結串列,將會被正確清理。

具體 userdata 的清理流程,會在後面展開解釋。

今天暫時先到這裡。