lua number 范围_深入Lua:线程和栈

前言

前面讲了一些Lua对象的实现细节,这一节要从总体上看Lua虚拟机是怎么创建出来的。

全局状态

一个Lua虚拟机所涉及的各种状态和数据,主要是由两个结构来管理的,一个是global_State,另一个是lua_State。global_State负责全局的状态,比如GC相关的,注册表,内存统计等等信息。而lua_State对应于一个Lua线程,当创建一个Lua虚拟机时会自动创建一个“主线程”,默认Lua代码就在这个主线程中执行。而通过协程库可以创建多个“线程”,并使Lua代码执行在不同的“线程”中,这一篇先忽略协程的东西,也就是只关注全局状态和主线程。

因为与虚拟机相关的状态都放在global_State或lua_State中,所以虚拟机的API是可重入的,可以多个系统线程中并行执行多个虚拟机,只要确保每个虚拟机一个时刻只在一个系统线程执行即可。

global_State的声明如下所示,这里我去掉与GC相关的东西(占了主要部分),等到以后说到GC时才列出来。

/*
** 'global state', shared by all threads of this state
  全局状态,所有线程共享这个状态
*/
typedef struct global_State {
  // 内存分配函数,以及关联的用户数据
  lua_Alloc frealloc;  /* function to reallocate memory */
  void *ud;         /* auxiliary data to 'frealloc' */
  // 短字符串哈希表
  stringtable strt;  /* hash table for strings */
  // 全局注册表
  TValue l_registry;
  // 随机函数种子
  unsigned int seed;  /* randomized seed for hashes */
  // 终止函数
  lua_CFunction panic;  /* to be called in unprotected errors */
  // 主线程
  struct lua_State *mainthread;
  // 版本号
  const lua_Number *version;  /* pointer to version number */
  // 错误消息
  TString *memerrmsg;  /* memory-error message */
  // 元方法名,初始化在luaT_init
  TString *tmname[TM_N];  /* array with tag-method names */ 
  // 基本类型的元方法,表和userdata之外的元表放这儿
  struct Table *mt[LUA_NUMTAGS];  /* metatables for basic types */  
  // 零结尾的字符串缓存
  TString *strcache[STRCACHE_N][STRCACHE_M];  /* cache for strings in API */
} global_State;

frealloc是设置给Lua的内存分配函数,从这可看出Lua是高度可定制的,你可以在调用lua_newstate时转入自己的分配函数,也可以调用luaL_newstate使用Lua提供的默认分配函数,代码如下:

// 默认的分配器:
// nsize == 0 : 行为和free一样
// nsize != 0: 行为和realloc一样,当ptr==NULL时,realloc和malloc一样;否则重分配内存,注意返回的地址和ptr可能不一样。
static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
  (void)ud; (void)osize;  /* not used */
  if (nsize == 0) {
    free(ptr);
    return NULL;
  }
  else
    return realloc(ptr, nsize);
}

后面Lua将只调用frealloc,而不会使用如malloc这样的C函数。lmem.h|c基于frealloc提供了更上层的分配函数,Lua代码通常只用lmem来分配对象或其他数据结构。

strt 为短字符串缓存,这在前面已经有描述。

l_registry 注册表,是一个Table对象,用于保存全局的Lua值,比如主线程对象,全局环境等等。

seed 是用于计算哈希的随机种子

panic 为终止函数,当代码出现错误且未被保护时,会调用panic函数并终止宿主程,通过lua_atpanic可设置终止函数。panic在luaD_throw中被调用,随后会调用abort结束程序。你有最后一个机会不让结束程序,就是在panic函数里调用longjmp,使panic永远不会返回,不过这种做法应该也很少用到。

mainthread 主线程。

线程和栈

在调用lua_newstate时创建global_State和一个lua_State,此即为线程状态。lua_State是一个Lua对象,代表一个线程,它里面最重要的的数据就是一个用于存放Lua值的栈,和函数的调用信息的链表(CallInfo),去掉和GC相关的以及调试相关的字段后,lua_State结构如下:

