您的位置:首页 > 健康 > 养生 > 成都明思源室内设计学校_动画素材网站_网站开发怎么做_百度联盟app

成都明思源室内设计学校_动画素材网站_网站开发怎么做_百度联盟app

2025/4/1 3:26:05 来源:https://blog.csdn.net/a1491758730/article/details/146503512  浏览:    关键词:成都明思源室内设计学校_动画素材网站_网站开发怎么做_百度联盟app
成都明思源室内设计学校_动画素材网站_网站开发怎么做_百度联盟app

本篇作为 《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 入口函数。它主要做了以下几件事:

  1. 停止 GC,执行一些加载或初始化操作,再重新启用 GC。

  2. 加载 Lua 标准库skynet.profileskynet.codecache 等模块,做一些必要的替换或增强,比如把 coroutine.resumecoroutine.wrap 换成了带 Profile 统计的版本。

  3. 设置路径到全局变量:包含 LUA_PATHLUA_CPATHLUA_SERVICE 等,后续就可以在 Lua 里使用 require、或 loadfile 来按照这些路径加载脚本。

  4. 加载并执行 loader.lua。这是关键:loader.lua 是一个特殊的 加载脚本,会根据服务的名字去找到对应的 Lua 服务文件并执行。

  5. 如果有环境变量 memlimit,则记录内存上限。

  6. 最终完成初始化并返回。

到这里为止,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)))

以上的主要逻辑是:

  1. 解析 init_cb() 传入的 args:这里通过 string.gmatch(strArgs, "%S+") 获取启动时的所有参数,并将第一个参数作为 SERVICE_NAME

  2. 根据 LUA_SERVICE 路径查找真正的服务脚本

  3. 修正 package.path 和一些全局变量:为了让此服务后续 require 能寻址到更多文件。

  4. 可选地执行 LUA_PRELOAD:如果 skynet_command(ctx, "GETENV", "preload") 有值,就先执行预加载脚本。

  5. 调用 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 来注册一个 回调函数,并执行初始化逻辑。其典型流程是:

  1. skynet.start(main) 会将 main 函数存起来,等到所有初始化skynet_require.init_all() 就绪以后,再执行 main

  2. skynet.uniqueservice('...')skynet.newservice('...') 则是向 Skynet 框架请求创建(或获取)相应名称的服务。

  3. 最后 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;
}

整个逻辑就是:

  1. 先在 Lua 中创建一个 callback_context 的 userdata。

  2. 创建一个新的 Lua 线程 cb_ctx->L 并把 traceback 函数、回调上下文等信息保存好。

  3. 将我们在 Lua 调用时传入的函数(如 skynet.dispatch_message)移动到这个新线程栈中。

  4. 最后调用 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 做进一步的分发处理。


六、流程总结

现在把所有步骤串联起来,会是这样的:

  1. C 入口:当 Snlua 服务通过 launcher 或类似机制被创建时,Skynet 内部会调用 init_cb(struct snlua *l, ...)

  2. init_cb

    • 打开 Lua VM 标准库、Profile、Codecache 等模块;

    • 设置 LUA_PATH, LUA_CPATH, LUA_SERVICE, LUA_PRELOAD 等全局变量;

    • 加载并执行 loader.lua

  3. loader.lua

    • 解析启动参数,找出第一个作为 SERVICE_NAME

    • 遍历 LUA_SERVICE 路径,找到正确的服务脚本(main);

    • 执行 main(select(2, table.unpack(args)))

  4. 服务脚本

    • 引入 skynet 模块,调用 skynet.start(main)

    • skynet.start 中:

      • 调用 c.callback(skynet.dispatch_message)skynet.dispatch_message 注册到 C 端;

      • 使用 skynet.init_service(main) 延后执行我们真正的 main 函数来进行初始化逻辑(创建其他服务等)。

  5. 消息回调:当有任何消息送到该服务(snlua)时,Skynet 内部会执行之前注册的回调 _cb_pre,进而调用 _cb,将消息推到 Lua 函数栈上,再调用 skynet.dispatch_message(Lua 函数)进行消息分发和处理。


七、核心要点

  1. 分层设计

    • 底层 (skynet_callback) 做消息循环;

    • snlua 服务在 init_cb 阶段做 Lua VM 的初始化和关键脚本的加载;

    • loader.lua 根据服务名找到实际的业务脚本and执行;

    • 最终在 Lua 层用 skynet.start 替用户设置消息回调并执行启动逻辑。

  2. 可插拔的服务模式snlua 是一种服务类型,也可以有别的服务类型(如 loggergate 等),它们都有自己的初始化方式,但大体思想是一致的:初始化 -> 注册消息分发函数

  3. 灵活的路径配置:通过 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 端,形成一个完整的 消息驱动 服务模型。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com