本篇作为 《Skynet 中 snlua 服务启动整体流程分析》的内容补充,主要是从 C 语言层面 一步步剖析,到 Lua 层面(loader.lua、服务启动脚本),最后再讲解如何将回调函数设为 skynet.dispatch_message
。主要希望能更好地理解 Skynet 如何初始化一个 snlua
服务,并让你对它的启动机制有一个全面、细致的认知。
在 Skynet 中,snlua
是最主要的 Lua VM 服务类型。它会启动一个独立的 Lua 虚拟机,加载指定的 Lua 代码,以便运行脚本逻辑。Skynet 的核心思想是 “一个服务进程内多个 Lua VM 服务并行运行”,互相通过消息通信来分工协作。
在 skynet
源码中,snlua
服务主要代码在 skynet\service-src\service_snlua.c 。下面是 init_cb()
源代码:
static int
init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
lua_State *L = l->L;
l->ctx = ctx;
// 1. 停止 GC
lua_gc(L, LUA_GCSTOP, 0);
// 2. 设置一些 Lua 环境,打开标准库
lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */
lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
luaL_openlibs(L);
// 3. 加载 "skynet.profile" 模块, 替换 coroutine 的方法
luaL_requiref(L, "skynet.profile", init_profile, 0);
int profile_lib = lua_gettop(L);
lua_getglobal(L, "coroutine");
lua_getfield(L, profile_lib, "resume");
lua_setfield(L, -2, "resume");
lua_getfield(L, profile_lib, "wrap");
lua_setfield(L, -2, "wrap");
lua_settop(L, profile_lib - 1);
// 4. 往注册表里塞一些数据
lua_pushlightuserdata(L, ctx);
lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");
// 5. 加载 codecache 模块 (允许 Skynet 做一些代码缓存逻辑)
luaL_requiref(L, "skynet.codecache", codecache , 0);
lua_pop(L,1);
// 6. 重新启动 GC (使用分代式 GC)
lua_gc(L, LUA_GCGEN, 0, 0);
// 7. 设置各种路径到全局变量 (LUA_PATH, LUA_CPATH, LUA_SERVICE 等)
const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
lua_pushstring(L, path);
lua_setglobal(L, "LUA_PATH");
const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
lua_pushstring(L, cpath);
lua_setglobal(L, "LUA_CPATH");
const char *service = optstring(ctx, "luaservice", "./service/?.lua");
lua_pushstring(L, service);
lua_setglobal(L, "LUA_SERVICE");
const char *preload = skynet_command(ctx, "GETENV", "preload");
lua_pushstring(L, preload);
lua_setglobal(L, "LUA_PRELOAD");
// 8. 压入 traceback 函数,保证出错时能打印堆栈
lua_pushcfunction(L, traceback);
assert(lua_gettop(L) == 1);
// 9. 加载并执行 loader.lua
const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
int r = luaL_loadfile(L,loader);
if (r != LUA_OK) {
skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
// 将 args (服务启动参数) 作为 loader.lua 的入参
lua_pushlstring(L, args, sz);
r = lua_pcall(L,1,0,1);
if (r != LUA_OK) {
skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
lua_settop(L,0);
// 10. 如果有内存限制 memlimit,就打印日志
if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) {
size_t limit = lua_tointeger(L, -1);
l->mem_limit = limit;
skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024));
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "memlimit");
}
lua_pop(L, 1);
lua_gc(L, LUA_GCRESTART, 0);
return 0;
}
从上面可以看出,init_cb()
是整个 snlua
服务初始化的 C 入口函数。它主要做了以下几件事:
停止 GC,执行一些加载或初始化操作,再重新启用 GC。
加载 Lua 标准库、skynet.profile
、skynet.codecache
等模块,做一些必要的替换或增强,比如把 coroutine.resume
和 coroutine.wrap
换成了带 Profile 统计的版本。
设置路径到全局变量:包含 LUA_PATH
、LUA_CPATH
、LUA_SERVICE
等,后续就可以在 Lua 里使用 require
、或 loadfile
来按照这些路径加载脚本。
加载并执行 loader.lua
。这是关键:loader.lua
是一个特殊的 加载脚本,会根据服务的名字去找到对应的 Lua 服务文件并执行。
如果有环境变量 memlimit
,则记录内存上限。
最终完成初始化并返回。
到这里为止,C 语言层面已经把相应的 Lua VM 准备好了,并且执行了 loader.lua
。一旦 loader.lua
加载成功,它就在 Lua 端继续完成后续的流程。
local strArgs, resumeX = ...
local args = {}
local filename
for word in string.gmatch(strArgs, "%S+") do
table.insert(args, word)
end
SERVICE_NAME = args[1]
-- 根据 SERVICE_NAME 去 LUALIB_SERVICE 路径里找到可执行脚本
local main, pattern
local err = {}
for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do
filename = string.gsub(pat, "?", SERVICE_NAME)
local f, msg = loadfile(filename)
if not f then
table.insert(err, msg)
else
pattern = pat
main = f
break
end
end
if not main then
error(table.concat(err, "\n"))
end
-- 把之前在全局设置的 LUA_PATH, LUA_CPATH, LUA_SERVICE 赋给 package
LUA_SERVICE = nil
package.path, LUA_PATH = LUA_PATH
package.cpath, LUA_CPATH = LUA_CPATH
-- 如果匹配到相对路径,就把对应目录加入到 package.path 当中
local service_path = string.match(pattern, "(.*/)[^/?]+$")
if service_path then
service_path = string.gsub(service_path, "?", args[1])
package.path = service_path .. "?.lua;" .. package.path
SERVICE_PATH = service_path
else
local p = string.match(pattern, "(.*/).+$")
SERVICE_PATH = p
end
-- 如果有 preload 脚本,则先执行它
if LUA_PRELOAD then
local f = assert(loadfile(LUA_PRELOAD))
f(table.unpack(args))
LUA_PRELOAD = nil
end
_G.require = (require "skynet.require").require
-- Tracy profiler 的一些逻辑 (省略)
-- ...
-- 最终执行 main 脚本(该脚本就是我们真正的服务脚本,就是在 启动配置中配置的启动入口文件 等等)
main(select(2, table.unpack(args)))
以上的主要逻辑是:
解析 init_cb() 传入的 args
:这里通过 string.gmatch(strArgs, "%S+")
获取启动时的所有参数,并将第一个参数作为 SERVICE_NAME
。
根据 LUA_SERVICE
路径查找真正的服务脚本
修正 package.path
和一些全局变量:为了让此服务后续 require
能寻址到更多文件。
可选地执行 LUA_PRELOAD
:如果 skynet_command(ctx, "GETENV", "preload")
有值,就先执行预加载脚本。
调用 main(...)
:这就是我们服务真正的 入口脚本,会传入除第一个以外的其他参数(select(2, table.unpack(args))
)。
到这里,loader.lua 成功找到了你指定的服务脚本,然后把控制权交给它。
skynet.start
继续看你提供的 启动脚本(示例是 的逻辑):
-- 启动脚本
local function main()
-- 省略业务启动逻辑
local XX= skynet.newservice('XX')
local XX= skynet.newservice('XX')
skynet.exit()
end
skynet.start(main)
这段脚本的核心在于 skynet.start(main)
。在 Skynet 中,每个服务启动时,都会调用 skynet.start
来注册一个 回调函数,并执行初始化逻辑。其典型流程是:
skynet.start(main)
会将 main
函数存起来,等到所有初始化skynet_require.init_all() 就绪以后,再执行 main
。
skynet.uniqueservice('...')
和 skynet.newservice('...')
则是向 Skynet 框架请求创建(或获取)相应名称的服务。
最后 skynet.exit()
用来让当前服务的主协程退出。
skynet.start
内部skynet.start(main)
在 Skynet 中位于skynet\lualib\skynet.lua,源码如下:
function skynet.start(start_func)
c.callback(skynet.dispatch_message) -- 这里 c.callback(...) 会调用到 C 代码
init_thread = skynet.timeout(0, function()
skynet.init_service(start_func)
init_thread = nil
end)
end
c.callback(skynet.dispatch_message)
就是调用了 lcallback
的 C 函数。这一步完成了 “将 Lua 中的一个回调函数 skynet.dispatch_message,注册到 C 端” 的操作。当有消息到达时,Skynet 会调用该回调,进而在 Lua 中调用 skynet.dispatch_message
做分发。
而 skynet.init_service(start_func)
会在一个定时器触发的时机中执行,用来调用你传入的 main
函数,并做一些初始化操作。
c.callback
的实现c.callback
所对应的 C 实现(也就是 lcallback
函数):
static int
lcallback(lua_State *L) {
struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
int forward = lua_toboolean(L, 2);
luaL_checktype(L,1,LUA_TFUNCTION);
lua_settop(L,1);
// 1. 创建 callback_context 结构
struct callback_context * cb_ctx = (struct callback_context *)lua_newuserdatauv(L, sizeof(*cb_ctx), 2);
cb_ctx->L = lua_newthread(L);
// 2. 给该 coroutine 保存 traceback 和 callback_context
lua_pushcfunction(cb_ctx->L, traceback);
lua_setiuservalue(L, -2, 1);
lua_getfield(L, LUA_REGISTRYINDEX, "callback_context");
lua_setiuservalue(L, -2, 2);
lua_setfield(L, LUA_REGISTRYINDEX, "callback_context");
// 3. 把你传进来的 Lua 函数 (例如 dispatch_message) 移动到 cb_ctx->L 中
lua_xmove(L, cb_ctx->L, 1);
// 4. 根据 forward 与否,设置具体回调函数 _forward_pre 或 _cb_pre
skynet_callback(context, cb_ctx, (forward)?(_forward_pre):(_cb_pre));
return 0;
}
整个逻辑就是:
先在 Lua 中创建一个 callback_context
的 userdata。
创建一个新的 Lua 线程 cb_ctx->L
并把 traceback 函数、回调上下文等信息保存好。
将我们在 Lua 调用时传入的函数(如 skynet.dispatch_message
)移动到这个新线程栈中。
最后调用 skynet_callback(context, cb_ctx, (forward)?(_forward_pre):(_cb_pre))
来将 _cb_pre
或 _forward_pre
这两个函数注册为 C 层面的回调。
当有消息到来时,Skynet 会调用这个回调函数 _cb_pre
或 _forward_pre
,它们最终会调用 _cb
。在 _cb
中会做类似:
lua_pushvalue(L,2); // 取到我们实际的回调函数(此处就是 dispatch_message)
lua_pushinteger(L, type);
lua_pushlightuserdata(L, (void *)msg);
lua_pushinteger(L, sz);
lua_pushinteger(L, session);
lua_pushinteger(L, source);
// lua_pcall(L, 5, 0 , trace);
这样就把消息分发给了 Lua 端的 skynet.dispatch_message
,后者再根据消息类型与 session 做进一步的分发处理。
现在把所有步骤串联起来,会是这样的:
C 入口:当 Snlua 服务通过 launcher 或类似机制被创建时,Skynet 内部会调用 init_cb(struct snlua *l, ...)
。
init_cb
:
打开 Lua VM 标准库、Profile、Codecache 等模块;
设置 LUA_PATH
, LUA_CPATH
, LUA_SERVICE
, LUA_PRELOAD
等全局变量;
加载并执行 loader.lua
。
loader.lua:
解析启动参数,找出第一个作为 SERVICE_NAME
;
遍历 LUA_SERVICE
路径,找到正确的服务脚本(main
);
执行 main(select(2, table.unpack(args)))
。
服务脚本:
引入 skynet
模块,调用 skynet.start(main)
;
skynet.start
中:
调用 c.callback(skynet.dispatch_message)
将 skynet.dispatch_message
注册到 C 端;
使用 skynet.init_service(main)
延后执行我们真正的 main
函数来进行初始化逻辑(创建其他服务等)。
消息回调:当有任何消息送到该服务(snlua
)时,Skynet 内部会执行之前注册的回调 _cb_pre
,进而调用 _cb
,将消息推到 Lua 函数栈上,再调用 skynet.dispatch_message
(Lua 函数)进行消息分发和处理。
分层设计:
底层 (skynet_callback
) 做消息循环;
snlua
服务在 init_cb
阶段做 Lua VM 的初始化和关键脚本的加载;
loader.lua
根据服务名找到实际的业务脚本and执行;
最终在 Lua 层用 skynet.start
替用户设置消息回调并执行启动逻辑。
可插拔的服务模式:snlua
是一种服务类型,也可以有别的服务类型(如 logger
、gate
等),它们都有自己的初始化方式,但大体思想是一致的:初始化 -> 注册消息分发函数。
灵活的路径配置:通过 LUA_SERVICE
, LUA_PATH
, LUA_CPATH
等实现了路径灵活可配置,可以在 Skynet 外部进行配置更改,而无需改代码。
综上所述,从 snlua
服务的 C 语言层面 说起,分析了 init_cb()
如何设置 Lua VM 环境并最终执行 loader.lua
;然后又在 Lua 层面 看 loader.lua
如何查找并执行实际的服务脚本;最后该脚本调用 skynet.start(main)
,将回调函数 skynet.dispatch_message
注册到 C 端,形成一个完整的 消息驱动 服务模型。