skynet启动流程
skynet程序只有skynet-src目录,./skynet ./example/config启动
skynet_main.c读取配置文件,设置环境变量,调用skynet_start.c的skynet_start函数
skynet_start函数初始基础服务,调用_start函数启动_timer线程、socket线程、_monitor线程以及配置的多个_worker线程开始工作
skynet运行机制
skynet每个服务最重要的是设置一个callback函数,服务向另一个服务发消息都会压入这个服务的消息队列,等待_worker线程从全局队列取出一个服务的消息队列,然后再从这个消息队列取出一个消息使用这个服务的callback处理
游戏服务器一般会启动一个socket服务教给gate.so管理,socket服务可以接受外部msg传递到内部服务来处理。
monitor
skynet对服务的监控做得比较简陋,从设计原则上来说,这样做也是对的,因为框架层能做的,基本就是上报和打日志,上层的业务是变化万千的,不论怎么写,都可能满足不了上层的业务需求。skynet中对服务的监控实现在skynet_monitor.c和skynet_monitor.h中,当服务可能陷入死循环的时候,就打一条日志。
每次消息派发,都会调用skynet_monitor_trigger,一共调两次,第一次参数source和destination是真实的值,也就是不为0。第二次调是在消息派发完成的时候,source和destination都赋0。
如果第一次trigger调用以后,消息派发迟迟不完成,monitor线程第一次检查,会将check_version的值赋为version。然后monitor线程第二次检查,这个时候version和check_version就会相等,而且这时候destination也不为0,就会进入释放目标服务和打印报警的流程。
timer
skynet的timer是做游戏用得比较频繁的一个功能,分析一下它的源码还是有意义的。而且核心的C源码除了timer和网络以外,已经基本分析得差不多了。其它都是跟lua c api相关,或者是跟lua交互比较多的。timer的源码在skynet-timer.c和skynet-timer.h中。
skynet的外部定时器是分为两部分存的,一部分存在叫near的数组里,另一部分存在一个二维数组里,分为四个级别。
skynet中有一个timer线程,每2.5毫秒就更新一下timer中的时间。每次更新都会对一个叫time的计数器做加1操作,所以这个计数器其实可以当作时间来看待,然后对near数组中的定时器进行触发。
1.main函数
main函数是skynet进程的入口点,它需要一个配置文件的路径作为参数。main函数先做一些内存分配工作,然后加载配置文件里的内容。再把配置文件里的内容设置到lua环境变量_ENV。然后从_ENV里读取配置到c配置中。最后调用skynet_start。
2.skynet_start函数
skynet_start函数根据配置确定服务是不是以后台方式启动,然后初始化timer/socket/module/mq这些数据结构,给它们分配内存,填充一些必要的字段。下一步创建和注册日志logger服务,这个logger服务是C层实现的。然后创建注册snax服务,做bootstrap操作。最后调用start函数。
3.start函数
这个函数会启动timer/socket/monitor/worker线程,timer/socket/monitor线程都只有一个,唯独worker线程是多个,在配置文件中以thread=num来配置的。如果配置文件中没有配,就是8个。第个worker线程都有自己的权重值,权重值越大的,给单个服务的时间片越小。
线程都启动完以后,就进入pthread_join,等待线程的终止。所有线程终止以后,skynet就会退出。
消息队列是skynet的核心功能之一,它的功能说白了就是入队出队,先进先出,这个数据结构都有讲过。源码实现在skynet_mq.h和skynet_mq.c中。下面来看一下这两个文件。
#ifndef SKYNET_MESSAGE_QUEUE_H
#define SKYNET_MESSAGE_QUEUE_H
#include <stdlib.h>
#include <stdint.h>
//消息结构体
struct skynet_message {
uint32_t source; //从哪里来
int session; //参考云风的博客,这个session是个标识
void * data; //消息体
size_t sz;//长度
};
// type is encoding in skynet_message.sz high 8bit
#define MESSAGE_TYPE_MASK (SIZE_MAX >> 8)
#define MESSAGE_TYPE_SHIFT ((sizeof(size_t)-1) * 8)
struct message_queue;
//全局消息入队
void skynet_globalmq_push(struct message_queue * queue);
//全局消息出队
struct message_queue * skynet_globalmq_pop(void);
//创建一个消息队列
struct message_queue * skynet_mq_create(uint32_t handle);
void skynet_mq_mark_release(struct message_queue *q);
//消息移除
typedef void (*message_drop)(struct skynet_message *, void *);
//队列释放
void skynet_mq_release(struct message_queue *q, message_drop drop_func, void *ud);
//消息处理者handler
uint32_t skynet_mq_handle(struct message_queue *);
// 0 for success
//消息出队
int skynet_mq_pop(struct message_queue *q, struct skynet_message *message);
//消息入队
void skynet_mq_push(struct message_queue *q, struct skynet_message *message);
// return the length of message queue, for debug
int skynet_mq_length(struct message_queue *q);
int skynet_mq_overload(struct message_queue *q);
void skynet_mq_init();
#endif
上面有价值的东西实际就是消息的结构体,其它都是声明而已。
//默认队列长度为64
#define DEFAULT_QUEUE_SIZE 64
//最大长度为max(16bit)+1=65536
#define MAX_GLOBAL_MQ 0x10000
// 0 means mq is not in global mq.
// 1 means mq is in global mq , or the message is dispatching.
#define MQ_IN_GLOBAL 1
#define MQ_OVERLOAD 1024
struct message_queue {
struct spinlock lock;
uint32_t handle; //目标handler
int cap; //容量
int head; //头位置
int tail; //末尾位置
int release; //释放标记
int in_global; //是否在全局队列中
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;
};
static struct global_queue *Q = NULL;
void
skynet_globalmq_push(struct message_queue * queue) {
struct global_queue *q= Q;
SPIN_LOCK(q)
assert(queue->next == NULL);
if(q->tail) { //链表不为空
q->tail->next = queue;
q->tail = queue;
} else { //链表为空
q->head = q->tail = queue;
}
SPIN_UNLOCK(q)
}
//取链表中第一个消息队列
struct message_queue *
skynet_globalmq_pop() {
struct global_queue *q = Q;
SPIN_LOCK(q)
struct message_queue *mq = q->head;
if(mq) {
//注意这里,队列取出来后,就从链表中删除了
q->head = mq->next;
if(q->head == NULL) {
assert(mq == q->tail);
q->tail = NULL;
}
mq->next = NULL;
}
SPIN_UNLOCK(q)
return mq;
}
//创建一个消息队列
struct message_queue *
skynet_mq_create(uint32_t handle) {
struct message_queue *q = skynet_malloc(sizeof(*q));
q->handle = handle;
q->cap = DEFAULT_QUEUE_SIZE;
q->head = 0;//刚开始头为0
q->tail = 0;//刚开始尾也为0
SPIN_INIT(q)
// When the queue is create (always between service create and service init) ,
// set in_global flag to avoid push it to global queue .
// If the service init success, skynet_context_new will call skynet_mq_push to push it to global queue.
q->in_global = MQ_IN_GLOBAL;
q->release = 0;
q->overload = 0;
q->overload_threshold = MQ_OVERLOAD;
///这里分配的是数组,是数组,是数组
q->queue = skynet_malloc(sizeof(struct skynet_message) * q->cap);
q->next = NULL;
return q;
}
//释放队列,回收内存
static void
_release(struct message_queue *q) {
assert(q->next == NULL);
SPIN_DESTROY(q)
skynet_free(q->queue);
skynet_free(q);
}
//返回队列的handler
uint32_t
skynet_mq_handle(struct message_queue *q) {
return q->handle;
}
//获取队列长度,注意数组被循环使用的情况
int
skynet_mq_length(struct message_queue *q) {
int head, tail,cap;
SPIN_LOCK(q)
head = q->head;
tail = q->tail;
cap = q->cap;
SPIN_UNLOCK(q)
//当还没有循环使用数组的时候
if (head <= tail) {
return tail - head;
}
//当数组已经被循环使用的时候
return tail + cap - head;
}
//获取负载情况
int
skynet_mq_overload(struct message_queue *q) {
if (q->overload) {
int overload = q->overload;
q->overload = 0; //这里清零是为了避免持续产生报警,在skynet-server.c中
return overload;
}
return 0;
}
//消息队列出队,从数组中出队
int
skynet_mq_pop(struct message_queue *q, struct skynet_message *message) {
int ret = 1;
SPIN_LOCK(q)
//说明队列不是空的
if (q->head != q->tail) {
*message = q->queue[q->head++]; //注意head++,数据不移动,移动的是游标
ret = 0;
int head = q->head;
int tail = q->tail;
int cap = q->cap;
//因为是循环数组,超出边界后要重头开始,所以设为0
if (head >= cap) {
q->head = head = 0;
}
//如果数组被循环使用了,那么tail < head
int length = tail - head;
if (length < 0) {
length += cap;
}
//长度要超过阀值了,扩容一倍,和c++的vector一样的策略
while (length > q->overload_threshold) {
q->overload = length;
q->overload_threshold *= 2;
}
} else { //队列是空的
// reset overload_threshold when queue is empty
q->overload_threshold = MQ_OVERLOAD;
}
if (ret) {
q->in_global = 0;
}
SPIN_UNLOCK(q)
return ret;
}
//为了方便和上面的函数对比,我把skynet_mq_push提上来了
void
skynet_mq_push(struct message_queue *q, struct skynet_message *message) {
assert(message);
SPIN_LOCK(q)
//入队
q->queue[q->tail] = *message;
//因为是循环数组,越界了要重头开始
if (++ q->tail >= q->cap) {
q->tail = 0;
}
//如果首尾重叠了,要扩展
if (q->head == q->tail) {
expand_queue(q);
}
//重新放回全局队列中
if (q->in_global == 0) {
q->in_global = MQ_IN_GLOBAL;
skynet_globalmq_push(q);
}
SPIN_UNLOCK(q)
}
//扩展循环数组
static void
expand_queue(struct message_queue *q) {
//新建一个数组
struct skynet_message *new_queue = skynet_malloc(sizeof(struct skynet_message) * q->cap * 2);
int i;
for (i=0;i<q->cap;i++) { //老数据拷过来
new_queue[i] = q->queue[(q->head + i) % q->cap];
}
q->head = 0; //重设head
q->tail = q->cap; //重设tail
q->cap *= 2;
skynet_free(q->queue); //释放老数组
q->queue = new_queue;
}
//初始化全局队列
void
skynet_mq_init() {
struct global_queue *q = skynet_malloc(sizeof(*q));
memset(q,0,sizeof(*q));
SPIN_INIT(q);
Q=q;
}
//服务释放标记
void
skynet_mq_mark_release(struct message_queue *q) {
SPIN_LOCK(q)
assert(q->release == 0);
q->release = 1;
if (q->in_global != MQ_IN_GLOBAL) {
skynet_globalmq_push(q);
}
SPIN_UNLOCK(q)
}
//释放服务,清空循环数组
static void
_drop_queue(struct message_queue *q, message_drop drop_func, void *ud) {
struct skynet_message msg;
while(!skynet_mq_pop(q, &msg)) {
drop_func(&msg, ud);
}
_release(q); //回收内存
}
//释放服务相关的队列
void
skynet_mq_release(struct message_queue *q, message_drop drop_func, void *ud) {
SPIN_LOCK(q)
if (q->release) {
SPIN_UNLOCK(q)
_drop_queue(q, drop_func, ud);
} else {
skynet_globalmq_push(q);
SPIN_UNLOCK(q)
}
}
代码到此分析结束,可以看出来,skynet的消息队列实际上是有两种,一种是全局消息队列,一种是服务消息队列。每个服务都有自己的消息队列,每个服务消息队列中都有服务的handle标识。这个涉及到消息的派发,这里就不展开了。每个服务消息队列被全局消息队列引用。
全局消息队列用的是经典的链表来实现的,而服务的消息队列用的是比较不直观,可能对有些人来说理解起来特别困难的循环数组来实现的。而且数组空间不够的时候,会动态扩展,容量扩展为当前容量的2倍。
消息队列的出队入队函数名都比较简单而且明了,push/pop。这个名字可能会带来一定的误解,如果改成enqueue/dequeue的话,就更符合它的实际功能。
给大家推荐一个关于skynet项目实战的一个训练营 现在报名相当于免费,主讲内容:
多核并发编程消息队列。线程池actor消息调度网络模块实现时间轮定时器实现lua/c/接口编程skynet编程精要demo演示actor编程思维
地址:点击观看视频