2017-08-12 20:43:48

skynet源码分析01-设计小览

不知不觉用skynet快一年了,几个月前有段时间从源码中大概看了一些设计思路,其实主要是看了消息处理的协程那块,即便如此,也对我后面的工作有很大的帮助。后面忙于业务逻辑上的处理,加上自己确实很懒,就暂时放下这块。近期又有点时间了,就慢慢整体梳理了下,除了cluster和snax基本看了一遍,这里也算是做个笔记。此篇主要将各个线程的主要工作流程抠出来看,不注重细节。我一直觉得认识一件新事物之前,应该先去窥全貌,才能理解的透彻。

建议先大致过一遍skynet的wiki
本文配合2016年下旬最新版skynet源码注释更佳

设计简述

简单说下skynet的思想(我也不管对不对,我的地盘我做主~):一切皆服务,服务间通过发送消息来通信,就像公司一样,每个员工都是一个独立个体,但是又需要相互协作(交流/通信),才能把公司办好。 skynet创建了多个线程,各个线程独立工作,却也相互配合。其中worker线程起调度消息作用,socket线程来与客户端通信,并将客户端发来的数据转发给其他的服务。

skynet的线程

一般我们只用到一个skynet进程就够了,就是工作在单节点模式。一个skynet进程有多个线程,主要为:

  • 启动线程
  • 一个 moniter 线程
  • 一个 timer 线程
  • 一个 socket 线程
  • 多个 worker 线程(视cpu核心数而定)

启动线程

这里的"启动线程"不知道这样称呼对不对(不是软件专业毕业的硬伤~),反正我就是这样称呼了,它主要是指创建各个线程之前的流程。它做的工作为:

  1. 整个skynet进程的入口
  2. 初始化环境变量
  3. 进程信号的处理
  4. 加载配置文件代码块并针对配置文件做一系列初始化工作
  5. 各个模块的初始化
  6. 创建第一个 logger 服务(为什么第一个创建呢,因为靠它来输出log)
  7. 加载snlua模块(所有的lua服务都是通过它来创建),通过给snlua服务发送第一个消息等工作线程创建完后来从消息队列中取出这个消息来创建bootstrap服务,然后bootstrap服务会进行又一轮的初始化工作
  8. 创建各个线程

worker线程

把它放在前面是因为"一切皆服务"的思想在我理解就在这里体现的。

  • 工作线程维护一个全局的消息队列,全局的消息队列中又是各个服务的消息队列。
  • 工作线程的工作其实很简单:从全局消息队列中弹出服务的消息队列,然后根据此线程权重来决定处理此服务的消息队列中的多少个消息,然后再把这个服务的消息队列重新加入到全局消息队列的队列尾,等待下一次它被取出
  • 每次将服务的消息队列从全局消息队列中弹出代表已经开始处理了,所以要先调用 skynet_monitor_trigger 将其此次消息处理加入到 moniter 线程的监控之下,然后再进行消息处理,处理完毕后调用skynet_monitor_trigger将此次消息处理的监控移除。

moniter线程

moniter线程,顾名思义,监控线程,主要是用来监控工作线程是否有异常:是否已经陷入死循环(这样说也不太对,主要是看是不是一个消息处理的时间过长:5秒钟),如果发现陷入死循环,仅仅打印一句log。其实就是一个辅助作用的线程,但是如果配合 debug_console 能很快的定位出陷入死循环的地方在哪,或者说哪个消息处理时花的时间过长。

timer线程

就是一个定时器线程,skynet 框架的定时任务(skynet.timeout)就是就是通过它来做的。它是模拟linux下的时间片轮转的方式。每过1/100秒就转一个刻度(想象一下水表)。定时器线程主要分为5个层级,从低层级到高层级的粒度分别为:256、64、64、64、64,当我们需要向skynet框架注册一个定时器时,就算出与当前时间对应的偏移值,将其链接到对应的层级的节点。就像齿轮一样,只要有动力它就会一直转,转到某个层级的节点如果发现其下链接有事件,就会将对应的事件链表依次取出,找到是哪个服务注册的定时器,将向那个服务压入一条定时器消息,等待worker线程的处理,一直到这个链表为空,齿轮才会转到下一个时间节点。如果低层级的齿轮转完了,就将高层级的一个节点继续分配到最低层级的齿轮上去。 链接参考: 浅析 Linux 中的时间编程和实现原理,第 3 部分: Linux 内核的工作,前面部分一起看可能会好点。

socket线程

socket线程的工作简单来说:

  • 监控进程内的请求:执行上层过来的发送socket数据的请求,通过select实现
  • 监控client发送到server的数据:将client过来的socket数据交给对应的服务(一般为gate服务),通过epoll实现(可选其他的方式)

稍微复杂点:

  1. 在启动线程中创建了1个管道,此管道主要用来接收上层的命令(监听、连接、发送数据等),它交给select监控是否可读/可写。如果上层需要对socket描述符进行写操作,唯一的方式是通过相应的接口向socket描述符对应的id(socket线程为每个socket描述符分配了一个id,上层只能通过这个id进行操作,而不能直接得到socket描述符文件)写一些数据,然后向此id写一个相应的命令(比如'S' 'L' 'D')
  2. 在启动线程初始化 socket 模块时创建了一个struct socket_server统领socket处理的全局,下面主要管理了:管道描述符、epoll描述符、epoll事件、每个socket描述符对应的一个struct socket
  3. 在2中说到的"每个socket描述符对应的一个struct socket"是socket线程处理的核心,它通过protocol字段告诉框架此socket采用的协议,通过type字段说明当前socket进行到哪一步了(是准备监听还是正在监听,是accept完成了还是正在进行,是否已经准备好接收数据了) 。它同时也作为epoll的自定义数据,这样当epoll发现有事件发生时skynet框架便能通过type字段知道进行到哪一步了,才能做出相应的处理。

工作模式

如果只看工作线程,假设是一个四核八线程的机器,thread也配置的是8,那么就会有8个线程同时工作,但是在skynet中,这也意味着最多只有8个协程在工作。只有这个协程让出了执行权,另外一个协程才能执行。

Permanent link of this article:http://nulls.cc/post/skynet_srccode_analysis01_design_overview

-- EOF --