skynet——上下文管理

什么是上下文

在 skynet 中,一个服务 ≠ 一个 Lua VM ≠ 一个进程,而是:

  • C 层有一个 struct skynet_context 结构体

  • Lua 层(snlua 模块)里有一个 Lua VM

  • 两者之间通过 context->ud 互相绑定

你可以把 context 理解为服务的"身份证 + 邮箱 + 把手":

  • 身份证:handle(服务地址,32位,高8位是 harbor id,低24位是本地序号)

  • 邮箱:queue(每个 context 绑定一个消息队列,worker 线程从这里取消息)

  • 把手:cb(消息回调)、mod(绑定的 C 模块,如 snlua)、data(模块私有数据,Lua 层就是 lua_State*)

c 复制代码
struct skynet_context {
    void * instance;          // 模块实例数据(snlua 下就是 lua_State*)
    struct skynet_module * mod;  // 绑定的模块(snlua / logger / gate ...)
    void * cb_ud;             // 回调用户数据(通常 = instance)
    skynet_cb cb;             // 消息回调函数(模块的 callback)
    struct message_queue * queue;  // 该服务的消息队列(!!关键)
    FILE * logfile;           // 服务级日志文件
    uint64_t cpu_cost;        // 统计:CPU 耗时
    uint64_t cpu_start;       // 统计:上次计时起点
    char result[32];          // 用于阻塞 API 的返回值暂存
    uint32_t handle;          // 服务地址(handle,全局唯一)
    int session_id;           // 自增 session(用于 call/response 配对)
    int ref;                  // 引用计数
    bool init;                // 是否已执行 module->init
    bool endless;             // 是否标记为"死循环"服务(监控用)
    // ... 其他字段(锁、消息统计等)
};

生命周期

创建

当在 Lua 里写 skynet.newservice("foo") 或在 C 里调 skynet_context_new("snlua", "foo"),流程是:

c 复制代码
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
    // 1. 查找模块(skynet_module 是 .so/.dll 的动态库抽象)
    struct skynet_module * mod = skynet_module_query(name);  // "snlua"
    
    // 2. 分配 context
    struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
    memset(ctx, 0, sizeof(*ctx));
    
    // 3. 创建模块实例(snlua 会在这里 new 一个 Lua VM)
    void *instance = mod->create(name, param);  // → snlua_create()
    ctx->mod = mod;
    ctx->instance = instance;
    ctx->ref = 2;   // 初始 ref=2:一个给 handle_storage,一个给创建者
    
    // 4. 分配 handle(服务地址)
    ctx->handle = skynet_handle_register(ctx);  // → 放进全局 handle 表
    
    // 5. 创建该服务的消息队列(!!每个服务一个私有队列)
    struct message_queue *q = skynet_mq_create(ctx->handle);
    ctx->queue = q;
    
    // 6. 调用模块 init(snlua 在这里会加载 foo.lua 并执行 skynet.start)
    int r = mod->init(ctx, instance);  // → snlua_init()
    ctx->init = true;
    
    // 7. 把队列挂到全局"待调度队列"(skynet_mq_force_push)
    //    之后 worker 线程才能取到这个服务的消息
    skynet_mq_push_global(q);
    
    return ctx;
}
  1. skynet_module_query → 找 snlua.so(或编译进来的内置模块)

  2. mod->create → snlua_create() 建 Lua VM,塞 ctx->instance

  3. skynet_handle_register → 分配 handle,放进全局 handle_storage

  4. skynet_mq_create → 每个服务独占一个 message_queue

  5. mod->init → snlua_init() 会往自己队列里压一条 "start" 消息,Lua 层 skynet.start 注册的回调就在处理这条消息时被调用

  6. skynet_mq_push_global → 把自己队列挂到全局"公海",worker 才能扫到

消息投递

Lua 层调 skynet.send(handle, "lua", ...) → C 层走 skynet_context_push(handle, msg):

