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 位换一下,本地查不到就去远端。

相关推荐
笨鸟先飞的橘猫9 天前
skynet——网络模块学习
网络·学习·skynet
笨鸟先飞的橘猫14 天前
skynet——sharetable学习
学习·skynet
笨鸟先飞的橘猫14 天前
skynet——分布式支持
分布式·skynet
笨鸟先飞的橘猫17 天前
skynet监控实践
监控·skynet
Huanzhi_Lin2 个月前
skynet笔记
笔记·lua·skynet·actor·actor模型
笨鸟先飞的橘猫2 个月前
MMO游戏中的“跨服团队副本”匹配与状态同步系统
分布式·学习·游戏·lua·skynet
沐风ya4 个月前
skynet消息(构成,session,type介绍),数据库与内存管理,消息调用链(发送层,调度层,lua层,总结),外部服务(介绍,skynet内置支持)
skynet
利来利往4 个月前
skynet指定的多个服务绑定同一个线程
lua·skynet·绑核
沐风ya4 个月前
skynet概述(service,lua虚拟机,架构设计,如何解决核心问题)
lua·skynet·actor