Chinaunix首页 | 论坛 | 博客
  • 博客访问: 37669
  • 博文数量: 6
  • 博客积分: 511
  • 博客等级: 下士
  • 技术积分: 72
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-19 08:49
文章分类
文章存档

2011年(3)

2010年(3)

分类: LINUX

2011-01-10 20:28:14

定时器及时间管理

 

       对于内核来说,它所经历的时间是非常重要的。内核中有很大一部分函数是时间驱动的,而非事件驱动。一些这种函数是周期的,如运行队列的平衡调度及屏幕的刷新。它们均以固定的频率发生,如100次每秒。内核调度着其它功能,如硬件的I/O延迟到一个相对未来的时间。再如,内核可能调度一个工作在此时以后的500ms再执行。总之,内核必须管理系统时间的更新,以及更新当前的日期及时刻。

 

       注意在相对时间与决对时间的差别,安排(调度)一个事件在未来的5s钟发生,没有使用到决对时间的概念,只用到了相对时间的思想。相反的,管理当前的时刻要求内核不仅要知道已经逝去了多少时间,而且要知道决对的测量点。以上这些概念对于时间的管理至关重要。

 

       而且对于内核来说 ,有不同的方法来处理关于事件如何周期的发生,以及内核调度在一个固定的未来的时间来处理事件。事件周期性的发生,比如每10ms,由系统定时器驱动。系统定时器是一个可编程的芯片,它以固定的频率发出一个中断。中断处理程序调度时钟中断,更新系统时间及完成周期性的工作。系统定时器及它的时钟中断处理程序对linux来说是极为重要的,这也是本章的焦点。

 

       本章的另一个焦点为动态定时器,这个工具被用来调度一个事件在指定的时间之后发生。内核可以动态的创建和销毁定时器。我们将会描述内核使用动态的定时器,以及它们的接口。

 

 

 

内核时间的观念

 

       明显,对于计算机来说时间的概念是模糊的,实际上内核必须与系统硬件一块工作才能理解和管理时间。硬件提供一个系统定时器(netratimer2)以供内核知道过去了多少时间。这个定时器与电子时钟源无关,它的功能如数字时钟或处理器时钟,以记录逝去的时间系统定时器(netra:time1)以指定的编程频率运行,称之为tick rate(滴答)。当它溢出时,它会发出一个中断,内核会通过一个特别的中断处理程序来处理它。

      

       因为内核知道编程的滴答率,因此它知道连续两次定时器中断之间的时间。这个时间被称为一个滴答为1/tick秒。这是系统如何跟踪墙上时间及系统运行时间的。墙上时间又称为当天的实际时间,这个对用户空间的应用程序非常重要。内核简单的跟踪这些时间是因为内核控制着定时器中断。系统运行时间是一个相对的时间,既从它启动到现在运行的时长,它对内核及用户空间都十分重要。很多代码都必须要关注消逝的时间。两者的区别也十分明显,后期是一个相对的概念。

 

       定时器中断对于操作系统的管理十分重要,很多代码及内核功能由消逝的时间来决定它们的生存或消去。一些周期性的工作由定时器中断来完成

1,  更新系统运行时间

2,  更新当前时刻

3,  SMP系统中,保证调度的平衡,如果没达到平衡,则平衡之。

4,  运行那些已过期的动态定时器

5,  更新资源及处理器时间的统计数据

 

       它们中的一些工作在每个定时器中断中完成,也就是它们是以滴答的频率来发生的。其它一些功能也是周期的执行的,只不过是每n个定时器中断执行一次。我们将会在定时器中断中更细致的描述这些内容。

 

 

 滴答速度:HZ

       系统定时器的频率在其启动时,就已经以定义的值HZ静态的编写在处理器中。HZ对于不同的架构其值是不同的。在一些支持的体系中,它也因机器的不同而有所不同。对于x86来说为100HZ也就是意味着每秒发生100次,同样对于我们的arm也是如此。

 

       在写内核代码时,决对不要假定HZ为一个给定的值,尽管近来这不是一个常见的错误,但是对于不同的体系结构,这个值有所不同。定时器中断非常重要,正如你可以看到的那样,它完成了很多工作,实际上内核关于时间的概念均来自于系统定时器的驱动。

 

