skynet源码分析2:模块
actor在skynet中称为模块,每个模块由皮囊和骨骼组成。皮囊承载用户逻辑,骨骼承载内部框架逻辑。
皮囊(skynet_module)
皮囊在框架中用skynet_module对象表示,实现在skynet-src/skynet_module.c中,代表一个动态库.下文用sm来称呼.
先来看看sm的定义,在skynet-src/skynet.h中
1 typedef void * (*skynet_dl_create)(void);
2 typedef int (*skynet_dl_init)(void * inst, struct skynet_context *, const char * parm);
3 typedef void (*skynet_dl_release)(void * inst);
4 typedef void (*skynet_dl_signal)(void * inst, int signal);
5
6 struct skynet_module {
7 const char * name;
8 void * module;
9 skynet_dl_create create;
10 skynet_dl_init init;
11 skynet_dl_release release;
12 skynet_dl_signal signal;
13 };
name:动态库名
module:动态库的句柄
create:契约函数之一,用来创建用户对象。
init:契约函数之一,用来初始化用户对象。inst为用户对象,skynet_context为模块骨骼对象(在皮囊层,不需要知道它是什么,只是调用框架方法时需要用到它,后续或分析它),parm为创建actor时带的参数,等同于main函数中的args参数。
release:契约函数之一,用来释放用户对象。
signal:契约函数之一,用来实现信号功能。signal为信号类型.
skynet_module.c这个部分实现了一个动态库加载器,有两个作用:
- 加载动态库,获取契约函数指针。
- 缓存已加载的动态库。
我们来看看它的实现吧,它头文件的接口定义如下:
1 void skynet_module_insert(struct skynet_module *mod);
2 struct skynet_module * skynet_module_query(const char * name);
3 void * skynet_module_instance_create(struct skynet_module *);
4 int skynet_module_instance_init(struct skynet_module *, void * inst, struct skynet_context *ctx, const char * parm);
5 void skynet_module_instance_release(struct skynet_module *, void *inst);
6 void skynet_module_instance_signal(struct skynet_module *, void *inst, int signal);
7
8 void skynet_module_init(const char *path);
在linux下,动态库相关接口定义在<dlfcn.h>中,主要有dlopen,dlsym,dlclose,dlerror函数,dlopen用来加载so库,得到句柄;dlsym用来获取符号的地址;dlclose用来卸载so库;dlerror获取前面几个函数调用失败的错误信息。这几个接口用起来都非常简单。
那么它的具体实现必然是使用dlfcn的几个接口。
下面主要来看看加载逻辑,也就是skynet_module_query函数,3-6的函数是对契约函数的代理调用。
在skynet_module.c中,可以看到如下内部定义:
1 #define MAX_MODULE_TYPE 32
2
3 struct modules {
4 int count;
5 struct spinlock lock;
6 const char * path;
7 struct skynet_module m[MAX_MODULE_TYPE];
8 };
9
10 static struct modules * M = NULL;
m是用于缓存sm,最大32个,path表示动态库的搜索路径,与lua的package的语义一致,后面再说。
来看skynet_module_query,在skynet-src/skynet_module.c的93行:
1 struct skynet_module *
2 skynet_module_query(const char * name) {
3 struct skynet_module * result = _query(name);
4 if (result)
5 return result;
6
7 SPIN_LOCK(M)
8
9 result = _query(name); // double check
10
11 if (result == NULL && M->count < MAX_MODULE_TYPE) {
12 int index = M->count;
13 void * dl = _try_open(M,name);
14 if (dl) {
15 M->m[index].name = name;
16 M->m[index].module = dl;
17
18 if (_open_sym(&M->m[index]) == 0) {
19 M->m[index].name = skynet_strdup(name);
20 M->count ++;
21 result = &M->m[index];
22 }
23 }
24 }
25
26 SPIN_UNLOCK(M)
27
28 return result;
29 }
逻辑比较简单,先从缓存查找,未找到就加载,然后添加至缓存。
3-5行是查找缓存,_query为一个数组搜索函数。用自旋锁保证线程安全,9又查找了一次缓存,因为可能在锁竞争的时候,其它线程加载了一个同样的so。
后面这个判断我不觉历,只能加载35个不同的so,也就是说框架不能有35个不同逻辑的actor。
_try_open来看一下,在24行:
1 static void *
2 _try_open(struct modules *m, const char * name) {
3 const char *l;
4 const char * path = m->path;
5 size_t path_size = strlen(path);
6 size_t name_size = strlen(name);
7
8 int sz = path_size + name_size;
9 //search path
10 void * dl = NULL;
11 char tmp[sz];
12 do
13 {
14 memset(tmp,0,sz);
15 while (*path == ';') path++;
16 if (*path == '\0') break;
17 l = strchr(path, ';');
18 if (l == NULL) l = path + strlen(path);
19 int len = l - path;
20 int i;
21 for (i=0;path[i]!='?' && i < len ;i++) {
22 tmp[i] = path[i];
23 }
24 memcpy(tmp+i,name,name_size);
25 if (path[i] == '?') {
26 strncpy(tmp+i+name_size,path+i+1,len - i - 1);
27 } else {
28 fprintf(stderr,"Invalid C service path\n");
29 exit(1);
30 }
31 dl = dlopen(tmp, RTLD_NOW | RTLD_GLOBAL);
32 path = l;
33 }while(dl == NULL);
34
35 if (dl == NULL) {
36 fprintf(stderr, "try open %s failed : %s\n",name,dlerror());
37 }
38
39 return dl;
40 }
主要是对path这个搜索路径的解析,path可以定义一组路径模板,每个路径用';'隔开,如:“./service/?.so;./http/?.so”,假如模块名为foo,那么就会被解析成"./service/foo.so;./http/foo.so",然后用这两个路径依次调用dlopen,直到有成功加载的。
_open_sym是调用dlsym获取四个契约函数的指针,从实现可以得知,契约函数的命名规则为:模块名_函数名.
最后将sm存入m.
骨骼(skynet_context)
下文用sc代指。
sc主要承载框架的调度逻辑。定义在skynet-src/skynet_server.c中:
struct skynet_context {
void * instance;
struct skynet_module * mod;
void * cb_ud;
skynet_cb cb;
struct message_queue *queue;
FILE * logfile;
char result[32];
uint32_t handle;
int session_id;
int ref;
bool init;
bool endless;
CHECKCALLING_DECL
};
mod:皮囊对象.
instance:用契约函数create创建的。
cb:处理消息的回调函数,由皮囊逻辑里注册。
cb_ud:回调函数的用户数据。
queue:actor的信箱,存放收到的消息。
handle:标识自己的句柄,用于生命周期的管理。
logfile:文件句柄,用与录像功能(将所有收到的消息记录与文件).
result:handle的16进制字符,便于传递。
session_id:上一次分配的session,用于分配不重复的session。
ref:引用计数。
init:是否初始化。
endless:是否在处理消息时死循环。
这里只分析sc的创建,先来看创建函数skynet_context_new,定义在skynet-src/skynet_server.c的119行:
1 struct skynet_context *
2 skynet_context_new(const char * name, const char *param) {
3 struct skynet_module * mod = skynet_module_query(name);
4
5 if (mod == NULL)
6 return NULL;
7
8 void *inst = skynet_module_instance_create(mod);
9 if (inst == NULL)
10 return NULL;
11 struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
12 CHECKCALLING_INIT(ctx)
13
14 ctx->mod = mod;
15 ctx->instance = inst;
16 ctx->ref = 2;
17 ctx->cb = NULL;
18 ctx->cb_ud = NULL;
19 ctx->session_id = 0;
20 ctx->logfile = NULL;
21
22 ctx->init = false;
23 ctx->endless = false;
24 // Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
25 ctx->handle = 0;
26 ctx->handle = skynet_handle_register(ctx);
27 struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
28 // init function maybe use ctx->handle, so it must init at last
29 context_inc();
30
31 CHECKCALLING_BEGIN(ctx)
32 int r = skynet_module_instance_init(mod, inst, ctx, param);
33 CHECKCALLING_END(ctx)
34 if (r == 0) {
35 struct skynet_context * ret = skynet_context_release(ctx);
36 if (ret) {
37 ctx->init = true;
38 }
39 skynet_globalmq_push(queue);
40 if (ret) {
41 skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
42 }
43 return ret;
44 } else {
45 skynet_error(ctx, "FAILED launch %s", name);
46 uint32_t handle = ctx->handle;
47 skynet_context_release(ctx);
48 skynet_handle_retire(handle);
49 struct drop_t d = { handle };
50 skynet_mq_release(queue, drop_message, &d);
51 return NULL;
52 }
53 }
1、加载sm对象,调用create取得用户对象.
2、分配sc,注册handle,分配信箱.
3、调用init初始化用户对象.
之所以到处有一些CALLINGCHECK宏,主要是为了检测调度是否正确,因为skynet调度时,每个actor只会被一个线程持有调度,也就是消息处理是单线程的。
未完待续,不当之处请道友指正。