最小堆定时器
我们将使用内核现有的数据结构和同步结构的 API 来说明怎样设计一个最小堆定时器,并扩展语义。内核在 2.6.16
就已经使用最小堆实现了一个叫着 hrtimer
的定时器,但是语义较复杂,我们将使用它的一些现成的数据结构和辅助函数来实现 timer_list
定时器。
最小堆定时器队列
最小堆数据结构的语义是有序的、线性的数据结构,“最小”的成员在容器的前端。在 内核定时器 一文中,我们讲解了时间轮算法,是由多个有规律的松散定时器队列构成的,这里我们用最小堆来管理排队的定时器,所以最小堆具有队列语义。我们用使用内核提供的红黑树来实现。
关于红黑树的实现网上有很多文章,我们这专注实现一个最小堆。
- 数据结构
struct timerqueue_node {
struct rb_node node;
uint64_t expires; /*我们将使用绝对时间,来判断是否超时*/
};
struct timerqueue_head {
struct rb_root head;
struct timerqueue_node *next;
};
还是内核一贯的风格, struct timerqueue_node
还是作为一个成员在需要排序的对象中,然后使用 container_of()
计算被浸入的对象的指针。
struct timerqueue_head
作为小堆容器的描述符,其中 next
字段指向最小堆的第一个元素,每次插入新的 struct timerqueue_node
可能被更新。
- 入队列
/*返回真,表示插入了最小堆*/
bool timerqueue_add(struct timerqueue_head *head, struct timerqueue_node *node)
{
struct timerqueue_node *ptr;
struct rb_node *parent = NULL;
struct rb_node **p = &head->head.rb_node;
/*二分查找,树的典型用法*/
while (*p) {
parent = *p;
ptr = rb_entry(parent, struct timerqueue_node, node);
if (node->expires < ptr->expires)
p = &(*p)->rb_left;
else
p = &(*p)->rb_right;
}
/*连接新元素,然后平衡,典型的红黑树用法*/
rb_link_node(&node->node, parent, p);
rb_insert_color(&node->node, &head->head);
/*如果新插入的是最小,则更新头*/
if (!head->next || node->expires < head->next->expires) {
head->next = node;
return true;
}
return false;
}
- 从队列中移除
/*返回真表示还有排队的对象*/
bool timerqueue_del(struct timerqueue_head *head, struct timerqueue_node *node)
{
/*如果是队列头,则更新*/
if (head->next == node) {
struct rb_node *rbn = rb_next(&node->node);
head->next = rb_entry_safe(rbn, struct timerqueue_node, node);
}
rb_erase(&node->node, &head->head);
RB_CLEAR_NODE(&node->node);
return head->next != NULL;
}
- 辅助函数
struct timerqueue_node *timerqueue_iterate_next(struct timerqueue_node *node)
{
struct rb_node *next;
if (!node || !(next = rb_next(&node->node)))
return NULL;
return container_of(next, struct timerqueue_node, node);
}
struct timerqueue_node *timerqueue_getnext(struct timerqueue_head *head)
{
return head->next;
}
定时器数据结构
我们暂时实现一个简单的版本,在操作定时器对象时,需要指定管理容器的描述符指针。后续我们将在全局设置固定数量的(与CPU核数相同)的容器描述符,从而简化使用。
- 定时器对象
struct ev_loop;
typedef void (*ev_timer_fn)(struct ev_timer *);
struct ev_timer {
ev_loop *loop; /**< 状态机*/
ev_timer_fn func;
struct timerqueue_node node;
};
- 定时器管理容器
后续我们会逐渐丰富这个 事件循环对象,因为定时器、描述符读写都属于事件。
struct ev_loop {
bool running;
mutex_t lock;
struct ev_timer *current;
struct timerqueue_head timer_queue;
};
running
是否正在循环
操作实现
我们实现的是微秒级别的定时器
- 修改定时器
/*
* 现在 比起 内核的动态定时器 __mod_timer() 此处多了一个目标事件循环描述符对象参数
*/
int ev_timer_modify(struct ev_loop *loop, struct ev_timer *utimer,
uint32_t expires)
{
int ret = 0;
struct ev_loop *old_loop = timer->loop;
/*加锁策略相同,略*/
/*如果不为空,则已排队,从容器中删除*/
if (old_loop) {
timerqueue_del(&loop->timer_queue, &timer->node);
ret = 1;
}
/*更新各个字段*/
timer->loop = loop;
timer->node.expires = get_timestamp() + expires;
/*初始化红黑树节点,略*/
/*排队到定时器堆*/
timerqueue_add(&loop->timer_queue, &timer->node);
/*解锁,略*/
return ret;
}
可以看出来函数的主框架是一致的,都是查看是否已排队,如果是已排队就是移除队列,然后再排队。
- 删除定时器
int ev_timer_delete(struct ev_timer *timer)
{
struct ev_loop *loop = timer->loop;
/*为空,则没有排队(没有启动定时器)*/
if (!loop)
return 0;
/*从容器中剥离*/
/*移除状态指示*/
timer->loop = NULL;
timerqueue_del(&loop->timer_queue, &timer->node);
return 1;
}
- 事件循环
现在我们事件循环只处理定时器,核心操作就是查看堆的头部,来确定应该休眠多久,带进程被唤醒时就依次从堆队列头开始处理,直到已到期的定时器全部处理完成,之后周而复始继续前面的动作。
需要一个休眠函数,而且不会被信号中断的。
/*微秒级别的休眠函数*/
void usleep_unintr(uint64_t usecs)
{
sigset_t _new_;
if (!usecs)
return;
struct timespec ts = {
.tv_sec = usecs / 1000000,
.tv_nsec = (usecs % 1000000) * 1000,
};
sigfillset(&_new_);
pselect(1, 0, 0, 0, &ts, &_new_);
}
需要一个时间戳获取函数
/*获取微秒级别的时间戳*/
uint64_t get_timestamp(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts->tv_sec * 1000000 + ts->tv_nsec / 1000;
}
事件主循环
void event_loop(struct ev_loop *loop)
{
uint64_t usecs;
struct ev_timer *timer;
struct timerqueue_head *timer_queue = &loop->timer_queue;
loop->running = true;
do {
/*计算应该休眠多久*/
usecs = calc_timeout(loop);
/*休眠*/
usleep_unintr(usecs);
/*进程被唤醒,检查超时的定时器并处理*/
process_timer(loop);
} while (loop->running);
}
循环的主框架就出来了,现在我们实现辅助函数即可。在实际的项目中,你也需要这样将问题分解开来考虑,不要所有东西都揉在一起。
uint64_t calc_timeout(struct ev_loop *loop)
{
/*如果没有任何定时器在排队,我们需要一个默认的休眠时间,当然这需要优化*/
uint64_t usecs = 1000;
/*查看队列头,就算确切的休眠时间*/
if (timer_queue->next) {
/*又一次使用了 container_of 这个`神器`*/
timer = container_of(timer_queue->next, struct ev_timer, node);
/*我们忽略时间戳回绕*/
usecs = timer->expires - get_timestamp();
}
return usecs;
}
现在实现定时器处理函数,其实就是将堆看成一个队列,查看队列头的定时器的绝对时间戳,如果比现在的时间小,则表示没有超时,否则已超时;从队列中弹出来,然后执行回调。
void process_timer(struct ev_loop *loop)
{
struct ev_timer *timer;
struct timerqueue_node *next;
uint64_t now = get_timestamp();
while ((next = timerqueue_getnext(&loop->timer_queue))) {
timer = container_of(next, struct ev_timer, node);
/*比较时间戳,使用绝对时间的好处,就是直接比较,不用转换*/
if (now < timer->expires)
break;
/*从队列中删除*/
timerqueue_del(&loop->timer_queue, &timer->node);
/*开始处理*/
loop->current = timer;
timer->func(timer);
loop->current = NULL;
}
}
- 辅助函数
这个模式时典型单线程复用模式,主线程初始化完毕后,启动循环,等待某个事件停止循环,主线程退出循环。
void stop_loop(struct ev_loop *loop)
{
loop->running = false;
}
来源:CSDN
作者:德阳凯子哥
链接:https://blog.csdn.net/CrazyHeroZK/article/details/104159365