Skynet源码赏析二 | 基础数据结构
module管理模块
我们所写的C服务编译成.so文件后放在cpath
变量路径下,程序会加载路径cpath = root.."cservice/?.so"
下的.so文件,通过static void * _try_open(struct modules *m, const char * name)
函数第一次打开,然后被加载到全局变量static struct modules * M
中。
module对应的数据结构如下:
//skynet_module.h
struct skynet_module {
const char * name; // C服务名称
void * module; // 访问so的dl句柄
skynet_dl_create create; // 绑定.so的 xxx_create 函数
skynet_dl_init init; // 绑定.so的 xxx_init 函数
skynet_dl_release release; // 绑定.so的 xxx_release 函数
skynet_dl_signal signal; // 绑定.so的 xxx_signal 函数
};
//skynet_modules.c
#define MAX_MODULE_TYPE 32
struct modules {
int count; // C服务数量
struct spinlock lock; // 自旋锁
const char * path; // C服务.so文件目录,由cpath变量控制置
struct skynet_module m[MAX_MODULE_TYPE]; // C服务列表
};
当要创建该类服务的实例时
- 从
modules
列表中取出该服务的skynet_module
句柄, 然后调用m->create()
创建实例。 - 将实例
inst
赋值到新的skynet_context
中。 - 将新的
skynet_context
注册到skynet_context
list中 - 初始化
inst
实例m->init()
。 对应的代码如下
struct skynet_context * skynet_context_new(const char * name, const char *param) {
struct skynet_module * mod = skynet_module_query(name); //1. 取出服务句柄
...
void *inst = skynet_module_instance_create(mod);//m->create 创建实例
struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
CHECKCALLING_INIT(ctx)
ctx->mod = mod;
ctx->instance = inst; //2. 将实例赋值懂啊 skynet_context中
...
ctx->handle = 0;
ctx->handle = skynet_handle_register(ctx); //3. 注册到skynet_context list中
...
// init function maybe use ctx->handle, so it must init at last
context_inc();
CHECKCALLING_BEGIN(ctx)
int r = skynet_module_instance_init(mod, inst, ctx, param); //4.m->init()
CHECKCALLING_END(ctx)
if (r == 0) {
...
} else {
...
}
}
skynet_context服务管理模块
创建一个新服务,首先要找到对应服务的modules,在创建并初始化modules之后绑定一个上下文skynet_context
,skynet通过全局变量static struct handle_storage *H
来管理服务。
主要的数据结构如下:
//skynet_server.c
struct skynet_context {
void * instance; // m-create()创建的实例
struct skynet_module * mod; // 引用mod服务的指针,方便 m 的init(),create()等函数调用
void * cb_ud; // 调用callback函数时,回传给callback的userdata,一般是instance指针
skynet_cb cb; // 服务的消息回调函数,一般在skynet_module的init函数里指定
struct message_queue *queue; // 服务专属的次级消息队列
ATOM_POINTER logfile; // 日志句柄
uint64_t cpu_cost; // in microsec
uint64_t cpu_start; // in microsec
char result[32]; // 操作skynet_context的返回值,会写到这里
uint32_t handle; // 标识唯一context的服务id
int session_id; // 在发出请求后,收到对方的返回消息时,通过session_id来匹配一个返回,对应哪个请求
ATOM_INT ref; // 引用计数变量,当为0时,表示内存可以被释放
int message_count; // 消息数量
bool init; // 是否完成初始化
bool endless; // 消息是否堵住
bool profile; // 配合 cpu_cost 和 cpu_start使用
CHECKCALLING_DECL
};
//skynet_handle.c
struct handle_name {
char * name; // 服务别名
uint32_t handle; // 标识唯一context的服务id
};
struct handle_storage {
struct rwlock lock; // 读写锁
uint32_t harbor; // harbor id
uint32_t handle_index; // 创建下一个服务时,该服务的slot idx,一般会先判断该slot是否被占用
int slot_size; // slot的大小,一定是2^n,初始值是4
struct skynet_context ** slot; // skynet_context list
int name_cap; // 别名列表大小,大小为2^n
int name_count; // 别名数量
struct handle_name *name; // 别名列表
};
消息与队列
在创建C服务module和对应的上下文skynet_context时,有一个对应的消息队列message_queue
与之绑定, 最后把该消息队列push到全局队列中。
数据结构如下:
//skynet_mq.c
struct message_queue {
struct spinlock lock; // 自旋锁 防止数据冲突
uint32_t handle; // 服务id 唯一
int cap; // 队列容量
int head; // 头部索引,当head=tail时会进行扩容。
int tail; // 尾部索引
int release; // 是否能释放消息
int in_global; // 是否在全局消息队列中,0表示不是,1表示是
int overload; // 是否过载
int overload_threshold;
struct skynet_message *queue; // 消息内容
struct message_queue *next; // 下一个次级消息队列
};
struct global_queue {
struct message_queue *head; // 全局消息队列头部指针
struct message_queue *tail; // 全局消息队列尾部指针
struct spinlock lock; // 自旋锁
};
消息通过函数struct message_queue * skynet_context_message_dispatch(struct skynet_monitor *sm, struct message_queue *q, int weight)
来处理,处理过程如下:
- 从全局消息队列中取出一个次级消息队列,
skynet_globalmq_pop();
; - 通过次级消息队列中的handle变量来获取对应服务的上下文,
struct skynet_context * ctx = skynet_handle_grab(handle);
; - 调用回调函数,
dispatch_message(ctx, &msg);
总结
本文从数据结构的层面,讲述了一个C服务的启动,以及消息的处理流程。
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2021/10/skynettwo/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接