skynet 源码阅读 -- 核心概念服务 skynet_context

本文从 Skynet 源码层面深入解读 服务(Service) 的创建流程。从最基础的概念出发,逐步深入 skynet_context_new 函数、相关数据结构(skynet_context, skynet_module, message_queue 等),并通过流程图、结构图、以及源码片段的细节分析,希望能对 Skynet 服务的创建有一个由浅入深的系统认识。​​


1. 前言

Skynet 中,"服务(Service)"是最核心的概念之一。它将所有逻辑视为一个个独立且消息驱动的"小进程",每个服务在单线程上下文中处理自己的消息队列。

  • 你可以把"服务"理解成"Actor"或"轻量进程"。类似于 erlang。
  • 每个服务都有自己的 skynet_context、自己的消息队列 message_queue、以及对应的 Lua/C 实例(skynet_module_instance_create)等,对于 lua 服务而言,mod 就是service_snlua。
  • 当你在 Lua 代码里调用 skynet.newservice("xxx")skynet.uniqueservice("xxx") 时,底层就是通过类似 skynet_context_new 来完成实际创建。所以。服务在 c 层次就是 skynet_context。

本篇将系统地分析服务创建过程:从查询 module、创建 context 到初始化,并将此过程与 Skynet 的"消息驱动"模型联系起来,期望让读者对 Skynet 源码进一步了解与掌握。


2. 服务概念与 Skynet 设计哲学

  1. 消息驱动模型
    Skynet 采用Actor 模式,每个服务独立处理消息,每个服务都有自己的消息队列;服务之间通过消息队列handle进行通信。类似的,在erlang 当中,进程间也是通过 message 通信。
  2. 轻量级"上下文"
    每个服务不需要独立进程或操作系统线程,而是共享工作线程, 因此需要用**skynet_context** 维护服务状态。正如 启动主流程 文中的分析,woker 线程的数量是在启动时候读配置固定创建的,而服务动态创建。
  3. C/S"模块"
    许多服务可以基于 Lua 脚本(snlua)、或者 C 模块(cservice 等),都由 skynet_module 统一加载。

在这样的设计下,"服务创建"过程就成了将一个 module 实例 绑定到一个 skynet_context 上,并分配消息队列handle 、以及进行init回调的过程。


3. 服务创建的整体流程概览

下面是简单的整体概览,后文会更深入解析:

  1. 查找 Module : skynet_module_query(name) -> 得到 struct skynet_module *mod
  2. 创建 Module 实例 : skynet_module_instance_create(mod) -> (通常是调用 mod->create)
  3. 分配 skynet_context : skynet_malloc(sizeof(*ctx))
  4. 注册 handle : ctx->handle = skynet_handle_register(ctx)
  5. 创建消息队列 : ctx->queue = skynet_mq_create(ctx->handle)
  6. 调用 module init 回调 : skynet_module_instance_init(mod, inst, ctx, param)
  7. 若 init成功 -> 将服务的消息队列加入全局消息队列 -> 返回 ctx; 否则 -> 清理并返回NULL

这就是skynet_context_new函数的最核心逻辑,也就是 服务在底层C层面被创建出来的过程。


4. 关键数据结构分析

