Chinaunix首页 | 论坛 | 博客
  • 博客访问: 351798
  • 博文数量: 67
  • 博客积分: 2550
  • 博客等级: 少校
  • 技术积分: 990
  • 用 户 组: 普通用户
  • 注册时间: 2008-09-17 18:24
文章分类

全部博文(67)

文章存档

2011年(6)

2010年(2)

2009年(40)

2008年(19)

我的朋友

分类: LINUX

2008-09-17 18:38:13

2007-02-10 13:23

Title: jiffies溢出与时间先后比较
编制: chinakapok@sina.com
日期:2005-05-25

1. 概述
在Linux内核中,TCP/IP协议栈在很多用到时间比较的地方都使用了jiffies?本文介绍了什么是jiffies,jiffies溢出可能造成的问题,使用time_after等宏来正确地比较时间及其背后的原理。

2. jiffies简介

2.1 时钟中断
在Linux内核中,TCP/IP协议栈在很多用到时间比较的地方都使用了jiffies。
那么jiffies是什么呢?我们知道,操作系统应该能够在将来某个时刻准时调度某个任务,所以需要一种能保证任务准时调度运行的机制。希望支持每种操作系统的微处理器必须包含一个可周期性中断它的可编程间隔定时器。这个周期性中断被称为系统时钟滴答(或system timer),它象节拍器一样来组织系统任务,也称为时钟中断。
时钟中断的发生频率设定为HZ,HZ是一个与体系结构无关的常数,在文件中定义。至少从2.0版到 2.1.43版,Alpha平台上Linux定义HZ的值为1024,而其他平台上定义为100。时钟中断对操作系统是非常重要的,当时钟中断发生时,将周期性地执行一些功能,例如:
. 更新系统的uptime
. 更新time of day
. 检查当前任务的时间片是否用光,如果是则需要重新调度任务
. 执行到期的dynamic timer
. 更新系统资源使用统计和任务所用的时间统计

2.2 jiffies及其溢出
全局变量jiffies取值为自操作系统启动以来的时钟滴答的数目,在头文件中定义,数据类型为 unsigned long volatile (32位无符号长整型)。关于jiffies为什么要采用volatile来限定,可参考《关于 volatile和jiffies.txt》。
jiffies转换为秒可采用公式:(jiffies/HZ)计算,将秒转换为jiffies可采用公式:(seconds*HZ)计算。
当时钟中断发生时,jiffies值就加1。因此连续累加一年又四个多月后就会溢出(假定HZ=100,1个jiffies等于1/100秒, jiffies可记录的最大秒数为(2^32 -1)/100=42949672.95秒,约合497天或1.38年),即当取值到达最大值时继续加1,就变为了0。
在Vxworks操作系统中,定义HZ的值为60,因此连续累加两年又三个多月后也将溢出(jiffies可记录的最大秒数为约合2.27年)。如果在 Vxworks操作系统上的应用程序对jiffies的溢出没有加以充分考虑,那么在连续运行两年又三个多月后,这些应用程序还能够稳定运行吗?
下面我们来考虑jiffies的溢出,我们将从以下几个方面来阐述:
. 无符号整型溢出的具体过程
. jiffies溢出造成程序逻辑出错
. Linux内核如何来防止jiffies溢出
. time_after等比较时间先/后的宏背后的原理
. 代码中使用time_after等比较时间先/后的宏

3. 无符号整型溢出的具体过程
我们首先来看看无符号长整型(unsigned long)溢出的具体过程。实际上,无符号整型的溢出过程都很类似。为了更具体地描述无符号长整型溢出的过程,我们以8位无符号整型为例来加以说明。
8位无符号整型从0开始持续增长,当增长到最大值255时,继续加1将变为0,然后该过程周而复始:
0, 1, 2, ..., 253, 254, 255,
0, 1, 2, ..., 253, 254, 255,
...

4. jiffies溢出造成程序逻辑出错
下面,通过一个例子来看jiffies的溢出。
例4-1:jiffies溢出造成程序逻辑出错
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */

/* do some work ... */
do_somework();

/* then see whether we took too long */
if (timeout > jiffies) {
/* we did not time out, call no_timeout_handler() ... */
no_timeout_handler();

} else {
/* we timed out, call timeout_handler() ... */
timeout_handler();
}
本例的目的是从当前时间起,如果在0.5秒内执行完do_somework(),则调用no_timeout_handler()。如果在0.5秒后执行完do_somework(),则调用timeout_handler()。
我们来看看本例中一种可能的溢出情况,即在设置timeout并执行do_somework()后,jiffies值溢出,取值为0。设在设置 timeout后,timeout的值临近无符号长整型的最大值,即小于2^32-1。设执行do_somework()花费了1秒,那么代码应当调用 timeout_handler()。但是当jiffies值溢出取值为0后,条件timeout > jiffies成立,jiffies值(等于0)小于timeout(临近但小于2^32-1),尽管从逻辑上讲jiffies值要比timeout大。但最终代码调用的是 no_timeout_handler(),而不是timeout_handler()。

