当前位置: 首页 > article >正文

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 设计哲学

  1. 消息驱动模型
    Skynet 采用Actor模式,每个服务独立处理消息,每个服务都有自己的消息队列;服务之间通过消息队列handle进行通信。类似的,在erlang 当中,进程间也是通过 message 通信。
  2. 轻量级“上下文”
    每个服务不需要独立进程或操作系统线程,而是共享工作线程,因此需要用skynet_context 维护服务状态。正如 启动主流程 文中的分析,woker 线程的数量是在启动时候读配置固定创建的,而服务动态创建。
  3. C/S“模块”
    许多服务可以基于 Lua 脚本(snlua)、或者 C 模块(cservice 等),都由 skynet_module 统一加载。

在这样的设计下,“服务创建”过程就成了将一个 module 实例绑定到一个 skynet_context上,并分配消息队列handle、以及进行init回调的过程。


3. 服务创建的整体流程概览

下面是简单的整体概览,后文会更深入解析:

  1. 查找 Module: skynet_module_query(name) -> 得到 struct skynet_module *mod
  2. 创建 Module 实例: skynet_module_instance_create(mod) -> (通常是调用 mod->create)
  3. 分配 skynet_context: skynet_malloc(sizeof(*ctx))
  4. 注册 handle: ctx->handle = skynet_handle_register(ctx)
  5. 创建消息队列: ctx->queue = skynet_mq_create(ctx->handle)
  6. 调用 module init 回调: skynet_module_instance_init(mod, inst, ctx, param)
  7. 若 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 机制)
  • ctx->init=false => 还没完成初始化

5.4 Step 4:注册 handle & 创建队列

  1. ctx->handle = skynet_handle_register(ctx)
    • 这里会到全局 Handle 管理中找一个新的id(如 #10, #11 ...), 并把 (handle -> ctx) 存起来。
  2. 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) => 释放 context
    • skynet_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服务模型:

  1. 每个服务对应一个 skynet_context
  2. 消息放到 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 lockmessage_queue or globalmq 里保证并发安全
    • skynet_context_new 里不怎么显式使用锁,但内部handle_register & mq_create 都会用到全局/队列锁
  1. 引用计数( ATOM_INT ref ):
    • 初始=2 => 代表这个 context 还有2个持有者:
      1. handle_storage 全局
      2. 自己(在 new 函数中)
    • skynet_context_release(ctx) => ref-- => 如果==0 => free context
    • 这样可以保证在init失败或成功后 context 处理都不会重复释放.

10. Skynet 服务模型的优势与局限

  • 优势
    1.  Actor 模式,context + queue => 易于并发下消息封装
    2. 强大的灵活性:C服务 / Lua服务都可 => 只要 module create + init + release
    3. 在游戏业务当中就是 不同的业务可以创建不同的 服务去处理,天然隔离,不用考虑并发问题,数据都是隔离的,仅通过消息传递。
  • 局限
    1. 需要对消息无共享(Actor风格), 适合“异步消息驱动”
    2. 单个服务最多占用单线程 的 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就是一个服务**:

  1. Module表征它的实现( C or Lua )
  2. Context整合状态(handle, instance, queue, cb...)
  3. Queue存放消息
  4. 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 等),并通过流程图、结构…...

论文阅读(十一):基因-表型关联贝叶斯网络模型的评分、搜索和评估