4.1 Skynet Context(skynet_context

复制代码
struct skynet_context {
    void * instance;                   // 对应 module 的具体实例指针,c 服务或者是 lua 沙盒服务
    struct skynet_module * mod;        // 指向加载的 module
    void * cb_ud;                      // user data for callback
    skynet_cb cb;                      // callback function
    struct message_queue *queue;       // 该服务的消息队列
    ATOM_POINTER logfile;              // 日志文件指针(原子操作)
    uint64_t cpu_cost;                 // 用于统计消耗
    uint64_t cpu_start;                // 开始时cpu时间
    char result[32];
    uint32_t handle;                   // 唯一 handle ID
    int session_id;                    // 记录当前 session
    ATOM_INT ref;                      // 引用计数
    int message_count;                 // 处理消息计数
    bool init;                         // 是否完成 init
    bool endless;                      // 是否endless
    bool profile;                      // 是否启用profile
    CHECKCALLING_DECL                  // debug calling
};

要点:

  • handle :Skynet 用一个全局Handle表(skynet_handle)来标识服务,handle即此服务ID。
  • instance:每个服务都有一个"module实例"指针(可能是C struct或Lua VM)
  • cb :当队列里有消息时,会调用 cb(ctx, ud, type, session, msg, sz) 这样的callback。
  • queue :指向自己专属的消息队列
  • ref:原子引用计数, 用来安全释放 context。

4.2 Skynet Module(skynet_module

复制代码
struct skynet_module {
    const char * name;
    void * module;
    skynet_dl_create create;
    skynet_dl_init init;
    skynet_dl_release release;
    skynet_dl_signal signal;
};
  • module:可理解为"动态库(.so) + 相关函数指针"
  • create, init, release, signal :函数指针, 用于C服务的生命周期管理。
    • create: 创建实例
    • init: 初始化该实例
    • release: 释放
    • signal: 处理信号

4.3 Message Queue(message_queue

复制代码
struct message_queue {
    struct spinlock lock;
    uint32_t handle;
    int cap;
    int head;
    int tail;
    int release;
    int in_global;
    int overload;
    int overload_threshold;
    struct skynet_message *queue;
    struct message_queue *next;
};
  • handle: 表示这个队列属于哪个服务
  • queue[] : 存放实际消息 (结构:skynet_message)
  • head, tail, cap: 环形队列实现
  • release: 标记是否已释放
  • lock: 自旋锁 保护并发(可能worker在 pop / push)

Worker线程 要向这个服务发送消息时,会push消息进 ctx->queue;当该服务执行时,会 pop 消息并调用 ctx->cb 处理。


5. 深度解读 skynet_context_new

下面是部分源码节选,并逐段说明:

复制代码
struct skynet_context * 
skynet_context_new(const char * name, const char *param) {
    // Step 1) 查找 module
    struct skynet_module * mod = skynet_module_query(name);
    if (mod == NULL)
        return NULL;

    // Step 2) 创建 module 实例
    void *inst = skynet_module_instance_create(mod);
    if (inst == NULL)
        return NULL;

    // Step 3) 分配 skynet_context
    struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
    CHECKCALLING_INIT(ctx)

    ctx->mod = mod;
    ctx->instance = inst;
    ATOM_INIT(&ctx->ref , 2);
    ctx->cb = NULL;
    ctx->cb_ud = NULL;
    ctx->session_id = 0;
    // ...

    // Step 4) 注册 handle, 并创建消息队列
    ctx->handle = skynet_handle_register(ctx);
    struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);

    // Step 5) 调用 module 的 init 回调
    int r = skynet_module_instance_init(mod, inst, ctx, param);
    if (r == 0) {
        struct skynet_context * ret = skynet_context_release(ctx);
        if (ret) {
            ctx->init = true;
        }
        skynet_globalmq_push(queue);
        if (ret) {
            skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
        }
        return ret;
    } else {
        // Step 6) 失败处理:清理
        skynet_error(ctx, "FAILED launch %s", name);
        uint32_t handle = ctx->handle;
        skynet_context_release(ctx);
        skynet_handle_retire(handle);
        struct drop_t d = { handle };
        skynet_mq_release(queue, drop_message, &d);
        return NULL;
    }
}

5.1 Step 1:skynet_module_query(name)

  • Skynet 初始化时,skynet_module_init 中会加载可用模块列表(记录 name->dlopen() + create/init...)。
  • skynet_module_query(name) 就是根据字符串 (如 "logger", "snlua", "cservice_xxx")来找到 struct skynet_module *.
  • 若没找到 => 返回 NULL => 创建失败。

5.2 Step 2:skynet_module_instance_create(mod)

  • 调用 mod->create 指针(由 C服务实现), 这通常会返回一个"实例"指针
    • 例如 logger 服务就返回 logger对象, snlua 服务就返回 snlua对象(Lua VM).

5.3 Step 3:分配 skynet_context

  • skynet_malloc(sizeof(*ctx)) => 得到一个新的 skynet_context
  • 初始化 ctx->mod = mod; ctx->instance = inst; ref=2; ...
    • ref=2 :初始引用计数为2, 这表示1是自己 + 1是别的地方使用(具体可参看 skynet_context_release 机制)
  • ctx->init=false => 还没完成初始化

5.4 Step 4:注册 handle & 创建队列

  1. ctx->handle = skynet_handle_register(ctx)
    • 这里会到全局 Handle 管理 中找一个新的id(如 #10, #11 ...), 并把 (handle -> ctx) 存起来。
  2. ctx->queue = skynet_mq_create(ctx->handle)
    • 新建一个 message_queue,并设置 handle = ctx->handle
    • 这样后续 发往这个 handle 的消息,会 push 到 ctx->queue.

5.5 Step 5:调用 module init 回调

  • int r = skynet_module_instance_init(mod, inst, ctx, param);
    • 这会执行 mod->init(inst, ctx, param), mod 初始化。
  • 如果 r == 0, 表示init成功
    • skynet_context_release(ctx) => 在成功情况下会减少ref计数, 可能最终 ref=1 => 也可让 context 继续活着.
    • ctx->init=true => 标记 init成功
    • skynet_globalmq_push(queue) => 把这个队列推到全局队列 => 后面 Worker 线程会处理它的消息
    • 最后返回 ctx

5.6 Step 6:失败处理

  • 若 init 返回非0 => 说明启动失败
    • 打印 "FAILED launch name"
    • skynet_context_release(ctx) => 释放 context
    • skynet_handle_retire(handle) => 将 handle 标记为"废弃"
    • skynet_mq_release(queue, ...) => 释放队列, 并尝试 drop 未处理消息
    • 返回 NULL

通过这样一步步 的流程, 该函数就创建(或失败)一个新的Skynet服务


6. 服务初始化与消息分发简述

  • skynet_context_new 成功返回后,Worker 线程就能从global queue中发现该服务的消息队列 => 开始分发消息
  • ctx->cb(回调)在 init 里可能被设定(例如 snlua 里 skynet_callback(ctx, l , launch_cb);), 之后 Worker 线程拿到消息, 就会调用 cb(ctx, cb_ud, msg...)

这就是Skynet服务模型:

  1. 每个服务对应一个 skynet_context
  2. 消息 放到 message_queue => Worker 线程调 cb() => 处理

7. 流程图:Service Creation

以下是简易时序图(ASCII示意):

复制代码
        +-----------------------------+
        | skynet_module_query(name)  |
        v                             |
[No mod? -> return NULL]             |
+-----------------------------+       |
| skynet_module_instance_create(mod) |
+-----------------------------+       |
        |  (inst)                     |
        v                             |
+-----------------------------+       |
| ctx = skynet_malloc(...)    |
| ctx->mod = mod; ctx->instance=inst |
+-----------------------------+       |
        |                             |
        v                             |
+-----------------------------+       |
| ctx->handle = skynet_handle_register(ctx)
| ctx->queue = skynet_mq_create(handle)
+-----------------------------+       |
        |                             |
        v                             |
+-------------------------------------------+
| r = skynet_module_instance_init(mod,...)  |
+-------------------------------------------+
          | if (r==0) success  |  else fail
          |                    |
 success---+                    +-----> fail:
  set ctx->init=true                 retire handle
  globalmq_push(queue)               release queue
  return ctx                         return NULL

8. 源码走读与关键步骤详解

8.1 skynet_module_query

  • 定位在 skynet_module.c,内部维护一个 static struct modules *M 全局结构,里面记录已经加载的C服务
  • skynet_module_query(name) 就遍历 M->m[] 里找 module->name == name => 返回指针.
  • 若找不到 => 返回NULL.

8.2 skynet_module_instance_create(mod)

  • mod->create(...) 常见写法是在 .so 里导出 xxx_create 函数 => 分配C struct
  • 例如 snlua_create 会新建一个 struct snlua(包含lua_State)
  • 这一步并没有调用 init, 只是一种"先建实例,再init"的两段式构造.

8.3 skynet_handle_register

  • skynet_handle_register(ctx) 就返回一个全局唯一的 handle(>=1).
  • 以后发送消息 时, 只需 skynet_send( to_handle, ... ), Skynet可以reverse handle->context => 找到 to_ctx->queue.

8.4 skynet_mq_create

  • mq = skynet_malloc(sizeof(*mq)) + ... => init capacity, lock=0, handle=..., etc.
  • 这样一个 队列就和ctx->handle 绑定
  • 之后push 消息 => mq->queue[tail] = message; tail=(tail+1)%cap;

8.5 skynet_module_instance_init

  • r = mod->init(inst, ctx, param)
  • 这是C服务具体写的init函数, 可能做Lua加载脚本, 也可能加载资源, etc.

9. 服务创建中涉及的锁与引用计数

    • spinlock lockmessage_queue or globalmq 里保证并发安全
    • skynet_context_new 里不怎么显式使用锁,但内部handle_register & mq_create 都会用到全局/队列锁
  1. 引用计数 ( ATOM_INT ref ):
    • 初始=2 => 代表这个 context 还有2个持有者:
      1. handle_storage 全局
      2. 自己(在 new 函数中)
    • skynet_context_release(ctx) => ref-- => 如果==0 => free context
    • 这样可以保证在init失败或成功后 context 处理都不会重复释放.

10. Skynet 服务模型的优势与局限

  • 优势
    1. Actor 模式,context + queue => 易于并发下消息封装
    2. 强大的灵活性:C服务 / Lua服务都可 => 只要 module create + init + release
    3. 在游戏业务当中就是 不同的业务可以创建不同的 服务去处理,天然隔离,不用考虑并发问题,数据都是隔离的,仅通过消息传递。
  • 局限
    1. 需要对消息无共享(Actor风格), 适合"异步消息驱动"
    2. 单个服务最多占用单线程 的 cpu,slg 大地图场景下,如果服务不好拆分,但是又需要大量计算的场景可能会有些麻烦。在java 中,可以直接起线程池,并行计算。skynet 中当然也可以启动 服务池 并行计算,但是数据的共享又会是个问题,后续研究:
      第一种:消息传递只传递指针,而不对消息进行拷贝。可行吗?
      第二种:共享内存来进行数据传递。

11. 总结

skynet_context_new 正是 Skynet 服务创建的核心。它背后包含模块系统(C服务管理)、Handle系统(服务ID分配)、消息队列系统(异步驱动),以及初始化回调(每种服务各自逻辑)等多个模块协同。

通过数据结构 ( skynet_context, skynet_module, message_queue)与关键流程 ( module_instance_create, handle_register...) 的介绍,我们可以看到Skynet 对"服务"这一抽象的高内聚 设计------一个 context就是一个服务**:

  1. Module表征它的实现( C or Lua )
  2. Context 整合状态(handle, instance, queue, cb...)
  3. Queue存放消息
  4. init / release 是服务的生命周期

后续深入阅读,追踪消息 是如何被投递服务 并由Worker 线程执行,则可以继续研究**skynet_context_message_dispatch等函数;如果你想了解 C服务如何编写 module,则可以研究 skynet_module.c** 以及具体 cservice 示例(logger.c, service_snlua.c

等)。

相关推荐
.生产的驴9 小时前
SpringBoot 接口限流Lua脚本接合Redis 服务熔断 自定义注解 接口保护
java·大数据·数据库·spring boot·redis·后端·lua
独隅2 天前
Lua 中,`if-else` 的详细用法
开发语言·junit·lua·lua5.4
hycccccch3 天前
超卖问题解决方案
java·笔记·lua
monstercl3 天前
【Lua】pcall使用详解
开发语言·lua
Thomas游戏开发3 天前
Unity3D事件驱动架构设计指南
前端框架·unity3d·游戏开发
monstercl3 天前
skynet.dispatch 使用详解
lua·skynet·游戏服务器
小豆同学1985083 天前
Lua在线运行网址
lua
知来者逆4 天前
探索生成式AI在游戏开发中的应用——3D角色生成式 AI 实现
人工智能·深度学习·神经网络·计算机视觉·3d·游戏开发
爱的叹息4 天前
Redis 除了数据类型外的核心功能 的详细说明,包含事务、流水线、发布/订阅、Lua 脚本的完整代码示例和表格总结
数据库·redis·lua
一个程序员(●—●)5 天前
xLua环境控制+xLua的Lua调用C#的1
开发语言·unity·c#·lua