最小堆定时器 —— 实现

时光怂恿深爱的人放手 提交于 2020-02-04 07:02:13

最小堆定时器

我们将使用内核现有的数据结构和同步结构的 API 来说明怎样设计一个最小堆定时器,并扩展语义。内核在 2.6.16 就已经使用最小堆实现了一个叫着 hrtimer 的定时器,但是语义较复杂,我们将使用它的一些现成的数据结构和辅助函数来实现 timer_list 定时器。

最小堆定时器队列

最小堆数据结构的语义是有序的、线性的数据结构,“最小”的成员在容器的前端。在 内核定时器 一文中,我们讲解了时间轮算法,是由多个有规律的松散定时器队列构成的,这里我们用最小堆来管理排队的定时器,所以最小堆具有队列语义。我们用使用内核提供的红黑树来实现。

关于红黑树的实现网上有很多文章,我们这专注实现一个最小堆。

  1. 数据结构
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 可能被更新。

  1. 入队列
/*返回真,表示插入了最小堆*/
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;
}
  1. 从队列中移除
/*返回真表示还有排队的对象*/
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;
}
  1. 辅助函数
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核数相同)的容器描述符,从而简化使用。

  1. 定时器对象
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;
};
  1. 定时器管理容器

后续我们会逐渐丰富这个 事件循环对象,因为定时器、描述符读写都属于事件。

struct ev_loop {
	bool running;
	mutex_t lock;
	struct ev_timer *current;
	struct timerqueue_head timer_queue;
};
  • running 是否正在循环

操作实现

我们实现的是微秒级别的定时器

  1. 修改定时器
/*
 * 现在 比起 内核的动态定时器 __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;
}

可以看出来函数的主框架是一致的,都是查看是否已排队,如果是已排队就是移除队列,然后再排队。

  1. 删除定时器
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;
}
  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;
	}
}
  1. 辅助函数

这个模式时典型单线程复用模式,主线程初始化完毕后,启动循环,等待某个事件停止循环,主线程退出循环。

void stop_loop(struct ev_loop *loop)
{
	loop->running = false;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!