向下之旅(十四):定时器和时间管理

Posted 画家丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了向下之旅(十四):定时器和时间管理相关的知识,希望对你有一定的参考价值。

  相对于事件驱动而言,内核中有大量的函数都是基于时间驱动的。有些函数是周期执行的,有些操作是需要等待一个相对的时间后才运行。除了上述两类函数需要内核提供时间外,内核还必须管理系统的运行时间以及当前日期和时间。

  其中相对时间和绝对时间是不同的,若某个事件在5秒后被调度执行,那么系统所需要的是——相对时间(相对现在起5秒)。如果涉及到日期和时间,内核不但要计算流逝的时间还要计算绝对时间。

  周期性产生的事件和推迟执行的时间之间的差别:前者比如每10毫秒一次——都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它能固定频率的产生中断。该中断就是所谓的定时器中断。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。

  另一个重点是动态定时器——一种用来推迟执行程序的工具。比如,如果软驱动马达在一定时间内都未活动,那么软盘驱动程序会使用动态定时器关闭软驱马达。内核可以动态创建驱动或销毁动态定时器。

  内核中的时间概念

  硬件为内核提供了一个系统定时器用以计算流逝的时间,系统定时器以某种频率自行触发(经常被称为击中或射中)时钟中断,该频率可以通过编程预定,称作节拍率。连续两次时钟中断的间隔时间叫做节拍,它等于节拍率分之一秒。内核就是靠这种已知的时钟中断间隔来计算墙上时间和系统运行时间。墙上时间——实际时间——对用户空间的应用程序来说是最重要的。此外内核也为用户空间提供了一组系统调用来获取实际日期和实际时间。系统运行时间——自系统启动开始所经过的时间——对用户空间和内核都很有用,因为许多程序都必须清楚流逝过的时间。通过两次(现在和以后)读取运行时间在计算它们的差,就可得到相对的流逝过的时间。

  利用时间中断来周期的执行工作有:

  1.更新系统运行时间

  2.更新实际时间

  3.在smp系统上,均衡调度程序各处理器上的运行时间。如果运行队列负载不均衡的话,尽量使它们均衡。

  4.检查当前进程是否用尽了自己的时间片,如果用尽,就重新进行调度。

  5.运行超时的动态定时器

  6.更新资源消耗和处理器时间的统计值

  节拍率:HZ

  系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ,在系统启动时按照HZ值对硬件进行设置。体系结构的不同,HZ的值也不同。若系统定时器频率为1000HZ,也就是说每秒钟1000次(每毫秒一次),周期为 1/HZ 秒。各种体系结构的节拍率如下:

  技术分享

注意,大多数体系结构的节拍率是可调的,并不是一成不变的。

  理想的HZ值

  提高节拍率意味着时钟中断产生得更加频繁,所以中断处理程序也会更频繁的执行。其好处有:

  1.更高的时钟中断解析度可以提高时间驱动时间的解析度。

  2.提高了时间驱动时间的准确度。

  以上两点的提升带来的好处是:

  1.内核定时器能够以更高的频度和更高的准确度运行

  2.依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行。

  3.对诸如资源消耗和系统运行时间等的测量会有更精细的解析度

  4.提高进程抢占的准确度

  当然也有坏的一面,提高节拍率,节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。因为处理器必须花时间来执行时钟中断处理程序,所以节拍率越高,中断处理程序占用的处理器的时间越多。

  jiffies

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

  将以秒的单位的时间转化为jiffies:

  (seconds * HZ)

  将jiffies转换为以秒为单位的时间:

  (jiffies / HZ)

  通常将秒转化为jiffies用的多一些,比如在代码经常需要设置一些将来的时间:

  unsigned long time_stamp = jiffies;  // 现在

  unsigned long next_tick  = jiffies+_1  //从现在开始1个节拍

  unsigned long later     = jiffies+5 * HZ //从现在开始5秒

  Jiffies 的内部表示

  jiffies变量重视无符号长整数,所以,在32位体系结构中是32位,在64位的体系结构中是64位。

  ld(1)脚本用来连接主内核映像,然后用jiffies_64变量覆盖初值jiffies变量:

   jiffies = jiffies_64;

  在64位体系结构上,jiffies_64和jiffies指的是同一个变量,低32位的值和在32位体系结构上的jiffies值一样。通过get_jiffies_64()函数,可以获得jiffies的全部64位的值。

  Jiffies 的回绕

  当jiffies变量的值超过它的最大存放范围后就会发生溢出。对于32位无符号长整形,最大取值2的32次方减一,即定时器节拍计数最大为4294967295。如果超过最大值后还要增加,则它的值会回绕到0。  

  内核中提供了四个宏来帮助比较节拍计数,它们能正确的处理了节拍计数回绕情况,

  技术分享

