本文总结对于 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 切换带来的消耗,充分利用多核优势。