// 线程对象
struct lua_State {
  // CallInfo数量,一个CallInfo代表一层函数调用
  unsigned short nci;  /* number of items in 'ci' list */
  // 线程状态:LUA_OK...
  lu_byte status;
  // 栈顶地址
  StkId top;  /* first free slot in the stack */
  // 全局状态
  global_State *l_G;
  // 当前函数的调用信息
  CallInfo *ci;  /* call info for current function */
  // [stack, stack_last]这个范围的栈槽位可用
  StkId stack_last;  /* last free slot in the stack */
  // 栈的起始地址
  StkId stack;  /* stack base */
  // 错误处理链表:当前的恢复点,看luaD_rawrunprotected
  struct lua_longjmp *errorJmp;  /* current error recover point */
  // 初始调用信息
  CallInfo base_ci;  /* CallInfo for first level (C calling Lua) */
  // 栈大小
  int stacksize;
};

StkId的类型为TValue*,实际上stack就是一个TValue数组。

statck, top, stack_last, stacksize这几个字段的含义用下图说明:

11fee04888cfb8ec03e40961419fc033.png

一开始线程创建一个大小为BASIC_STACK_SIZE的TValue数组,statck指向这个数组的首地址,statcksize为这个栈的大小,top为当前栈顶,向栈压值后top会往下移(增长)。stack_last为最后可用的位置,即正常的栈操作可以在[stack, stack_last]之间。剩下的EXTRA_STACK个槽位预留,用于元表调用或错误处理的栈操作,也就是这些扩展槽位可以让某些操作不用考虑栈空间是否足够,而导致要重分配栈空间的行为。

对栈的原始操作并不会自动增长栈空间,那样每次都要检查空间,对性能比较有影响。对于每个C函数的调用,Lua确保一开始有LUA_MINSTACK(20)个空闲槽位可以用,一般情况是非常足够了,对于需要在循环里不断压入元素的操作,应该调用lua_checkstack

// 检查当前线程的栈空间是否足够,如果不够会扩大整个栈,
// 同时如果当前函数的栈范围不够,也会扩大
LUA_API int lua_checkstack (lua_State *L, int n) {
  int res;
  // 当前的函数调用信息
  CallInfo *ci = L->ci;
  lua_lock(L);
  api_check(L, n >= 0, "negative 'n'");
  if (L->stack_last - L->top > n)  /* stack large enough? */
    res = 1;  /* yes; check is OK */
  else {  /* no; need to grow stack */
    // 计算出正在使用的大小,EXTRA_STACK也认为是使用的部分
    int inuse = cast_int(L->top - L->stack) + EXTRA_STACK;
    if (inuse > LUAI_MAXSTACK - n)  /* can grow without overflow? */
      res = 0;  /* no */
    else  /* try to grow stack */
      // 在保护模式下调用growstack
      res = (luaD_rawrunprotected(L, &growstack, &n) == LUA_OK);
  }
  // 调整当前CI的栈顶
  if (res && ci->top < L->top + n)
    ci->top = L->top + n;  /* adjust frame top */
  lua_unlock(L);
  return res;
}

这个函数有几个重要的信息:

  • 栈有一个最大的尺寸LUAI_MAXSTACK,超过这个最大尺寸则不能增长栈,这个值很大,在int为32位以上的机器上,它是100万个。
  • 实际增长空间的函数是growstack,它是在保护模式下调用的,关于保护模式的实现这里先略过。
  • 增长完毕后,还要调整当前CallInfo的栈使用范围,这个下面会说。

growstack调用的是luaD_growstack,它会尝试以2倍的大小扩充栈,最终扩充栈的是luaD_reallocstack函数:

// 重新分配栈空间
void luaD_reallocstack (lua_State *L, int newsize) {
  TValue *oldstack = L->stack;
  int lim = L->stacksize;
  lua_assert(newsize <= LUAI_MAXSTACK || newsize == ERRORSTACKSIZE);
  lua_assert(L->stack_last - L->stack == L->stacksize - EXTRA_STACK);
  luaM_reallocvector(L, L->stack, L->stacksize, newsize, TValue);
  // 对多出来的槽位填充为nil
  for (; lim < newsize; lim++)
    setnilvalue(L->stack + lim); /* erase new segment */
  // 调整大小字段
  L->stacksize = newsize;
  L->stack_last = L->stack + newsize - EXTRA_STACK;
  // 分配完之后,可能L->stack和oldstack为不同的地址,所以要矫正依赖于栈地址的其他数据
  correctstack(L, oldstack);
}

前面的代码都好理解,主要是correctstack这个,它的作用是矫正依赖于栈地址的其他数据,因为当调用luaM_reallocvector之后可能会重新分配内存地址,所以必须对那些依赖的地方作调整:

