前言:
在 Skynet 中,Lua 扮演了极其重要的角色。Skynet 大多数业务逻辑都跑在一个个 Lua 服务里,而能够将 Lua 环境嵌入到 Skynet 框架下,并与 Skynet 消息调度机制完美结合,正是 snlua
服务所承担的核心功能。
本文将着重分析
snlua
服务的核心实现,包括其初始化过程、协程扩展(Profile)、内存管理,以及如何与 Skynet 主循环交互等细节,帮助你在阅读 Skynet 代码或自定义服务时更好地理解这一部分的原理与运作。
snlua
服务是 Skynet 提供的一个 C 语言服务(又称为 service ),它的主要职责是:
lua_newstate
建立独立的 Lua 环境。skynet_callback
,让 Lua 脚本处理游戏或业务逻辑的各种消息。coroutine.resume
和 coroutine.wrap
,并使用钩子(hook)来计量协程执行的耗时。lalloc
,实现对 Lua 内存占用的统计与警告甚至限制。lualoader.lua
,并从外部接收启动参数,将其传递给 Lua 环境初始化服务逻辑。通过 snlua
服务,Skynet 可以在 C 层面精准控制和监控 Lua 层的一些关键点(如内存、协程)。这使得 Skynet 保持了高性能的特性,且拥有动态脚本语言的灵活性。
在 snlua.c
中,最核心的结构体是:
struct snlua {
lua_State * L; // 该服务所属的 Lua VM 主线程
struct skynet_context * ctx; // Skynet 服务上下文
size_t mem; // 当前 Lua VM 内存总占用
size_t mem_report; // 触发内存预警的阈值
size_t mem_limit; // 内存上限,如果 mem 超过此值,分配器将返回 NULL
lua_State * activeL; // 当前处于活跃状态的协程
ATOM_INT trap; // 用于处理 signal_hook 的标志
};
L
:当前服务对应的 Lua 主线程(Lua VM)。ctx
:Skynet 中表示一个服务的上下文,用于在 C 层与 Skynet 通信。mem
:当前分配的 Lua 内存总大小。mem_report
:当内存超出该值时,会触发一次警告日志,并将此值翻倍。mem_limit
:Lua 内存分配的上限,若超出则直接返回 NULL 并引发错误。activeL
:当前执行的协程,配合信号机制进行调试或中断操作。trap
:一个原子变量,处理调试信号(signal_hook)的标志位。snlua
struct snlua * snlua_create(void) {
struct snlua * l = skynet_malloc(sizeof(*l));
memset(l,0,sizeof(*l));
l->mem_report = MEMORY_WARNING_REPORT; // 默认32M触发报警
l->mem_limit = 0; // 默认为0,即无限制
l->L = lua_newstate(lalloc, l); // 创建独立的 Lua VM,并使用自定义分配器
l->activeL = NULL;
ATOM_INIT(&l->trap , 0);
return l;
}
这里做了几件事:
snlua
结构体,并初始化所有字段。l->mem_report
默认 32M,用于记录内存使用量到达 32M 时进行一次报警,并将阈值翻倍。lua_newstate
创建新的 Lua VM,分配器采用自定义的 lalloc
用于内存统计。trap
原子变量为 0
,表示当前没有任何调试请求。当 Skynet 创建一个 snlua
服务时,会指定一个启动函数 launch_cb
作为回调:
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
// 将 args 的内容复制一份,随后发送给自己的服务句柄
// 由 launch_cb 来进行后续处理
skynet_callback(ctx, l , launch_cb);
...
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
return 0;
}
skynet_callback(ctx, l, launch_cb)
:当收到第一个消息时,launch_cb
会被调用。snlua_init
将传入的 args
数据打包后再以消息方式发送给自己,从而触发 launch_cb
。在 launch_cb
中,执行真正的初始化函数 init_cb
:
static int launch_cb(struct skynet_context * context, void *ud, int type, int session,
uint32_t source , const void * msg, size_t sz) {
struct snlua *l = ud;
skynet_callback(context, NULL, NULL);
int err = init_cb(l, context, msg, sz);
if (err) {
skynet_command(context, "EXIT", NULL);
}
return 0;
}
此时将回调置空,保证后续消息交给 Lua 层处理。若初始化失败,则发送退出命令。
init_cb
:Lua 环境设置与脚本加载static int init_cb(struct snlua *l, struct skynet_context *ctx,
const char * args, size_t sz) {
// 1. 绑定 luaL_openlibs, 初始化标准库和 profile 扩展
// 2. 设置 Lua 环境变量 (LUA_PATH, LUA_CPATH, LUA_SERVICE 等)
// 3. 加载并执行 loader.lua
// 4. 处理内存限制
...
return 0;
}
其中几个关键点:
注册标准库与 profile
调用 luaL_openlibs(L)
加载基础标准库,同时调用 luaL_requiref(L, "skynet.profile", init_profile, 0)
引入了自定义的 skynet.profile
模块,以实现对 Lua 协程的耗时统计。
覆盖 coroutine.resume
、coroutine.wrap
Skynet 会将 resume
、wrap
替换为自己封装的函数,从而统计协程的运行时长。
设置全局变量
如 LUA_PATH
, LUA_CPATH
, LUA_SERVICE
, LUA_PRELOAD
,方便在 Lua 中通过相应的全局变量查找脚本或动态库。
加载 loader.lua
默认路径是 ./lualib/loader.lua
,这是 Skynet 启动 Lua 服务的核心脚本,其会加载真正的服务主脚本并传入 args
作为启动参数。
内存限制
如果在 Lua 中设置了 memlimit
,则会读取并保存到 l->mem_limit
,用于在分配器 lalloc
中进行判定。
整个过程完成后,Skynet 中的 Lua 环境就处于可运行状态,后续所有消息将交由 Lua 层脚本来处理。
lalloc
static void * lalloc(void * ud, void *ptr, size_t osize, size_t nsize) {
struct snlua *l = ud;
size_t mem = l->mem;
l->mem += nsize;
if (ptr)
l->mem -= osize;
if (l->mem_limit != 0 && l->mem > l->mem_limit) {
// 如果超出限制,分配失败
if (ptr == NULL || nsize > osize) {
l->mem = mem;
return NULL;
}
}
// 报警逻辑
if (l->mem > l->mem_report) {
l->mem_report *= 2;
skynet_error(l->ctx, "Memory warning %.2f M", (float)l->mem / (1024 * 1024));
}
return skynet_lalloc(ptr, osize, nsize);
}
l->mem
。mem_limit != 0
且 l->mem > l->mem_limit
,代表超出上限,直接返回 NULL
,从而导致 Lua 层内存分配失败并产生错误。l->mem
超过 l->mem_report
,则打印一条警告日志并将 mem_report
翻倍。这样就保证了每个 snlua
服务可以独立监控内存使用量,防止无限制地占用系统资源。
Skynet 对 Lua 协程做了扩展,使得我们可以统计每个协程的运行时间。主要思路:
替换 coroutine.resume
、coroutine.wrap
在 init_profile
函数中,用自定义的函数 luaB_coresume
和 luaB_cowrap
覆盖原版,以进行耗时计算。
开始与结束
用 start_time
、total_time
表示协程本次启动时间和累计耗时。每次协程 resume
时记下开始时间,协程返回或 yield
时累加耗时。
结果
在 profile.stop()
时,可以拿到协程执行的总耗时,帮助开发者做性能分析。
signal_hook
)static void signal_hook(lua_State *L, lua_Debug *ar) {
...
if (ATOM_LOAD(&l->trap)) {
ATOM_STORE(&l->trap , 0);
luaL_error(L, "signal 0");
}
}
snlua_signal(l, 0)
设置 trap
时,Skynet 会在下一次指令执行前(通过 lua_sethook
)抛出一个错误,强制中断 Lua 执行。这在调试或终止协程时非常有用。signal = 1
,则仅打印当前内存使用量。snlua
服务启动流程小结snlua
结构,设置自定义分配器与内存统计逻辑。launch_cb
中调用 init_cb
完成 Lua VM 初始化:
LUA_PATH
、LUA_CPATH
、LUA_SERVICE
等全局环境。loader.lua
,启动 Lua 端业务逻辑。snlua
服务时,就会交由 Lua 层的服务脚本来处理,通常通过 skynet.dispatch
或 skynet.start
设定的回调函数进行处理。config
中配置在 Skynet 的 config
文件或启动命令行中,你可以指定服务类型为 snlua
来启动一个 Lua 服务。例如:
# config
thread = 8
bootstrap = "snlua bootstrap"
lua_path = "./lualib/?.lua;./lualib/?/init.lua;"
lua_cpath = "./luaclib/?.so;"
从命令行或在启动脚本中,可以通过 skynet.newservice("xxx")
或 skynet.uniqueservice("xxx")
来创建一个新的 Lua 服务,底层就是在调度器里分配一个 snlua
服务并调用 snlua_init
,随后再用 launch_cb
加载 xxx.lua
脚本。
当某个 Lua 服务的内存使用量超过 MEMORY_WARNING_REPORT
(默认为 32M)时,会打印一条:
[:0000000x] Memory warning 64.00 M
表示该服务已经使用了 64M(后续阈值会翻倍到 128M),便于开发者及时定位内存问题。
snlua
服务作为 Skynet 框架与 Lua 语言结合的关键模块,不仅为 Lua 逻辑层提供了隔离、独立的执行环境,还在 C 层面通过内存管理、协程扩展、信号捕获等手段,为服务器开发者提供了更高的可控性和可调试性。