Skynet source code analysis: operation and main thread worker and message queue

skynet startup process

The skynet program only has the skynet-src directory. ./skynet ./example/config starts
skynet_main.c to read the configuration file, set environment variables, and call the skynet_start function of skynet_start.c

The skynet_start function is the initial basic service, calling the _start function to start the _timer thread, socket thread, _monitor thread, and the configured multiple _worker threads to start working

skynet operating mechanism

The most important thing for each service of skynet is to set a callback function. When a service sends a message to another service, it will be pressed into the message queue of this service, waiting for the _worker thread to take out a service message queue from the global queue, and then take it out from this message queue. A message is processed using the callback of this service. The
game server generally starts a socket service to teach gate.so management. The socket service can accept external msg and pass it to the internal service for processing.

monitor

Skynet's service monitoring is relatively simple. From a design principle, this is also correct, because the framework layer can basically report and log, and the upper-level business is very changing, no matter how you write it, it It may not meet the business needs of the upper level. The monitoring of services in skynet is implemented in skynet_monitor.c and skynet_monitor.h. When the service may fall into an infinite loop, a log is created.

Every time a message is dispatched, skynet_monitor_trigger is called, which is adjusted twice. The first time the parameters source and destination are real values, that is, they are not 0. The second adjustment is when the message distribution is completed, both source and destination are assigned 0.

If the message distribution is not completed after the first trigger call, the monitor thread checks for the first time and assigns the value of check_version to version. Then the monitor thread checks for the second time. At this time, the version and check_version will be equal, and the destination is not 0 at this time, and it will enter the process of releasing the target service and printing the alarm.

timer

Skynet's timer is a function that is frequently used in games. It makes sense to analyze its source code. Besides, the core C source code has been basically analyzed except for timer and network. Others are related to lua c api, or interact with lua more. The source code of timer is in skynet-timer.c and skynet-timer.h.
Skynet's external timer is stored in two parts, one part is stored in an array called near, and the other part is stored in a two-dimensional array, divided into four levels.
There is a timer thread in skynet, which updates the time in the timer every 2.5 milliseconds. Each update will add 1 to a counter called time, so this counter can actually be treated as time, and then the timer in the near array is triggered.

1.main function

The main function is the entry point of the skynet process, and it needs a configuration file path as a parameter. The main function first does some memory allocation work, and then loads the content in the configuration file. Then set the content in the configuration file to the lua environment variable _ENV. Then read the configuration from _ENV to the c configuration. Finally, skynet_start is called.

2.skynet_start function

The skynet_start function determines whether the service is started in the background according to the configuration, and then initializes the data structures of timer/socket/module/mq, allocates memory to them, and fills in some necessary fields. The next step is to create and register a log logger service, which is implemented by the C layer. Then create and register the snax service and do the bootstrap operation. Finally, the start function is called.

3.start function

This function will start the timer/socket/monitor/worker thread. There is only one timer/socket/monitor thread, but there are multiple worker threads, which are configured in the configuration file with thread=num. If there is no configuration in the configuration file, there are 8. The first worker thread has its own weight value. The larger the weight value, the smaller the time slice for a single service.

After the threads are started, they enter pthread_join and wait for the termination of the threads. After all threads terminate, skynet will exit.

The message queue is one of the core functions of skynet. To put it bluntly, its function is to enter and leave the queue, first in, first out. This data structure has been discussed. The source code is implemented in skynet_mq.h and skynet_mq.c. Let's take a look at these two files.

#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)
    }
}

The analysis of the code is over. It can be seen that there are actually two kinds of message queues in skynet, one is the global message queue, and the other is the service message queue. Each service has its own message queue, and each service message queue has the handle identifier of the service. This involves the distribution of messages, so I won't expand it here. Each service message queue is referenced by the global message queue.

The global message queue is implemented using a classic linked list, while the service message queue is implemented using a circular array that is relatively unintuitive and may be particularly difficult for some people to understand. And when the array space is not enough, it will dynamically expand, and the capacity will be expanded to twice the current capacity.

The name of the dequeue and enqueue function of the message queue is relatively simple and clear, push/pop. This name may bring some misunderstandings, if it is changed to enqueue/dequeue, it is more in line with its actual function.

Recommend to everyone a training camp about the actual combat of the skynet project. Registration is now equivalent to free. The main content:

Multi-core concurrent programming
message queue. Thread pool
actor message scheduling
Network module implementation
Time wheel timer implementation
lua/c/interface programming
Skynet programming essentials
demo demonstrates actor programming thinking

Address: Click to watch the video.
More skynet information plus group: 832218493 Get it for free!
Insert picture description here

Guess you like

Origin blog.csdn.net/lingshengxueyuan/article/details/112321336