skynet源码分析2:模块

2024-05-07 05:18
文章标签 分析 模块 源码 skynet

本文主要是介绍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. 加载动态库,获取契约函数指针。
  2. 缓存已加载的动态库。

我们来看看它的实现吧,它头文件的接口定义如下:

复制代码
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只会被一个线程持有调度,也就是消息处理是单线程的。


未完待续,不当之处请道友指正。

这篇关于skynet源码分析2:模块的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/966427

相关文章

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

慢sql提前分析预警和动态sql替换-Mybatis-SQL

《慢sql提前分析预警和动态sql替换-Mybatis-SQL》为防止慢SQL问题而开发的MyBatis组件,该组件能够在开发、测试阶段自动分析SQL语句,并在出现慢SQL问题时通过Ducc配置实现动... 目录背景解决思路开源方案调研设计方案详细设计使用方法1、引入依赖jar包2、配置组件XML3、核心配

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Python中模块graphviz使用入门

《Python中模块graphviz使用入门》graphviz是一个用于创建和操作图形的Python库,本文主要介绍了Python中模块graphviz使用入门,具有一定的参考价值,感兴趣的可以了解一... 目录1.安装2. 基本用法2.1 输出图像格式2.2 图像style设置2.3 属性2.4 子图和聚

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

Python中的getopt模块用法小结

《Python中的getopt模块用法小结》getopt.getopt()函数是Python中用于解析命令行参数的标准库函数,该函数可以从命令行中提取选项和参数,并对它们进行处理,本文详细介绍了Pyt... 目录getopt模块介绍getopt.getopt函数的介绍getopt模块的常用用法getopt模

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思