1.论文链接:Scoring, Searching and Evaluating Bayesian Network Models of Gene-phenotype Association 摘要: 全基因组关联研究(GWAS)的到来为识别常见疾病的遗传变异(单核苷酸多态性(SNP)&…...

企业微信远程一直显示正在加载

企业微信远程一直显示正在加载 1.问题描述2.问题解决 系统:Win10 1.问题描述 某天使用企业微信给同事进行远程协助的时候,发现一直卡在正在加载的页面,如下图所示 2.问题解决 经过一番查找资料后,我发现可能是2个地方出了问题…...

人工智能 - 1

深度强化学习(Deep Reinforcement Learning) 图神经网络(Graph Neural Networks, GNNs) Transformer 一种深度学习模型 大语言模型(Large Language Models, LLMs) 人工智能 • Marvin Minsky 将其定义…...

留学生scratch计算机haskell函数ocaml编程ruby语言prolog作业VB

您列出了一系列编程语言和技术,这些可能是您在留学期间需要学习或完成作业的内容。以下是对每个项目的简要说明和它们可能涉及的领域或用途: Scratch: Scratch是一种图形化编程语言,专为儿童和初学者设计,用于教授编程…...

LeetCode题练习与总结:最长和谐子序列--594

一、题目描述 和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。 给你一个整数数组 nums ,请你在所有可能的 子序列 中找到最长的和谐子序列的长度。 数组的 子序列 是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素…...

Linux_线程同步生产者消费者模型

同步的相关概念 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。 同步的…...

Github 2025-01-30 Go开源项目日报 Top10

根据Github Trendings的统计,今日(2025-01-30统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目10Ollama: 本地大型语言模型设置与运行 创建周期:248 天开发语言:Go协议类型:MIT LicenseStar数量:42421 个Fork数量:2724 次关注人…...

FortiOS 存在身份验证绕过导致命令执行漏洞(CVE-2024-55591)

免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...

【Rust自学】17.2. 使用trait对象来存储不同值的类型

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 17.2.1. 需求 这篇文章以一个例子来介绍如何在Rust中使用trait对象来存储不同值的类型。 …...

毛选原文-实践论

实践论 论认识和实践的关系——知和行的关系 (一九三七年七月) 马克思以前的唯物论,离开人的社会性,离开人的历史发展,去观察认识问题,因此不能了解认识对社会实践的依赖关系,即认识对生产…...

PPT自动化 python-pptx -7: 占位符(placeholder)

占位符(placeholder)是演示文稿中用于容纳内容的预格式化容器。它们通过让模板设计者定义格式选项,简化了创建视觉一致幻灯片的过程,同时让最终用户专注于添加内容。这加快了演示文稿的开发速度,并确保幻灯片之间的外观…...

VLLM性能调优

1. 抢占 显存不够的时候,某些request会被抢占。其KV cache被清除,腾退给其他request,下次调度到它,重新计算KV cache。 报这条消息,说明已被抢占: WARNING 05-09 00:49:33 scheduler.py:1057 Sequence gr…...

Java线程认识和Object的一些方法

本文目标: 要对Java线程有整体了解,深入认识到里面的一些方法和Object对象方法的区别。认识到Java对象的ObjectMonitor,这有助于后面的Synchronized和锁的认识。利用Synchronized wait/notify 完成一道经典的多线程题目:实现ABC…...

数据库管理-第287期 Oracle DB 23.7新特性一览(20250124)

数据库管理287期 2025-01-24 数据库管理-第287期 Oracle DB 23.7新特性一览(20250124)1 AI向量搜索:算术和聚合运算2 更改Compatible至23.6.0,以使用23.6或更高版本中的新AI向量搜索功能3 Cloud Developer包4 DBMS_DEVELOPER.GET_…...

LruCache实现

LRU最近最少使用算法 一、LRU算法 1.简介 LRU(Least Recently Used,最近最少使用)算法是一种常用的缓存淘汰策略,当缓存达到其容量上限时,它会移除那些最久没有被访问的数据项。这种策略基于这样一个假设&#xff1…...

DDD架构实战第五讲总结:将领域模型转化为代码

云架构师系列课程之DDD架构实战第五讲总结:将领域模型转化为代码 一、引言 在前几讲中,我们讨论了领域模型的重要性及其在业务分析中的渐进获得方法。本讲将聚焦于如何将领域模型转化为代码,使得开发人员能够更轻松地实现用户的领域模型。 二、从模型到代码:领域驱动设计…...

【MySQL】MySQL客户端连接用 localhost和127.0.0.1的区别

# systemctl status mysqld # ss -tan | grep 3306 # mysql -V localhost与127.0.0.1的区别是什么? 相信有人会说是本地IP,曾有人说,用127.0.0.1比localhost好,可以减少一次解析。 看来这个入门问题还有人不清楚,其实…...

蓝桥杯例题五

无论你面对多大的困难和挑战,都要保持坚定的信念和积极的态度。相信自己的能力和潜力,努力不懈地追求自己的目标和梦想。不要害怕失败,因为失败是成功的垫脚石。相信自己的选择和决策,不要被他人的意见和批评左右。坚持不懈地努力…...

DeepSeek R1本地部署详细指南

DeepSeek R1 是由中国 AI 初创公司深度求索开发的先进推理模型,其性能在数学、编码和逻辑推理等任务上表现出色。在本地部署该模型可以带来更低的延迟、更高的隐私性以及对 AI 应用的更大控制权。本文将详细介绍如何在本地环境中部署 DeepSeek R1 模型。 前提条件 …...

MySQL(高级特性篇) 14 章——MySQL事务日志

事务有4种特性:原子性、一致性、隔离性和持久性 事务的隔离性由锁机制实现事务的原子性、一致性和持久性由事务的redo日志和undo日志来保证(1)REDO LOG称为重做日志,用来保证事务的持久性(2)UNDO LOG称为回…...

爬虫基础(五)爬虫基本原理

目录 一、爬虫是什么 二、爬虫过程 (1)获取网页 (2)提取信息 (3)保存数据 三、爬虫可爬的数据 四、爬虫问题 一、爬虫是什么 互联网,后面有个网字,我们可以把它看成一张蜘蛛网…...

【Block总结】HWD,小波下采样,适用分类、分割、目标检测等任务|即插即用

论文信息 Haar wavelet downsampling (HWD) 是一项针对语义分割的创新模块,旨在通过减少特征图的空间分辨率来提高深度卷积神经网络(DCNNs)的性能。该论文的主要贡献在于提出了一种新的下采样方法,能够在下采样阶段有效地减少信息…...

【解决方案】MuMu模拟器移植系统进度条卡住98%无法打开

之前在Vmware虚拟机里配置了mumu模拟器,现在想要移植到宿主机中 1、虚拟机中的MuMu模拟器12-1是目标系统,对应的目录如下 C:\Program Files\Netease\MuMu Player 12\vms\MuMuPlayer-12.0-1 2、Vmware-虚拟机-设置-选项,启用共享文件夹 3、复…...

力扣面试150 快乐数 循环链表找环 链表抽象 哈希

Problem: 202. 快乐数 👩‍🏫 参考题解 Code public class Solution {public int squareSum(int n) {int sum 0;while(n > 0){int digit n % 10;sum digit * digit;n / 10;}return sum;}public boolean isHappy(int n) {int slow n, fast squa…...

安卓(android)实现注册界面【Android移动开发基础案例教程(第2版)黑马程序员】

一、实验目的(如果代码有错漏,可查看源码) 1.掌握LinearLayout、RelativeLayout、FrameLayout等布局的综合使用。 2.掌握ImageView、TextView、EditText、CheckBox、Button、RadioGroup、RadioButton、ListView、RecyclerView等控件在项目中的…...

SpringSecurity:There is no PasswordEncoder mapped for the id “null“

文章目录 一、情景说明二、分析三、解决 一、情景说明 在整合SpringSecurity功能的时候 我先是去实现认证功能 也就是,去数据库比对用户名和密码 相关的类: UserDetailsServiceImpl implements UserDetailsService 用于SpringSecurity查询数据库 Logi…...

微服务入门(go)

微服务入门(go) 和单体服务对比:里面的服务仅仅用于某个特定的业务 一、领域驱动设计(DDD) 基本概念 领域和子域 领域:有范围的界限(边界) 子域:划分的小范围 核心域…...

996引擎 - NPC-动态创建NPC

996引擎 - NPC-动态创建NPC 创建脚本服务端脚本客户端脚本添加自定义音效添加音效文件修改配置参考资料有个小问题,创建NPC时没有控制朝向的参数。所以。。。自己考虑怎么找补吧。 多重影分身 创建脚本 服务端脚本 Mir200\Envir\Market_Def\test\test001-3.lua -- NPC八门名…...

使用 MySQL JSON 查询筛选嵌套字段的值

在日常开发中,随着项目需求的不断复杂化,许多表字段可能会存储 JSON 格式的数据。例如,我们有一张 site_device 表,其中有一个名为 detail 的字段,保存了设备的详细信息。这些信息存储为 JSON 数据,如下所示…...