Linux学习笔记之时间管理和定时器

目录

时间管理

定时器

理想的HZ值

高HZ的优势

高HZ的劣势

是否存在无节拍的OS?

全局变量jiffies

使用定时器

定时器的竞争条件

延迟运行

忙等待

短延迟

更理想的延迟方法

schedule_timeout()的实现

总结


时间管理

时间管理在内核中的作用非常重要。

因为内核中有大量函数都是基于时间驱动的。这其中有些函数是按周期执行,比如调度程序中的运行队列进行平衡调整或者对屏幕进行刷新这样的函数,都是需要定期执行的;而另一些函数则是需要等待一个相对时间后才运行,比如说内核会在500ms后再执行某个任务。除了上述两种函数需要内核提供时间外,内核还必须管理系统的运行时间以及当前日期和时间。

要区分相对时间和绝对时间的概念。

比如某个事件在5秒后被调度执行,那么系统所需要的时间就是相对时间,也就是相对现在起5秒后;相反,如果要求管理当前日期和当前时间,则内核不但要计算流逝的时间,还要计算绝对时间。

定时器

系统定时器(有时也成为动态定时器或内核定时器)是内核定时机制中最为重要的角色,是管理内核流逝的时间的基础。尽管不同的体系结构中的定时器实现不尽相同,但是系统定时器的根本思想没有区别——提供一种周期性触发中断机制。定时器作为一种工具,满足了能够使工作在指定时间点上执行的需求。

时间概念对计算机来说不太好理解。所以内核必须在硬件的帮助下才能计算和管理时间,硬件为内核提供了一个系统定时器用来计算流逝的时间。

系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称为“节拍率”。系统对于预编的节拍率是已知的,也就是两次中断的间隔时间是已知的。间隔时间被称为“节拍”,节拍=1/节拍率秒。关键的来了:内核就是靠这种已知的时钟中断间隔时间,来计算墙上时间和系统运行时间的。PS:墙上时间指从进程开始运行到结束,系统时钟走过的时间。

关于系统定时器频率(节拍率),上面说过了是可以通过编程预定的,也就意味着我们可以定义这个值的高低。因为时钟中断能处理很多内核任务,所以其对于内核来说很重要。选择一个合适的频率让内核能够发挥它的所有功力,也显得非常重要。

体系结构还提供了另外一种设备进行计时:实时时钟。实时时钟时用来持久存放系统时间的设备,即便系统关闭后,也可以靠主板上的微型电池提供的电力保持系统的计时。

理想的HZ值

Linux自发行一来,i386体系结构中时钟中断频率就设定为100HZ,但在2.5开发板内核中,中断频率被提高到了1000HZ。这个频率到底是调高了好还是调低了好?别忘了任何事物都具有两面性,让我们一起来探究一下这个问题。

由于内核中众多子系统都需要依赖时钟中断工作,所以改变中断频率会对系统造成非常大的影响。

先聊聊提高节拍率会对系统造成什么样的影响:

提高了节拍率,意味着时钟中断会产生得更加频繁,所以中断处理程序也会更加频繁地执行,这么一来给系统带来了不少好处:

  • 更高的时钟中断解析度(resolution)可提高时间驱动事件得解析度
  • 提高了时间驱动事件的准确度(accuracy)

高HZ的优势

先解释一下什么是解析度。比如节拍率为100HZ的时钟执行粒度为10ms,即系统中的周期事件最快为每10ms执行一次,也就是解析度为10ms。但是当HZ=1000时,解析度就是1ms,比前者精细了10倍。

提高了解析度同时也提高了准确度。假定内核在某个时刻随机触发定时器,然而它可能在任何时间超时,但由于只有在时钟中断到来时才可能执行它,所以平均误差大约为半个时钟中断周期。通俗的说,就是假设时钟周期HZ=100,那么事件平均在设定时刻的+/-5ms内发生,所以平均误差为5ms。相比之下当HZ=1000时,那么平均误差为0.5ms——准确度提高了10倍!

高HZ的劣势

很明显的是,高节拍率意味着高时钟频率,也意味着系统负担越重,意味着处理器必须花更多时间来执行时钟中断处理程序。这样不但减少了处理器处理其他工作的时间,而且会更频繁地打乱处理器高速缓存并增加耗电。但是至少在现代计算机系统上,时钟频率为1000HZ并不会导致难以承受的负担,并且不会对系统性能造成较大的影响。

是否存在无节拍的OS?

一开始在学习的时候,我就有一个疑问:操作系统就一定要有固定时钟吗?

其实在Linux内核中,是支持“无节拍操作“这样的选项的。当编译内核设置了CONFIG_HZ配置选项,系统就会开始动态调度时钟中断。也就是按需动态调度和重新设置。如果下一个时钟频率设置为3ms,那就每隔3ms触发一次时钟中断。再往后如果50ms内都无事可做,那么内核将以50ms重新调度时钟中断。大大减少了性能的开销,实质性受益的还是省电方面。

