目录
一:前言
我之前有两篇文章是写定时器方案的,大家可以看一看,这篇文章是基于之前的文章写出来的。
二:手搓时间表盘
1、任务结点,层级,表盘的结构体
对于时间表盘,通过之前的讲解,我们可以得知,它类似于钟表的结构,有时分秒以及指针。
表盘:整个时间表盘的结构,它包含层级和任务结点,其中含有三个层级:时分秒。这些层级使用数组表示。还有锁和当前的时间。
层级:层级使用数组表示了,那么他的每个坑位使用一个指针指向这个地址。
任务结点:这些结点包含当前的时间和任务。
struct timer_node { //表盘上一个一个的结点
struct timer_node *next; //指向下一个
uint32_t expire; //时间
handler_pt callback; //回调函数
uint8_t cancel; //是否被删除
}timer_node_t;
typedef struct link_list {
timer_node_t head;
timer_node_t *tail;
}link_list_t;
typedef struct timer {
link_list_t second[SECONDS]; //层级
link_list_t minute[MINUTES];
link_list_t hour[HOURS];
spinlock_t lock; //自旋锁
uint32_t time; //时间
time_t current_point; //当前的时间
}timer_st;
2、表盘的初始化
首先就是要开辟成表盘结构体的内存,然后通过link_clear将每个层级的每个坑位进行初始化操作。大家如果想象不到这个图的话,那就看看这个。
//初始化表盘
void
init_timer(void) {
TI = create_timer();
TI->current_point = now_time();
}
//创建表盘
static timer_st *
create_timer() {
timer_st *r = (timer_st *)malloc(sizeof(timer_st)); //为表盘开辟空间
memset(r,0,sizeof(*r));
//下面是进行对时分秒行的初始化,在每个表盘层级的数组坑位中添加一个可以连一串结点的头节点。
int i;
for (i=0; i<SECONDS; i++) {
link_clear(&r->second[i]);
}
for (i=0; i<MINUTES; i++) {
link_clear(&r->minute[i]);
}
for (i=0; i<HOURS; i++) {
link_clear(&r->hour[i]);
}
spinlock_init(&r->lock); //初始化完之后进行初始化这个自旋锁。
return r;
}
time_t
now_time() {
struct timespec ti;
clock_gettime(CLOCK_MONOTONIC, &ti);
// 1ns = 1/1000000000 s = 1/1000000 ms
return ti.tv_sec;
}
static timer_node_t *
link_clear(link_list_t *list) {
timer_node_t * ret = list->head.next; //创建个结点,复制第一个结点
list->head.next = 0;
list->tail = &(list->head); //将尾指针也指向这里。
return ret;
}
3、添加定时任务
这个添加定时任务的整体就是,先创建一个任务结点,将这个任务节点进行初始化,如添加时间,添加回调函数。紧接着就是判断时间与当前的时间,如果时间是已经过去的时间,那么我们就立刻让此任务执行。
然而当任务是未来要发生的,那么就要将这个创建好的结点进行插入操作。从图中看的话也就是将这个结点先找到所在的层级,然后所在的位置,在最后连接到一连串的结点中去。
//添加时间任务结点
//整体是:创建时间结点,加锁,对这个结点初始化(时间,回调函数),判断时间,时间为负立即执行,时间为正,则添加到行中去。
timer_node_t*
add_timer(int time, handler_pt func) { //这个包含时间和回调函数
timer_node_t *node = (timer_node_t *)malloc(sizeof(*node)); //创建一个时间结点
spinlock_lock(&TI->lock); //对这个添加时间结点进行加锁
node->expire = time+TI->time; //下面就是存放时间和回调函数
printf("add timer at %u, expire at %u, now_time at %lu\n", TI->time, node->expire, now_time());
node->callback = func;
node->cancel = 0;
if (time <= 0) { //如果传入的时间是负数,代表这个任务需要马上执行
spinlock_unlock(&TI->lock); //进行解锁,并执行回调函数,并释放这个结点。
node->callback(node);
free(node);
return NULL;
} //如果是传入正常的时间,那就进行添加这个结点的操作。
add_node(TI, node); //通过这个添加到行层的操作,我们就已经把定时任务添加到不同的行层中去了。
spinlock_unlock(&TI->lock);
return node;
}
//下面是添加到行中的操作(时间为正的操作)\
这里举个例子:1时30分30秒,那么它要先存放到1小时的位置,等到时间指针移动到小时这里,那么我们检查这个小时这个结点\
我们要将这个结点进行删除一小时,然后发现时间为30分30秒,我们又重新调整位置,把这个结点取出来,存放到分的层级,以此类推。
static void
add_node(timer_st *T, timer_node_t *node) { //传入哪个表盘的哪个结点
uint32_t time=node->expire;
uint32_t current_time=T->time;
uint32_t msec = time - current_time; //这里表示间隔的时间,也就是多少时间后触发:6541秒后触发
if (msec < ONE_MINUTE) { //下面的link_to是将新添加的结点存放到层级中去,也就是连接到指定位置的后面
link_to(&T->second[time % SECONDS], node); //这里是秒的行层
} else if (msec < ONE_HOUR) {
link_to(&T->minute[(uint32_t)(time/ONE_MINUTE) % MINUTES], node); //分的层级
} else {
link_to(&T->hour[(uint32_t)(time/ONE_HOUR) % HOURS], node); //时的层级
}
//也就是根据多长时间后触发,根据这个时间,把他存放在哪一层。
}
//将结点连接起来,每次都添加到结点的后面
static void
link_to(link_list_t *list, timer_node_t *node) { //他这里传入的是层级中的一块位置,添加到这个位置的后面。
list->tail->next = node; //挪动尾指针,一直指向行层的末尾。
list->tail = node;
node->next=0; //新结点的后面为空。
}
4、删除定时任务
这里的删除定时任务很简单,就是将他的cancel值设置为1。它并不是让这个结点消失,如果删除这个结点的话,我们就要按着正常的删除结点的步骤写,很麻烦,如果这样的话,写起来很简单。
//设置他的cancel值,将他默认为被删除
void
del_timer(timer_node_t *node) {
node->cancel = 1;
}
5、检查任务是否超时
讲一下这里的流程:当我们执行这个检查的函数时,我们首先会获得时间差,这里的时间差是通过下面休眠得来的。时间差是两秒,至于为什么是两秒,往后看就明白了,这里先记住。
由于时间差是两秒,那么这个循环也就是循环两次,进入这个timer_update函数后。通过execute函数执行超时的任务,在里面我们可以得到秒数,我们通过这个秒数就可以判断当前的时间是几秒,然后通过找到第几秒的位置。通过link_clear函数得到这一连串任务结点的头部 。这一连串的任务结点就是同一时刻的全部任务,我们通过这个头部,可以完成这一连串的任务,并且释放掉。
这一秒的任务已经执行完了,那么通过timer_shift中将时间进行++操作,我们就得到下一秒的时间了,时间移动了,那么我们要更新整个时间表盘,也就是重新映射,为什么呢?如果加这一秒正好让时针移动,那么这一小时内的全部任务要向上映射,全部存储到分层级中去。如果是分针移动,那么分的要向上映射到秒中去。
这里的重新映射也就是将任务结点所在的位置进行调整,将他们全部拿出来然后再重新插入就好了。如果看不懂,可以去看我之前的文章。这里主要是怎么实现。
//检查时间是否超时
void
check_timer(int *stop) {
while (*stop == 0) {
time_t cp = now_time(); //获取当前的时间
if (cp != TI->current_point) {
uint32_t diff = (uint32_t)(cp - TI->current_point); //获得他们之间的时间差,为什么会有时间差?因为下面休眠了。
TI->current_point = cp;
int i;
for (i=0; i<diff; i++) { //根据这个时间差进行表盘的更新操作。
timer_update(TI); //更新时间表盘
}
}
usleep(200000); //为什么要休眠呢?这里休眠会给前面造成时间差,时间差也就是这里的休眠时长:两秒。那为什么两秒呢?\
上面timer_update中我们执行了两次,其中还加了一秒。\
如果当前时间为4秒,那我们通过timer_update会直接执行第四秒的任务,并重新映射,然后执行第五秒的任务,然后再循环一次,到第6秒(循环两次)\
从4秒到了6秒,那我们再次走timer_update,会发现直接再次执行第6秒,这样并没有漏掉时间或者多走时间。\
并且再次执行第6秒是为了避免这个时候又突然加入这一时刻的任务。
}
}
//时间表盘的更新,重新对时间任务进行映射。
static void
timer_update(timer_st *T) {
spinlock_lock(&T->lock); //表盘的更新操作要加锁。
timer_execute(T); //定时任务的执行
timer_shift(T); //重新映射完
timer_execute(T); //再次进行执行的操作,因为在上面重新映射的时候,将时间+1了,所以这一秒也需要进行执行。
spinlock_unlock(&T->lock);
}
//定时任务的执行,拿出坑位中全部的任务
static void
timer_execute(timer_st *T) {
//在这里是得到具体的秒数,然后对这一秒的任务进行处理操作。
uint32_t idx = T->time % SECONDS;
//这个函数是执行任务的函数,需要在秒这一层进行搜索,也就是挪动指针,如果发现任务超时,那么就执行。
while (T->second[idx].head.next) { //在这一秒的数组坑位中,他的后面可能有多个同时的任务
timer_node_t *current = link_clear(&T->second[idx]); //因此我们需要将这一个坑位中全部的任务进行处理,也就是通过link_clear拿出全部的结点。
spinlock_unlock(&T->lock);
dispatch_list(current); //将这里面全部的任务进行执行。
spinlock_lock(&T->lock);
}
}
//处理任务,将拿出的任务全部进行处理
static void
dispatch_list(timer_node_t *current) {
do {
timer_node_t * temp = current;
current=current->next; //通过不断挪动指针,遍历整个任务
if (temp->cancel == 0) //如果没有被删除。那么就执行他的回调函数
temp->callback(temp);
free(temp); //执行完要进行释放操作。
} while (current);
}
//这里就是重新映射层级
static void
timer_shift(timer_st *T) {
//因为咱们存储的时是12小时,对于下午几点的时间要进行单独的设置,比如这里,通过加一,然后对半天的时间取余操作,就得到了下午的时间。
//下面的操作就是重新映射所在的位置。时间在增加,每次根据增加的时间,来进行重新映射所在的位置。
uint32_t ct = ++T->time % HALF_DAY; //++后得到下一秒的时间,因此需要再次执行一遍timer_execute。
if (ct == 0) { //如果正好是12点整,那么就存放到小时的第0号位置。
remap(T, T->hour, 0);
}
else { //其他时间
if (ct % SECONDS == 0) { //秒是最上面的层级,不需要进行调整,
{
uint32_t idx = (uint32_t)(ct / ONE_MINUTE) % MINUTES;
if (idx != 0) {
remap(T, T->minute, idx);
return;
}
}
{
uint32_t idx = (uint32_t)(ct / ONE_HOUR) % HOURS;
if (idx != 0) {
remap(T, T->hour, idx);
}
}
}
}
}
6、清空任务
这个清空也很简单,就是将每个坑位全部提取出来然后释放掉就可以。
//将全部的任务结点进行删除
void
clear_timer() {
int i;
for (i=0; i<SECONDS; i++) { //遍历整个层级
link_list_t * list = &TI->second[i]; //获得层级中的每一个坑位
timer_node_t* current = list->head.next;
while(current) { //通过循环将这个坑位中的一串任务结点全部清空。
timer_node_t * temp = current;
current = current->next;
free(temp);
}
link_clear(&TI->second[i]); //清空任务结点
} //下面也是一样
for (i=0; i<MINUTES; i++) {
link_list_t * list = &TI->minute[i];
timer_node_t* current = list->head.next;
while(current) {
timer_node_t * temp = current;
current = current->next;
free(temp);
}
link_clear(&TI->minute[i]);
}
for (i=0; i<HOURS; i++) {
link_list_t * list = &TI->hour[i];
timer_node_t* current = list->head.next;
while(current) {
timer_node_t * temp = current;
current = current->next;
free(temp);
}
link_clear(&TI->hour[i]);
}
}
今天的讲解结束了,看不懂的可以去先看之前的文章,将大概梳理清楚,然后再来看这篇实现文章。https://xxetb.xetslk.com/s/2D96kH