其中unkown参数通常是jiffies,known的参数是需要比较的值。宏time_after(unknown,known),当时间unknown超过指定的known时,返回真,否则返回假。宏time_before(unknown,known)

  用户空间和HZ

  在2.6以前的内核,如果改变内核的HZ的值,会使用户空间中某种程序造成异常结果。因此内核定义了USER_HZ来表示用户空间看到的HZ值,内核可以使用宏jiffies_to_clockt()将一个由HZ表示的节拍计数器转换成一个由USER_HZ表示的节拍计数。通过函数jiffies_64_to_clock_t()将64位的jifies值的单位从HZ转换为USER_HZ。

  硬时钟和定时器

  体系结构提供了两种设备进行计时——一种是我们刚说的系统定时器,另一种是实时时钟(RTC)。

+9+ 实时时钟(RTC)是用来持久存放系统时间的设备,即便关闭系系统后,它也可以靠主板上的微型电池保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。实时时钟最主要的作用是在启动时初始化xtime变量。

  时钟中断处理程序

  时钟中断处理程序可以分为两个部分:体系结构相关部分和体系结构无关部分。

  与体系结构相关的例程作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。一般要执行以下工作:

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

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

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

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

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

  1.给jiffies_64变量增加1(这个操作即使是在32位体系结构上也是安全的,因为前面已经获得了xtime_lock锁)。

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

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

  4.执行scheduler_tick()函数

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

  6.计算平均负载值。

  实际时间

技术分享

  xtime.tv_sec以秒为单位,存放着自1970年7月1日(UTC)以来经过的时间,1970年1月1日被称为纪元,多数Unix系统的墙上时间都是基于该纪元而言的。xtime.tv_nsec记录自上一秒开始经过的纳秒数。

  读写xtime变量需要使用xtime_lock锁,该锁不是普通的自旋锁而是一个seqlock锁(操作前后读取标示,来判断状态)。读取xtime时,也要使用read_seqbegin()和read_seqretry()函数。

  从用户空间取得墙上时间的主要接口是gettimeofday(),C库函数也提供了一些墙上时间相关的库调用。比如ftime()和ctime()。

  定时器

  定时器——也成为动态定时器或内核定时器——是管理内核时间的基础。有时我们需要一种工具,能够使工作在指定的时间点上执行——不长不短,正好在希望的时间点上。内核定时器正是解决这个问题的理想工具。

  定时器使用很简单,执行一些初始化操作,设置一个超时时间,指定超时后发生的函数,然后激活定时器就可以了。指定的函数在定时器到期时自动执行。

  使用定时器

  定时器的结构为time_list,结构如下:

  技术分享

  使用定时器时,不需要深入的了解该数据结构,内核提供了一组与定时器相关的接口用来简化管理定时器操作。

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

  struct time_list my_timer;

  接着调用init_timer(&my_timer)来初始化定时器数据结构的内部值。

  然后填充结构中需要的值:

  my_timer.expires = jiffies + delay;   // 定时器超时时的节拍数

  my_timer.data = 0;          //给定时器处理函数传入0值

  my_timer.function = my_function;   //定时器超时时调用的函数

  处理函数必须符合下面函数的原型:

  void my_timer_function(unsigned long data);

  定时器处理函数有可能被延误执行,所以不能用定时器来实现任何硬实时任务。

  通过函数 mod_timer(&my_timer,jiffies+new_delay) 来改变已经激活的定时器超时时间。

  通过 del_timer (&my_timer) 在定时器超时前停止定时器。

  当删除定时器时,必须小心一个潜在的竞争条件,当del_timer()返回后,可以保证的只是:定时器不会再被激活(也就是,将来不会执行),但是在多处理器机器上定时器中断可能已经在其他处理器上运行了,所以删除定时器时需要等待可能在其他处理器上运行的定时器处理程序都退出,这是就要使用del_timer_sync()函数执行删除工作:

  del_timer_sync(&my_timer);

  与del_timer()函数不同,该函数不能在中断上下文中使用。

  内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。通过函数来处理当前处理器上运行的所有超时定时器。

  延迟执行

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

  1. 忙等待

  最简单的延迟方法(也通常是最不理想的方法)是忙等待。但要注意该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不高的时候才可以使用。

  2.短延迟

  这种方法在内核需要很短的延迟和要求延迟的时间很精确的时候使用。多发生在与硬件同步时,等待时间往往小于1毫秒。

  3.schedule_timeout()

  此为更理想的延迟执行方法,该方法会让需要延迟执行的任务睡眠到执行的延迟时间耗尽后在重复运行。但该方法也不能保证睡眠时间正好等于指定的延迟时间——只能尽量的使睡眠时间接近指定的延迟时间。当指定的时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列。

 

  参考自:《Linux Kernel Development》.

以上是关于向下之旅(十四):定时器和时间管理的主要内容,如果未能解决你的问题,请参考以下文章

向下之旅(二十):进程地址空间

向下之旅(二十五):调试

向下之旅(十三):内核同步方法

范畴和函子,以及它们在 Haskell 中的应用——洪峰老师讲创客道(三十四)

Hasen的linux设备驱动开发学习之旅--时钟

Docker之旅-基本概念-02