Skynet 中 snlua 服务 init 细节

打印 上一主题 下一主题

主题 1582|帖子 1582|积分 4746

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本篇作为 《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() 源代码
  1. static int
  2. init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
  3.         lua_State *L = l->L;
  4.         l->ctx = ctx;
  5.         // 1. 停止 GC
  6.         lua_gc(L, LUA_GCSTOP, 0);
  7.         // 2. 设置一些 Lua 环境,打开标准库
  8.         lua_pushboolean(L, 1);  /* signal for libraries to ignore env. vars. */
  9.         lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
  10.         luaL_openlibs(L);
  11.         // 3. 加载 "skynet.profile" 模块, 替换 coroutine 的方法
  12.         luaL_requiref(L, "skynet.profile", init_profile, 0);
  13.         int profile_lib = lua_gettop(L);
  14.         lua_getglobal(L, "coroutine");
  15.         lua_getfield(L, profile_lib, "resume");
  16.         lua_setfield(L, -2, "resume");
  17.         lua_getfield(L, profile_lib, "wrap");
  18.         lua_setfield(L, -2, "wrap");
  19.         lua_settop(L, profile_lib - 1);
  20.         // 4. 往注册表里塞一些数据
  21.         lua_pushlightuserdata(L, ctx);
  22.         lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");
  23.         // 5. 加载 codecache 模块 (允许 Skynet 做一些代码缓存逻辑)
  24.         luaL_requiref(L, "skynet.codecache", codecache , 0);
  25.         lua_pop(L,1);
  26.         // 6. 重新启动 GC (使用分代式 GC)
  27.         lua_gc(L, LUA_GCGEN, 0, 0);
  28.         // 7. 设置各种路径到全局变量 (LUA_PATH, LUA_CPATH, LUA_SERVICE 等)
  29.         const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
  30.         lua_pushstring(L, path);
  31.         lua_setglobal(L, "LUA_PATH");
  32.         const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
  33.         lua_pushstring(L, cpath);
  34.         lua_setglobal(L, "LUA_CPATH");
  35.         const char *service = optstring(ctx, "luaservice", "./service/?.lua");
  36.         lua_pushstring(L, service);
  37.         lua_setglobal(L, "LUA_SERVICE");
  38.         const char *preload = skynet_command(ctx, "GETENV", "preload");
  39.         lua_pushstring(L, preload);
  40.         lua_setglobal(L, "LUA_PRELOAD");
  41.         // 8. 压入 traceback 函数,保证出错时能打印堆栈
  42.         lua_pushcfunction(L, traceback);
  43.         assert(lua_gettop(L) == 1);
  44.         // 9. 加载并执行 loader.lua
  45.         const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
  46.         int r = luaL_loadfile(L,loader);
  47.         if (r != LUA_OK) {
  48.                 skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));
  49.                 report_launcher_error(ctx);
  50.                 return 1;
  51.         }
  52.         // 将 args (服务启动参数) 作为 loader.lua 的入参
  53.         lua_pushlstring(L, args, sz);
  54.         r = lua_pcall(L,1,0,1);
  55.         if (r != LUA_OK) {
  56.                 skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
  57.                 report_launcher_error(ctx);
  58.                 return 1;
  59.         }
  60.         lua_settop(L,0);
  61.         // 10. 如果有内存限制 memlimit,就打印日志
  62.         if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) {
  63.                 size_t limit = lua_tointeger(L, -1);
  64.                 l->mem_limit = limit;
  65.                 skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024));
  66.                 lua_pushnil(L);
  67.                 lua_setfield(L, LUA_REGISTRYINDEX, "memlimit");
  68.         }
  69.         lua_pop(L, 1);
  70.         lua_gc(L, LUA_GCRESTART, 0);
  71.         return 0;
  72. }
