Lua 5.3 源码分析(十)线程的执行与中断
Lua 的程序运行时以线程为单位的。每个Lua 线程可以独立运行直到自行中断,把中断的信息留在状态机中。每条线程的执行互不干扰,可以独立延续之前中断的执行过程。
Lua 线程和系统线程无关,所以不会为每条 Lua 线程创建独立的系统堆栈,而是利用自己维护的线程栈,内存开销也就远小于系统线程。
Lua 是一门嵌入式语言,和 C 语言混合编程是一种常态。 一旦Lua 调用的 C 库中企图中断线程,延续它就是一个巨大的难题。
异常处理
如果Lua 被实现为一个纯粹的运行在字节码 VM 上的语言,只要不出 VM,可以很容易的实现自己的线程和异常处理。
事实上,Lua 的函数调用层次上只要没有 C 函数,是不会在 C 层面的调用栈上深入下去的。
但当Lua 函数调用了 C 函数,而这个 C 函数又进一步回调了 Lua 函数,这个问题就复杂很多。
Lua 的标准库中的 pairs 函数,就是一个典型的 C 扩展函数,却又回调了 Lua 函数。
Lua 底层把异常和线程中断用同一种机制来处理,也就是使用了 C 语言标准的 longjmp 机制来解决这个问题。
#if !defined(LUAI_THROW) /* { */
#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP) /* { */
/* C++ exceptions */
#define LUAI_THROW(L,c) throw(c)
#define LUAI_TRY(L,c,a) \
try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }
#define luai_jmpbuf int /* dummy variable */
#elif defined(LUA_USE_POSIX) /* }{ */
/* in POSIX, try _longjmp/_setjmp (more efficient) */
#define LUAI_THROW(L,c) _longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a) if (_setjmp((c)->b) == 0) { a }
#define luai_jmpbuf jmp_buf
#else /* }{ */
/* ISO C handling with long jumps */
#define LUAI_THROW(L,c) longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a }
#define luai_jmpbuf jmp_buf
#endif /* } */
#endif /* } */
每条线程 L 中保存了当前的 longjmp 返回点: errorJmp ,其结构定义 为 struct lua_longjmp 。这是一条链表,每次运行一段受保护的 Lua 代码,都会生成一个新的错误返回点,链到这条链表上。
/* chain list of long jump buffers */
struct lua_longjmp {
struct lua_longjmp *previous;
luai_jmpbuf b;
volatile int status; /* error code */
};
设置 longjmp 返回点是由 luaD_rawrunprotected 完成的。
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
unsigned short oldnCcalls = L->nCcalls;
struct lua_longjmp lj;
lj.status = LUA_OK;
lj.previous = L->errorJmp; /* chain new error handler */
L->errorJmp = &lj;
LUAI_TRY(L, &lj,
(*f)(L, ud);
);
L->errorJmp = lj.previous; /* restore old error handler */
L->nCcalls = oldnCcalls;
return lj.status;
}
这段代码很容易理解:设置新的 jmpbuf,串到链表上,调用函数。调用完成后恢复进入时状态。
如果想回直接返回到最近的错误恢复点,只需要调用 longjmp。Lua 使用一个内部API luaD_throw 封装了这个过程。
l_noret luaD_throw (lua_State *L, int errcode) {
if (L->errorJmp) { /* thread has an error handler? */
L->errorJmp->status = errcode; /* set status */
LUAI_THROW(L, L->errorJmp); /* jump to it */
}
else { /* thread has no error handler */
global_State *g = G(L);
L->status = cast_byte(errcode); /* mark it as dead */
if (g->mainthread->errorJmp) { /* main thread has a handler? */
setobjs2s(L, g->mainthread->top++, L->top - 1); /* copy error obj. */
luaD_throw(g->mainthread, errcode); /* re-throw in main thread */
}
else { /* no handler at all; abort */
if (g->panic) { /* panic function? */
seterrorobj(L, errcode, L->top); /* assume EXTRA_STACK */
if (L->ci->top < L->top)
L->ci->top = L->top; /* pushing msg. can break this invariant */
lua_unlock(L);
g->panic(L); /* call panic function (last chance to jump out) */
}
abort();
}
}
}
考虑到新构造的线程可能在不受保护的情况下运行。这时的任何错误都必须被捕获,不能让程序崩溃。这种情况合理的处理方式就是把正在运行的线程标记为死线程,并且在主线程中抛出异常。
函数调用
函数调用分为受保护调用和不受保护的调用。
受保护的函数调用可以看到一个 C 层面意义上完整的过程。在Lua 代码中,pcall 是用函数而不是语音机制完成的。受保护的函数调用一定在 C 层面进出一次调用栈。
它使用一个独立的内部API luaD_pcall 来实现。公开 API luaD_pcallk 仅仅是对它做了一些封装。
int luaD_pcall (lua_State *L, Pfunc func, void *u,
ptrdiff_t old_top, ptrdiff_t ef) {
int status;
CallInfo *old_ci = L->ci;
lu_byte old_allowhooks = L->allowhook;
unsigned short old_nny = L->nny;
ptrdiff_t old_errfunc = L->errfunc;
L->errfunc = ef;
status = luaD_rawrunprotected(L, func, u);
if (status != LUA_OK) { /* an error occurred? */
StkId oldtop = restorestack(L, old_top);
luaF_close(L, oldtop); /* close possible pending closures */
seterrorobj(L, status, oldtop);
L->ci = old_ci;
L->allowhook = old_allowhooks;
L->nny = old_nny;
luaD_shrinkstack(L);
}
L->errfunc = old_errfunc;
return status;
}
从这段代码我们可以看到 pcall 的处理模式:用 C 层面的堆栈来保护和恢复状态。
L->ci、L->allowhook、L->nny( nny 的全称是 number of
non-yieldable calls。由于 C 语言本身无法提供延续点的支持,所以 Lua 也无法让所有函数都是 yieldable 的。当一级函数处于 non-yieldable 状态时,更深的层次都无法 yieldable。这个变量用于监督这个状态,在错误发生报告。每级 C 调用是否允许 yield 取决于是否有设置 C 延续点,或是 Lua 内核实现时认为这次调用在发生 yield 时无法正确处理。这些都是由 luaD_call 的最后一个参数来制定。)、L->errfunc 都保存在 luaD_pcall 的 C 堆栈上,一旦 luaD_rawrunprotected 就可以正确恢复。
luaD_rawrunprotected 没有正确返回时,需要根据 old_top 找到堆栈上刚才调用的函数,给它做收尾工作(调用luaF_close 涉及 upvalue 的 gc 流程)。
因为 luaD_rawrunprotected 调用的是一个函数对象,而不是数据栈上的索引,这就需要额外的变量来定位了。
这里使用 restorestack 这个宏来定位栈上的地址,是因为数据栈的内存地址是会随着数据栈的大小而变化。保存地址是不可能的,而应该记住一个相对量。 savestack 和 restorestack这两个宏就是做这个工作的。
#define savestack(L,p) ((char )(p) - (char )L->stack)
#define restorestack(L,n) ((TValue )((char )L->stack + (n)))
一般的 Lua 层面的函数调用并不对应一个 C 层面上函数调用行为。对于Lua 函数而言,应该看成是生成新的 CallInfo,修正数据栈,然后把字节码的执行位置调整到被调用的函数开头。而Lua 函数的return 操作则做了 相反的操作,恢复数据栈,弹出 CallInfo ,修改字节码的执行位置,恢复到原有的执行序列上。
理解了这一点就能明白,在底层 API 中,为何分为 luaD_precall 和 luaD_poscall 。
luaD_precall 执行的是函数调用部分的工作,而 l