• Linux Kernel Development——定时器和时间管理


    几个重要的名词

     

    • HZ:系统定时器频率HZ用来定义系统定时器每隔1秒产生多少个时钟中断
    • Tick:HZ的倒数,系统定时器两次时钟中断的时间间隔
    • Xtime:记录Wall time值,也就是UTC时间,是一个struct timeval结构,在用户空间通过gettimeofday读取
    • Jiffies:记录系统开机以来经过了多少次Tick,定义为unsigned long volatile __jiffy_data jiffies;
    • RTC:实时时钟,是一个硬件时钟,用来持久存放系统时间

    HZ

     

    HZ是静态编译到内核中的,其定义如下:

    // /usr/include/asm-generic/param.h文件中

    #ifdef __KERNEL__

    # define HZ CONFIG_HZ /* Internal kernel timer frequency */

    # define USER_HZ 100 /* some user interfaces are */

    # define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */

    #endif

    #ifndef HZ

    #define HZ 100

    #endif

    在2.6以前的内核中,如果改变内核中的HZ值会给用户空间中某些程序造成异常结果。因为内核是以节拍数/秒的形式给用户空间导出这个值的,应用程序便依赖这个特定的HZ值。如果在内核中改变了HZ的定义值,就打破了用户空间的常量关系---用户空间并不知道新的HZ值。

    内核更改所有导出的jiffies值。内核定义了USER_HZ来代表用户空间看到的HZ值。在x86体系结构上,由于HZ值原来一直是100,所以USER_HZ值就定义为100。内核可以使用宏jiffies_to_clock_t()将一个有HZ表示的节拍计数转换为一个由USER_HZ表示的节拍计数:

    start=jiffies;

    //doing some jobs

    total_time=jiffies-start;

    ticks=jiffies_to_clock_t(total_time);

    jiffies_to_clock_t()函数的定义如下:

    /*

    * Convert jiffies/jiffies_64 to clock_t and back.

    */

    clock_t jiffies_to_clock_t(unsigned long x)

    {

    #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0

    # if HZ < USER_HZ

    return x * (USER_HZ / HZ);

    # else

    return x / (HZ / USER_HZ);

    # endif

    #else

    return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);

    #endif

    }

    Jiffies

     

    Jiffies记录了系统启动以来所经过的ticks数,在开机时该值是0,随后每次时钟中断发生时加1.

    根据jiffies可以计算系统的开机时间:jiffies/HZ 秒

    Jiffies的定义为unsigned long volatile __jiffy_data jiffies;

    由于jiffies存在溢出的可能,内核定义了一组辅助函数来处理jiffies的比较操作,操作jiffies时最好使用这些辅助函数:

    /*

    * These inlines deal with timer wrapping correctly. You are

    * strongly encouraged to use them

    * 1. Because people otherwise forget

    * 2. Because if the timer wrap changes in future you won't have to

    * alter your driver code.

    *

    * time_after(a,b) returns true if the time a is after time b.

    *

    * Do this with "<0" and ">=0" to only test the sign of the result. A

    * good compiler would generate better code (and a really good compiler

    * wouldn't care). Gcc is currently neither.

    */

    #define time_after(a,b) \

    (typecheck(unsigned long, a) && \

    typecheck(unsigned long, b) && \

    ((long)(b) - (long)(a) < 0))

    #define time_before(a,b) time_after(b,a)

     

    #define time_after_eq(a,b) \

    (typecheck(unsigned long, a) && \

    typecheck(unsigned long, b) && \

    ((long)(a) - (long)(b) >= 0))

    #define time_before_eq(a,b) time_after_eq(b,a)

    实时时钟RTC

     

    实时时钟是一个硬件时钟,用来持久存放系统时间,系统关闭后靠主板上的微型电池保持计时;

    系统启动时,内核通过读取RTC来初始化Wall Time, 并存放在xtime变量中,这是RTC最主要的作用;

    当用户修改了时间后,可以用hwclock –w将其保存到RTC中。

    系统定时器

     

    每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号,作为系统定时器 system timer。当发生时钟中断时,就会自动调用时钟中断处理程序。

    时钟中断处理程序分为两个部分:体系结构相关部分和体系结构无关部分。相关的部分作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应地运行。执行的工作如下:

    1.获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。

    2.需要时应答或重新设置系统时钟。

    3.周期性地使用墙上时间更新实时时钟。

    4.调用体系结构无关的时间例程:do_timer().

    中断服务程序主要通过调用与体系结构无关的例程do_timer()执行下面的工作:

    1.给jiffies_64变量加1.

    2.更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。

    3.执行已经到期的动态定时器.

    4.执行scheduler_tick()函数.

    5.更新墙上时间,该时间存放在xtime变量中.

    6.计算平均负载值.

    Xtime

     

    记录Wall time值,也就是UTC时间,是一个struct timeval结构。

    在内核空间读写这个xtime变量需要xtime_lock锁,该锁是一个顺序锁(seqlock)。

    而在用户空间通过gettimeofday读取xtime,它在内核中对应系统调用为sys_gettimeofday()。

    动态定时器

     

    动态定时器并不周期执行,它在超时后就自行销毁。定义器由定义在linux/timer.h中的time_list表示,如下:

    struct timer_list {

    /*

    * All fields that change during normal runtime grouped to the

    * same cacheline

    */

    struct list_head entry;

    unsigned long expires;

    struct tvec_base *base;

     

    void (*function)(unsigned long);

    unsigned long data;

     

    int slack;

    }

    内核提供了一组函数用来简化管理定时器的操作。所有这些接口都声明在文件linux/timer.h中,大多数接口在文件kernel/timer.c中获得实现。有了这些接口,我们要做的事情就很简单了:

    1.创建定时器:struct timer_list my_timer;

    2.初始化定时器:init_timer(&my_timer);

    3.根据需要,设置定时器了:

    my_timer.expires = jiffies + delay;

    my_timer.data = 0;

    my_timer.function = my_function;

    4.激活定时器:add_timer(&my_timer);

    经过上面的几步,定时器就可以开始工作了。然而,一般来说,定时器都在超时后马上就会执行,但是也有可能被推迟到下一时钟节拍时才能运行,所以不能使用它来实现硬实时。

    如果修改定时器,使用mod_timer(&my_timer,jiffies+new_delay)来修改已经激活的定时器时间。它也可以操作那些已经初始化,但还没有被激活的定时器,如果定时器未被激活,mod_timer会激活它。如果调用时定时器时未被激活,该函数返回0;否则返回1。但不论哪种情况,一旦从mod_timer函数返回,定时器都将被激活而且设置了新的定时值。

    当然你也可以在超时前删除定时器用:del_timer(&my_timer); 另外需要注意的是在多处理器上定时器中断可能已经在其它机器上运行了,这是就需要等待可能在其它处理器上运行的定时器处理程序都退出后再删除该定时器。这是就要使用del_timer_sync()函数执行删除工作。这个函数参数和上面一个一样,只是不能在中断上下文中使用而已。定时器是独立与当前代码的,这意味着可能存在竞争条件,这个就要特别小心,从这个意义上讲后者删除比前者更加安全。

    延迟执行

     

    内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外还提供了许多延迟的方法来处理各种延迟请求。

    1. 忙等待(也叫忙循环):通常是最不理想的方法,因为处理器被白白占用而无法做其他的事情。该方法仅仅在想要延迟的时间是节拍的整数倍或者精确率要求不高时才可以使用。实现起来还是挺简单的,就是在循环中不断旋转直到希望的时钟节拍数耗尽。比如:

      unsigned long delay = jiffies+10; //10个节拍

      while(time_before(jiffies,delay)) ;

    缺点很明显,更好的方法是在代码等待时,允许内核重新调度执行其他任务,如下:

    unsigned long delay = jiffies+10; //10个节拍

    while(time_before(jiffies,delay))

    cond_resched();

    cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后才能生效。换句话说,就是系统中存在更重要的任务需要运行。再由于该方法需要调用调度程序,所以它不能在中断上下文中使用----只能在进程上下文中使用。事实上,所有延迟方法在进程上下文中使用,因为中断处理程序都应该尽可能快的执行。另外,延迟执行不管在哪种情况下都不应该在持有锁时或者禁止中断时发生。

    1. udelay mlelay

    至于说那些需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间很精确,这种情况多发生在和硬件同步时,也就是说需要短暂等待某个动作的完成----等待时间往往小于1ms,所以不可能使用像前面例子中那种基于jiffies的延迟方法。这时,就可以使用在linux/delay.h中定义的两个函数,它们不使用,这两个函数可以处理微秒和毫秒级别的延迟的时间,如下所示:

    void udelay(unsigned long usecs);

    void mdelay(unsigned long msecs);

    前者是依靠执行次数循环来达到延迟效果的,而mdelay()函数又是通过udelay()函数实现的。因为内核知道处理器在一秒内能执行多少次循环,所以udelay()函数仅仅需要根据指定的延迟时间在1秒中占的比例,就能决定需要进行多少次循环就能达到需要的推迟时间。udelay()函数仅能在要求的延迟时间很短的情况下执行,而在高速机器中时间很长的延迟会造成溢出,经验表明,不要试图在延迟超过1ms的情况下使用这个函数。这两个函数其实和忙等待一样,如果不是非常必要,还是不要用了算了。

    1. schedule_timeout

    更理想的延迟执行方法是使用schedule_timeout()函数,该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。但该方法也不能保证睡眠时间正好等于指定的延迟时间----只能尽量是睡眠时间接近指定的延迟时间。当指定的时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列,如下:

    set_current_state(TASK_INTERRUPTIBLE);

    schedule_timeout(s*HZ);

    唯一的参数是延迟的相对时间,单位是jiffies,上例中将相应的任务推入可中断睡眠队列,睡眠s秒。在调用函数schedule_timeout之前,要将任务设置成可中断或不和中断的一种,否则任务不会休眠。这个函数需要调用调度程序,所以调用它的代码必须保证能够睡眠,简而言之,调用代码必须处于进程上下文中,并且不能持有锁。

    最后,等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期——就看谁来得更快。这种情况下,代码可以简单的使用scedule_timeout()函数代替schedule()函数,这样一来,当希望指定时间到期后,任务都会被唤醒,当然,代码需要检查被唤醒的原因----有可能是被事件唤醒,也有可能是因为延迟的时间到期,还可能是因为接收到了信号——然后执行相应的操作。

     

    参考资料:

    http://blog.csdn.net/zhandoushi1982/article/details/5536210

    http://blog.csdn.net/qinzhonghello/article/details/3588224

    《Linux Kernel Development》

  • 相关阅读:
    QQ企业通--客户端登陆模块设计---知识点2
    C# Show()与ShowDialog()的区别-----转载
    docker入门学习
    人生感悟
    mysql权限管理命令
    JAVA程序员工作常用英语(细心整理)
    spring知识梳理
    快速搭建MHA
    MySQL Performance Schema都建议开启哪些监控采集指标(除了默认自动开启的指标)
    慢SQL引发MySQL高可用切换排查全过程
  • 原文地址:https://www.cnblogs.com/feisky/p/2958220.html
Copyright © 2020-2023  润新知