理想的HZ

       在初始的linux版本i386架构使用定时器的频率为100HZ,在2.5内核开发时使用了1000,引起了广泛的争议。尽管这个值现在为100,但它是可以配置的,允许用户编译内核时配置它。

 

       增加tick的速度意味着,定时器中断将会以更快的频率发生。相应的,它所进行的工作将会更快的发生,这将会产生以下好处:

1,  定时器中断将会有着更高的分辨率,且时间事件也将会有更高的分辨力。

2,  时间事件的准确度有所改善。

 

滴答加快一定程度上提高了分辨率,例如,当HZ=100时,定时器的粒度为10ms。换句话说,也就是所有周期的事件按定时器中断每10ms发生,没有更精确的粒度。当HZ=1000时,分辨率为1ms,为上一分辨率的十倍。但是虽然内核可以创建1ms的定时器,却没人可以保证它所提供的精确度比10ms的更好。同样的准确度也以相同的方式发生改善。

 

HZ的优点

       高的分辨力及高的准确度提供了更多的优点:

1,  内核定时器以更高的分辨率执行,且增加了准确度

2,  系统调用如pollselect可以进行优化一个超时时间以改善精度

3,  测量,如系统资源及系统运行时间,可以以更高的分辨力统计

4,  进行抢占更会更将会更准确的发生。

 

注:高的tick分辨率,之所以会提高进程抢占的准确度,减少调度延迟,是因为定时器中断时它将会减小正在运行进程的的时间片计数,当计数到零时,need_resched设置,内核尽快运行调度器。

 

HZ的缺点

       那么增加HZ必然会带来一些问题,要不开始就会使用1000或更高的值了。实际上,增大HZ就出现了这样的一个问题,大量的产生不必要的中断,消耗了更多的时间来处理中断处理函数。这不仅减少了处理器正常工作的时间,而且增加了处理器的功耗。

 

滴答

       全局变量jiffies保存着从系统启动到现在为止所发生的滴答数。每个定时器中断,它要加1,一秒中有HZ个中断,那么他一秒钟要加HZ次。一般它被设置为-300HZ,也就是5分钟后他会自动清零,真正的jiffies的值要减去这个offset

 

Jiffies变量定义在linux/jiffies.h

       Extern     unsigned long volatile jiffies;

让我们来看下内核中关于它的一些例子,以下公式把秒转换为jiffies

       Seconds*HZ

相反的以下公式将jiffies转换为秒

       Jiffies/HZ

前者把秒转换为滴答数更为常见,例如一些代码需要设置一些将来的时间

       Unsigned long time_stamp=jiffies,                            NOW

       Unsigned long next_tick=jiffies+1;                     ONE TICK FROM NOW

       Unsigned long later=jiffies+5*HZ,                            FIVE SECONDS FROM NOW

       Unsigned long fraction=jiffies+HZ/10                a tenth of a secondd from now

       由于内核很少关心决对时间,所以把滴答转换为秒,在用户空间与内核空间是一个典型的应用。

 

       Jiffie如同c语言中的整型常量一样,当它增加至最大值时,再增加时会变为零。如下:

              Unsigned long timeout   =jiffies+HZ/2;      //in 0.5s timeout

              Do some work;

              If(timeout>jiffies)

             

                     我们没有超时,正确

             

              Else

              {

                     超时,错误

}

这段代码本来是准备在0.5s内执行完成的,代码的开始处理了一些工作,可能是操作硬件然后等待反应。完成后,计算出所花费时间是否超时,分别做相应的处理。那么在这部分代码中就可能存在风险-----------jiffies在设置完timeout之后变为0,那么以上的处理逻辑就会存在问题。为了解决以上问题,内核中提供了以下代码供使用:

