《Linux内核的设计与实现》第十一章笔记

时间管理在内核中占有非常重要的地位。相对于事件驱动 而言,内核中有大量的函数都是基于时间驱动的。其中有些函数是周期执行的,像对调度程序中的运行队列进行平衡调整或对屏幕进行刷新这样的函数,都需要定期执行;而另外一些函数,比如需要推后执行的磁盘1/0操作等,则需要等待一个相对时间后才运行。

系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,也负责执行需要周期性运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢,本章将着重讨论它们。

11.1 内核中的时间概念

时间概念对计算机来说有些模糊,事实上内核必须在硬件的帮助下才能计算和管理时间。硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可看成是一个电子时间资源,比如数字时钟或处理器频率等。系统定时器以某种频率自行触发(经常被称为击中(hitting)或射中(popping))时钟中断,该频率可以通过编程预定,称作节拍率(tick rate),当对钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。

因为预编的节拍率对内核来说是可知的,所以内核知道连续两次时钟中断的间隔时间。这个间隔时间就称为节拍(tick),它等于节拍率分之一(1/(tick rate))秒。正如你所看到的,内核就是靠这种已知的时钟中断间隔来计算墙上时间和系统运行时间的。墙上时间(也就是实际时间)对用户空间的应用程序来说是最重要的。内核通过控制时钟中断维护实际时间,另外内核也为用户空间提供了一组系统调用以获取实际日期和实际时间。系统运行时间(自系统启动开始所经的时间)对用户空间和内核都很有用,因为许多程序都必须清楚流逝的时间。通过两次(现在和以后)读取运行时间再计算它们的差,就可以得到相对的流逝的时间了。

11.2 节拍率

系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。

内核在<asm/param.h>文件中定义了这个值。节拍率有一个HZ频率,一个周期为1/HZ秒。例如,x86体系结构中,系统定时器频率默认值为100,因此,x86上时钟中断的频率就为100Hz,也就是说在1386处理上的每秒钟时钟中断100次(百分之一秒,即每10ms产生一次)。

11.2.1 理想的HZ值

我们接下来就分析系统定时器使用高频率与使用低频率各有哪些优势。

高HZ的优势

更高的时钟中断频度和更高的准确度又会带来如下优点:

  • 内核定时器能够以更高的频度和更高的准确度(它带来了大量的好处,下一条便是其中之一)运行。
  • 依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行。
  • 对诸如资源消耗和系统运行时间等的测量会有更精细的解析度。
  • 提高进程抢占的准确度。

高HZ的劣势

现在该谈谈另一面了,提高节拍率会产生副作用。节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。因为处理器必须花时间来执行时钟中断处理程序,所以节拍率越高,中断处理程序占用的处理器的时间越多。这样不但减少了处理器处理其他工作的时间,而且还会更频繁地打乱处理器高速缓存并增加耗电。

无节拍的OS?
Linux内核支持 “无节拍操作” 这样的选项。当编译内核时设置了CONFIG_HZ配置选项,系统就根据这个选项动态调度时钟中断。并不是每隔固定的时间间隔(比如1ms)触发时钟中断,而是按需动态调度和重新设置。如果下一个时钟频率设置为3ms,就每3ms触发一次时钟中断。之后,如果50ms内都无事可做,内核以50ms重新调度时钟中断
减少开销总是受欢迎的,但是实质性受益还是省电。

11.3 jiffies

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

extern unsigned long volatile jiffies;

11.3.1 jiffies 的回绕

和任何C整型一样,当jffes变量的值超过它的最大存放范围后就会发生溢出。对于32位无符号长整型,最大取值为2-1,所以在溢出前,定时器节拍计数最大为4294967295。如果节拍计数达到了最大值后还要继续增加的话,它的值会回绕(wrap around)到0。

由于jiffies回绕有可能造成if判断结果刚好相反的情况。

内核提供了四个宏来帮助比较节拍计数,它们能正确地处理节拍计数回绕情况。这些宏定义在文件<linux/jiffies.h>中,这里列出的宏是简化版:

