什么是上下文
在 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;
}
-
skynet_module_query → 找 snlua.so(或编译进来的内置模块)
-
mod->create → snlua_create() 建 Lua VM,塞 ctx->instance
-
skynet_handle_register → 分配 handle,放进全局 handle_storage
-
skynet_mq_create → 每个服务独占一个 message_queue
-
mod->init → snlua_init() 会往自己队列里压一条 "start" 消息,Lua 层 skynet.start 注册的回调就在处理这条消息时被调用
-
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 位换一下,本地查不到就去远端。