#define time_after(unknown, known) ((long)(known) - (long)(unknown) < 0)

#define time_before(unknown, known) ((long)(unknown) - (long)(known) < 0)

#define time_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0)

#define time_before_eq(unknown, known) ((long)(known) - (long)(unknown) >= 0)

它们被定义在linux/jiffies.h中。未知参数典型的应该是jiffies,而known参数是你想比较的。

Time_after宏返回unknow是否在known之后。

那么对于上例就可以变为以下代码:

              Unsigned long timeout   =jiffies+HZ/2;      //in 0.5s timeout

              Do some work;

              If(time_after(jiffies,timeout))

             

                     我们没有超时,正确

             

              Else

              {

                     超时,错误

}

 

用户空间及HZ

       先前于2.6的内核改变HZ,将会导致用户空间一些奇怪的问题。这是因为导入到用户空间的值是以每秒多少tick为单位。因为接口不变的原因,应用时间的增长依靠HZ的值,那么改变了内核空间中的HZ,而用户空间却不知晓,可能读系统运行时间为20小时,而实际却只有两个小时。为了解决这个问题,内核必须调整导入用户空间的jiffies值。

 

 

硬件时钟及定时器

       体系结构提供两个硬件设备来帮助时间的保持:系统定时器,这是我们以上所讨论的;以及硬件实时时钟。对于这两种设备的应用对于不同的机器都有不同,但是基本的目的及设计方法却是一致的。

 

实时时钟

       它是一种非易失性的存储系统时间的设备。既使在掉电后它依然在跟踪着逝去的时间。在启动的时刻,内核去读取rtc用它来初始化墙上时间,墙上时间存储于xtime变量中。之后内核不会再去读这个值;但是一些支持的体系如x86却周期性的把当前的墙上时间存回rtc尽管如此,rtc的重要性仅在于系统启动的时刻,初始化xtime变量。

 

系统定时器

       系统定时器在内核维护时间上扮演着重要的角色。无论体系架构,在系统定时器背后的原因都是一样的:提供一个周期性中断的机制。

 

定时器中断处理函数

       现在我们已经理解HZjiffies的概念,而且知道系统定时器的角色,让我们来看看实际的定时器中断处理函数。定时器中断被分为两部分:体系相关的,及体系独立的部分。体系相关部分是为系统定时器注册中断处理函数,当中断产生时运行这个函数。它所进行的细致工作取决于指定的体系,当前多数处理工作完成以下任务:

1,  获取xtime_lock锁,它保护对jiffies_64wall_timer 的值,xtime

2,  承认或复位系统定时器

3,  周期性的保存和更新wall_timertc

4,  调用体系相关部分定时器函数,tick_periodic()netra平台中并非如此

体系相关部分函数tick_periodic(),主要完成以下任务:

1,  每次增加jiffies_64

2,  更新资源的使用,如系统消耗,用户时间,以及当前运行的进行

3,  运行那些已经过期的动态定时器。

4,  执行scheduler_tick()函数

5,  更新墙上时间,存储于xtime

6,  计算负载。

这个函数的流程还是比较简单的,因为其它函数完成了它的大多数工作:

static void tick_periodic(int cpu)

{

if (tick_do_timer_cpu == cpu) {

write_seqlock(&xtime_lock);

/* Keep track of the next tick event */

tick_next_period = ktime_add(tick_next_period, tick_period);

do_timer(1);

write_sequnlock(&xtime_lock);

}

update_process_times(user_mode(get_irq_regs()));

profile_tick(CPU_PROFILING);

}

       多数重要的工作在do_timer()update_process_times中使能。前者主要负责完成jiffies_64的增加:

void do_timer(unsigned long ticks)

{

jiffies_64 += ticks;

update_wall_time();

calc_global_load();

}