c 复制代码
int
skynet_context_push(uint32_t handle, struct skynet_message *msg) {
    // 1. 通过 handle 查 context(从全局 handle_storage 的 slot 数组 O(1) 取出)
    struct skynet_context * ctx = skynet_handle_grab(handle);
    if (ctx == NULL) return -1;
    
    // 2. 把消息压进该服务的私有队列
    skynet_mq_push(ctx->queue, msg);
    
    // 3. ref--
    skynet_context_release(ctx);
    return 0;
}

**注意:**skynet_handle_grab 会 ref++,release 会 ref--。这是 context 不被提前释放的关键。

Worker 线程取消息

c 复制代码
// skynet_worker.c
static void *
worker(void *p) {
    struct worker_parm *wp = p;
    for (;;) {
        // 1. 从全局"公海"弹出一个服务的队列
        struct message_queue *q = skynet_mq_pop_global();
        
        // 2. 通过队列里的 handle 拿到 context
        uint32_t handle = skynet_mq_handle(q);
        struct skynet_context *ctx = skynet_handle_find(handle);
        
        // 3. 从这个队列里取一条消息
        struct skynet_message *msg = skynet_mq_pop(q);
        
        // 4. 调用 ctx->cb(ctx, ctx->cb_ud, msg->type, ...)
        //    snlua 的 cb 是 snlua_cb() → 把消息扔进 Lua VM 走 coroutine
        ctx->cb(ctx, ctx->cb_ud, msg->type, msg->session, 
                msg->source, msg->data, msg->sz);
        
        // 5. 如果队列还有消息,再挂回全局"公海"等下次扫
        //    如果空了,就不挂回去(懒激活)
    }
}

销毁

Lua 层 skynet.exit() → C 层 skynet_context_end():

c 复制代码
void
skynet_context_release(struct skynet_context *ctx) {
    if (ctx == NULL) return;
    
    // ref--,如果降到 0 才真正释放
    if (__sync_sub_and_fetch(&ctx->ref, 1) == 0) {
        // 1. 从 handle_storage 摘除(外部再也查不到这个 handle)
        skynet_handle_retire(ctx->handle);
        
        // 2. 调用模块 release(snlua → 关 Lua VM)
        if (ctx->mod->release) {
            ctx->mod->release(ctx->instance);
        }
        
        // 3. 释放队列、日志文件等
        skynet_mq_mark_release(ctx->queue);
        // ...
        skynet_free(ctx);
    }
}

ref 引用计数的规则(这是最容易晕的地方):

  • skynet_context_new 时 ref = 2(1 个归 handle_storage 持有,1 个归"创建者/当前调用栈"持有)

  • skynet_handle_grab(查 handle 拿 context)时 ref++

  • skynet_context_release 时 ref--

  • skynet_handle_retire(从全局表摘掉)时 ref--(handle_storage 那份释放)

  • 当所有引用方(发消息的、worker 正在处理的、timer 回调持有的)都 release 后,ref 降到 0,才真正 free

这样保证:worker 正处理某服务消息时,即使 Lua 层调了 exit,context 也不会中途被释放。

Handle Storage:全局 context 注册表

所有 context 都登记在一个全局结构里,叫 handle_storage:

c 复制代码
struct handle_storage {
    struct rwlock lock;
    uint32_t harbor;              // 本节点 harbor id(用于 cluster)
    uint32_t handle_index;        // 下一个可用的 handle 序号(低24位循环)
    int slot_size;               // 哈希桶大小(默认 2^16?实际是动态扩)
    struct skynet_context ** slot;  // 核心:一个数组,下标 ≈ handle 低 bits
};

uint32_t skynet_handle_register(struct skynet_context *ctx) {
    // 找一个空闲 slot,把 ctx 塞进去,返回 handle = (harbor << 24) | slot_idx
}
  • handle 是 32 位:高 8 位 = harbor(区分集群节点),低 24 位 = 本节点序号

  • 查询 O(1):skynet_handle_find(handle) 直接 slothandle \& MASK 取 context

  • harbor = 0​ 时(单机/cluster 但 harbor 配 0),高 8 位为 0,handle 就是纯本地序号

这就是为什么 skynet 里 skynet.self() 拿到的是一个整数,发消息时直接传这个整数就能定位到对端 context。