全局变量jiffies

jiffies用来记录自系统启动以来产生的节拍总数。启动时,内核将该变量初始化为0,此后每次时钟中断处理程序就会增加改变了的值。因为一秒钟内时钟中断的次数=HZ,所以jiffies一秒钟内增加的值也为HZ。系统运行时间以秒为单位计算,也就是说运行的秒数=等于jiffies/HZ。

但在实际系统中可能会更复杂一下:内核给jiffies赋予一个特殊的初值,引起这个变量不断地溢出,由此捕捉bug。当找到实际的jiffies值后,就首先把这个”特殊的偏差“减去。

使用定时器

定时器的使用流程大致分为:

  1. 初始化
  2. 设置超时时间
  3. 指定超时发生后执行的函数
  4. 激活定时器

定时器由结构time_list表示,定义在文件<linux/timer.h>

struct time_list{
    struct list_head entry;             /*定时器链表的入口*/
    unsigned long expires;              /*以jiffies为单位的定时值*/
    void (*function)(unsigned long);    /*定时器处理函数*/
    unsigned long data;                 /*传给处理函数的长整型参数*/
    struct tcec_t_base_s *base;         /*定时器内部值,用户不要使用*/
}

创建定时器时需要先定义它:

struct timer_list my_timer;

接着需要通过一个辅助函数来初始化定时器数据结构的内部值,初始化必须在其他定时器处理函数对定时器进行操作前完成:

init_timer(&my_timer);

现在就可以开始填充结构中所需要的值了:

my_timer.expires = jiffies + delay;        /*定时器超时时的节拍*/
my_timer.data = 0;                         /*给定时器处理函数传入0值*/
my_timer.function = my_function;           /*定时器超时时调用的函数*/

以上的my_timer.expires表示超时时间,它是以节拍为单位的绝对计数值。如果当前的jiffies计数值等于或大于my_timer.expires,那么my_timer_function指向的处理函数就会开始执行,另外,该处理函数还要使用长整型参数my_timer.data。所以正如我们从time_list结构看到的形式,处理函数必须符合下面的函数原型:

void my_timer_function(unsigned long data);

data参数使我们可以利用同一个处理函数注册多个定时器,我们只需用不同的data就可以区分同时注册的多个定时器。如果不需要这个参数,简单地传个0或者任何值就可以。

最后,激活定时器:

add_timer(&my_timer);

这时候定时器就可以工作了!!!

但是还有一点要注意,最好别用定时器来实现任何硬实时任务:

虽然内核可以保证不会在超时之前运行定时器处理函数,但是它有可能延误定时器的执行。一般来说,定时器会在超时后马上执行,但是也可能推迟到下一时钟节拍才能运行。

有时可能需要更改已经激活的定时器的超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:

mod_timer(&my_timer, jiffies + new_delay);        /*新的定时值*/

mod_timer()函数可以操作那些已经初始化但是未被激活的定时器。如果其未被激活,mod_timer()函数会激活他。若定时器未被激活,mod_timer()函数返回0,反之1。

如果需要在定时器超时前停止定时器,可以使用del_timer()函数:

del_timer(&my_timer);

被激活或者未被激活的定时器都可以使用这个函数。若未被激活返回0,反之1;注意:不要为已经超时的定时器调用该函数,因为它们会自动删除。

在使用del_timer()函数时要注意:当你想在定时器超时前停止定时器,只能保证定时器将来不会执行,但是在多处理器机器上定时器中断可能已经在其他处理器上执行了,所以还需要等待其他处理器上的定时器处理程序都退出。这时候就要使用del_timer_sync()函数执行删除工作:

del_timer_sync(&my_timer);

del_timer()函数不同,del_timer_sync()函数不能再中断的上下文中使用。

定时器的竞争条件

因为定时器与当前执行代码是异步的,因此就有可能存在潜在的竞争条件。首先,不能用以下代码来替代mod_timer()函数来改变定时器的超时时间。这样的代码在多处理机器上是不安全的:

del_timer(my_timer);
my_timer -> expires = jiffies + new_delay;
add_timer(my_timer);

其次,一般情况下应该使用del_timer_sync()函数取代del_timer()函数,因为无法确定在删除定时器时,它是否正在其他处理器上运行。否则,对定时器执行删除操作后,代码会继续执行,但其有可能会去操作在其他处理器上运行的定时器正在使用的资源,因而造成并发访问

延迟运行

内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外,还需要其他方法来推迟执行任务。内核提供了许多延迟方法处理各种延迟要求。不同的发方法有不同的处理特点,有的是在延迟任务时挂起处理器,防止处理器执行任何实际工作;另一些不会挂起处理器,所以也不能确保延迟的代码能够在指定的延迟时间运行。

忙等待

最简单的延迟方法就是忙等待,同时也是最不理想的办法。但要注意的是该方法只适用与想要延迟的时间是节拍的整数倍的情况下,或者精确度要求不高的情况下才可以使用。