#define time_after(unknown,known)((long)(known)-(1ong)(unknown)<0)
#define time_before(unknown,known)((long)(unknown)-(1ong)(known)<0)
#define time_after_eq(unknown,known)((1ong)(unknown)-(1ong)(known)>=0)
#define time_before_eq(unknown,known)((1ong)(known)-(1ong)(unknown)>=0)

其中unkown参数通常是jiffies,known参数是需要对比的值。

宏time_after(unknown,known),当时间unknown超过指定的known时,返回真,否则返回假:宏time_before(unknown,known),当时间unknow没超过指定的know时,返回真,否则返回假。后面两个宏作用和前面两个宏一样,只有当两个参数相等时,它们才返回真。

11.4 硬时钟和定时器

体系结构提供了两种设备进行计时——一种是我们前面讨论过的系统定时器;另一种是实时时钟。虽然在不同机器上这两种时钟的实现并不相同,但是它们有着相同的作用和设计思路。

11.4.1 实时时钟

实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。在PC体系结构中,RTC和CMOS集成在一起,而且RTC的运行和BIOS的保存设置都是通过同一个电池供电的。

当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。虽然内核通常不会在系统启动后再读取xtime变量,但是有些体系结构(比如x86)会周期性地将当前时间值存回RTC中。尽管如此,实时时钟最主要的作用仍是在启动时初始化xtime变量。

11.4.2系统定时器

系统定时器是内核定时机制中最为重要的角色。尽管不同体系结构中的定时器实现不尽相同,但是系统定时器的根本思想并没有区别——提供一种周期性触发中断机制。

x86体系结构中的其他的时钟资源还包括本地APIC时钟和时间戳计数(TSC)等。

11.5 时间中断处理程序

下面将分析时钟中断处理程序是如何实现的。时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。

与体系结构相关的例程作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应地运行。虽然处理程序的具体工作依赖于特定的体系结构,但是绝大多数处理程序最低限度也都要执行如下工作:

  • 获得xtime lock锁,以便对访问jiffies 64和墙上时间xtime进行保护。
  • 需要时应答或重新设置系统时钟。
  • 周期性地使用墙上时间更新实时时钟。
  • 调用体系结构无关的时钟例程:tick_periodic()。

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

  • 给jiffies 64变量增加1(这个操作即使是在32位体系结构上也是安全的,因为前面已经获得了xtime_lock锁)。
  • 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
  • 执行已经到期的动态定时器(11.6节将讨论)。
  • 执行第4章曾讨论的sheduler_tick()函数。
  • 更新墙上时间,该时间存放在xtime变量中。
  • 计算平均负载值。

11.6 实际时间

当前实际时间(墙上时间)定义在文件kerneltime/timekeeping.c中:

struct timespec xtime;

timespec数据结构定义在文件<linux/time.h>中,形式如下:

struct timespec{
	_kernel_time_t  tv_sec;    /*秒*/
	long tv_nsec;              /*ns*/
};

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

读写xtime变量需要使用xtime_lock锁,该锁不是普通自旋锁而是一个seglock锁。

从用户空间取得墙上时间的主要接口是gettimeofday(),在内核中对应的系统调用为sys_gettimeofday(),定义于kernel/time.c。

除了更新xtime时间以外,内核不会像用户空间程序那样频繁使用xime,但也有需要注意的特殊情况,那就是在文件系统的实现代码中存放访问时间戳(创建、存取、修改等)时需要使用xtime。

11.7 定时器

定时器(有时也称为动态定时器或内核定时器)是管理内核流逝的时间的基础。使工作在指定时间点上执行——不长不短,正好在希望的时间点上。内核定时器正是解决这个问题的理想工具。

定时器的使用很简单。你只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。注意定时器并不周期运行,它在超时后就自行撤销,这也正是这种定时器被称为动态定时器的一个原因:动态定时器不断地创建和撤销,而且它的运行次数也不受限制。

11.7.1 使用定时器