Context 与 Lua VM 的绑定(snlua 这一层)

snlua 是 skynet 最重要的模块,它把 C context 和 Lua VM 粘起来:

c 复制代码
// lualib-src/lua-skynet.c
struct snlua {
    lua_State *L;           // Lua VM
    struct skynet_context *ctx;  // 反向绑回 C context
    int thread_count;       // 正在跑的 coroutine 数
    // ...
};

static int
snlua_create(...) {
    struct snlua *l = skynet_malloc(sizeof(*l));
    l->L = luaL_newstate();      // 新建 Lua VM
    l->ctx = NULL;               // 此时 ctx 还没传进来
    return l;
}

static int
snlua_init(...) {
    struct snlua *l = instance;
    l->ctx = ctx;                         // ← 绑回去
    ctx->cb_ud = l;                       // ctx->cb_ud = snlua 实例
    ctx->cb = snlua_cb;                   // ctx->cb = snlua_cb(消息入口)
    
    // 初始化 Lua 标准库、package.path、把 skynet 库塞进去
    // 然后 luaL_dofile(L, "service/foo.lua") → 执行 foo.lua
    // foo.lua 里 skynet.start(f) → 把 f 存在 registry 里,等第一条 "start" 消息来时调
    // snlua_init 最后给自己队列 push 一条 "start" 消息
}

所以 一条消息从 worker 到 Lua 协程的路径是:

复制代码
worker 取到 q → ctx->cb(ctx, cb_ud, type, session, source, data, sz)
              → snlua_cb() 拿到 l = cb_ud (即 struct snlua*)
              → 从 l->L 里起一个 coroutine
              → 把 data 按协议(lua / text / sproto)解码,call 进 Lua
              → skynet.start 注册的那个函数被执行

常见困惑

Q1: Context 和 service 是一一对应的吗?

是的。一个 Lua 文件 skynet.newservice("foo") → 一个 snlua 进程 → 一个 skynet_context → 一个 lua_State。但 C 模块(如 logger、gate)也可以直接 skynet_context_new("logger", ...) 建 context,不走 Lua。

Q2: 为什么每个服务一个队列,而不是全局一个队列?

全局队列的话,N 个 worker 抢一个锁,竞争激烈。每个服务一个队列 + 全局"公海"只挂有消息待处理的队列,锁竞争降到 O(活跃服务数) 而非 O(总服务数),且 worker 取队列时只需要拿队列自身的锁(很短)。

Q3: ref 到底防什么?

典型场景:Worker A 正在处理服务 S 的消息(持 ctx 指针),此时服务 S 的 Lua 层调了 skynet.exit(),另一线程想把 ctx 释放。如果没有 ref,就 UAF 了。ref 保证:任何持有 ctx 指针的人都必须 grab,用完 release,最后一个人 release 时才 free。

Q4: handle 为什么高 8 位是 harbor?

为了 cluster:NodeA 的 handle 0x01000042 和 NodeB 的 0x02000042 不冲突。cluster 发消息时 cluster.send("node_b", address, ...) 底层就是把 harbor 位换一下,本地查不到就去远端。

相关推荐
笨鸟先飞的橘猫4 天前
skynet线上问题查证练习记录(更新中)
skynet
笨鸟先飞的橘猫12 天前
skynet——RPC 超时机制优化方案
游戏·rpc·skynet
笨鸟先飞的橘猫12 天前
skynet热更新实现问题与排错记录
学习·lua·skynet
笨鸟先飞的橘猫4 个月前
skynet——网络模块学习
网络·学习·skynet
笨鸟先飞的橘猫4 个月前
skynet——sharetable学习
学习·skynet
笨鸟先飞的橘猫4 个月前
skynet——分布式支持
分布式·skynet
笨鸟先飞的橘猫4 个月前
skynet监控实践
监控·skynet
Huanzhi_Lin6 个月前
skynet笔记
笔记·lua·skynet·actor·actor模型
笨鸟先飞的橘猫6 个月前
MMO游戏中的“跨服团队副本”匹配与状态同步系统
分布式·学习·游戏·lua·skynet