函数update_wall_time,正如其名根据消逝的tick数来更新墙上时间,而calc_global_load则主要更新平均负载统计。当do_timer函数完全返回时,update_process_times被调用以更新由于消逝的tick引起变化的各种统计参数。计算是通过user_tick来决定它是发生在用户空间还是内核空间:

void update_process_times(int user_tick)

{

struct task_struct *p = current;

int cpu = smp_processor_id();

/* Note: this timer irq context must be accounted for as well. */

account_process_tick(p, user_tick);

run_local_timers();

rcu_check_callbacks(cpu, user_tick);

printk_tick();

scheduler_tick();

run_posix_cpu_timers(p);

}

       可以看出tick_periodic获取的user_tick是通过查询系统寄存器的值:

update_process_times(user_mode(get_irq_regs()));

account_process_tick这个函数实际上完成了进程时间的更新:

void account_process_tick(struct task_struct *p, int user_tick)

{

cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);

struct rq *rq = this_rq();

if (user_tick)

account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);

else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))

account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,

one_jiffy_scaled);

else

account_idle_time(cputime_one_jiffy);

}

你可能会发现:这个方法意味着内核相信在上一个滴答中断发生前,无论处理器处于什么工作模式,都是只有一个进程在运行。然而实际上,进程有可能进入或退出内核模式很多次,在上一滴答中。也可能它根本在上一滴答中就完全没有运行。这种以进程为考虑粒度的方法是典型的unix方法,它无需更复杂的手段,也是内核可以提供的最好的方法。这也是内核需要更高的tick频率的原因。

接下来,run_local_timers函数标记软中断,以完成那些超时的任务。在接下来我们将再重新讨论关于软件定时器部分。

最后scheduler_tick函数把当前运行的进程的时间片减1,而且如果有必要则设置need_resched.

 

当前时刻

当前时刻被定义在:kernel/time/timekeeping.c.       struct timespec xtime:

struct timespec {

__kernel_time_t tv_sec; /* seconds */

long tv_nsec; /* nanoseconds */

};

xtime.tv_sec中存储着自197011号至现在已经过去的秒数。另一数据由存储着上一秒至现在过去的纳秒数。读取或写xtime要求持有xtime_lock锁,它不是一个普通的原子锁,而是一个seqlock

 

为了更新update xtime,一个写seqlock是被要求的:

Write_seqlock(&xtime_lock);

/*                  update  xtime                     */

       Write_sequnlock(&xtime_lock);

读取xtime要求使用read_seqbegin()read_seqretry()函数:

       Unsigned long seq

       do {

unsigned long lost;

seq = read_seqbegin(&xtime_lock);

usec = timer->get_offset();

lost = jiffies - wall_jiffies;

if (lost)

usec += lost * (1000000 / HZ);

sec = xtime.tv_sec;

usec += (xtime.tv_nsec / 1000);

} while (read_seqretry(&xtime_lock, seq));

这个程序循环进行,走到它确定在读数据的过程中,没有被打断。在循环中如果一个中断发生了,而且更新了xtime,那么返回的序列号是非法的,循环将重复。

 

用户获取墙上时间的主要数据接口为gettimeofday(),它又调用了sys_gettimeofday(),位于:kernel\time.c中:

asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz)

{

if (likely(tv)) {

struct timeval ktv;

do_gettimeofday(&ktv);

if (copy_to_user(tv, &ktv, sizeof(ktv)))

return -EFAULT;

}

if (unlikely(tz)) {

if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))

return -EFAULT;

}

return 0;

}

如果用户提供了一个非空指针tv,那么体系相关的do_gettimeofday将会被调用,这个函数主要完成如上所述的xtime读操作。如果tz非空,系统时区也将会被返回给用户。

 

软件定时器