5. Linux内核如何来防止jiffies溢出
Linux内核中提供了以下四个宏,可有效地解决由于jiffies溢出而造成程序逻辑出错的情况。下面是从Linux Kernel 2.6.7版本中摘取出来的代码:
/*
* 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)

在宏time_after中,首先确保两个输入参数a和b的数据类型为unsigned long,然后才执行实际的比较。这是以后编码中应当注意的地方。

6. time_after等比较时间先后的宏背后的原理
那么,上述time_after等比较时间先/后的宏为什么能够解决jiffies溢出造成的错误情况呢?
我们仍然以8位无符号整型(unsigned char)为例来加以说明。仿照上面的time_after宏,我们可以给出简化的8位无符号整型对应的after宏:
#define uc_after(a, b) ((char)(b) - (char)(a) < 0)

设a和b的数据类型为unsigned char,b为临近8位无符号整型最大值附近的一个固定值254,下面给出随着a(设其初始值为254)变化而得到的计算值:
a b (char)(b) - (char)(a)
254 254 0
255 - 1
0 - 2
1 - 3
...
124 -126
125 -127
126 -128
127 127
128 126
...
252 2
253 1

从上面的计算可以看出,设定b不变,随着a(设其初始值为254)不断增长1,a的取值变化为:
254, 255, (一次产生溢出)
0, 1, ..., 124, 125, 126, 127, 126, ..., 253, 254, 255, (二次产生溢出)
0, 1, ...
...

而(char)(b) - (char)(a)的变化为:
0, -1,
-2, -3, ..., -126, -127, -128, 127, 126, ..., 1, 0, -1,
-2, -3, ...
...

从上面的详细过程可以看出,当a取值为254,255, 接着在(一次产生溢出)之后变为0,然后增长到127之前,uc_after(a,b)的结果都显示a是在b之后,这也与我们的预期相符。但在a取值为127之后,uc_after(a,b)的结果却显示a是在b之前。
从上面的运算过程可以得出以下结论:
使用uc_after(a,b)宏来计算两个8位无符号整型a和b之间的大小(或先/后,before/after),那么a和b的取值应当满足以下限定条件:
. 两个值之间相差从逻辑值来讲应小于有符号整型的最大值。
. 对于8位无符号整型,两个值之间相差从逻辑值来讲应小于128。
从上面可以类推出以下结论:
对于time_after等比较jiffies先/后的宏,两个值的取值应当满足以下限定条件:
两个值之间相差从逻辑值来讲应小于有符号整型的最大值。
对于32位无符号整型,两个值之间相差从逻辑值来讲应小于2147483647。
对于HZ=100,那么两个时间值之间相差不应当超过2147483647/100秒 = 0.69年 = 248.5天。对于HZ=60,那么两个时间值之间相差不应当超过2147483647/60秒 = 1.135年。在实际代码应用中,需要比较先/后的两个时间值之间一般都相差很小,范围大致在1 秒~1天左右,所以以上time_after等比较时间先/后的宏完全可以放心地用于实际的代码中。

7. 代码中使用time_after等比较时间先/后的宏
下面代码是针对例4-1修改后的正确代码:
例7-1:在例4-1基础上使用time_before宏后的正确代码
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */

/* do some work ... */
do_somework();

/* then see whether we took too long */
if (time_before(jiffies, timeout)) {
/* we did not time out, call no_timeout_handler() ... */
no_timeout_handler();

} else {
/* we timed out, call timeout_handler() ... */
timeout_handler();
}

8. 结论
系统中采用jiffies来计算时间,但由于jiffies溢出可能造成时间比较的错误,因而强烈建议在编码中使用time_after等宏来比较时间先后关系,这些宏可以放心使用。

 Description:
