前言
本文总结对于 skynet 服务管理器,skynet_handle.c 源文件的学习。
总览
设计思路
为每一个服务绑定一个永不重复(即使模块退出)的数字 id 作为其 handle
服务管理器完成的核心工作
存放所有服务对象,skynet 用服务对象的指针数组作为容器,限定了单进程内最大容纳服务数为 2^24 个,之所以是不是 2^32 次方,是因为高 8 位的 2 个字节用于存放用于远程服务的节点 id。初始容器大小为 4,然后当服务数超过容器当前大小时,按当前大小的 2 倍来扩容。每个服务有一个 id 来唯一标识,并且通过这个 id 能够找到该服务在容器中的位置,而 id 是自增的一个整数,容器位置是有限的,这就需要一个 hash 函数可以通过传入一个服务 id,返回服务在容器中的位置,且不能冲突。这个 hash 函数很简单,就是用 id 跟容器的当前容量来取余数。服务分为匿名服务和有名服务,区别是有名服务可以通过也可以通过名字获取服务 id。
#define DEFAULT_SLOT_SIZE 4 // 初始大小
#define MAX_SLOT_SIZE 0x40000000 // 名字数组最大容量,这个值远大于服务最大数量限制,有点奇怪
#define HASH_HANDLE(s, handle) (handle & (s->slot_size-1)) // hash 函数
struct handle_name {
char * name; // 服务名字 调用单独接口为服务自定义名字,默认是没有名字的,即匿名服务
uint32_t handle;
};
struct handle_storage {
struct rwlock lock;
uint32_t harbor; /* toby@2022-03-02): 每个进程一个8bit的标志 用作集群节点 */
uint32_t handle_index; /* toby@2022-03-02): 每个服务对应一个24位范围内的id 0 被保留*/
int slot_size; /* toby@2022-03-02): 可以容纳的服务数 */
struct skynet_context ** slot; /* toby@2022-03-02): 服务的指针数组 */
int name_cap;
int name_count;
struct handle_name *name; /* toby@2022-03-04): 名字数组,按字符串大小排序 */
};
static struct handle_storage *H = NULL; // 管理器单例对象,启动时会初始化
接口
-
服务注册接口,在创建好一个服务后,将服务对象注册(存放)到管理器中,返回管理器分配的服务 id。
uint32_t skynet_handle_register(struct skynet_context *);
uint32_t skynet_handle_register(struct skynet_context *ctx) { struct handle_storage *s = H; // 拿到管理器单例对象 rwlock_wlock(&s->lock); // 写锁保护,因为将要对容器数据进行改动 for (;;) { // 死循环来分配一个空闲的数组位置,拿到了会主动退出 int i; // 查找次数,超过当前容器大小则说明容器已满,需要扩容 uint32_t handle = s->handle_index; // 当前自增 id 的值 for (i=0;i<s->slot_size;i++,handle++) { // 最多循环遍历完当前容器中所有位置 if (handle > HANDLE_MASK) { // 自增 id 超过最大值则重置为 1 // 可以重置是因为服务退出工作后,分配给他的 id 被回收了 // 0 is reserved // 0 这个 id 是保留给系统使用的 handle = 1; } int hash = HASH_HANDLE(s, handle); // 取到 id 对应的数组位置 if (s->slot[hash] == NULL) { // 如果该位置没有存放服务即是找到了可用的 id s->slot[hash] = ctx; // 将服务对象的地址存放到管理器中 s->handle_index = handle + 1; // id 自增 rwlock_wunlock(&s->lock); // 数据改变操作结束就可以解锁了 handle |= s->harbor; // 为 id 添加远程用的节点编号头,高 8 位 return handle; } } //(toby@2022-03-01): 位置不够用了 按 2 倍扩展 assert((s->slot_size*2 - 1) <= HANDLE_MASK); // 不能超过最大限制 struct skynet_context ** new_slot = skynet_malloc(s->slot_size * 2 * sizeof(struct skynet_context *)); // 分配一个新的内存空间 memset(new_slot, 0, s->slot_size * 2 * sizeof(struct skynet_context *)); // 初始化该内存空间,相当于所有位置存放 NULL for (i=0;i<s->slot_size;i++) { // 将所有服务对象的地址复制到新的数组中 int hash = skynet_context_handle(s->slot[i]) & (s->slot_size * 2 - 1); // 因为容器的大小变了,所以需要为所有 id 重新 hash 计算一下新的位置 assert(new_slot[hash] == NULL); new_slot[hash] = s->slot[i]; } skynet_free(s->slot); // 释放旧的数组内存 s->slot = new_slot; // 绑定新的数组 s->slot_size *= 2; } }
-
通过服务 id 获取服务对象
struct skynet_context * skynet_handle_grab(uint32_t handle);
struct skynet_context * skynet_handle_grab(uint32_t handle) { struct handle_storage *s = H; struct skynet_context * result = NULL; rwlock_rlock(&s->lock); // 读锁保护,因为这里只是获取服务对象,并不会修改管理器本身数据 uint32_t hash = HASH_HANDLE(s, handle); struct skynet_context * ctx = s->slot[hash]; if (ctx && skynet_context_handle(ctx) == handle) { // 检查 handle 是否一致,避免通过过期 id 访问 result = ctx; skynet_context_grab(result); // 服务本身具有引用计数,这里计数 +1,使用完之后会调用减少计数的接口,如果计数归零,则服务将被释放 } rwlock_runlock(&s->lock); return result; }
-
回收句柄(服务id),销毁服务
int skynet_handle_retire(uint32_t handle); // 回收单个服务
void skynet_handle_retireall(); // 回收所有服务int skynet_handle_retire(uint32_t handle) { int ret = 0; struct handle_storage *s = H; rwlock_wlock(&s->lock); // 写锁保护,要回收服务,自然会改动管理器数据 uint32_t hash = HASH_HANDLE(s, handle); struct skynet_context * ctx = s->slot[hash]; // 先拿到指定服务 if (ctx != NULL && skynet_context_handle(ctx) == handle) { // 检查一致性 s->slot[hash] = NULL; // 从容器中移除服务对象 ret = 1; /*如果该服务是有名服务,还需要释放对象的名字结构,这里所有服务释放都会遍历名字数组,可以考虑优化,权衡设置标志的内存占用和遍历数组的耗时,且需结合具体场景是否有频繁释放服务的需求*/ int i; int j=0, n=s->name_count; for (i=0; i<n; ++i) { if (s->name[i].handle == handle) { skynet_free(s->name[i].name); // 找到该服务,释放名字结构占用的内存,直接进入下一轮循环,下一轮就会存在 j < i 的情况了,达到前移数据的目的 continue; } else if (i!=j) { /* toby@2022-03-02): 后面的值都向前移动 */ s->name[j] = s->name[i]; } ++j; } s->name_count = j; } else { ctx = NULL; } rwlock_wunlock(&s->lock); if (ctx) { // release ctx may call skynet_handle_* , so wunlock first. /*这里注释的意思是,服务自定义的 release 过程中可能会调用到管理器的某些接口,而管理器的接口基本上都会先请求锁保护,如果不先解锁,这里可能出现死锁。 但是,正常写法就应该是在做完对管理器数据修改的逻辑之后就立即解锁,如果在函数末尾才解锁,本就于理不通。 所以这个注释可有可无,可能是改 bug 留下的*/ skynet_context_release(ctx); // 减少服务的引用计数 } return ret; }
-
给服务命名,有名服务通常作为工具服务,只有一个对象,能直接通过名字来查询服务 id。例如日志服务 “logger” 。
const char * skynet_handle_namehandle(uint32_t handle, const char *name);
static const char * _insert_name(struct handle_storage *s, const char * name, uint32_t handle) { int begin = 0; int end = s->name_count - 1; while (begin<=end) { int mid = (begin+end)/2; struct handle_name *n = &s->name[mid]; int c = strcmp(n->name, name); if (c==0) { // 相等 发现名字已存在 return NULL; } if (c<0) { // 中间位置的名字小于当前名字,从后半段继续找位置 begin = mid + 1; } else { // 中间位置的名字大于当前名字,从前半段继续找位置 end = mid - 1; } } char * result = skynet_strdup(name); _insert_name_before(s, result, handle, begin); // 插入 begin 所在位置 return result; } const char * skynet_handle_namehandle(uint32_t handle, const char *name) { rwlock_wlock(&H->lock); const char * ret = _insert_name(H, name, handle); // 用二分法插入新创建的名字 rwlock_wunlock(&H->lock); return ret; }
-
通过名字查找服务 id
uint32_t skynet_handle_findname(const char * name);
uint32_t skynet_handle_findname(const char * name) { // 二分查找 struct handle_storage *s = H; rwlock_rlock(&s->lock); uint32_t handle = 0; int begin = 0; int end = s->name_count - 1; while (begin<=end) { int mid = (begin+end)/2; struct handle_name *n = &s->name[mid]; int c = strcmp(n->name, name); if (c==0) { handle = n->handle; break; } if (c<0) { begin = mid + 1; } else { end = mid - 1; } } rwlock_runlock(&s->lock); return handle; }
结语
用读写锁的原因是,读操作(获取服务)的频率通常是远高于写操作(注册服务、回收服务)的。该读写锁的实现是基于自旋锁的。skynet 内的锁基本都是在自旋锁的基础上来实现的,猜想这样做是为了让工作线程尽量不让出 cpu,减少 cpu 切换带来的消耗,充分利用多核优势。