【Lua进阶系列】lua_Stack
大家好,我是Lampard~~
欢迎来到Lua进阶系列的博客
前文再续,书接上一回。今天和大家讲解一下lua_Stack。
还记得之前我们曾经写过一篇【lua基础系列】之C/C++与lua的交互方式 ,这篇文章简要提到了lua和c之间的交互过程以及TValue的数据结构,大家没看过的可以先点进去瞅一眼打个底。
示意图如下:
(一)lua_Stack究竟由什么组成?
c与lua之间交互离不开lua堆栈,那么lua堆栈究竟是什么东西,由什么组成?
对于这个问题,我感觉没有比看源代码更有说服力的答案了(以下是lua5.4最新版代码)。
直接定位到lua_State定义的文件“lstate.h”中,我们发现了以下结构体还有一堆英文注释,我们现在来一个个分析着看。
struct lua_State {
CommonHeader;
unsigned short nci; /* 存储一共多少个CallInfo number of items in 'ci' list */
lu_byte status;
StkId top; /* 指向栈的顶部,压入数据,都通过移动栈顶指针来实现。 first free slot in the stack */
global_State *l_G;
CallInfo *ci; /* 当前运行函数信息 call info for current function */
const Instruction *oldpc; /* last pc traced */
StkId stack_last; /* 指向栈的底部,但是会预留空间作宝物处理 last free slot in the stack */
StkId stack; /* 指向栈的底部 stack base */
UpVal *openupval; /* list of open upvalues in this stack */
GCObject *gclist;
struct lua_State *twups; /* list of threads with open upvalues */
struct lua_longjmp *errorJmp; /* current error recover point */
CallInfo base_ci; /* 调用栈的头部指针 CallInfo for first level (C calling Lua) */
volatile lua_Hook hook;
ptrdiff_t errfunc; /* current error handling function (stack index) */
int stacksize; /* 栈的大小 */
int basehookcount;
int hookcount;
unsigned short nny; /* number of non-yieldable calls in stack */
unsigned short nCcalls; /* number of nested C calls */
l_signalT hookmask;
lu_byte allowhook;
};
(1) CommonHeader -- GC的通用头
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked
CommonHeader 是使用引用计数机制进行垃圾回收的通用头。如果可以正确的遵从gc的使用规则,也就是说你可以正确无误的使用智能指针,那么理论上bai说,就不可能存在内存泄漏。
(2) nci -- 记录调用栈item个数的变量
nci是16位unsigned short类型的一个变量,用于记录有多少个item在调用栈(ci)中。
(3) status -- 表示当前这个lua_Stack线程的状态
注意这里的线程类型不要与操作系统线程混淆,Lua的线程类型是Lua虚拟机实现一种数据类型,简单来说也就是代表这个lua_Stack的状态。
我们看看lua线程的所有状态(存放在“lua.h”文件中):
LUA_OK -- 正常运行,LUA_YIELD -- 挂起, LUA_ERRRUN -- 运行时错误,LUA_ERRSYNTAX -- 编译错误 ,LUA_ERRMEM -- 内存分配错误,LUA_ERRGCMM -- GC内存回收错误,LUA_ERRERR --在运行错误处理函数时发生的错误
/* thread status */
#define LUA_OK 0
#define LUA_YIELD 1
#define LUA_ERRRUN 2
#define LUA_ERRSYNTAX 3
#define LUA_ERRMEM 4
#define LUA_ERRGCMM 5
#define LUA_ERRERR 6
(4) l_G -- 全局状态机,维护全局字符串表、内存管理函数、gc等信息
在5.4之前l_G并不是global_State全局状态机类型,它是一个把lua_Stack和global_State关联起来的一个结构体变量,不过很明显5.4之后lua底层直接把这个global_State暴露出来了,不过变量名还没有改(还是l_G)。
我们不是在讲lua_State吗?为啥又来一个global_State呢?
简而言之lua_state是是暴露给用户的数据类型(线程)用户通过它来调用C_API,global_State维护全局字符串表、内存管理函数、gc等信息。
关于具体的global_State我们可以看一下我写的这篇博客:【Lua进阶系列】全局状态机global_State
两者大体上的区别如下:
执行状态机 -- lua_state(暴露给用户调用)
lua_state 是暴露给用户的数据类型,既表示一个 lua 程序的执行状态,也指代 lua 的一个线程(在官方文档中)。
每个线程拥有独立的数据栈以及函数调用栈,还有独立的调试钩子和错误处理设置。
lua_state是一个lua 线程的执行状态。所有的lua C API 都是围绕这个状态机:
lua_State是围绕程序如何执行来设计的,数据栈和调用栈都在其中。全局状态机 -- 同一虚拟机中的所有执行线程(实际的虚拟机,一个全局状态机的数据多个lua_Stack共享)
global_state 里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表
有内存管理函数,有GC 需要的把所有对象串联起来的相关信息,以及一切 lua 在工作时需要的工作内存。
通过 lua_newstate 创建一个新的 lua 虚拟机时,第一块申请的内存将用来保存主线程和这个全局状态机。
结构示意图:
(5) StkId -- 数据栈
StkId top; /* first free slot in the stack */
StkId stack_last; /* last free slot in the stack */
StkId stack; /* stack base */
前文提及在ua和C/C++是通过这个lua_State进行交互的,而lua_State就是利用StkId这个数据栈对数据进行暂存的。
下面我们看看这个StkId的代码定义:
我们可以看出,StkId其实是TValue的数组,那么TValue又是什么结构呢?
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;
#define TValuefields Value value_; int tt_
typedef struct lua_TValue {
TValuefields;
} TValue;
通过上述代码关系我们可以看出,实际存储数据的数据结构是Value,而TValue是为了区分联合中存放的数据类型(使用tt字段),再额外绑定一个类型字段。
lua 中的数据可以这样分为两类:值类型和引用类型。值类型可以被任意复制,而引用类型共享一份数据。
Value存放了gc和几个属性值,属性值分别对应了lua的值类型数据,而gc则管理lua中的引用数据的生命周期。
从下面的图可以的得出如下结论:
1. lua中, number, boolean, nil, light userdata四种类型的值是直接存在栈上元素里的, 和垃圾回收无关.
2. lua中, string, table, closure, userdata, thread存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收.
3.lua_state 的数据栈,就是一个 TValue 的数组。代码中用 StkId 类型来指代对 TValue 的引用。
(6) CallInfo -- 调用栈
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
CallInfo *ci; /* call info for current function */
- 主要由一个CallInfo的结构组成。CallInfo是一个双向链表结构。通过双向链表结构来管理每一个Lua的函数调用栈信息。
- Lua一共有三种类型的函数:C语言闭包函数(例如pmain)、Lua的C函数库(例如str字符串函数)和LUA语言
- 每一个函数的调用,都会新生产一个CallInfo的调用栈结构,用于管理函数调用的栈指针信息。当一个函数调用结束后,会返回CallInfo链表的前一个调用栈,直到所有的调用栈结束回到L->base_ci。
- 调用栈最终都会指向数据栈上,通过一个个调用栈,用于管理不同的函数调用。
typedef struct CallInfo {
StkId func; /* ci->func:指向正在调用操作的栈底位置。 function index in the stack */
StkId top; /* 指向调用栈的栈顶部分 top for this function */
struct CallInfo *previous, *next; /* previous和next是双向链表指针,用于连接各个调用栈。当执行完一个函数,通过previous回滚到上一个调用栈
CI 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;
这里可以举一个栗子,就是函数A调用函数B,函数b也调用函数C,那么此时base_ci的next就是函数A的callinfo,ci就是函数c的callinfo
实际上,遍历 L 中的 base_ci域指向的 CallInfo双向链表可以获得完整的 lua 调用栈。而每一级的 CallInfo 中,都可以进一步的通过 func 域取得所在函数的更详细信息。
当 func 为一个 lua 函数时,根据它的函数原型可以获得源文件名、行号等诸多调试信息。
(7) HOOK 相关-- 服务于debug模块
int basehookcount;
int hookcount;
volatile lua_Hook hook;
l_signalT hookmask;
lu_byte allowhook;
volatile lua_Hook hook 存放了debug调用的钩子函数
struct lua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) 'global', 'local', 'field', 'method' */
const char *what; /* (S) 'Lua', 'C', 'main', 'tail' */
const char *source; /* (S) */
int currentline; /* (l) */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
unsigned char nups; /* (u) number of upvalues */
unsigned char nparams;/* (u) number of parameters */
char isvararg; /* (u) */
char istailcall; /* (t) */
char short_src[LUA_IDSIZE]; /* (S) */
/* private part */
struct CallInfo *i_ci; /* active function */
};
关于debug库的一些介绍可以看一下我的这篇博客:【Lua进阶系列】之Debug库
(8) GC 垃圾回收
GCObject *gclist;