skynet源码分析(六)handle的管理

  handle 作为服务的句柄,在 skynet 的消息发送和消息处理中都起到了不可或缺的作用。handle 的管理模块主要提供了 “通过 handle 查询服务地址” 和 “通过服务名字查询 handle” 这两个功能。本篇来讨论一下 skynet 是如何对 handle 进行管理的。

管理器结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct handle_name {
    char *name;
    uint32_t handle;
};

struct handle_storage {
    struct rwlock lock; // 读写锁,因为读取的频率远远高于写入
    uint32_t harbor; // 本节点的 harbor id
    uint32_t handle_index; // 当前的索引值,会累加
    int slot_size; // slot 的长度
    struct skynet_context **slot; // 全部 ctx

    int name_cap; // 名字列表的容量
    int name_count; // 名字列表的总数
    struct handle_name *name; // 保存全部的名字
};

static struct handle_storage *H = NULL;

  每个 skynet 进程都有一个全局的 handle 管理器,会在进程启动时被初始化。它是个带有读写锁的结构,因为大部分的操作都是查询操作,所以读写锁保证了查询的效率。保存 handle的哈希→服务 键值对的 slot 是一个动态数组,不够了会扩容,初始长度是 4,每次扩容会在原长度上翻倍。name 用来保存全部的 handle 和名字的对应关系,是一个有序的动态数组,按 handle_name 的 name 字段的字符顺序进行排序,初始长度是 2,每次扩容在原长度上翻倍。

通过 handle 查询服务地址

注册 handle

  想要查询首先要注册数据。服务在创建的时候会调用 skynet_handle_register 为服务注册一个 handle 并且插入到 slot 中。

1
uint32_t skynet_handle_register(struct skynet_context *ctx)

  插入数据的部分值得讲一讲,slot 的 key 是通过 handle 对 slot_size 取余数的来的。handle 有一个 handle_index 做标记量,是从 1 开始的。如果不考虑服务退出的话,其实 handle 就一直累加就好了,也不需要取余数了,不够了就扩容。实际在服务退出的时候,handle 不会回收,但是 slot 中占据的位置要清掉。这就造成了 slot 的空洞问题,插入数据就变得麻烦了起来,因为要找到空洞安放数据。

  • 以写模式锁住读写锁
  • 把 handle 赋值为 handle_index 的值
  • 用 handle 对 slot_size 取余,拿到一个位置
  • 如果这个位置是空的,则找到了合适的位置,保存在 slot 中,解开读写锁,返回 handle 即可
  • 如果这个位置不为空,则累加 handle,再检查,一直累加整个 slot_size 的数值,此时所有 slot 的位置都被检查了一遍了
  • 如果还没找到,那就要扩容了,把 slot 扩容到原先的两倍,然后重复遍历检查的步骤

  这里有两个小限制,首先 slot_size 不能超过 16777215,还有就是当 handle 超过 16777215 以后,handle 会从 1 再次开始,理论上来说有重复的风险。

释放 handle

  skynet 会在服务退出后移除掉 slot 中对应的项。在 slot 中形成空洞,这也是为什么在插入的时候要遍历整个 slot 找空位置的原因。

查询

  可以通过 handle 直接查询到服务的地址。

1
struct skynet_context *skynet_handle_grab(uint32_t handle)

  有了上面的注册以后,这个查询就很简单了,算出 handle 的哈希,直接从 slot 里面取出来服务的地址返回即可。因为不涉及到修改,所以查询只锁读锁就行了。

通过服务名字查询 handle

注册名字

  可以通过调用 skynet.name 来注册一个指定服务的名字。该接口最终会调用到 C 层的

1
const char *skynet_handle_namehandle(uint32_t handle, const char *name)

  这个函数会锁上管理器的写锁,并且把自己的名字和 handle 插入到管理器的 name 数组中去。因为 name 是一个有序的动态数组,所以在插入的时候同样需要维持顺序。实际实现是通过二分查找搜索目标的名字,如果查到了,则说明名字重复了,返回了 NULL,不会覆盖旧名字。如果没有查到,则结果位置就是合适的插入位置,然后把名字和 handle 组合一下插入到 name 的合适位置中去。插入过程有可能引起数组的扩容,每次把容量扩充到当前容量的两倍。
  注册名字是如果是本地名字则应该在前面加上一个点号,类似 “.xxx” 来区别本地名字和 harbor 的全局名字。本地名字没限制,harbor 的服务名字不能超过 16 个字符。

查询

  因为 name 是个有序的数组,所以查询这一步同样使用二分查找搜索数组即可。因为不涉及到修改,所以只锁上读锁即可。

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