Lua知識樹整理————lua源碼分析


lua源碼分析

  • [x] 數據類型

    • 數據類型的分類

      #define LUA_TNONE		(-1) //空類型
      
      #define LUA_TNIL		0 //nil
      #define LUA_TBOOLEAN		1 //布爾
      #define LUA_TLIGHTUSERDATA	2//指針
      #define LUA_TNUMBER		3//數字
      #define LUA_TSTRING		4//字符串
      #define LUA_TTABLE		5//表
      #define LUA_TFUNCTION		6//函數
      #define LUA_TUSERDATA		7//指針
      #define LUA_TTHREAD		8//虛擬機,攜程
      
      //可以看到除了LUA_TNONE,lua支持了9種基本類型。其實對於某些基本類型,lua還用了一個叫做variant tag的標記來定義其子類型,具體參見下圖:
      /* Variant tags for functions */
      #define LUA_TLCL	(LUA_TFUNCTION | (0 << 4))  /* Lua closure (對於lua函數來說沒有函數這個概念,所有的lua函數都是一個lua閉包)*/
      #define LUA_TLCF	(LUA_TFUNCTION | (1 << 4))  /* light C function (C函數) */
      #define LUA_TCCL	(LUA_TFUNCTION | (2 << 4))  /* C closure (c閉包)*/
      
      
      /* Variant tags for strings */
      #define LUA_TSHRSTR	(LUA_TSTRING | (0 << 4))  /* short strings */
      #define LUA_TLNGSTR	(LUA_TSTRING | (1 << 4))  /* long strings */
      
      
      /* Variant tags for numbers */
      #define LUA_TNUMFLT	(LUA_TNUMBER | (0 << 4))  /* float numbers */
      #define LUA_TNUMINT	(LUA_TNUMBER | (1 << 4))  /* integer numbers */
      //string又細分為短string和長string,主要區別在於它們的hash值生成;而number也細分為了float和integer。
      
    • Type的組織方式

      • Value和TValue

        首先,lua為了方便對所有的類型進行統一管理,把它們都抽象成了一個叫做Value的union結構,具體定義如下

        /*
        ** Tagged Values. This is the basic representation of values in Lua,
        ** an actual value plus a tag with its type.
        */
        
        /*
        ** Union of all Lua values
        */
        typedef union Value {
          GCObject *gc;    /* collectable objects */
          void *p;         /* light userdata */
          int b;           /* booleans */
          lua_CFunction f; /* light C functions */
          lua_Integer i;   /* integer numbers */
          lua_Number n;    /* float numbers */
        } Value;
        /* 從定義可以看出,主要把這些類型划分為了需要GC的類型和不需要GC的類型。由於Value是union的結構,所以每個Value實例里同時只會有一個字段是有效的。而為了知道具體哪個字段是有效的,也就是具體該Value是什么類型,從而有了TValue這個struct結構,主要在Value基礎上wrap了一個_tt字段來標識Value的具體類型。TValue的定義如下:*/
        #define TValuefields	Value value_; int tt_
        typedef struct lua_TValue {
          TValuefields;
        } TValue;
        
      • GCUnion、GCObject、CommonHeader

        lua把所有值按是否需要被GC,划分為了GCObject和一般類型。所有需要被GC的類型,被定義在了GCUnion里:

        /*
        ** Union of all collectable objects (only for conversions)
        */
        union GCUnion {
          GCObject gc;  /* common header */
          struct TString ts;
          struct Udata u;
          union Closure cl;
          struct Table h;
          struct Proto p;
          struct lua_State th;  /* thread */
        };
        /*可以發現String、UserData、Closure、Table、Proto、luaState等類型都是需要被GC的,GCUnion結構和Value類似,也是同時只有一個字段是有效的。所以我們自然而然會想到,是不是類似TValue一樣,在外面給包一層type呢,但是lua實現這邊並沒有這樣做,而是讓TString、UData這些"子類"都在各自定義的開頭定義這個type字段。實際上,是定義了一個叫做CommonHeader的宏,這個宏里包含了type和一些其他字段,而每個GC類型都需要在在其struct頭部定義該宏,從而可以造成一種所有GC類型都繼承自一個帶有CommonHeader宏的基類的假象。該宏定義如下:*/
        #define CommonHeader	GCObject *next; lu_byte tt; lu_byte marked
        struct GCObject {
          CommonHeader;
        };
        
        /*
        可以發現,它一共有三個字段:
        tt,即該GC對象的具體類型
        next,指向GCObject的指針,用於GC算法內部實現鏈表
        marked,用於GC算法內部實現
        GCObject,其實它就是把CommonHeader這個數據區包成了一個struct,它的好處在於lua可以把所有的GC類型的對象都視作是一個GCObject,比如在lua_State結構里就定義了一個GC列表: GCObject* gclist。 
        */
        
  • [x] 字符串

    • 長串和短串

      在講String的數據結構和重要函數前,先強調一點,出於對性能和內存等方面的考慮,lua對String實現的方方面面,都把短字符串和長字符串區分開來處理了。比如短串會走一個先查詢再創建的路子,而長串不查詢,直接創建。我個人的理解大概是出於以下兩個方面考慮:

      • 復用度。短串復用度會比長串要高,或者說短串在全局的引用計數一般會比長串高。比如obj["id"] = 12, obj["type"] = 0,類似"id"、"type"這種短串可能會在程序很多處地方使用到,如果開辟多份就有點浪費了。而長串則很少會有重復的,比如我一篇文章的文本,一般不會在兩個地方重復打出來。
      • 哈希效率。由於長串的字符串比較多,如果要把組成它的字符序列進行哈希,耗時會比短串長。
      • 從定義可以看出,長度大於40的,在lua中處理為長串,反之則為短串。
    • String的基本數據結構

      • lua中string的實現為TString結構體:

        typedef struct TString {
          CommonHeader; //gc用結構體
          lu_byte extra;  /* 對於短串,主要用於實現保留字符串;對於長串,作為一個標識,來表示該串有沒有進行過hash,這點可以結合hash字段來理解。*/
          lu_byte shrlen;  /* 短串的長度,對於長串沒有意義。 */
          unsigned int hash;/* 該字符串的hash值,如果是短串,該hash值是在創建時就計算出來的,這是因為短串會加入到全局的stringtable這個hashmap結構中;而對於長串來說,這個hash字段是按需的,只有真正需要它的hash值時,手動調用luaS_hashlongstr函數,才會生成該值,lua內部現在只有在把長串作為table的key時,才會去計算它。當extra字段為0時,表示該長串的hash還沒計算過,否則表示已經計算過了 */
          union {
              /* 當是短串時,由於會被加入到全局stringtable的鏈表中,所以在該結構中保存了指向下一個TString的指針;當是長串時,表示該長串的長度。注意長串和短串沒有共用一個字段來表示它們的長度,主要是長串的長度可以很長,而短串最長就為40,一個byte就夠用了,這邊也體現了lua實現是很摳細節的,反倒是把這兩個不相關的字段打包到一個union里來節約內存了。 */
            size_t lnglen;  /* length for long strings */
            struct TString *hnext;  /* linked list for hash table */
          } u;
        } TString;
        
      • stringtable

        typedef struct stringtable {
          TString **hash;//基於TString的hashmap,也叫做散列桶。基本結構是一個數組,每個數組里存的是相同hash值的TString的鏈表。
          int nuse;  /* 當前實際的元素數 */
          int size; /* 桶個數 */
        } stringtable;
        
    • string實現中的重要函數

      • luaS_newlstr函數

        /*創建給定長度的string接口,由於lua內部對短串和長串的處理采用了兩套方案,所以該函數會根據string的實際長度來決定調用短串處理函數(internshrstr)還是長串處理函數(luaS_createlngstrobj)。*/
        TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
          if (l <= LUAI_MAXSHORTLEN)  /* short string? */
            return internshrstr(L, str, l);
          else {
            TString *ts;
            if (l >= (MAX_SIZE - sizeof(TString))/sizeof(char))
              luaM_toobig(L);
            ts = luaS_createlngstrobj(L, l);
            memcpy(getstr(ts), str, l * sizeof(char));
            return ts;
          }
        }
        
      • internshrstr函數

        static TString *internshrstr (lua_State *L, const char *str, size_t l) {
            TString *ts;
            global_State *g = G(L);
            /*
                hash查找。會先調用luaS_hash來得到字符串的hash值,然后去全局stringtable里查找,如果有就直接返回該TString對象。hash的具體實現可以參照luaS_hash函數,為了對長度較長的字符串不逐位hash,內部也是根據長度的2次冪計算出了一個步長step,來加速hash的過程。
            */
            unsigned int h = luaS_hash(str, l, g->seed);
            TString **list = &g->strt.hash[lmod(h, g->strt.size)];
            lua_assert(str != NULL);  /* otherwise 'memcmp'/'memcpy' are undefined */
            for (ts = *list; ts != NULL; ts = ts->u.hnext) {
                if (l == ts->shrlen &&
                    (memcmp(str, getstr(ts), l * sizeof(char)) == 0)) {
                  /* found! */
                  if (isdead(g, ts))  /* dead (but not collected yet)? */
                    changewhite(ts);  /* resurrect it */
                  return ts;
                }
            }
            /*
            hashtable按需resize。如果第一步查找失敗了,並且stringtable的元素數量已經大於桶數,那么以兩倍的尺寸對stringtable進行resize。具體調用的是luaS_resize函數。
            */
            if (g->strt.nuse >= g->strt.size && g->strt.size <= MAX_INT/2) {
            luaS_resize(L, g->strt.size * 2);
            list = &g->strt.hash[lmod(h, g->strt.size)];  /* recompute with new size */
            }
            /*
            實際的TString創建工作。主要是調用createstrobj函數來實現。其中包括了內存的分配,CommonHeader的填充,TString特化字段的填充等。
            */
            ts = createstrobj(L, l, LUA_TSHRSTR, h);
            memcpy(getstr(ts), str, l * sizeof(char));
            ts->shrlen = cast_byte(l);
            ts->u.hnext = *list;
            *list = ts;
            g->strt.nuse++;
            /*
            更新stringtable信息。比如更新stringtable的鏈表,以及對stringtable的元素數量加1。
            */
            return ts;
        }
        
      • luaS_reisze函數

        /*
        實際改變stringtable桶數量的函數。它目前只會被兩個地方調用到:
        1.短string創建時(internshrstr函數),如果發現桶數量小於了元素數量,說明散列比較擁擠了,會對桶進行兩倍的擴容。
        2.在gc時,如果發現桶數量大於了4倍的元素數量,說明散列太稀疏了,會對桶數量進行減半操作。
        */
        void luaS_resize (lua_State *L, int newsize) {
          int i;
          stringtable *tb = &G(L)->strt;
          /*
              newsize>oldsize。這個時候的順序是,先進行擴容,然后進行rehash。擴容跟到里面去調用的就是realloc函數。
              而rehash的代碼也很簡潔,就是簡單的遍歷每個桶,把每個桶里的元素再哈希到正確的桶里去
          */
          if (newsize > tb->size) {  /* grow table if needed */
            luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *);
            for (i = tb->size; i < newsize; i++)
              tb->hash[i] = NULL;
          }
          for (i = 0; i < tb->size; i++) {  /* rehash */
            TString *p = tb->hash[i];
            tb->hash[i] = NULL;
            while (p) {  /* for each node in the list */
              TString *hnext = p->u.hnext;  /* save next */
              unsigned int h = lmod(p->hash, newsize);  /* new position */
              p->u.hnext = tb->hash[h];  /* chain it */
              tb->hash[h] = p;
              p = hnext;
            }
          }
          /*
          newsize < oldsize。順序是倒過來的,需要先根據newsize進行rehash,然后在保證所有元素已經收縮到newsize個數的桶里以后,才能進行shrink操作,這里也是調用的realloc函數來實現。
          */
          if (newsize < tb->size) {  /* shrink table if needed */
            /* vanishing slice should be empty */
            lua_assert(tb->hash[newsize] == NULL && tb->hash[tb->size - 1] == NULL);
            luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *);
          }
          tb->size = newsize;
        }
        /*
        luaM_realloc_速覽,
        frealloc(ud, NULL, x, s) 可以創建一個新的內存塊,大小為s
        frealloc(ud, p, x, 0) 釋放內存塊b
        底層調用(*g->frealloc),lua就是普通malloc,如果想設計的話可以嘗試類似gunc的pool malloc或者bit map malloc做緩存池,我后面有空來試試,通過lua_setallocf接口可以設置這個frealloc函數
        */
        void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) {
            void *newblock;
            global_State *g = G(L);
            size_t realosize = (block) ? osize : 0;
            lua_assert((realosize == 0) == (block == NULL));
            #if defined(HARDMEMTESTS)
            if (nsize > realosize && g->gcrunning)
            luaC_fullgc(L, 1);  /* force a GC whenever possible */
            #endif
            newblock = (*g->frealloc)(g->ud, block, osize, nsize);
            if (newblock == NULL && nsize > 0) {
            lua_assert(nsize > realosize);  /* cannot fail when shrinking a block */
            if (g->version) {  /* is state fully built? */
              luaC_fullgc(L, 1);  /* try to free some memory... */
              newblock = (*g->frealloc)(g->ud, block, osize, nsize);  /* try again */
            }
            if (newblock == NULL)
              luaD_throw(L, LUA_ERRMEM);
            }
            lua_assert((nsize == 0) == (newblock == NULL));
            g->GCdebt = (g->GCdebt + nsize) - realosize;
            return newblock;
        }
        
      • createstrobj函數

        static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) {
            TString *ts;
            GCObject *o;
            size_t totalsize;  /* total size of TString object */
            totalsize = sizelstring(l);//計算該string需要占用的內存大小size。lua實際把string的char數組緊貼UTString結構來存儲,所以一個string實例實際占用內存大小其實是UTString結構占用,再加上(charlength+1)個char大小:
            o = luaC_newobj(L, tag, totalsize);//調用luaC_newobj來創建一個GC對象,該函數也是lua中相當重要的函數了。它負責了所有GCObject子類的創建,它會根據實際傳入的內存大小來開辟空間,並幫忙填充掉CommonHeader的數據。最后,它還會把該obj掛接到global_State中的allgc列表中,以供GC模塊使用。
            ts = gco2ts(o);
            ts->hash = h;
            ts->extra = 0;
            getstr(ts)[l] = '\0';  //填充對象的TString特化字段。並且給char數組末尾添'\0'。
            return ts;
        }
        
    • 總結

      在理完了string牽涉的主要數據結構和函數以后。從整個宏觀的情況來理解lua的string:

      • 首先,它是一個GCObject的子類,也就意味着它是被垃圾回收模塊所管理的。
      • 其次,它的長度決定了它的內部實現機制:短串意味着hashtable管理和內存去重;長串意味着內存副本。
      • 最后,全局的hashtable大小,取決於string的創建和gc的sizecheck檢查。
  • [x] 虛擬機

    • 虛擬機基本概念
      • 虛擬機指借助軟件系統對物理機器指令執行進行的一種模擬。首先,對於物理機器的執行,主要是機器從內存中fetch指令,通過總線傳輸到CPU,然后進行譯碼、執行、結果存儲等步驟。既然虛擬機是對其進行的一種模擬,那么也逃不過以下幾個特點:
      • 將源碼編譯成VM所能執行的字節碼。
      • 字節碼格式(指令格式),例如三元式、四元式、波蘭式等。
      • 函數調用的相關棧結構,函數的出入口和傳參方式。
      • 指令指針,類似於物理機的指令寄存器(EIP)。
      • 虛擬CPU。 instruction dispatche
        • 取指:通過IP fetch下一條指令
        • 譯碼:對指令進行翻譯,得到指令類型,並且解析其操作數。
        • 執行:跳到對應邏輯塊進行執行。
    • 棧式虛擬機和寄存器式虛擬機

      • 棧式虛擬機

        • 采用棧式虛擬機的語言有JVM、CPython以及.Net CLR等。

        • 基於棧的虛擬機有一個操作數棧的概念,虛擬機在進行真正的運算時都是直接與操作數棧(operand stack)進行交互,不能直接操作內存中數據(其實這句話不嚴謹的,虛擬機的操作數棧也是布局在內存上的),也就是說不管進行何種操作都要通過操作數棧來進行,即使是數據傳遞這種簡單的操作。這樣做的直接好處就是虛擬機可以無視具體的物理架構,特別是寄存器。但缺點也顯而易見,就是速度慢,因為無論什么操作都要通過操作數棧這一結構。:

          這里寫圖片描述

      • 寄存器式虛擬機

        • 采用寄存器式的虛擬機有lua和Dalvik等。
        • 這種實現沒有操作數棧這一概念,但是會有許多的虛擬寄存器。這類虛擬寄存器有別於CPU的寄存器,因為CPU寄存器往往是定址的(比如DX本身就是能存東西),而寄存器式的虛擬機中的寄存器通常有兩層含義:
          • 寄存器別名(比如lua里的RA、RB、RC、RBx等),它們往往只是起到一個地址映射的功能,它會根據指令中跟操作數相關的字段計算出操作數實際的內存地址,從而取出操作數進行計算;
          • 實際寄存器,有點類似操作數棧,也是一個全局的運行時棧,只不過這個棧是跟函數走的,一個函數對應一個棧幀,棧幀里每個slot就是一個寄存器,第1步中通過別名映射后的地址就是每個slot的地址。具體的棧幀可以參考后文講CallInfo時的棧幀圖。 好處是指令條數少,數據轉移次數少。壞處是單挑指令長度較長。
        • 具體來看,lua里的實際寄存器數組是用TValue結構的棧來模擬的,這個棧也是lua和C進行交互的虛擬棧。 lua里的字節碼叫做opcode,
    • 關鍵函數和結構分析

      • luaL_dofile

        //包含了luaL_loadfile和lua_pcall兩個步驟,分別對應了函數的解析和執行階段。
        #define luaL_dofile(L, fn) \
        	(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
        
      • luaL_loadfile

        #define luaL_loadfile(L,f)	luaL_loadfilex(L,f,NULL)
        
        //再跟進去核心是會調用到lua_load
        
        LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                              const char *chunkname, const char *mode) {
          ZIO z;
          int status;
          lua_lock(L);
          if (!chunkname) chunkname = "?";
          luaZ_init(L, &z, reader, data);
          status = luaD_protectedparser(L, &z, chunkname, mode);//這句話是重點,對lua代碼進行進行詞法和語法分析,把source轉化成opcode,並創建Proto結構保存該opcode和該函數的元信息
          if (status == LUA_OK) {  /* no errors? */
            LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
            if (f->nupvalues >= 1) {  /* does it have an upvalue? */
              /* get global table from registry */
              Table *reg = hvalue(&G(L)->l_registry);
              const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
              /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
              setobj(L, f->upvals[0]->v, gt);
              luaC_upvalbarrier(L, f->upvals[0]);
            }
          }
          lua_unlock(L);
          return status;
        }
        //---快進n步到closefunc函數里面就初始化了Proto結構和信息
        static void close_func (LexState *ls) {
          lua_State *L = ls->L;
          FuncState *fs = ls->fs;
          Proto *f = fs->f;
          luaK_ret(fs, 0, 0);  /* final return */
          leaveblock(fs);
          luaM_reallocvector(L, f->code, f->sizecode, fs->pc, Instruction);
          f->sizecode = fs->pc;
          luaM_reallocvector(L, f->lineinfo, f->sizelineinfo, fs->pc, int);
          f->sizelineinfo = fs->pc;
          luaM_reallocvector(L, f->k, f->sizek, fs->nk, TValue);
          f->sizek = fs->nk;
          luaM_reallocvector(L, f->p, f->sizep, fs->np, Proto *);
          f->sizep = fs->np;
          luaM_reallocvector(L, f->locvars, f->sizelocvars, fs->nlocvars, LocVar);
          f->sizelocvars = fs->nlocvars;
          luaM_reallocvector(L, f->upvalues, f->sizeupvalues, fs->nups, Upvaldesc);
          f->sizeupvalues = fs->nups;
          lua_assert(fs->bl == NULL);
          ls->fs = fs->prev;
          luaC_checkGC(L);
        }
        // Proto結構如下:
        typedef struct Proto {
          CommonHeader;
          lu_byte numparams;  /* number of fixed parameters */
          lu_byte is_vararg;
          lu_byte maxstacksize;  /* number of registers needed by this function */
          int sizeupvalues;  /* size of 'upvalues' */
          int sizek;  /* size of 'k' */
          int sizecode;
          int sizelineinfo;
          int sizep;  /* size of 'p' */
          int sizelocvars;
          int linedefined;  /* debug information  */
          int lastlinedefined;  /* debug information  */
          TValue *k;  /* constants used by the function */
          Instruction *code;  /* opcodes */
          struct Proto **p;  /* functions defined inside the function */
          int *lineinfo;  /* map from opcodes to source lines (debug information) */
          LocVar *locvars;  /* information about local variables (debug information) */
          Upvaldesc *upvalues;  /* upvalue information */
          struct LClosure *cache;  /* last-created closure with this prototype */
          TString  *source;  /* used for debug information */
          GCObject *gclist;
        } Proto;
        /*
        該結構基本涵蓋了parse階段該函數的所有分析信息。主要包括以下幾部分:
        
        常量表。比如在函數里寫了a = 1 + 2,那這里的1和2就會放在常量表里。
        局部變量信息。包含了局部變量的名字和它在函數中的生存周期區間(用pc來衡量)。
        Upvalue信息。包含了該upvalue的名字和它是否歸屬於本函數棧還是外層函數棧的標記。
        opcode列表。包含了該函數實際調用的所有指令。其實就是一個int32類型的列表,因為lua虛擬機里每個指令對應一個int32.
        */
        
      • lua_pcall

        這個函數最終會調到luaD_call,也就是lua虛擬機里函數執行的主要函數。

        void luaD_call (lua_State *L, StkId func, int nResults) {
          if (++L->nCcalls >= LUAI_MAXCCALLS)
            stackerror(L);
          if (!luaD_precall(L, func, nResults))  /* is a Lua function? */
            luaV_execute(L);  /* call it */
          L->nCcalls--;
        }
        
        • luaD_precall:

          • 如果是C函數或者C閉包,會直接創建單個函數調用的運行時結構CallInfo,來完成函數的進棧和出棧。
          • 如果是lua閉包,在precall中只會做函數調用前的准備工作,實際執行會在后一步luaV_execute中進行。這里的准備工作主要包括:(1)處理lua的不定長參數、參數數量不夠時的nil填充等。(2)分配CallInfo結構,並填充該函數運行時所需的base、top、opcode等信息,注意CallInfo結構里還有個很關鍵的func字段,它指向棧里對應的LClosure結構,這個結構為虛擬機后續執行提供upvalue表和常量表的查詢,畢竟后續對常量和upvalue的read操作,都是需要把它們從這兩個表中加載到寄存器里的。
        • luaV_execute

          • 這一步就是我們前面提到的lua虛擬機的CPU了,因為所有指令的實際執行都是在這個函數里完成的。它做的主要工作,就是在一個大循環里,不斷的fetch和dispatch指令。每次的fetch就是把pc加1,而dispatch就是一個大的swtich-case,每個不同類型的opcode對應不同的執行邏輯。
        • CallInfo

          //CallInfo結構,包含了單個函數調用,lua虛擬機所需要的輔助數據結構,它的結構如下:
          typedef struct CallInfo {
            StkId func;  /* function index in the stack */
            StkId	top;  /* top for this function */
            struct CallInfo *previous, *next;  /* dynamic call link */
            union {
              struct {  /* only for Lua functions */
                StkId base;  /* base for this function */
                const Instruction *savedpc;
              } l;
              struct {  /* only for C functions */
                lua_KFunction k;  /* continuation in case of yields */
                ptrdiff_t old_errfunc;
                lua_KContext ctx;  /* context info. in case of yields */
              } c;
            } u;
            ptrdiff_t extra;
            short nresults;  /* expected number of results from this function */
            unsigned short callstatus;
          } CallInfo;
          /*
          我們來看下lua_State里與之相關的幾個字段:
          
          stack。TValue*類型,記錄了"內存"起始地址。
          base。TValue*類型,記錄當前函數的第一個參數位置。
          top。TValue*類型,記錄當前函數的棧頂。
          base_ci。當前棧里所有的函數調用CallInfo數組。
          ci。當前函數的CallInfo。
          可以發現,通過這樣的組織結構,luavm可以方便的獲取到任意函數的位置以及其中的所有參數位置。而每個CallInfo里又記錄了函數的執行pc,因此vm對函數的執行可以說是了如指掌了。
          */
          
    • [x] Table

      • Table的設計特點

        • 容器功能:與其他語言相似,lua也內置了容器功能,也就是table。而與其他語言不同的是,lua內置容器只有table。正因為如此,為了適配不同的應用需求,table的內部結構也比較考究,分為了數組和哈希表兩個部分,根據不同需求來決定使用哪個部分。
        • 面向對象功能:與其他語言不同的時,lua並沒有把面向對象的功能以語法的形式包裝給開發者。而是保留了這樣一種能力,待開發者去實現自己的面向對象。而這一保留的能力,也是封裝在table里的:table里可以組合一個metatable,這個metatable本身也是一個table,它的字段用來描述原table的行為。
      • Table的數據結構

        typedef struct Table {
          CommonHeader;//垃圾回收通用結構
          lu_byte flags;  /*用於cache該表中實現了哪些元方法 */
          lu_byte lsizenode;  /* 哈希表大小取log2(哈希表大小只會為2的n次冪) */
          unsigned int sizearray;  /* 數組大小(數組大小只會為2的n次冪) */
          TValue *array;  /* 數組頭指針 */
          Node *node;//哈希表頭指針
          Node *lastfree;  /* 哈希表可用尾指針,可用的節點只會小於該lastfree節點。 */
          struct Table *metatable;//元表
          GCObject *gclist;//GC的鏈表,用於垃圾回收
        } Table;
        

        img

      • Table的重要操作

      • 查詢操作

        /*
        查詢key是否存在,分為了int和非int類型。如果key是int類型並且小於sizearray,那么直接返回對應slot。
        否則走hash表查詢該key對應的slot。
        */
        const TValue *luaH_get (Table *t, const TValue *key) {
          switch (ttype(key)) {
            case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key));
            case LUA_TNUMINT: return luaH_getint(t, ivalue(key));/* integer numbers */
            case LUA_TNIL: return luaO_nilobject;
            case LUA_TNUMFLT: {/* float numbers */
              lua_Integer k;
              if (luaV_tointeger(key, &k, 0)) /* index is int? */
                return luaH_getint(t, k);  /* use specialized version */
              /* else... */
            }  /* FALLTHROUGH */
            default:
              return getgeneric(t, key);
          }
        }
        
      • 新增元素

        //由於lua對於下標超過數組的大小的數字,都會存儲在散列表部分去,所以數組部分的插值不會觸發rehash
        //散列表的組織,就是多個mainposition,每個單獨的mainposition會對應一個數據鏈表,當插入一個key的時候,會調用luaHset\luaH_setnum\luaH_setstr,來獲得該key對應的TValue指針,如果沒有,則調用內部的newkey函數來分配一個新的key
        TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
          Node *mp;
          TValue aux;
          //...安全性校驗,略
          mp = mainposition(t, key);//根據key尋找再當前hash中的位置
          if (!ttisnil(gval(mp)) || isdummy(t)) {  /*如果該位置已經有數據了(ttisnil(gval(mp)))或者找不到該位置 (isdummy(t))*/
            Node *othern;
            Node *f = getfreepos(t);  /* 嘗試獲取一個空閑位置 */
            if (f == NULL) { 
           	  //如果freepos為NULL(沒有可用空間了),調用rehash來擴容。rehash的具體細節將在后文詳解。
              rehash(L, t, key);  /* grow table */
              return luaH_set(L, t, key);  /* insert key into grown table */
            }
            lua_assert(!isdummy(t));
            //找到newkey的mainposition,看是否可用,如果可用直接使用
            othern = mainposition(t, gkey(mp));
            if (othern != mp) { //如果占用節點的hash值與newkey不同,說明該節點是被“擠”到該位置來的,那么把該節點挪到freepos去,然后讓newkey入住其mainposition。
              while (othern + gnext(othern) != mp)  /* find previous */
                othern += gnext(othern);
              gnext(othern) = cast_int(f - othern);  /* rechain to point to 'f' */
              *f = *mp;  /* copy colliding node into free pos. (mp->next also goes) */
              if (gnext(mp) != 0) {
                gnext(f) += cast_int(mp - f);  /* correct 'next' */
                gnext(mp) = 0;  /* now 'mp' is free */
              }
              setnilvalue(gval(mp));
            }
            else { //占用的節點和newkey的哈希值相同,那么直接插入到該mainposition的next。
              if (gnext(mp) != 0)
                gnext(f) = cast_int((mp + gnext(mp)) - f);  /* chain new position */
              else lua_assert(gnext(f) == 0);
              gnext(mp) = cast_int(f - mp);
              mp = f;
            }
          }
          setnodekey(L, &mp->i_key, key);
          luaC_barrierback(L, t, key);
          lua_assert(ttisnil(gval(mp)));
          return gval(mp);
        }
        
      • rehash操作:

        static void rehash (lua_State *L, Table *t, const TValue *ek) {
          unsigned int asize;  /* 最終數組的大小(一定為2的次冪)。 */
          unsigned int na;  /* 最終歸入數組部分的key的個數。 */
          unsigned int nums[MAXABITS + 1];//它的第i個位置存儲的是key在2^(i-1)~2^i區間內的數量。
          int i;
          int totaluse;//總共的key個數。
          for (i = 0; i <= MAXABITS; i++) nums[i] = 0;  /* reset counts */
          na = numusearray(t, nums);  //遍歷當前的array部分,按其中key的分布來更新nums數組。同時返回na。
          totaluse = na;  /* 將totaluse加上na。 */
          totaluse += numusehash(t, nums, &na);  /* 遍歷當前的hash表部分,如果其中的key為整數,na++並且更新nums數組,對於每個遍歷的元素,totaluse++ */
          na += countint(ek, nums);/* - countint:將newkey傳進去,如果是整型的,那么na++。 - */
          totaluse++;
          asize = computesizes(nums, &na);//計算optimal的array部分大小。這個函數根據整型key在2^(i-1)~2^i之間的填充率,來決定最終的array大小。一旦遇到某個子區間的填充率小於1/2,那么后續的整型key都存儲到hash表中去,這一步是為了防止數組過於稀疏而浪費內存。函數將返回asize以及na
          luaH_resize(L, t, asize, totaluse - na);//根據上一步計算出的最終數組和哈希表大小,進行resize操作。當然,如果hash表的尺寸有變化,會對原來哈希表中的元素進行真正的rehash。
        }
        
      • 迭代操作

        //在使用測主要是ipairs和pairs兩個函數。這兩個函數都會在vm內部臨時創建出兩個變量state和index,用於對lua表進行迭代訪問,每次訪問的時候,會調用luaH_next函數
        //該函數首先會根據findindex函數,找出迭代器對應lua表的哪個部分,是數組部分還是hash表部分。如果是數組部分,index會小於sizearray,否則會大於sizearray。注意該函數中的兩個循環,只會進一個,取決於搜尋出來的key的index是否小於sizearray。
        int luaH_next (lua_State *L, Table *t, StkId key) {
          unsigned int i = findindex(L, t, key);  /* find original element */
          for (; i < t->sizearray; i++) {  /* try first array part */
            if (!ttisnil(&t->array[i])) {  /* a non-nil value? */
              setivalue(key, i + 1);
              setobj2s(L, key+1, &t->array[i]);
              return 1;
            }
          }
          for (i -= t->sizearray; cast_int(i) < sizenode(t); i++) {  /* hash part */
            if (!ttisnil(gval(gnode(t, i)))) {  /* a non-nil value? */
              setobj2s(L, key, gkey(gnode(t, i)));
              setobj2s(L, key+1, gval(gnode(t, i)));
              return 1;
            }
          }
          return 0;  /* no more elements */
        }
        
    • [x] 函數

      • 函數閉包

        首先,所有的lua函數,都是一個函數閉包。函數閉包是目前主流語言幾乎都支持的一個機制,它指的是一個內部結構,該結構存儲了函數本身以及一個在詞法上包圍該函數的環境,該環境包含了函數外圍作用域的局部變量,通常這些局部變量又稱作upvalue。

        #define ClosureHeader \
        	CommonHeader; lu_byte nupvalues; GCObject *gclist
        
        typedef struct CClosure {
          ClosureHeader;
          lua_CFunction f;
          TValue upvalue[1];  /* list of upvalues */
        } CClosure;
        
        
        typedef struct LClosure {
          ClosureHeader;//ClosureHeader:跟GC相關的結構,因為函數與是參與GC的。
          struct Proto *p;//因為Closure=函數+upvalue嘛,所以p封裝的就是純粹的函數原型。該結構中封裝了函數的很多基本特性,如局部變量、字節碼序列、函數嵌套、常量表等。
          UpVal *upvals[1];  /* upvals:函數的upvalue指針列表,記錄了該函數引用的所有upvals。正是由於該字段的存在,導致函數對upvalue的訪問要快於從全局表_G中向下查找。函數對upvalue的訪問,一般就2個步驟:(1)從closure的upvals數組中按索引號取出upvalue。(2)將upvalue加到luastate的stack中 */
        } LClosure;
        
        
        typedef union Closure {
          CClosure c;
          LClosure l;
        } Closure;
        
      • 其他參見虛擬機的proto部分

    • [x] GC

      • 基本原理

        • 和C#、Java類似,lua采用了Mark&Sweep的算法來進行垃圾回收,與之相對的還有個常用算法是Automatic Reference Counting(ARC)。Mark&Sweep的優點在於不用像ARC在每次賦值操作去操作引用計數(對於動態語言效率有較大影響),也不會有環形引用的問題,但是由於Mark&Sweep在觸發時,需要從root節點全量遍歷被引用到的節點,通常是比較耗時的操作。

          lua5.1之前,lua采用了雙色的mark&sweep,它的大致流程如下:

          • mark階段
            • 從根節點出發,先將root置黑
            • 將root節點直接引用的節點置黑
            • 遞歸將黑色節點引用到的節點置黑,直到不再有新增的黑色節點
          • sweep階段:
            • 仍為白色的節點視為沒有引用,從而將作為垃圾進行回收

          該算法是一個不可中斷的同步過程,非常容易造成CPU突刺。針對這個問題,lua從5.1以后便引入了增量GC的實現,將這樣一整個同步的回收cycle,均攤到很多個可以增量執行的分步上,從而達到降低CPU突刺的目的。

      • 增量Collector與Program的關系

        增量Collector與程序是交叉運行的,GC執行完一個step,就會把控制權交還給上層lua程序繼續執行,上層執行一段時間又會觸發下一個GC的step。基於這個運行時間線,Collector需要考慮的問題是:
        1、如何將一整個Mark&Sweep的cycle切分成邏輯上的子步驟。
        2、如何為每一個GC Step分配合理的工作量,使得一方面GC的頻率可以滿足應用程序對內存的需求,另一方面還要讓每一個GC子步驟不至於造成應用程序的CPU突刺。

        3、如何解決在兩個GC Step間,Mutator會改變GC狀態的問題

      • GC狀態機

        • 針對問題(1),lua的做法是,使用了一個線性的狀態機,將一整個Mark&Sweep的cycle拆分成若干子狀態,每個子狀態下去執行對應的action:

          img

          比如pause狀態負責mark roots;propagate狀態負責進行mark狀態的傳播,最終mark出所有reachable的節點;幾個swp***狀態負責清理unreachable節點的內存;callfin負責調用對象的finalizer。但是有的狀態也是不可拆分的,具體情況具體分析。

        • GC Step工作量評估

        • 如果只是以3.1模型中的以單個子狀態為一個GC Step進行增量GC,實際上效果並不理想。這是因為:

          • 就算每個狀態的工作量是相等的,那每個狀態的執行時間也只是整個cycle的1/8,而一般整個cycle的時間,拿手游項目為例,通常為幾十ms幾百ms不等,均攤到每個狀態的耗時,也仍然有幾ms幾十ms,仍然會造成明顯的CPU突刺。
          • 更壞的情況是,各個子狀態的耗時通常是非常不均等的,像propagate這種狀態,要去全量遞歸的尋找reachable的節點,往往是一個耗時的大頭,這樣會導致在這個狀態的耗時會非常突出。

          基於以上兩個考慮,可以得到的結論是,不能僅僅以GC的子狀態為增量的最小單元。

          lua為了解決這個問題,引入了一個“打工還債”的模型:把內存申請(如newstring,newtable等)量化成增加債務,當欠債達到一定規模時,程序運行會從Program態轉變為Collector態,Collector會去打工還債,打工的內容就是上圖中各個子狀態下的action。為了進一步細化增量的最小單位,lua進一步對打工的工作量進行了評估,比如在propagate狀態下,工作量就是traverse對象的內存量。有了這個債務與工作量的量化指標,那lua就可以把增量的單位進一步細化到工作量上

        • 三色Mark&Sweep算法

          • 對於問題(3)-如何解決在兩個Step之間,Mutator會改變GC狀態的問題。lua采用了下圖的3色Mark&Sweep模型來解決:

            img

            該算法的主要步驟還是mark和sweep兩個大階段。mark負責標記當前正在被程序使用的obj,sweep階段負責釋放沒有被程序使用的obj。

            相對於lua5.1之前的GC模型,該模型的主要特點是:
            1.把GC的obj划分成3個顏色

            • 黑色節點:已經完全掃描過的obj。
            • 灰色節點:在掃描黑色節點時候初步掃描到,但是還未完全掃描的obj,這類obj會被放到一個待處理列表中進行逐個完全掃描。
            • 白色節點:還未被任何黑色節點所引用的obj(因為一旦被黑色節點引用將被置為黑色或灰色)。這里白色又被進一步細分為cur white和old white,lua會記錄當前的cur white顏色,每個obj新創建的時候都是cur white,lua會在mark階段結束的時候翻轉這個cur white的位,從而使得這之前創建的白色obj都是old的,在sweep階段能夠得到正確釋放。

            2.引用屏障
            在mark階段,黑色節點只能指向黑色或灰色節點,不能指向白色節點;灰色節點可以指向灰色節點或白色節點。換句話說,灰色節點充當了黑、白節點間的屏障作用(圖中的紅色虛線)。

            這一屏障功能是合理的,想象一下,如果黑色節點跨越屏障引用到了白色節點,說明該白色節點實際上是被引用狀態,不應該被GC釋放。但是由於黑色節點已經遍歷過,不會再重新遍歷,會導致Collector在后面sweep階段看到該obj為白色而將其錯誤的釋放。

            為了保證這一屏障功能,lua內部實現了barrier機制。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM