本文从 Skynet 源码层面深入解读 服务(Service) 的创建流程。从最基础的概念出发,逐步深入 skynet_context_new
函数、相关数据结构(skynet_context
, skynet_module
, message_queue
等),并通过流程图、结构图、以及源码片段的细节分析,希望能对 Skynet 服务的创建有一个由浅入深的系统认识。
在 Skynet 中,“服务(Service)”是最核心的概念之一。它将所有逻辑视为一个个独立且消息驱动的“小进程”,每个服务在单线程上下文中处理自己的消息队列。
skynet_context
、自己的消息队列 message_queue
、以及对应的 Lua/C 实例(skynet_module_instance_create
)等,对于 lua 服务而言,mod 就是service_snlua。skynet.newservice("xxx")
或 skynet.uniqueservice("xxx")
时,底层就是通过类似 skynet_context_new
来完成实际创建。所以。服务在 c 层次就是 skynet_context。
本篇将系统地分析服务创建过程:从查询 module、创建 context 到初始化,并将此过程与 Skynet 的“消息驱动”模型联系起来,期望让读者对 Skynet 源码进一步了解与掌握。
skynet_context
维护服务状态。正如 启动主流程 文中的分析,woker 线程的数量是在启动时候读配置固定创建的,而服务动态创建。snlua
)、或者 C 模块(cservice
等),都由 skynet_module
统一加载。在这样的设计下,“服务创建”过程就成了将一个 module 实例绑定到一个 skynet_context上,并分配消息队列、handle、以及进行init回调的过程。
下面是简单的整体概览,后文会更深入解析:
skynet_module_query(name)
-> 得到 struct skynet_module *mod
skynet_module_instance_create(mod)
-> (通常是调用 mod->create)skynet_malloc(sizeof(*ctx))
ctx->handle = skynet_handle_register(ctx)
ctx->queue = skynet_mq_create(ctx->handle)
skynet_module_instance_init(mod, inst, ctx, param)
ctx
; 否则 -> 清理并返回NULL这就是skynet_context_new
函数的最核心逻辑,也就是服务在底层C层面被创建出来的过程。
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。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;
};
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
处理。
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;
}
}
skynet_module_query(name)
skynet_module_init
中会加载可用模块列表(记录 name->dlopen() + create/init...
)。skynet_module_query(name)
就是根据字符串(如 "logger", "snlua", "cservice_xxx")来找到 struct skynet_module *
.skynet_module_instance_create(mod)
mod->create
指针(由 C服务实现), 这通常会返回一个“实例”指针。
skynet_context
skynet_malloc(sizeof(*ctx))
=> 得到一个新的 skynet_context
ctx->mod = mod; ctx->instance = inst; ref=2; ...
skynet_context_release
机制)ctx->init=false
=> 还没完成初始化ctx->handle = skynet_handle_register(ctx)
(handle -> ctx)
存起来。ctx->queue = skynet_mq_create(ctx->handle)
message_queue
,并设置 handle = ctx->handle
ctx->queue
.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
skynet_context_release(ctx)
=> 释放 contextskynet_handle_retire(handle)
=> 将 handle 标记为“废弃”skynet_mq_release(queue, ...)
=> 释放队列, 并尝试 drop 未处理消息通过这样一步步的流程, 该函数就创建(或失败)一个新的Skynet服务。
skynet_context_new
成功返回后,Worker 线程就能从global queue中发现该服务的消息队列 => 开始分发消息ctx->cb
(回调)在 init 里可能被设定(例如 snlua
里 skynet_callback(ctx, l , launch_cb);), 之后 Worker 线程拿到消息, 就会调用 cb(ctx, cb_ud, msg...)这就是Skynet的服务模型:
skynet_context
message_queue
=> Worker 线程调 cb()
=> 处理以下是简易时序图(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
skynet_module_query
skynet_module.c
,内部维护一个 static struct modules *M
全局结构,里面记录已经加载的C服务。skynet_module_query(name)
就遍历 M->m[] 里找 module->name == name
=> 返回指针.skynet_module_instance_create(mod)
mod->create(...)
常见写法是在 .so
里导出 xxx_create
函数 => 分配C structsnlua_create
会新建一个 struct snlua
(包含lua_State)skynet_handle_register
skynet_handle_register(ctx)
就返回一个全局唯一的 handle(>=1).skynet_send( to_handle, ... )
, Skynet可以reverse handle->context => 找到 to_ctx->queue
.skynet_mq_create
mq = skynet_malloc(sizeof(*mq)) + ...
=> init capacity, lock=0, handle=..., etc.ctx->handle
绑定mq->queue[tail] = message; tail=(tail+1)%cap;
skynet_module_instance_init
r = mod->init(inst, ctx, param)
spinlock lock
在 message_queue
or globalmq 里保证并发安全skynet_context_new
里不怎么显式使用锁,但内部handle_register & mq_create 都会用到全局/队列锁ATOM_INT ref
):
skynet_context_release(ctx)
=> ref-- => 如果==0 => free contextskynet_context_new
正是 Skynet 服务创建的核心。它背后包含模块系统(C服务管理)、Handle系统(服务ID分配)、消息队列系统(异步驱动),以及初始化回调(每种服务各自逻辑)等多个模块协同。
通过数据结构( skynet_context
, skynet_module
, message_queue
)与关键流程( module_instance_create
, handle_register
...) 的介绍,我们可以看到Skynet对“服务”这一抽象的高内聚设计——一个 context就是一个服务**:
后续深入阅读,追踪消息是如何被投递到服务并由Worker线程执行,则可以继续研究**skynet_context_message_dispatch
等函数;如果你想了解C服务如何编写 module,则可以研究skynet_module.c
** 以及具体 cservice 示例(logger.c
, service_snlua.c
等)。