前言:
这已经不是我第一次阅读skynet的源码了,以前每次都是走马观花的看了自己关心的部分内容。对其内部的结构和流程只是有个大概的了解。最近又开始研究使用skynet这次我下定决定系统化的阅读一遍源码,将源码的大部分的内容加上方便理解和查看的注释(我拉了一个分支专门写注释:https://github.com/xzben/skynet.git),另外也计划将整理框架的流程细节总结下。方便使用中减少不必要的采坑和细节小错误。
一、整体结构
1、线程模型,skynet 包含了 一个网络线程,一个定时器线程,一个检测线程(用于检测任务线程是否有卡死的情况),若干(可配置)工作任务线程。
2、skynet 中没有使用锁的,大部分是使用原子操作来控制多线程的竞争问题。另外在结构设计上skynet本身就避免了很多的线程竞争问题,比如工作线程通过使用二级队列并限制一个服务的消息队列只会出现在一个线程中处理,比如网络模块通过管道的方式进行网络的事件控制操作。
3、任务队列模型为双极队列模式。我们框架中每个逻辑单元(我们称之为 “服务” service)都有一个自己的专属消息队列(这个队列就是第二级队列),全局有一个大的消息队列,里面存放当前有消息需要处理的二级队列。
我们工作线程的循环工作就是从全局队列中读取二级队列出来,然后消耗二级队列的消息,每次只会处理二级队列中一定数量的消息(具体数量和线程配置的权重有关,但是最多一次消耗当前队列的一般消息)就将二级队列塞入全局队列尾部让出工作线程,防止一个服务任务太多饿死其它服务。
这里还有一个很特殊的地方就是我们全局消息队列中的二级队列不会同时出现两次,也就是不会同时不会有两个线程会处理同一个服务的消息。这个点很重要,那就是这个设计能够保证我们每个服务的消息处理都是单线程也就是写逻辑的时候单服务不需要考虑统一服务中的资源存在多线程竞争问题。这是个很棒的设计能让业务层写逻辑抛开多线程的烦恼。
4、网络层skynet 采用的单线程的模型,linux 下用epoll驱动网络事件。
这里需要特殊指出的是网络层由于数据结构的限制只支持(1<<16 = 65536)个连接。如果你要增加需要改下 MAX_SOCKET_P 这个宏定义。
另外网络的所有操作都是网络线程操作处理的,也就是网络线程不仅处理网络的io事件监听处理,还同时具有操作相关监听操作的处理,而不是放在逻辑线程处理。大部分的框架的网络事件的操作都是放在工作线程来处理的。那么就会引入锁这种东西增加了很大的复杂度。逻辑上就没那么的清晰简单了。skynet 通过创建一个 管道 的读写通道外部逻辑线程发送操作命令,网络线程读取操作命令并执行。完美的别开了网络管理结构的多线程竞争问题。
5、定时器功能,skynet单独配置了一个线程来做定时器的任务这样我们的定时器的时间准确性要高很多,唯一的延迟只会消耗在定时到达后消息从产生到被收到这一派发过程的延迟了,但是这种延迟基本可以忽略了。
6、监视线程,skynet 为了达到监控 业务线程是否会出现死循环类的卡死现象专门配置一个线程坐死循环检测,原理就是工作线程每次处理一个消息都回升级一次线程的工作版本号,而监视线程每隔五秒中对比一次每个工作线程的工作版本号,如果五秒钟版本号没变则可能陷入死循环卡死状态,则输出报错日志警告。
到此skynet的整体大框架就讲述完毕了,核心的内容就是这么多。剩下的逐步解析下各个比较重要的模块的细节
二、各个核心模块的深入解析
1、逻辑单元管理模块(skynet_context) skynet-src/skynet_server.c
在skynet中一个逻辑单元管理模块 数据结构上就是一个 skynet_context,数据结构如下
struct skynet_context {
void * instance; //当前服务对象指针
struct skynet_module * mod; //服务所属模块
uint32_t handle; //服务id
int ref; //引用数量
char result[32]; // 用于操作 服务时的cmd命令存储返回值用
void * cb_ud; // 服务消息处理回调函数的 user data
skynet_cb cb; // 服务消息处理回调函数
int session_id;//服务的消息会话id
struct message_queue *queue; //服务的消息队列
bool init; //服务对象是否已经初始化过
bool endless; // 服务是否进入死循环卡死状态
CHECKCALLING_DECL
};
通过我们查看上面的数据结构注释我们可以指定,其实一个逻辑模块的组成还是比较简单的,主要是一个 专属的消息队列,一个消息消耗执行的回调函数,另外就是模块的标识id,控制内存释放的引用计数器,剩下的 比较特殊的属性是比较有特色的一些内容,就是我们的逻辑单元模块的实现是可以动态定制一个skynet_module,也就是使用者可以根据自己的情况很方便的定制自己的逻辑单元模块,将其编译成动态库并导出三个关键接口就行了。skynet_module 的结构如下:
struct skynet_module {
const char * name; //库的名字
void * module; // 动态库的 打开句柄
skynet_dl_create create; // 库对外暴露的 create 接口
skynet_dl_init init; // 库对外暴露的 init 接口
skynet_dl_release release;// 库对外暴露的 release 接口
};
就像上述的结构体一样我们只要定制我们自己的模块,只需要定制一个 动态库,并导出 create, init,release 单个关键接口,然后init内部设置消息监听 回调函数 skynet_cb 就能成为一个skynet的逻辑服,正常接收到消息做自己的逻辑处理。简单的动态库的实现可以参考 service-src/service_logger.c 文件这是一个最精简的模块了。
2、逻辑模块的管理器( handle_storage )skynet-src/skynet_handle.c
首先我们看下结构体
struct handle_storage {
//读写锁,这里的锁使用的是原子操作实现,而非我们常见的线程锁之类的
struct rwlock lock;
//当前进程的节点id,主要是 harbor 分布式架构中使用的,
//如果使用 cluster 模式这个值都是0,我们服务的handle 值高两个字节存储的就是 harbor值
uint32_t harbor;
// 用于寻找空闲服务存储槽位的index值
uint32_t handle_index;
// 当前可用存储服务的数组大小
int slot_size;
// 存储服务的指针数组
struct skynet_context ** slot;
//当前存储服务handle 映射名字的 数组容量
int name_cap;
// 当前存储的映射数据数量
int name_count;
// 存储映射关系的数据,这个存储结构存储的方案使用的是按名字的字符顺序
// 从小到大的方式存储的。主要是为了快速查找名字所对应的handle
struct handle_name *name;
};
其实这个管理模块的功能很简单,就是存储服务,和服务handle别名name的映射关系。
3、外部自定义服务模块管理(modules) skynet-src/skynet_module.c
前面提到了skynet允许用户自定自己的逻辑服务模块,只需要编译好自己的模块动态,并在动态库导出create init release 接口并在init的时候设置好skynet_cb 就可以使用。具体 modules 模块的管理机制就是我们这里要介绍的。首先看下管理模块的数据结构
struct modules {
int count; //当前拥有的模块数量
int lock; //数据结构控制读取的锁,这里用原子操作控制的,所以int 能当锁控制
const char * path; // 模块动态库的搜索匹配路径配置,有点类似 lua 的 package.cpath 类似的配置
struct skynet_module m[MAX_MODULE_TYPE]; //存储模块的数组,限定大小为32,如果需要扩展修改宏 MAX_MODULE_TYPE 就行了
};
skynet 现在目前代码限制的第三方模块大小为 32,如果需要扩展更多自行修改宏定义就行了。这里的管理模块采用的是惰性加载的模式,也就是只有当第一次使用这个模块的时候才会去加载对应的模块,并解析模块的的必要接口存储起来。
4、网络模块 (socket_server) skynet-src/skynet_server.c skynet-src/skynet_socket.c
skynet 网络模块,主要有几个特点,1、网络模块的逻辑驱动只有一个线程运行 2、网络事件linux 使用epoll,apple 使用kqueue 3、网络事件操控是通过 pip 管道从逻辑线程发送给网络线程运行的,这样避免了对网络管理结构体的多线程处理问题。4、网络中的socket 套接字都是用非阻塞模式的。这样能够更大程度的利用单线程的性能,避免不必要的IO阻塞等待时间。5、网络模块限定的管理数组大小为(1<<16 = 65536)个连接,如果需要增加请自行改宏定义 MAX_SOCKET 6、网络层支持发送消息的高、低两种优先级。 7、所有的连接socket 在走完基本流程时,还需要业务层自己主动open或start 启动正式使用否则网络层是没有处理这些socket的网络事件的,这样做就相当于可以给业务层去对连接的一个过滤和验证过程。 8、网络模块最终收到的消息发送给注册的业务逻辑服时他的消息都是 socket_message结构体,网络层是没有处理粘包问题的。网络层只是单纯的不断接收buffer然后发给关心的业务层。业务层接收到网络消息后需要用 netpack(lualib-src/lua-netpack) 插件工具使用 netpack.filter 接口去处理粘包然后将通信的数据流拆分成一个一个独立的网络消息buffer。netpack.filter 接口返回 queue,buffer 的lightuserdaba, size 三个返回值,queue 是一个buffer的队列,用于中转不完整的buffer流。
5、lua服务的核心模块 (snlua) server-src/server_snlua.c
前面我们的基础框架其实没有具体限定我们业务逻辑模块具体是什么结构的。只是简单的提供了模块管理结构和消息分发的工作线程。而我们接触到底skynet 服务大多数是lua写的,这里其实就是通过 这个框架自带的外部库 snlua 实现的。按云凤的说法其实我们具体的业务服务用什么语言都是没有关系的只要你自己写好这个业务扩展的模块就行了。那么不管你用啥都行。因为本身skynet的核心并不关心 你的逻辑用什么做它只是帮我们管理好网络事件,任务消息分发执行,剩下的用我们自定义的服务扩展模块做就行了。比如框架自带的lua模块 snlua。
其实我们查看snlua的代码也是很简单的,它主要的工作就是提供一个lua虚拟机,并且在init 设置了一个一次性的skynet_cb 用于启动加载lua对应的服务文件和给这个lua虚拟机设置我们再启动配置中配置的 lua服务查找配置路径和预加载前缀文件LUA_PRELOAD(就是所有服务加载前都会先加载这个文件)。