注册并激活一个中断处理程序:
    int request_irq(unsigned int irq,
                    irqreturn_t (*handler)(int, void *, struct pt_regs *),
                    unsigned long irqflags,
                    const char * devname,
                    void *dev_id);
    第一个参数irq表示要分配的中断号。
    第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。它的典型声明如下:
       static irqreturn_t intr_handler(int irq,
                                       void *dev_id,     /*必需与上面函数中的dev_id一致*/
                                         struct pt_regs *regs);
    第三个参数irqflags可以为0,也可以是下例中一个彧多个的组合:
       * SA_INTERRUPT: 禁止所有本地中断。默认情况下(没有这个标志),除了正在运行的中断处
        理程序对应的那条中断线被屏蔽外,其它所有中断都是激活的。除了时钟中断外,绝大多数
        中断都不使用该标志。
       * SA_SAMPLE_RANDOM: 此标志表明这个设备产生的中断对内核熵池有贡献。
       * SA_SHIRQ: 此标志表明可以在多个处理程序之间共享中断线。
    第四个参数devname是怀中断相关的设备的ASCII文本表示法。
    第五个参数dev_id主要用于共享中断线,提供惟一的标志信息。当中断处理程序要释放时,以便从
       共享中断线的诸多处理程序中删除指定的那一个。如果没有这个参数,内核不可能知道在给定的
       中断线上到底要删除哪一个处理程序。如果无需共享中断线,那么为该值赋空(NULL)就可以了。
    request_irq()成功执行返回0, 非0则表示有错误发生,在这种情况下指定的中断处理程序不会被注册。

释放中断处理程序:
    free_irq(unsigned int irq, void *dev_id);
    注:必须在进程上下文中调用free_irq()。

中断控制方法列表:
         函 数                                说 明
    local_irq_disable()                  禁止本地中断传递
    local_irq_enable()                   激活本地中断传递
     /* unsigned long flags */
    local_irq_save(flags)                保存本地中断的当前状态,然后禁止本地中断传递
    local_irq_restore(flags)             恢复本地中断到给定的状态
    disable_irq(unsigned int irq)        禁止给定中断线,并确保在该函数返回之前在该中断
                                             线上没有处理程序在运行
    disable_irq_nosync(unsigned int irq) 禁止给定中断线
    enable_irq(unsigned int irq)         激活给定的中断线
    synchronize_irq(unsigned int irq)    等待一个特定的中断处理程序退出
    irqs_disabled()                      如果本地中断传递被禁止,则返回0,否则返回非0
    in_interrupt()                       如果在中断上下文中,则返回非0,如果在进程上下文中,则返回0
    in_irq()                             如果当前正在执行中断处理程序,则返回非0,否则返回0

使用软中断:
    1 在编译其间,可以通过中定义一个枚举类型来静态地声明软中断。
    2 在运行时通过调用open_softirq()注册软中断处理程序。
    3 用raise_softirq()或raise_softirq_irqoff()来触发软中断。

使用tasklet
    1 声明自已的tasklet
       静态创建:
           DECLARE_TASKLET(name, func, data) /*该tasklet处于激活状态*/
           DECLARE_TASKLET_DISABLED(name, func, data) /*该tasklet处于禁止状态*/  
       动态创建:
           tasklet_init(struct tasklet_struct *t,
                         void (*func)(unsigned long),
                         unsigned long data);
    2 触发你的tasklet
          tasklet_schedule(struct tasklet_struct *t);
          /* 在tasklet被调度以后,只要有机会它就会尽可能早地运行 */
    相关函数:
       tasklet_disable(struct tasklet_struct *t); /*禁止指定tasklet*/
       tasklet_enable(struct tasklet_struct *t); /*激活指定tasklet*/

使用工作队列
    1 创建推后的工作
       DECLARE_WORK(name, void (*func) (void *), void *data); /*静态创建*/
       INIT_WORK(struct work_struct *work, void (*func) (void *), void *data);/*动态创建*/
    2 对工作队列进行调度
       schedule_work(struct work_struct *work); /*一旦其所在的处理器上的工作者线程被唤醒,它
                                                就会被执行*/
       schedule_delayed_work(struct work_struct *work, unsigned long delay);
                           /*指定的work_struct直到delay指定的时钟节拍用完后才会执行*/
    相关函数:
        void flush_scheduled_work(void);
          /*刷新工作队列,函数一直会等待,直到队列中所有对象都被执行以后才返回。会进入休眠*/
       int cancel_delayed_work(struct work_struct *work);
          /*函数取消任何与work_struct相关的挂起工作*/
       struct workqueue_struct *create_workqueue(const char *name);
          /*创建一个新的任务队列和与之相关的工作者线程*/
       int queue_work(struct workqueue_struct *wq, struct work_struct *work);
       int queue_delayed_work(struct workqueue_struct *wq,
                              struct work_struct *work,
                              unsigned long delay);
            /*这两个函数与schedule_work()及schedule_delayed_work()相近,惟一的区别就在于
            它们针对给定的工作队列而不是默认的event队列进行操作*/
        void flush_workqueue(struct workqueue_struct *wq);
             /*刷新指定的工作队列*/
下半部机制控制函数
       void local_bh_disable()  /*禁止本地处理器的软中断和tasklet的处理*/
       void local_bh_enable()   /*激活本地处理器的软中断和tasklet的处理*/
阅读(1160) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:linux address

给主人留下些什么吧!~~