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 设计哲学
- 消息驱动模型
Skynet 采用Actor模式,每个服务独立处理消息,每个服务都有自己的消息队列;服务之间通过消息队列和handle进行通信。类似的,在erlang 当中,进程间也是通过 message 通信。 - 轻量级“上下文”
每个服务不需要独立进程或操作系统线程,而是共享工作线程,因此需要用skynet_context
维护服务状态。正如 启动主流程 文中的分析,woker 线程的数量是在启动时候读配置固定创建的,而服务动态创建。 - C/S“模块”
许多服务可以基于 Lua 脚本(snlua
)、或者 C 模块(cservice
等),都由skynet_module
统一加载。
在这样的设计下,“服务创建”过程就成了将一个 module 实例绑定到一个 skynet_context上,并分配消息队列、handle、以及进行init回调的过程。
3. 服务创建的整体流程概览
下面是简单的整体概览,后文会更深入解析:
- 查找 Module:
skynet_module_query(name)
-> 得到struct skynet_module *mod
- 创建 Module 实例:
skynet_module_instance_create(mod)
-> (通常是调用 mod->create) - 分配 skynet_context:
skynet_malloc(sizeof(*ctx))
- 注册 handle:
ctx->handle = skynet_handle_register(ctx)
- 创建消息队列:
ctx->queue = skynet_mq_create(ctx->handle)
- 调用 module init 回调:
skynet_module_instance_init(mod, inst, ctx, param)
- 若 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; // 指向加载的 modulevoid * cb_ud; // user data for callbackskynet_cb cb; // callback functionstruct message_queue *queue; // 该服务的消息队列ATOM_POINTER logfile; // 日志文件指针(原子操作)uint64_t cpu_cost; // 用于统计消耗uint64_t cpu_start; // 开始时cpu时间char result[32];uint32_t handle; // 唯一 handle IDint session_id; // 记录当前 sessionATOM_INT ref; // 引用计数int message_count; // 处理消息计数bool init; // 是否完成 initbool endless; // 是否endlessbool profile; // 是否启用profileCHECKCALLING_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) 查找 modulestruct 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_contextstruct 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
机制)
- ref=2:初始引用计数为2, 这表示1是自己 + 1是别的地方使用(具体可参看
ctx->init=false
=> 还没完成初始化
5.4 Step 4:注册 handle & 创建队列
ctx->handle = skynet_handle_register(ctx)
- 这里会到全局 Handle 管理中找一个新的id(如 #10, #11 ...), 并把
(handle -> ctx)
存起来。
- 这里会到全局 Handle 管理中找一个新的id(如 #10, #11 ...), 并把
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)
=> 释放 contextskynet_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的服务模型:
- 每个服务都对应一个
skynet_context
- 消息放到
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 handleglobalmq_push(queue) release queuereturn 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 lock
在message_queue
or globalmq 里保证并发安全- 在
skynet_context_new
里不怎么显式使用锁,但内部handle_register & mq_create 都会用到全局/队列锁
- 引用计数(
ATOM_INT ref
):- 初始=2 => 代表这个 context 还有2个持有者:
- handle_storage 全局
- 自己(在 new 函数中)
skynet_context_release(ctx)
=> ref-- => 如果==0 => free context- 这样可以保证在init失败或成功后 context 处理都不会重复释放.
- 初始=2 => 代表这个 context 还有2个持有者:
10. Skynet 服务模型的优势与局限
- 优势
- Actor 模式,context + queue => 易于并发下消息封装
- 强大的灵活性:C服务 / Lua服务都可 => 只要 module create + init + release
- 在游戏业务当中就是 不同的业务可以创建不同的 服务去处理,天然隔离,不用考虑并发问题,数据都是隔离的,仅通过消息传递。
- 局限
- 需要对消息无共享(Actor风格), 适合“异步消息驱动”
- 单个服务最多占用单线程 的 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就是一个服务**:
- Module表征它的实现( C or Lua )
- Context整合状态(handle, instance, queue, cb...)
- Queue存放消息
- init / release 是服务的生命周期
后续深入阅读,追踪消息是如何被投递到服务并由Worker线程执行,则可以继续研究**skynet_context_message_dispatch
等函数;如果你想了解C服务如何编写 module,则可以研究skynet_module.c
** 以及具体 cservice 示例(logger.c
, service_snlua.c
等)。
相关文章:
skynet 源码阅读 -- 核心概念服务 skynet_context
本文从 Skynet 源码层面深入解读 服务(Service) 的创建流程。从最基础的概念出发,逐步深入 skynet_context_new 函数、相关数据结构(skynet_context, skynet_module, message_queue 等),并通过流程图、结构…...

每日十题八股-2025年1月23日
1.快排为什么时间复杂度最差是O(n^2) 2.快排这么强,那冒泡排序还有必要吗? 3.如果要对一个很大的数据集,进行排序,而没办法一次性在内存排序,这时候怎么办? 4.面试官:你的…...

MongoDB部署模式
目录 单节点模式(Standalone) 副本集模式(Replica Set) 分片集群模式(Sharded Cluster) MongoDB有多种部署模式,可以根据业务需求选择适合的架构和部署方式。 单节点模式(Standa…...

opencv笔记2
图像灰度 彩色图像转化为灰度图像的过程是图像的灰度化处理。彩色图像中的每个像素的颜色由R,G,B三个分量决定,而每个分量中可取值0-255,这样一个像素点可以有256*256*256变化。而灰度图像是R,G,B三个分量…...
springboot使用ssl连接elasticsearch
使用es时ssl证书报错 unable to find valid certification path to requested target 1.依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency>2…...
Linux内核中的InfiniBand核心驱动:verbs.c分析
InfiniBand(IB)是一种高性能、低延迟的网络互连技术,广泛应用于高性能计算(HPC)、数据中心和云计算等领域。Linux内核中的InfiniBand子系统通过提供一组核心API(称为Verbs API)来支持InfiniBand设备的操作。drivers/infiniband/core/verbs.c是InfiniBand核心驱动的重要组…...
把网站程序数据上传到服务器的方法和注意事项
将网站程序数据上传到服务器是一个常见的网站开发和部署流程。主要涉及到FTP上传、FileZilla、rsync(在Linux下)、或其他相关的文件同步工具。以下是一般步骤和方法: 使用FTP: 1. 选择FTP客户端软件: - 常见的FTP客户端包括FileZilla(开源)、…...

完全平方数——唯一分解定理
文章目录 一、唯一分解定理是什么?1.定义2.示例3.代码模板 二、例题1>问题描述(2021蓝桥杯省赛)输入格式输出格式样例输入 1样例输出 1样例输入 2样例输出 2评测用例规模与约定 2>解题思路3>假娃3>C嘎嘎 一、唯一分解定理是什么&…...

(详细)Springboot 整合动态多数据源 这里有mysql(分为master 和 slave) 和oracle,根据不同路径适配不同数据源
文章目录 Springboot 整合多动态数据源 这里有mysql(分为master 和 slave) 和oracle1. 引入相关的依赖2. 创建相关配置文件3. 在相关目录下进行编码,不同路径会使用不同数据源 Springboot 整合多动态数据源 这里有mysql(分为maste…...

mock可视化生成前端代码
介绍:mock是我们前后端分离的必要一环、ts、axios编写起来也很麻烦。我们就可以使用以下插件,来解决我们的问题。目前支持vite和webpack。(配置超级简单!) 欢迎小伙伴们提issues、我们共建。提升我们的开发体验。 vi…...

Spring Boot(6)解决ruoyi框架连续快速发送post请求时,弹出“数据正在处理,请勿重复提交”提醒的问题
一、整个前言 在基于 Ruoyi 框架进行系统开发的过程中,我们常常会遇到各种有趣且具有挑战性的问题。今天,我们就来深入探讨一个在实际开发中较为常见的问题:当连续快速发送 Post 请求时,前端会弹出 “数据正在处理,请…...

鸿蒙Harmony json转对象(1)
案例1 运行代码如下 上图的运行结果如下: 附加1 Json_msg interface 案例2 import {JSON } from kit.ArkTS; export interface commonRes {status: numberreturnJSON: ESObject;time: string } export interface returnRes {uid: stringuserType: number; }Entry Component …...
常见的RocketMQ面试题及其简要答案
以下是一些常见的RocketMQ面试题及其简要答案: 一、基础概念与架构 简述RocketMQ是什么,并说明其主要作用。 答案: RocketMQ:是阿里巴巴在2012年开源的一款分布式消息中间件,目前已经捐赠给Apache软件基金会ÿ…...
C#Object类型的索引,序列化和反序列化
前言 最近在编写一篇关于标准Mes接口框架的文章。其中有一个非常需要考究的内容时如果实现数据灵活和可使用性强。因为考虑数据灵活性,所以我一开始选取了Object类型作为数据类型,Object作为数据Value字段,String作为数据Key字段,…...
Unity3D项目开发中的资源加密详解
前言 在Unity3D游戏开发中,保护游戏资源不被非法获取和篡改是至关重要的一环。资源加密作为一种有效的技术手段,可以帮助开发者维护游戏的知识产权和安全性。本文将详细介绍Unity3D项目中如何进行资源加密,并提供相应的技术详解和代码实现。…...

微调Qwen2:7B模型,加入未知信息语料
对于QWen2这样的模型,在微调的时候,语料的投喂格式满足ChatML这样的格式!!! OpenAI - ChatML: 下面是ChatML格式的介绍: https://github.com/openai/openai-python/blob/release-v0.28.0/chatml.mdhttps://github.com/openai/openai-python/blob/release-v0.28.0/chat…...

【Ubuntu】安装SSH启用远程连接
【Ubuntu】安装OpenSSH启用远程连接 零、安装软件 使用如下代码安装OpenSSH服务端: sudo apt install openssh-server壹、启动服务 使用如下代码启动OpenSSH服务端: sudo systemctl start ssh贰、配置SSH(可跳过) 配置文件 …...

【理论】测试开发工程师进阶路线
一、腾讯与阿里的质量保证服务参考 阿里云效测试能力与架构 腾讯 WeTest 测试能力全景图 二、测试开发技术体系 1.用户端测试: Web/App 测试 Web/App 自动化测试 用户端专项测试 用户端安全测试 2.服务端测试: 接口协议与 Mock 接口自动化测试 服务端…...

【BQ3568HM开发板】如何在OpenHarmony上通过校园网的上网认证
引言 前面已经对BQ3568HM开发板进行了初步测试,后面我要实现MQTT的工作,但是遇到一个问题,就是开发板无法通过校园网的认证操作。未认证的话会,学校使用的深澜软件系统会屏蔽所有除了认证用的流量。好在我们学校使用的认证系统和…...
動態住宅IP提升網站訪問成功率
動態住宅IP通常與普通家庭用戶的網路連接相關聯。這種IP地址的特點在於,它是動態變化的,用戶在每次連接時可能會獲得不同的IP地址。這與靜態IP形成了鮮明對比,後者在連接期間保持不變。傳統上,IP地址分為住宅IP和數據中心IP兩類。…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...