软件定时器有时被称为动态时钟或内核定时器,它对于内核管理时间至关重要。内核代码经学需要把一个工作推迟执行,在下半部机制中,它们依赖于把工作推迟执行,执行环境一般为中断上下文推迟是多久这是一个比较模糊的概念,下半部的机制并不是把工作推迟多久,而只是说工作现在不做。我们所需要的工具只是简单的推迟工作至一个指定的时间,这个时间不多于我们所期望的。分辨率为内核定时器决定。

内核定时器非常简单,你只需进行一些初始化工作,指定一个超时时间及一个需要执行的函数,最后激活内核定时器即可。指定函数在定时器超时后既会运行,执行后定时器既被销毁,它并不是周期的。这也是定时器这一术语的由来,动态的创建及销毁,对于它的个数是没有限制的,它在内核中各处可见。

 

 

 

 

 

使用内核定时器

它的数据结构为struct timer_list,定义于linux/timer.h

struct timer_list {

struct list_head entry; /* entry in linked list of timers */

unsigned long expires; /* expiration value, in jiffies */

void (*function)(unsigned long); /* the timer handler function */

unsigned long data; /* lone argument to the handler */

struct tvec_t_base_s *base; /* internal timer field, do not touch */

};

它的用法只要看懂上面的数据结构既可,使用定时器进行以下步骤:

第一步,定义一个定时器

struct timer_list my_timer;

第二步,初始化

       Init_timer(&my_timer);

       My_timer.expires=jiffies+delay;

       My_timer.data=0;

       My_timer.function=my_function;

my_function 原型应该为void my_function(unsigned long data)

第三步,激活定时器

       Add_timer(&my_timer);

那么当jiffies大于或等于my_timer.expires,处理函数my_timer.function将会使用参数my_timer.data运行。参数使你可以使用同一个处理函数注册多个定时器。

 

可以明显的看出,内核定时器无法实现任何的实时处理。有时你可能需要超时时间,内核对此也提供了相应的函数

       Mod_timer(&mytimer,jiffies+new_delay);

对于那些已经初始化但尚未执行的定时器,这个函数是可以完成任务的,如果定时器未被激活,那么使用了这个函数后,它将被激活。

如果你想删除某些定时器,使用以下函数既可完成

       Del_timer(&my_timer)

它对于激活及未激活的定时器均有效。对于多处理器的机器要防止引起竞争,可以使用以下函数:

              Del_timer_sync(&my_timer)               (无法在中断环境中使用)

 

 

延迟执行

       经常的,内核代码需要一种除了使用内核定时器或下半部实现机制的方法以外的其它方法,来实现延迟一个工作的执行。如在硬性规定的时间完成给定的任务,通常规定的时间很短,例如,网卡标准要求改变网络的工作模式为2ms,在设置完期望的速度之后,驱动应该至少等待2ms才能继续。

       内核提供了不同的方法,针对不同的延迟,每种方法都有不同的特性。一些持续拥有处理器,如实时任务。其它则不持续拥有,它并不能完全保证你的代码在指定的时间执行。

 

 

忙循环

       最简单的方法是使用忙等待或忙循环。这种方法仅适用于,延迟为滴答时间的整数倍或精确度并不是十分重要的情况。

       这种方法的思想是十分简单的:在一个循环中旋转,直到指定的tick过完。例如:

              unsigned long timeout = jiffies + 10; /* ten ticks */

while (time_before(jiffies, timeout))

    循环不停的进行,直到jiffies大于delay,既在十个滴答过后,在x86上当HZ=1000时,这个结果为10ms,相似地有:

unsigned long delay = jiffies + 2*HZ; /* two seconds */

while (time_before(jiffies, delay));

这个旋转直到2*HZtick,这个时间总是2秒。

      

       以上方法对于系统的其它部分并不是十分的友好,因为它一直占有处理器,而不去进行其它有意义的任务。你不应该把这种方法放在心中,它之所以在这里被展示,是因为它比较简单。

 

       一个好点的解决方法是再次调度你的进程,在你的代码等待的过程中,以允许处理器去完成其它工作,如下:

