本篇作为 《Skynet 中 snlua 服务启动整体流程分析》的内容补充,主要是从 C 语言层面 一步步剖析,到 Lua 层面(loader.lua、服务启动脚本),最后再讲解如何将回调函数设为 skynet.dispatch_message
。主要希望能更好地理解 Skynet 如何初始化一个 snlua
服务,并让你对它的启动机制有一个全面、细致的认知。
一、前置背景:snlua 服务是什么?
在 Skynet 中,snlua
是最主要的 Lua VM 服务类型。它会启动一个独立的 Lua 虚拟机,加载指定的 Lua 代码,以便运行脚本逻辑。Skynet 的核心思想是 “一个服务进程内多个 Lua VM 服务并行运行”,互相通过消息通信来分工协作。
二、C 层面:init_cb 的执行流程
在 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. 停止 GClua_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.luaconst 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 端继续完成后续的流程。
三、Lua 层面:loader.lua
local strArgs, resumeX = ...
local args = {}
local filename
for word in string.gmatch(strArgs, "%S+") dotable.insert(args, word)
endSERVICE_NAME = args[1]-- 根据 SERVICE_NAME 去 LUALIB_SERVICE 路径里找到可执行脚本
local main, pattern
local err = {}
for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") dofilename = string.gsub(pat, "?", SERVICE_NAME)local f, msg = loadfile(filename)if not f thentable.insert(err, msg)elsepattern = patmain = fbreakend
endif not main thenerror(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 thenservice_path = string.gsub(service_path, "?", args[1])package.path = service_path .. "?.lua;" .. package.pathSERVICE_PATH = service_path
elselocal p = string.match(pattern, "(.*/).+$")SERVICE_PATH = p
end-- 如果有 preload 脚本,则先执行它
if LUA_PRELOAD thenlocal 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 成功找到了你指定的服务脚本,然后把控制权交给它。
四、服务启动脚本:main 函数与 skynet.start
继续看你提供的 启动脚本(示例是 的逻辑):
-- 启动脚本local function main()-- 省略业务启动逻辑local XX= skynet.newservice('XX')local XX= skynet.newservice('XX')skynet.exit()
endskynet.start(main)
这段脚本的核心在于 skynet.start(main)
。在 Skynet 中,每个服务启动时,都会调用 skynet.start
来注册一个 回调函数,并执行初始化逻辑。其典型流程是:
-
skynet.start(main)
会将main
函数存起来,等到所有初始化skynet_require.init_all() 就绪以后,再执行main
。 -
skynet.uniqueservice('...')
和skynet.newservice('...')
则是向 Skynet 框架请求创建(或获取)相应名称的服务。 -
最后
skynet.exit()
用来让当前服务的主协程退出。
五、C 层面:c.callback / skynet_callback 与 dispatch_message
1. 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 = nilend)
end
c.callback(skynet.dispatch_message)
就是调用了 lcallback
的 C 函数。这一步完成了 “将 Lua 中的一个回调函数 skynet.dispatch_message,注册到 C 端” 的操作。当有消息到达时,Skynet 会调用该回调,进而在 Lua 中调用 skynet.dispatch_message
做分发。
而 skynet.init_service(start_func)
会在一个定时器触发的时机中执行,用来调用你传入的 main
函数,并做一些初始化操作。
2. 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_contextlua_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_preskynet_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)))
。
-
-
服务脚本:
-
消息回调:当有任何消息送到该服务(
snlua
)时,Skynet 内部会执行之前注册的回调_cb_pre
,进而调用_cb
,将消息推到 Lua 函数栈上,再调用skynet.dispatch_message
(Lua 函数)进行消息分发和处理。
七、核心要点
-
分层设计:
-
可插拔的服务模式:
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 端,形成一个完整的 消息驱动 服务模型。