复制代码
从上面可以看出,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

  1. local strArgs, resumeX = ...
  2. local args = {}
  3. local filename
  4. for word in string.gmatch(strArgs, "%S+") do
  5.     table.insert(args, word)
  6. end
  7. SERVICE_NAME = args[1]
  8. -- 根据 SERVICE_NAME 去 LUALIB_SERVICE 路径里找到可执行脚本
  9. local main, pattern
  10. local err = {}
  11. for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do
  12.     filename = string.gsub(pat, "?", SERVICE_NAME)
  13.     local f, msg = loadfile(filename)
  14.     if not f then
  15.         table.insert(err, msg)
  16.     else
  17.         pattern = pat
  18.         main = f
  19.         break
  20.     end
  21. end
  22. if not main then
  23.     error(table.concat(err, "\n"))
  24. end
  25. -- 把之前在全局设置的 LUA_PATH, LUA_CPATH, LUA_SERVICE 赋给 package
  26. LUA_SERVICE = nil
  27. package.path, LUA_PATH = LUA_PATH
  28. package.cpath, LUA_CPATH = LUA_CPATH
  29. -- 如果匹配到相对路径,就把对应目录加入到 package.path 当中
  30. local service_path = string.match(pattern, "(.*/)[^/?]+$")
  31. if service_path then
  32.     service_path = string.gsub(service_path, "?", args[1])
  33.     package.path = service_path .. "?.lua;" .. package.path
  34.     SERVICE_PATH = service_path
  35. else
  36.     local p = string.match(pattern, "(.*/).+$")
  37.     SERVICE_PATH = p
  38. end
  39. -- 如果有 preload 脚本,则先执行它
  40. if LUA_PRELOAD then
  41.     local f = assert(loadfile(LUA_PRELOAD))
  42.     f(table.unpack(args))
  43.     LUA_PRELOAD = nil
  44. end
  45. _G.require = (require "skynet.require").require
  46. -- Tracy profiler 的一些逻辑 (省略)
  47. -- ...
  48. -- 最终执行 main 脚本(该脚本就是我们真正的服务脚本,就是在 启动配置中配置的启动入口文件 等等)
  49. 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

继承看你提供的 启动脚本(示例是 的逻辑):
  1. -- 启动脚本
  2. local function main()
  3.     --  省略业务启动逻辑
  4.     local XX= skynet.newservice('XX')
  5.     local XX= skynet.newservice('XX')
  6.     skynet.exit()
  7. end
  8. 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,源码如下:
  1. function skynet.start(start_func)
  2.     c.callback(skynet.dispatch_message)     -- 这里 c.callback(...) 会调用到 C 代码
  3.     init_thread = skynet.timeout(0, function()
  4.         skynet.init_service(start_func)
  5.         init_thread = nil
  6.     end)
  7. 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 函数):
  1. static int
  2. lcallback(lua_State *L) {
  3.     struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
  4.     int forward = lua_toboolean(L, 2);
  5.     luaL_checktype(L,1,LUA_TFUNCTION);
  6.     lua_settop(L,1);
  7.     // 1. 创建 callback_context 结构
  8.     struct callback_context * cb_ctx = (struct callback_context *)lua_newuserdatauv(L, sizeof(*cb_ctx), 2);
  9.     cb_ctx->L = lua_newthread(L);
  10.     // 2. 给该 coroutine 保存 traceback 和 callback_context
  11.     lua_pushcfunction(cb_ctx->L, traceback);
  12.     lua_setiuservalue(L, -2, 1);
  13.     lua_getfield(L, LUA_REGISTRYINDEX, "callback_context");
  14.     lua_setiuservalue(L, -2, 2);
  15.     lua_setfield(L, LUA_REGISTRYINDEX, "callback_context");
  16.     // 3. 把你传进来的 Lua 函数 (例如 dispatch_message) 移动到 cb_ctx->L 中
  17.     lua_xmove(L, cb_ctx->L, 1);
  18.     // 4. 根据 forward 与否,设置具体回调函数 _forward_pre 或 _cb_pre
  19.     skynet_callback(context, cb_ctx, (forward)?(_forward_pre):(_cb_pre));
  20.     return 0;
  21. }
复制代码
整个逻辑就是:

  • 先在 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 中会做雷同:
  1. lua_pushvalue(L,2);          // 取到我们实际的回调函数(此处就是 dispatch_message)
  2. lua_pushinteger(L, type);
  3. lua_pushlightuserdata(L, (void *)msg);
  4. lua_pushinteger(L, sz);
  5. lua_pushinteger(L, session);
  6. lua_pushinteger(L, source);
  7. // 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企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

渣渣兔

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表