内核提供了一组与定时器相关的接口用来简化管理定时器的操作。所有这些接口都声明在文件<linux/timer.h>中,大多数接口在文件kemel/timer.c中获得实现。

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

struct timer_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);

data参数使你可以利用同一个处理函数注册多个定时器,只需通过该参数就能区别对待它们。如果你不需要这个参数,就可以简单地传递0(或任何其他值)给处理函数。

最后,你必须激活定时器:

add_timer(&my_timer);

注意,虽然内核可以保证不会在超时时间到期前运行定时器处理函数,但是有可能延误定时器的执行。一般来说,定时器都在超时后马上就会执行,但是也有可能推迟到下一次时钟节拍时才能运行,所以不能用定时器来实现任何硬实时任务。

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

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

mod_timer()函数也可操作那些已经初始化,但还没有被激活的定时器,如果定时器未被激活,mod_timer()会激活它。

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

del_timer(&my_timer);

被激活或未被激活的定时器都可以使用该函数,如果定时器还未被激活,该函数返回0;否则返回1,注意,不需要为已经超时的定时器调用该函数,因为它们会自动删除。

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

del_timer_sync(&my timer);

和del timer()函数不同,del_timer_sync()函数不能在中断上下文中使用。

11.8 延迟执行

内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外,还需要其他方法来推迟执行任务。这种推迟通常发生在等待硬件完成某些工作时,而且等待的时间往往非常短,比如,重新设置网卡的以太模式需要花费2ms,所以在设定网卡速度后,驱动程序必须至少等待2ms才能继续运行。

内核提供了许多延迟方法处理各种延迟要求。不同的方法有不同的处理特点,有些是在延迟任务时挂起处理器,防止处理器执行任何实际工作;另一些不会挂起处理器,所以也不能确保被延迟的代码能够在指定的延迟时间运行。

11.8.1 忙等待

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

忙循环实现起来很简单——在循环中不断旋转直到希望的时钟节拍数耗尽,比如:

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

循环不断执行,直到jiffies大于delay为止,总共的循环时间为10个节拍。在HZ值等于1000的x86体系结构上,耗时为10ms。

对于系统的其他部分,忙循环方法算不上一个好办法。因为当代码等待时,处理器只能在原地旋转等待——它不会去处理其他任何任务!事实上,你几乎不会用到这种低效率的办法,这里介绍它仅仅因为它是最简单最直接的延迟方法。当然你也可能在那些鳖脚的代码中发现它的身影。

更好的方法应该是在代码等待时,允许内核重新调度执行其他任务。

11.8.2 短延迟

有时内核代码(通常也是驱动程序)不但需要很短暂的延迟(比时钟节拍还短),而且还要求延迟的时间很精确。这种情况多发生在和硬件同步时,也就是说需要短暂等待某个动作的完成等待时间往往小于1ms),所以不可能使用像前面例子中那种基于jiffes的延迟方法。

幸运的是,内核提供了三个可以处理ms,ns和ms级别的延迟函数,它们定义在文件<linux/delay.h>和<asm/delay.h>中,可以看到它们并不使用jiffies:

void udelay(unsigned long usecs)
void ndelay(unsigned long nsecs)
void ndelay(unsigned long msecs)

这个函数用起来很简单:

udelay(150);       /*延迟150us*/

11.8.3 schedule_timeout()

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

/*将任务设置为可中断睡眠状态*/
set_current_state(TASK INTERRUPTIBLE);
/*小睡会儿,“a"秒后唤醒*/
schedule_timeout(s*Hz);

唯一的参数是延迟的相对时间,单位为jiffies,上例中将相应的任务推入可中断睡眠队列,睡眠s秒。因为任务处于可中断状态,所以如果任务收到信号将被唤醒。如果睡眠任务不想接收信号,可以将任务状态设置为TASK_UNINTERRUPTIBLE,然后睡眠。注意,在调用sechedule_timeout()函数前必须首先将任务设置成上面两种状态之一,否则任务不会睡眠。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值