渣渣兔 发表于 2025-3-29 16:14:22

Skynet 中 snlua 服务 init 细节

本篇作为 《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. 停止 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 端继承完成后续的流程。
三、Lua 层面:loader.lua

local strArgs, resumeX = ...
local args = {}
local filename
for word in string.gmatch(strArgs, "%S+") do
    table.insert(args, word)
end

SERVICE_NAME = args

-- 根据 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)
    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 乐成找到了你指定的服务脚本,然后把控制权交给它。
四、服务启动脚本:main 函数与 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() 用来让当前服务的主协程退出。
五、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 = 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 函数,并做一些初始化操作。
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_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 端,形成一个完备的 消息驱动 服务模子。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Skynet 中 snlua 服务 init 细节