Skynet 中 snlua 服务 init 细节

news/2025/3/29 5:55:16/

本篇作为 《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 端,形成一个完整的 消息驱动 服务模型。


http://www.ppmy.cn/news/1583174.html

相关文章

31天Python入门——第10天:深入理解值传递·引用传递以及深浅拷贝问题

你好,我是安然无虞。 文章目录 1. 什么是对象2. 对象类型3. 引用传递3.1 基本概念3.2 不可变对象和可变对象的引用传递不可变对象可变对象 3.3 函数参数传递中的引用传递不可变对象作为参数可变对象作为参数 3.4 如何避免可变对象引用传递带来的问题3.5 总结: 值传…

蓝桥杯第十届 特别的数

题目描述 小明对数位中含有 2、0、1、9 的数字很感兴趣(不包括前导 0),在 1 到 40 中这样的数包括 1、2、9、10 至 32、39 和 40,共 28 个,他们的和是 574。 请问,在 1 到 n 中,所有这样的数的…

单纯形法之大M法

1. 问题背景与标准化 在求解某些线性规划问题时,往往难以直接找到初始的基本可行解。特别是当约束中存在等式或 “≥” 类型的不等式时,我们需要引入人工变量来构造一个初始可行解。 考虑如下标准形式问题(假设为最大化问题)&am…

MongoDB 的索引是提高查询性能的核心机制,类似于传统关系型数据库的索引。以下是对 MongoDB 索引的详细说明:

MongoDB 的索引是提高查询性能的核心机制,类似于传统关系型数据库的索引。以下是对 MongoDB 索引的详细说明: 一、索引基础 1. 索引的作用 加速查询:通过索引快速定位数据,避免全集合扫描(COLLSCAN)。 排…

一文详解redis

redis 5种数据类型 string 字符串是 Redis 里最基础的数据类型,一个键对应一个值。 设置值 SET key value例如: SET name "John"获取值 GET key例如: GET namelist 列表是简单的字符串列表,按插入顺序排序。 在列…

[从零开始学习JAVA] IO流下的高级流

前言: 在前面我们从IO流体系出发,分别介绍了字节流和字符流,并且详细讲解了其下沿的各种基本流,而在今天我们就要学习一下IO流中的几个高级流。 1.缓冲流 缓冲流其实就是可以让我们自己手动提供缓冲区,以此来增加读写速…

NLP高频面试题(十一)——RLHF的流程有哪些

随着大语言模型(如GPT系列)的快速发展,RLHF(Reinforcement Learning from Human Feedback,即基于人类反馈的强化学习)逐渐成为训练高质量模型的重要方法。本文将简单清晰地介绍RLHF的整体流程。 一、RLHF …

Python第六章12:序列切片练习题

# 序列切片的课后题 # 有字符串:“蛋八狗,的十八臭个这,娃芦葫爱最曼特奥” # 请使用学过的任何方式,得到“臭八十的” # 参考方法: # 1.倒序字符串,切片取出或切片取出,然后倒序 # 2.s…