// 重新分配栈之后,可能L->stack和oldstack为不同的地址,所以要矫正依赖于栈地址的其他数据
static void correctstack (lua_State *L, TValue *oldstack) {
  CallInfo *ci;
  UpVal *up;
  // 矫正栈顶
  L->top = (L->top - oldstack) + L->stack;
  // open upvalue
  for (up = L->openupval; up != NULL; up = up->u.open.next)
    up->v = (up->v - oldstack) + L->stack;
  // 调用栈帧
  for (ci = L->ci; ci != NULL; ci = ci->previous) {
    ci->top = (ci->top - oldstack) + L->stack;
    ci->func = (ci->func - oldstack) + L->stack;
    if (isLua(ci))
      ci->u.l.base = (ci->u.l.base - oldstack) + L->stack;
  }
}

这里主要调整3个地方,一个是线程的栈顶,打开的upvalue,和CallInfo链表。

这里说一点个人见解,对于依赖于栈的数据,能否保存偏移,而不是直接保存地址?比如上面的L->top,或ci中的top, func,如果它们都是基于L->stack的偏移值,那么当栈扩充后,这些变量就完全不需要调整。取栈元素时变成这样:StkId e = L->stack + L->top,这里看起来虽然是多了一个相对寻址,但我认为性能应该不会影响多少,相反代码上肯定会简洁得多。

创建虚拟机

Lua创建一个虚拟机很简单,只需要下面的代码:

// 创建一个虚拟机,L为虚拟机的主线程
lua_State *L = luaL_newstate();
// 打开标准库,如果不需要标准库,下面这一行都可以不要。
luaL_openlibs(L);

luaL_newstate是一个上层封装,主要是调用lua_newstate,并指定默认的内存分析函数,和panic函数:

// 创建虚拟机,并设置panic回调
LUALIB_API lua_State *luaL_newstate (void) {
  lua_State *L = lua_newstate(l_alloc, NULL); 
  if (L) lua_atpanic(L, &panic);
  return L;
}

lua_newstate才是真正创建虚拟机的地方,在里面创建glocal_state和主线程lua_State并对它们初始化,不过它的创建手法有点奇妙,它调用分配函数创建这个结构:

typedef struct LG {
  LX l;
  global_State g;
} LG;

typedef struct LX {
  lu_byte extra_[LUA_EXTRASPACE];
  lua_State l;
} LX;

也就是一性次把lua_State和global_State一起创建出来了,这样释放主线程,全局状态也跟着回收掉,可见Lua对空间的利用是多么紧凑。释放的相关代码是:

#define fromstate(L)    (cast(LX *, cast(lu_byte *, (L)) - offsetof(LX, l)))
LX *l = fromstate(L1);
luaM_free(L, l);

lua_State的前面还有一点附加空间,可以容纳一个void*指针,这个附加空间Lua并没有使用,外部可以用它来保存和虚拟机相关的数据。

创建好虚拟机后,在保护模式下初始化一些其他的数据,在f_luaopen函数中:

static void f_luaopen (lua_State *L, void *ud) {
  global_State *g = G(L);
  UNUSED(ud);
  // 初始化主线程的栈
  stack_init(L, L);  /* init stack */
  // 注册表
  init_registry(L, g);
  // 字符串
  luaS_init(L);
  // 元表
  luaT_init(L);
  // 标识符
  luaX_init(L);
  // 开启GC
  g->gcrunning = 1;  /* allow gc */
  // 版本号
  g->version = lua_version(NULL);
  luai_userstateopen(L);
}

初始化注册表的代码可以看一下:

static void init_registry (lua_State *L, global_State *g) {
  TValue temp;
  /* create registry */
  // 创建注册表
  Table *registry = luaH_new(L);
  sethvalue(L, &g->l_registry, registry);
  luaH_resize(L, registry, LUA_RIDX_LAST, 0);
  /* registry[LUA_RIDX_MAINTHREAD] = L */
  // 保存主线程
  setthvalue(L, &temp, L);  /* temp = L */
  luaH_setint(L, registry, LUA_RIDX_MAINTHREAD, &temp);
  /* registry[LUA_RIDX_GLOBALS] = table of globals */
  // 创建全局环境
  sethvalue(L, &temp, luaH_new(L));  /* temp = new table (global table) */
  luaH_setint(L, registry, LUA_RIDX_GLOBALS, &temp);
}

在创建注册表的时候,会把主线程保存在LUA_RIDX_MAINTHREAD字段,同时创建一个全局环境,后面的标准库模块都保存在这个全局环境中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值