现在很多游戏引擎都是C++
+ lua
的结构,一旦某个服务器开发人员大意写出死循环代码,很容易导致服务无响应,影响服务器稳定。所以引擎中最好能提供一个死循环的检测机制,一旦出现死循环则执行一些行为打断当前流程。
死循环的检测是一个停机问题。我们无法判断到底是任务执行时间过长,还是进入了真正的死循环,好在这对我们的服务来说区别并不重要。所以一个简单的判断条件是,执行时间是否超过了预定的阈值。
C++
中集成lua
,调用到游戏逻辑时,一般通过pcall,但是一旦调用了pcall
,代码的执行路径便进入了lua
的世界,除非通过信号机制才能在当前线程中中断,实现执行其他分支的目的。除此之外,lua
还提供了debug.sethook
函数,可以在执行正常逻辑中触发hook
,实现监测超时的功能。所以我们有以下两种方案:
使用debug.sethook()
来实现
debug.sethook ([thread,] hook, mask [, count]) Sets the given function as a hook. The string mask and the number count describe when the hook will be called. The string mask may have the following characters, with the given meaning:
“c”: the hook is called every time Lua calls a function; “r”: the hook is called every time Lua returns from a function; “l”: the hook is called every time Lua enters a new line of code. With a count different from zero, the hook is called after every count instructions.
所以我们只要在执行pcall
之前设定类似如下的代码:
debug.sethook(function()error("timeout")end, "c", 10000)
理论上只要代码指令数超过10000条就能触发error
。好像挺完美的。
But,在luajit
下这条不一定成立,因为执行的逻辑被jit
编译了,而在这种情况下,hook
是不会触发的
If your program is running in a tight loop and never falls back to the interpreter, the debug hook never runs and can’t throw the “interrupted!” error.
但是还有一个未公开的编译选项LUAJIT_ENABLE_CHECKHOOK
,在lj_record.c
文件的最后面,上面写道
Regularly check for instruction/line hooks from compiled code and exit to the interpreter if the hooks are set.
This is a compile-time option and disabled by default, since the hook checks may be quite expensive in tight loops.
看似可以,但是注意,如果hook
被设置了,则执行的代价是比较昂贵的。对于游戏而言,大部分的时间都在lua
层,而为了监测死循环,几乎
要在所有的lua执行过程中设置hook
,这是不太容易接受的。好在下面的注释提到了
You can set the instruction hook via lua_sethook() with a count of 1 from a signal handler or another native thread. Please have a look at the first few functions in luajit.c for an example (Ctrl-C handler).
嗯,看样子只能使用第二种方案了。
使用信号来实现
在lua的命令行程序中我们可以通过Ctrl-C
中断正在执行的程序
> for i=1,10000000 do sum = sum + i end
^Cinterrupted!
stack traceback:
stdin:1: in main chunk
[C]: in ?
仔细看lua.c
文件,可以看到以下代码
static void lstop (lua_State *L, lua_Debug *ar) {
(void)ar; /* unused arg. */
lua_sethook(L, NULL, 0, 0);
luaL_error(L, "interrupted!");
}
static void laction (int i) {
signal(i, SIG_DFL); /* if another SIGINT happens before lstop,
terminate process (default action) */
lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1);
}
// ....
//in docall
signal(SIGINT, laction);
status = lua_pcall(L, narg, (clear ? 0 : LUA_MULTRET), base);
signal(SIGINT, SIG_DFL);
嗯,在执行pcall
之前设置了信号处理函数,捕捉Ctrl-C
的信号,一旦发生,则立马调用lua_sethook
函数,指定在执行下一行代码时调用lstop
,而在lstop
中就直接抛出error
了。所以问题是 lua_sethook
是可以在信号处理函数中调用的?
答案:是
从源码中可以看到
/* This function can be called asynchronously (e.g. during a signal). */
LUA_API int lua_sethook(lua_State *L, lua_Hook func, int mask, int count)
除此之外,从luajit
的源码注释来看,不仅仅在信号处理函数中,在其他线程中也能被调用
from a signal handler or another native thread.
所以,这种方案是可行的。因此,对于单线程程序而言,可以通过设置alarm
来实现超时设置
alarm(10);// trigger after 10s
signal(SIGALRM, laction);
status = lua_pcall(L, narg, (clear ? 0 : LUA_MULTRET), base);
alarm(0)
signal(SIGALRM, SIG_DFL);
而对于多线程程序,可以直接启一个定时器来来check
,而不用使用很恶心的信号。
值得一提的是,使用这种方式触发超时error
可以很轻易地在pcall
中捕获,从而而已实现堆栈的打印等功能,方便查找和定位问题。