自制操作系统 - 定时器

定时器

Posted by 王富杰 on Wednesday, March 27, 2024

一、定时器

加入内核定时器机制,使得任务可以在一定时间之后执行特定的功能。比如:任务睡眠、蜂鸣器超时、阻塞超时。 我们首先需要实现一下定时器机制,代码如下:

typedef struct timer_t
{
    list_node_t node;                  // 链表节点
    struct task_t *task;               // 相关任务
    u32 expires;                       // 超时时间
    void (*handler)(struct timer_t *); // 超时处理函数
    void *arg;                         // 参数
    bool active;                       // 激活状态
} timer_t;

static timer_t *timer_get()     // 从内核堆内存中分配一个定时器
{
    timer_t *timer = (timer_t *)kmalloc(sizeof(timer_t));
    return timer;
}

void timer_put(timer_t *timer)  // 释放一个定时器
{
    list_remove(&timer->node);
    kfree(timer);
}

void default_timeout(timer_t *timer)  // 默认超时函数,当超时解除进程阻塞
{
    assert(timer->task->node.next);
    task_unblock(timer->task, -ETIME);
}

timer_t *timer_add(u32 expire_ms, handler_t handler, void *arg)  // 添加一个定时器
{
    timer_t *timer = timer_get();  // 分配一个定时器
    timer->task = running_task();  // 获取当前进程
    timer->expires = jiffies + expire_ms / jiffy;   // 设置超时时间
    timer->handler = handler;  // 设置超时处理函数
    timer->arg = arg;
    timer->active = false;
    list_insert_sort(&timer_list, &timer->node, element_node_offset(timer_t, node, expires));  // 把定时器插入排序到链表
    return timer;
}

u32 timer_expires()  // 得到列表中最近一个定时器的超时时间
{
    if (list_empty(&timer_list))
    {
        return EOF;
    }
    timer_t *timer = element_entry(timer_t, node, timer_list.head.next);
    return timer->expires;
}
 
void timer_init()  // 定时器初始化,即定时器链表的初始化
{
    LOGK("timer init...\n");
    list_init(&timer_list);
}

void timer_remove(task_t *task)  // 从定时器链表中找到 task 任务的定时器,删除之,用于 task_exit
{
    list_t *list = &timer_list;
    for (list_node_t *ptr = list->head.next; ptr != &list->tail;)
    {
        timer_t *timer = element_entry(timer_t, node, ptr);
        ptr = ptr->next;
        if (timer->task != task)
            continue;
        timer_put(timer);
    }
}

void timer_wakeup()  // 轮询执行
{
    while (timer_expires() <= jiffies)   // 循环判断是否有超时的定时器, 如果没有定时器timer_expires()返回EOF即-1
    {
        timer_t *timer = element_entry(timer_t, node, timer_list.head.next); 
        timer->active = true;

        assert(timer->expires <= jiffies);
        if (timer->handler)
        {
            timer->handler(timer);
        }
        else
        {
            default_timeout(timer);
        }
        timer_put(timer);
    }
}

如上所示:定时器按照超时时间顺序存放到一个链表中,在时钟中断的处理函数中调用 timer_wakeup 。 因此每次时钟中断都会去唤醒超时的进程。 如果设置了阻塞时间,那就需要生成一个定时器并将该定时器插入链表。统一当进程退出时应该移除该定时器。

二、定时器测试

我们这里写一个应用程序来进行定时器的测试,代码如下:

#include <onix/syscall.h>
#include <onix/stdio.h>

int main(int argc, char const *argv[])
{
    int counter = 1;
    while (counter)
    {
        printf("hello onix %d\a\n", counter++);
        sleep(1000);
    }
    return 0;
}

如上所示,我们的程序会一直打印文字并响铃铛,并且每次打印完后睡眠1秒。我们来整理下运行流程。

  1. 系统启动时,此时没有定时器的。 timer_expires()返回EOF,即-1。-1属于有符号数,当它与无符号数jiffies进行比较,C规则会把-1转换为无符号数4294967295。此时判断为false直接结束。
  2. 当执行count程序时,先进行打印和响铃,响铃函数设置了 task_sleep(100),即进程睡眠100ms。此时产生了一个100ms的定时器,当系统时间没有达到时,判断会大于jiffies直接跳过。
  3. 当定时器时间到了,就会从链表中获取定时器,并结束休眠,此时响铃也就结束了。程序继续运行。执行 sleep(1000), 再次产生一个定时器,进程进入阻塞态。
  4. 等到时钟中断继续判断到阻塞时间到了之后,进行进程唤醒,继续运行。如果反复循环,我们就会再中断不停地看待打印和响铃了。

这里其实存在一个问题,当jiffies达到u32的最大值4294967295时,再次++就会出现回绕变为0。假设当jiffies已经到了4294967290,此时产生一个定时器延迟15个周期,计算后的定时器是一个比较小的数10。当下次时钟中断jiffies为4294967291判断时就会出现误判,提前超时。这里应该(s32)(jiffies - expires) >= 0)这样来判断,得到结果是u32 4294967281 转换为 s32 是 -15, 就不会提前超时了。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付