下面有两种实现逻辑,第一种是设置延迟的节拍数,第二种是设置延迟的秒数。

1.实现忙循环——也就是在循环中不断旋转直到希望的时钟节拍数耗尽为止,其中设置的是延迟的节拍数。

unsigned long timeout = jiffies + 10        /*延迟10个节拍*/
while (time_before(jiffies, timeout));

循环不断执行,总共的循环时间为10个节拍。

2.设置一个固定时间的delay。其中循环不断执行,直到jiffies大于delay为止。

unsigned long delay = jiffies + 2*HZ;           /* 2秒 */
while(time_before(jiffies, delay));

程序循环要等待2*HZ个时钟节拍,意味无论时钟节拍率如何,都将等待2s。

忙循环算不上是一个好办法,因为在代码等待时,处理器不会去处理其他任务。在实际工作中忙循环是几乎用不上的,这里介绍它仅仅是因为忙循环是最简单直接的延迟方法。

下面介绍一种更好的方法,这种方法在代码等待时,允许内核重新调度执行其他任务:

unsigned long delay = jiffies + 5*HZ;
while(time_before(jiffies, delay))
    cond_resched();

以上的cond_resched()函数将调度一个新程序投入运行,但它只有在need_resched()后才能生效——也就是在系统中存在更重要的任务要运行,该方法才能生效。

短延迟

有时内核代码(通常也是驱动程序)不但需要很短的延迟(比时钟节拍还短),而且还要求延迟的时间很准确,这种情况多发生在硬件同步期间。也就是说对于频率为100HZ的时钟中断,无法满足小于10ms的短延迟,所以不可能像上面的例子那样使用基于jiffies的延迟方法,需要寻找其他能满足更短更精确延迟要求的方法。

内核中提供了三个可以处理ms、ns和us级别的延迟函数,分别定义在<linux/delay.h>和<asm/delay.h>中,显然他们没有使用jiffies:

void mdelay(unsigned long msece)
void ndelay(unsigned long nsece)
void udelay(unsigned long usece)

众所周知,1s = 1000ms = 1000000 μs。

udelay(150);        /* 延迟150μs */

udelay()函数应在小延迟中调用,因为在快速机器上的大延迟可能导致溢出。通常超过1ms的范围不要使用udelay()进行延迟。对于较长的延迟,应使用mdelay() 

更理想的延迟方法

下面介绍一下schedule_timeout()函数。

schedule_timeout()函数会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。但是该方法精确度有限,只能保证尽量接近指定的延迟时间。当指定的时间到期后,内核将唤醒被延迟的任务并将其重新放回运行队列,用法如下:

/* 必须选择两种状态的其一进行设置任务才能进入休眠。另一种是TASK_UNINTERRUPTIBLE */
set_current_state(TASK_TNTERRUPTIBLE);

/* 小睡一会,"s"秒后唤醒 */
schedule_timeout(s*HZ);

上例中将相应的任务推入可中断睡眠队列,睡眠s秒。因为任务处于可中断状态,所以收到信号就会被唤醒。如果在睡眠中不想接收信号,可以将任务状态设置成TASK_UNINTERRUPTIBLE,然后进入睡眠。

schedule_timeout()的实现

signed long schedule_timeout(signed long timeout)
{ 
    timer_t timer;            /* 创建定时器 */
    unsigned long expire;     


    switch(timeout)            /* 设置超时时间 */
    {    
    case MAX_SCHEDULE_TIMEOUT
        schedule();
        goto out;
    default:
        if(timeout < 0)
        {
            printk(KERN_ERR "schedule_timeout: wrong timeout"
            "value %lx from %p\n", timeout,
            __builtin_return_address(0));
        current -> state = TASK_RUNNING;
        goto out;                          

        }
    }


    expire = timeout + jiffies;  /* 定时器超时的节拍数 */

    init_timer(&timer);
    timer.expires = expire;
    timer.data = (unsigned long) current;
    timer.function = process_timeout;    /* 将process_time()设置为定时器超时时执行的函数 */
    
    add_timer(&timer);            /* 激活定时器 */
    schedule();                   /* 进程调度函数 */
    del_timer_sync(&timer);       
/* 若任务被提前唤醒(比如收到信号)那么定时器被撤销,process_timeout()函数会返回剩余的时间 */


    timeout = expire - jiffies;

out:
    return timeout < 0 ? 0 : timeout;

}

其中,当定时器超时时,process_timeout()函数会被调用:

void process_timeout(unsigned long data)
{
    wake_up_process((task_t *)data);
}

上述函数将任务设置为TASK_RUNNING状态,然后将其放入运行队列。

总结

本章中主要学习了墙上时钟与计算机的正常运行时间是如何管理的,对比了相对时间与绝对时间,了解了周期事件。还学习了时钟中断、时钟节拍、HZ以及jiffies等概念。

还考察了定时器的实现,了解了如何将这些运用到自己的内核代码中。最后还学习了开发者用于延迟的其他方法。

​​​​​​​

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值