unsigned long delay = jiffies + 5*HZ;

while (time_before(jiffies, delay))

cond_resched();

       调用cond_resched时,若need_resched被设置了,那么将会调度一个新的进程。换句话说,这种方法条件性的调用了调度器,如果有更重要的需要执行的任务要执行。这也意味着,你只能在中断环境中使用它,你只能在进程上下文件中使用。

 

小的延迟

       有时,内核代码需要短的更精确的延迟,这经常是为了同步硬件,使用最小的时间来完成指定的活动,这经常小于1ms使用jiffies来完成这种延迟是不可能的。当HZ100时,滴答大于10ms,即使当HZ=1000时,它也大于1ms。其它的方法清楚的有必要的以处理更小的延迟,更准确的精度。

       幸运的是,内核提供了三种延迟函数,usnsms,分别定义于linux/delay.h,asm/delay.h中,它们并不使用jiffies      

       Void udelay(unsigned long usecs);

       Void ndelay(unsigned long nseds);

       Void mdelay(unsigned long msecs);

前者延迟时执行忙循环微秒,用法如下:

       Udelay150);//延迟150微秒

       因为内核知道,处理器在1秒内可以完成多少次迭代,udelay函数简单的调整后既可用来完成微秒延迟。这些函数一般不被使用,也不推荐使用,带锁条件或中断禁止条件下使用这些函数对系统的性能有着严重的影响。但是如果你要求精度,那么这些函数无疑是你最好的选择,典型的应用是在微秒级别上。

注:BogoMIPS是一个关于忙循环中,处理器可以在指定周期中完成的迭代次数,它测量处理器可以以多快的速度处理空循环。

Schedule_timeout()及其实现

       一个更为优化的关于推迟执行的方法是使用schedule_timeout函数。这个函数的调用将会把你的任务睡眼,直到指定的时间已经过去。这个函数无法保证睡眼的时间为你准确指定的时间,它只能保证至少在你指定的时间之后执行。当指定的时间已经过去,内核唤醒挂起的任务,把它放在运行队列上。使用它很简单:

/* set task’s state to interruptible sleep */

set_current_state(TASK_INTERRUPTIBLE);

/* take a nap and wake up in “s” seconds */

schedule_timeout(s * HZ);

       这个函数的调用只需要一个参数,那就是期望的相对时间,单位为jiffies。这个例子把任务放入可中断的睡眼状态,s秒。因为这个任务被标记为可中断的,所以它是可以被唤醒的,只要它收到信号。如果代码不希望处理进程信号,那么可以设置为TASK_UNINTERRUPTIBLE。任务必须处于这两种状态,然后才可以去调用schedule_timeout否则任务不会去睡眼。

       注意由于schedule_timeout调用了scheduler,调用它的代码必须可以睡眼。也就是说你必须处于进程上下文中,而且不能持有锁。

  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;

add_timer(&timer);

schedule();//再次调度时返回位置

del_timer_sync(&timer);

timeout = expire - jiffies;

out:

return timeout < 0 ? 0 : timeout;

}

这个函数创建了一个内核定时器,设置超时时间,然后设置定时器超时执行函数为process_timeout。最后激活内核定时器,然后调用调度器。因为任务已经被假定为两种睡眼状态,所以调度器不运行当前任务,而是调度一个新的任务。关于process_timeout函数如下:

void process_timeout(unsigned long data)

{

wake_up_process((task_t *) data);

}

这个函数把任务设置为运行状态,然后把它放入运行队列。当任务再次被调度时,它会返回至schedule_timeout剩余部分,也既是schedule函数之后。如果任务过早的被唤醒,那么定时器将会被销毁,schedule_timeout将返回睡眼了多少时间。

阅读(3485) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

Helianthus_lu2012-12-15 15:42:41

对源码中时间函数分析的蛮清楚的,赞~