skynet源码分析(五)认识服务

  skynet 是一个 actor 模型的消息框架,每个 actor 在 skynet 中就是一个服务。服务可以说是 skynet 中最重要的概念,本篇会介绍 skynet 中的服务。

概述

  skynet 中的服务是基于上文中前文中提到的 module 在创建的,module 为服务提供了私有数据的存储位置。服务作为一个 acotr,拥有自己的消息队列,可以向别的服务发送消息,也可以接收别的服务的消息并把消息交给消息回调函数进行处理。

结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct skynet_context {
    void *instance; // C 模块副本的引用
    struct skynet_module *mod; // C 模块的地址
    void *cb_ud; // callback userdata
    skynet_cb cb; // 回调函数
    struct message_queue *queue; // 服务消息队列
    ATOM_POINTER logfile; // 文件指针是个原子指针
    uint64_t cpu_cost;    // in microsec // 消耗的总 cpu 时间
    uint64_t cpu_start;    // in microsec // 本次消息处理的起始时间点
    char result[32]; // 用来保存命令的执行结果
    uint32_t handle; // 本服务注册获得的对应 handle
    int session_id; // 消息的 session id 分配器,不断累加
    ATOM_INT ref; // 引用着的数量
    int message_count; // 处理过的消息总数
    bool init; // 初始化完成标记
    bool endless; // 死循环标志
    bool profile; // 是否开启了 profile

    CHECKCALLING_DECL
};

  虽然 skynet_context 中的数据项很多,但是其实核心的数据就是跟消息处理有关的那几个。worker 线程会不断为消息队列中每个消息调用回调函数进行处理。

创建服务

1
struct skynet_context *skynet_context_new(const char *name, const char *param)

  不管是从 C 层还是从 Lua 层创建一个新的服务,最后的创建函数都是 skynet_context_new,这个函数接受一个 module 名字和一个传给 module 的 init 函数的额外参数,返回一个 struct skynet_context 变量指针。
  服务在创建的时候有几个关键步骤:

  • 通过 module 名字去查找 module 的地址,然后调用 module 的 create 接口创建一个数据副本,这两个值会保存在 skynet_context 的 mod 和 instance 字段中。
  • 注册服务的 handle,服务的 handle 是用来完成 名字 → handle 和 handle → 服务 的映射。
  • 创建服务的消息队列。
  • 调用 module 的 init 函数初始化之前创建好的 module 数据副本。
  • 把服务的消息队列 push 到全局的消息队列中去。

引用计数

  每个服务都有一个原子类型的引用计数变量 ref,当有地方引用一个服务时,调用 skynet_context_grab 来增加服务的引用计数,在引用结束以后需要手动调用 skynet_context_release 来减少服务的引用计数。

1
2
void skynet_context_grab(struct skynet_context *ctx)
struct skynet_context *skynet_context_release(struct skynet_context *ctx)

  ref 在服务创建完毕以后会的值为 1,正常的引用和解引用是不会使 ref 变为 0 的,如果需要删除一个服务,可以在服务的逻辑中调用 skynet.exit 或者是通过后台命令来减少服务的引用次数,然后等别的地方的引用全部结束了,ref 会变为 0 来触发删除服务的函数 delete_context.

1
static void delete_context(struct skynet_context *ctx)

  之所以要进入一个引用计数而不是调用一个删除函数直接进行删除,主要原因是因为要删除的目标服务可能正在被别处引用,比如它的消息队列正在被 worker 线程处理,直接删除会有问题,所以虽然维护引用计数比较麻烦,但是其实已经是一个比较巧妙的解法了。

消息队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct message_queue {
    struct spinlock lock; // 自旋锁
    uint32_t handle; // 消息队列的 handle
    int cap; // 容量,模仿 vector 的实现,不够了会自己扩容
    int head; // 消息的头指针
    int tail; // 消息的尾指针
    int release; // 标记是否已经被释放
    int in_global; // 标记是否在全局队列中
    int overload; // 现在的负载
    int overload_threshold; // 超载警告的阈值
    struct skynet_message *queue; // 消息队列数组
    struct message_queue *next; // 下一个消息队列,链表结构
};

  每个服务都有属于自己的一个 struct message_queue 结构的消息队列。消息队列是有锁结构,实现方式是一个动态环形数组,使用 head 表示目前队列中最早的一条消息的索引值,tail 表示目前队列中最晚的一条消息的索引值。在 push 消息进来的时候会检查是否还有剩余位置,如果没有了会调用 expand_queue 扩容这个数组,容量初始的时候是 64,每次扩容会把当前容量直接翻倍。

handle

  上文提到了,handle 的作用是用来完成 名字 → handle 和 handle → 服务 的映射。首先,skynet 中同进程不同服务之间发送消息的本质就是把消息 push 到目标服务的消息队列中,这个操作中最重要的一个步骤就是拿到目标服务的消息队列的地址,又因为有了目标服务的地址就能拿到其消息队列的地址,所以这个步骤变为了拿到目标服务的地址。
  skynet 中发送消息的时候,指定目标可以通过服务的名字或是 handle,如果目标的参数是名字,需要两步转换,首先要根据服务的名字拿到服务的 handle,然后再根据 handle 拿到服务的地址。如果参数是 handle 则直接根据 handle 拿到服务的地址即可。
  这个过程能不能简化呢?在 C 语言中很难实现,如果是 C++ 的话,可以考虑强制要求服务一定要有名字,然后使用一个 unordered_map 把服务的名字和地址对应起来,发送消息的时候也只能通过名字来指定目标服务,这样可以简化掉 handle 这个转化过程,但是也加了不小的限制。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计