当前位置: 首页 > news >正文

宁波专业平台网站建设wordpress怎么做产品列表页

宁波专业平台网站建设,wordpress怎么做产品列表页,51ppt模板网免费,湖南省住建云公共服务平台前言 时间在一个操作系统内核中占据着重要的地位#xff0c;它是驱动一个OS内核运行的“起博器”。一般说来#xff0c;内核主要需要两种类型的时间#xff1a; 1. 在内核运行期间持续记录当前的时间与日期#xff0c;以便内核对某些对象和事件作时间标记#xff08;tim…前言  时间在一个操作系统内核中占据着重要的地位它是驱动一个OS内核运行的“起博器”。一般说来内核主要需要两种类型的时间  1. 在内核运行期间持续记录当前的时间与日期以便内核对某些对象和事件作时间标记timestamp也称为“时间戳”或供用户通过时间syscall进行检索。  2. 维持一个固定周期的定时器以提醒内核或用户一段时间已经过去了。  PC机中的时间是有三种时钟硬件提供的而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件是1实时时钟Real Time ClockRTC2可编程间隔定时器Programmable Interval TimerPIT3时间戳计数器Time Stamp CounterTSC。  71 时钟硬件  711 实时时钟RTC  自从IBM PC AT起所有的PC机就都包含了一个叫做实时时钟RTC的时钟芯片以便在PC机断电后仍然能够继续保持时间。显然RTC是通过主板上的电池来供电的而不是通过PC机电源来供电的因此当PC机关掉电源后RTC仍然会继续工作。通常CMOS RAM和RTC被集成到一块芯片上因此RTC也称作“CMOS Timer”。最常见的RTC芯片是MC146818Motorola和DS12887maximDS12887完全兼容于MC146818并有一定的扩展。本节内容主要基于MC146818这一标准的RTC芯片。具体内容可以参考MC146818的Datasheet。  7111 RTC寄存器  MC146818 RTC芯片一共有64个寄存器。它们的芯片内部地址编号为0x000x3F不是I/O端口地址这些寄存器一共可以分为三组  1时钟与日历寄存器组共有10个0x00~0x09表示时间、日历的具体信息。在PC机中这些寄存器中的值都是以BCD格式来存储的比如23dec0x23BCD。  2状态和控制寄存器组共有4个0x0A~0x0D控制RTC芯片的工作方式并表示当前的状态。  3CMOS配置数据通用的CMOS RAM它们与时间无关因此我们不关心它。  时钟与日历寄存器组的详细解释如下  Address Function  00 Current second for RTC  01 Alarm second  02 Current minute  03 Alarm minute  04 Current hour  05 Alarm hour  06 Current day of week01Sunday  07 Current date of month  08 Current month  09 Current yearfinal two digitseg93  状态寄存器A地址0x0A的格式如下  其中  1bit7——UIP标志Update in Progress为1表示RTC正在更新日历寄存器组中的值此时日历寄存器组是不可访问的此时访问它们将得到一个无意义的渐变值。  2bit64——这三位是“除法器控制位”divider-control bits用来定义RTC的操作频率。各种可能的值如下  Divider bits Time-base frequency Divider Reset Operation Mode  DV2 DV1 DV0  0 0 0 4.194304 MHZ NO YES  0 0 1 1.048576 MHZ NO YES  0 1 0 32.769 KHZ NO YES  1 1 0/1 任何 YES NO  PC机通常将Divider bits设置成“010”。  3bit30——速率选择位Rate Selection bits用于周期性或方波信号输出。  RS bits 4.194304或1.048578 MHZ 32.768 KHZ  RS3 RS2 RS1 RS0 周期性中断 方波 周期性中断 方波  0 0 0 0 None None None None  0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ  0 0 1 0 61.035μs 16.384 KHZ  0 0 1 1 122.070μs 8.192KHZ  0 1 0 0 244.141μs 4.096KHZ  0 1 0 1 488.281μs 2.048KHZ  0 1 1 0 976.562μs 1.024KHZ  0 1 1 1 1.953125ms 512HZ  1 0 0 0 3.90625ms 256HZ  1 0 0 1 7.8125ms 128HZ  1 0 1 0 15.625ms 64HZ  1 0 1 1 31.25ms 32HZ  1 1 0 0 62.5ms 16HZ  1 1 0 1 125ms 8HZ  1 1 1 0 250ms 4HZ  1 1 1 1 500ms 2HZ  PC机BIOS对其默认的设置值是“0110”。  状态寄存器B的格式如下所示  各位的含义如下  1bit7——SET标志。为1表示RTC的所有更新过程都将终止用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。  2bit6——PIE标志周期性中断使能标志。  3bit5——AIE标志告警中断使能标志。  4bit4——UIE标志更新结束中断使能标志。  5bit3——SQWE标志方波信号使能标志。  6bit2——DM标志用来控制日历寄存器组的数据模式0BCD1BINARY。BIOS总是将它设置为0。  7bit1——2412标志用来控制hour寄存器0表示12小时制1表示24小时制。PC机BIOS总是将它设置为1。  8bit0——DSE标志。BIOS总是将它设置为0。  状态寄存器C的格式如下  1bit7——IRQF标志中断请求标志当该位为1时说明寄存器B中断请求发生。  2bit6——PF标志周期性中断标志为1表示发生周期性中断请求。  3bit5——AF标志告警中断标志为1表示发生告警中断请求。  4bit4——UF标志更新结束中断标志为1表示发生更新结束中断请求。  状态寄存器D的格式如下  1bit7——VRT标志Valid RAM and Time为1表示OK为0表示RTC已经掉电。  2bit60——总是为0未定义。  7112 通过I/O端口访问RTC  在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中端口0x70是RTC的寄存器地址索引端口0x71是数据端口。  读RTC芯片寄存器的步骤是  mov al, addr  out 70h, al ; Select reg_addr in RTC chip  jmp $2 ; a slight delay to settle thing  in al, 71h ;  写RTC寄存器的步骤如下  mov al, addr  out 70h, al ; Select reg_addr in RTC chip  jmp $2 ; a slight delay to settle thing  mov al, value  out 71h, al  712 可编程间隔定时器PIT  每个PC机中都有一个PIT以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片它的I/O端口地址是0x40~0x43。  Intel 8254 PIT有3个计时通道每个通道都有其不同的用途  1 通道0用来负责更新系统时钟。每当一个时钟滴答过去时它就会通过IRQ0向系统产生一次时钟中断。  2 通道1通常用于控制DMAC对RAM的刷新。  3 通道2被连接到PC机的扬声器以产生方波信号。  每个通道都有一个向下减小的计数器8254 PIT的输入时钟信号的频率是1193181HZ也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1一直减到0值。因此对于通道0而言当他的计数器减到0时PIT就向系统产生一次时钟中断表示一个时钟滴答已经过去了。当各通道的计数器减到0时我们就说该通道处于“Terminal count”状态。  通道计数器的最大值是10000h所对应的时钟中断频率是11931816553618.2HZ也就是说此时一秒钟之内将产生18.2次时钟中断。  7121 PIT的I/O端口  在i386平台上8254芯片的各寄存器的I/O端口地址如下  Port Description  40h Channel 0 counterread/write  41h Channel 1 counterread/write  42h Channel 2 counterread/write  43h PIT control wordwrite only  其中由于通道0、1、2的计数器是一个16位寄存器而相应的端口却都是8位的因此读写通道计数器必须进行进行两次I/O端口读写操作分别对应于计数器的高字节和低字节至于是先读写高字节再读写低字节还是先读写低字节再读写高字节则由PIT的控制寄存器来决定。8254 PIT的控制寄存器的格式如下  1bit76——Select Counter选择对那个计数器进行操作。“00”表示选择Counter 0“01”表示选择Counter 1“10”表示选择Counter 2“11”表示Read-Back Command仅对于8254对于8253无效。  2bit54——Read/Write/Latch格式位。“00”表示锁存Latch当前计数器的值“01”只读写计数器的高字节MSB“10”只读写计数器的低字节LSB“11”表示先读写计数器的LSB再读写MSB。  3bit31——Mode bits控制各通道的工作模式。“000”对应Mode 0“001”对应Mode 1“010”对应Mode 2“011”对应Mode 3“100”对应Mode 4“101”对应Mode 5。  4bit0——控制计数器的存储模式。0表示以二进制格式存储1表示计数器中的值以BCD格式存储。  7122 PIT通道的工作模式  PIT各通道可以工作在下列6种模式下  1. Mode 0当通道处于“Terminal count”状态时产生中断信号。  2. Mode 1Hardware retriggerable one-shot。  3. Mode 2Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间OUT管脚将变为低电平直到计数器的值减到0。当计数器的值被自动地重新加载后OUT管脚又变成高电平然后重复上述过程。通道0通常工作在这个模式下。  4. Mode 3方波信号发生器。  5. Mode 4Software triggered strobe。  6. Mode 5Hardware triggered strobe。  7123 锁存计数器Latch Counter  当控制寄存器中的bit54设置成0时将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值因为计数器表面上已经停止向下计数PIT芯片内部并没有停止向下计数。NOTE一旦发出了锁存命令就要马上读计数器的值。  713 时间戳记数器TSC  从Pentium开始所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器TSC的寄存器。该寄存器实际上是一个不断增加的计数器它在CPU的每个时钟信号到来时加1也即每一个clock-cycle输入CPU时该计数器的值就加1。  汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ那么TSC就将每2.5纳秒增加一次。 dreamice 回复于2008-11-06 17:56:022 Linux内核对RTC的编程 MC146818 RTC芯片或其他兼容芯片如DS12887可以在IRQ8上产生周期性的中断中断的频率在2HZ8192HZ之间。与MC146818 RTC对应的设备驱动程序实现在include/linux/rtc.h和driverschar/rtc.c文件中对应的设备文件是dev/rtcmajor10,minor135只读字符设备。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活IRQ8线从而将RTC当作一个闹钟来用。 而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时它从RTC中读取时间与日期的基准值。然后再运行期间内核就完全抛开RTC从而以软件的形式维护系统的当前时间与日期并在需要时将时间回写到RTC芯片中。 Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义了mc146818 RTC芯片各寄存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件中。 721 RTC芯片的I/O端口操作 Linux在include/asm-i386/mc146818rtc.h头文件中定义了RTC芯片的I/O端口操作。端口0x70被称为“RTC端口0”端口0x71被称为“RTC端口1”如下所示 #ifndef RTC_PORT #define RTC_PORT(x) (0x70  (x)) #define RTC_ALWAYS_BCD 1 /* RTC operates in binary mode */ #endif 显然RTC_PORT(0)就是指端口0x70RTC_PORT(1)就是指I/O端口0x71。 端口0x70被用作RTC芯片内部寄存器的地址索引端口而端口0x71则被用作RTC芯片内部寄存器的数据端口。再读写一个RTC寄存器之前必须先把该寄存器在RTC芯片内部的地址索引值写到端口0x70中。根据这一点读写一个RTC寄存器的宏定义CMOS_READ()和CMOS_WRITE()如下 #define CMOS_READ(addr) ({ \ outb_p((addr),RTC_PORT(0)); \ inb_p(RTC_PORT(1)); \ }) #define CMOS_WRITE(val, addr) ({ \ outb_p((addr),RTC_PORT(0)); \ outb_p((val),RTC_PORT(1)); \ }) #define RTC_IRQ 8 在上述宏定义中参数addr是RTC寄存器在芯片内部的地址值取值范围是0x00~0x3F参数val是待写入寄存器的值。宏RTC_IRQ是指RTC芯片所连接的中断请求输入线号通常是8。 722 对RTC寄存器的定义 Linux在include/linux/mc146818rtc.h这个头文件中定义了RTC各寄存器的含义。 1寄存器内部地址索引的定义 Linux内核仅使用RTC芯片的时间与日期寄存器组和控制寄存器组地址为0x00~0x09之间的10个时间与日期寄存器的定义如下 #define RTC_SECONDS 0 #define RTC_SECONDS_ALARM 1 #define RTC_MINUTES 2 #define RTC_MINUTES_ALARM 3 #define RTC_HOURS 4 #define RTC_HOURS_ALARM 5 /* RTC_*_alarm is always true if 2 MSBs are set */ # define RTC_ALARM_DONT_CARE 0xC0 #define RTC_DAY_OF_WEEK 6 #define RTC_DAY_OF_MONTH 7 #define RTC_MONTH 8 #define RTC_YEAR 9 四个控制寄存器的地址定义如下 #define RTC_REG_A 10 #define RTC_REG_B 11 #define RTC_REG_C 12 #define RTC_REG_D 13 2各控制寄存器的状态位的详细定义 控制寄存器A0x0A主要用于选择RTC芯片的工作频率因此也称为RTC频率选择寄存器。因此Linux用一个宏别名RTC_FREQ_SELECT来表示控制寄存器A如下 #define RTC_FREQ_SELECT RTC_REG_A RTC频率寄存器中的位被分为三组①bit7表示UIP标志②bit64用于除法器的频率选择③bit30用于速率选择。它们的定义如下 # define RTC_UIP 0x80 # define RTC_DIV_CTL 0x70 /* Periodic intr. / Square wave rate select. 0none, 132.8kHz,... 152Hz */ # define RTC_RATE_SELECT 0x0F 正如7.1.1.1节所介绍的那样bit64有5中可能的取值分别为除法器选择不同的工作频率或用于重置除法器各种可能的取值如下定义所示 /* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */ # define RTC_REF_CLCK_4MHZ 0x00 # define RTC_REF_CLCK_1MHZ 0x10 # define RTC_REF_CLCK_32KHZ 0x20 /* 2 values for divider stage reset, others for testing purposes only */ # define RTC_DIV_RESET1 0x60 # define RTC_DIV_RESET2 0x70 寄存器B中的各位用于使能禁止RTC的各种特性因此控制寄存器B0x0B也称为“控制寄存器”Linux用宏别名RTC_CONTROL来表示控制寄存器B它与其中的各标志位的定义如下所示 #define RTC_CONTROL RTC_REG_B # define RTC_SET 0x80 /* disable updates for clock setting */ # define RTC_PIE 0x40 /* periodic interrupt enable */ # define RTC_AIE 0x20 /* alarm interrupt enable */ # define RTC_UIE 0x10 /* update-finished interrupt enable */ # define RTC_SQWE 0x08 /* enable square-wave output */ # define RTC_DM_BINARY 0x04 /* all time/date values are BCD if clear */ # define RTC_24H 0x02 /* 24 hour mode - else hours bit 7 means pm */ # define RTC_DST_EN 0x01 /* auto switch DST - works f. USA only */ 寄存器C是RTC芯片的中断请求状态寄存器Linux用宏别名RTC_INTR_FLAGS来表示寄存器C它与其中的各标志位的定义如下所示 #define RTC_INTR_FLAGS RTC_REG_C /* caution - cleared by read */ # define RTC_IRQF 0x80 /* any of the following 3 is active */ # define RTC_PF 0x40 # define RTC_AF 0x20 # define RTC_UF 0x10 寄存器D仅定义了其最高位bit7以表示RTC芯片是否有效。因此寄存器D也称为RTC的有效寄存器。Linux用宏别名RTC_VALID来表示寄存器D如下 #define RTC_VALID RTC_REG_D # define RTC_VRT 0x80 /* valid RAM and time */ 3二进制格式与BCD格式的相互转换 由于时间与日期寄存器中的值可能以BCD格式存储也可能以二进制格式存储因此需要定义二进制格式与BCD格式之间的相互转换宏以方便编程。如下 #ifndef BCD_TO_BIN #define BCD_TO_BIN(val) ((val)((val)15)  ((val)4)*10) #endif #ifndef BIN_TO_BCD #define BIN_TO_BCD(val) ((val)(((val)/10)4)  (val)%10) #endif 723 内核对RTC的操作 如前所述Linux内核与RTC进行互操作的时机只有两个1内核在启动时从RTC中读取启动时的时间与日期2内核在需要时将时间与日期回写到RTC中。为此Linux内核在arch/i386/kernel/time.c文件中实现了函数get_cmos_time()来进行对RTC的第一种操作。显然get_cmos_time()函数仅仅在内核启动时被调用一次。而对于第二种操作Linux则同样在arch/i386/kernel/time.c文件中实现了函数set_rtc_mmss()以支持向RTC中回写当前时间与日期。下面我们将来分析这二个函数的实现。 在分析get_cmos_time()函数之前我们先来看看RTC芯片对其时间与日期寄存器组的更新原理。 1Update In Progress 当控制寄存器B中的SET标志位为0时MC146818芯片每秒都会在芯片内部执行一个“更新周期”Update Cycle其作用是增加秒寄存器的值并检查秒寄存器是否溢出。如果溢出则增加分钟寄存器的值如此一致下去直到年寄存器。在“更新周期”期间时间与日期寄存器组0x00~0x09是不可用的此时如果读取它们的值将得到未定义的值因为MC146818在整个更新周期期间会把时间与日期寄存器组从CPU总线上脱离从而防止软件程序读到一个渐变的数据。 在MC146818的输入时钟频率也即晶体增荡器的频率为4.194304MHZ或1.048576MHZ的情况下“更新周期”需要花费248us而对于输入时钟频率为32.768KHZ的情况“更新周期”需要花费1984us1.984ms。控制寄存器A中的UIP标志位用来表示MC146818是否正处于更新周期中当UIP从0变为1的那个时刻就表示MC146818将在稍后马上就开更新周期。在UIP从0变到1的那个时刻与MC146818真正开始Update Cycle的那个时刻之间时有一段时间间隔的通常是244us。也就是说在UIP从0变到1的244us之后时间与日期寄存器组中的值才会真正开始改变而在这之间的244us间隔内它们的值并不会真正改变。如下图所示 2get_cmos_time()函数 该函数只被内核的初始化例程time_init()和内核的APM模块所调用。其源码如下 /* not static: needed by APM */ unsigned long get_cmos_time(void) { unsigned int year, mon, day, hour, min, sec; int i; /* The Linux interpretation of the CMOS clock register contents: * When the Update-In-Progress (UIP) flag goes from 1 to 0, the * RTC registers show the second which has precisely just started. * Lets hope other operating systems interpret the RTC the same way. */ /* read RTC exactly on falling edge of update flag */ for (i  0 ; i  1000000 ; i) /* may take up to 1 second... */ if (CMOS_READ(RTC_FREQ_SELECT)  RTC_UIP) break; for (i  0 ; i  1000000 ; i) /* must try at least 2.228 ms */ if (!(CMOS_READ(RTC_FREQ_SELECT)  RTC_UIP)) break; do { /* Isnt this overkill ? UIP above should guarantee consistency */ sec  CMOS_READ(RTC_SECONDS); min  CMOS_READ(RTC_MINUTES); hour  CMOS_READ(RTC_HOURS); day  CMOS_READ(RTC_DAY_OF_MONTH); mon  CMOS_READ(RTC_MONTH); year  CMOS_READ(RTC_YEAR); } while (sec ! CMOS_READ(RTC_SECONDS)); if (!(CMOS_READ(RTC_CONTROL)  RTC_DM_BINARY) || RTC_ALWAYS_BCD) { BCD_TO_BIN(sec); BCD_TO_BIN(min); BCD_TO_BIN(hour); BCD_TO_BIN(day); BCD_TO_BIN(mon); BCD_TO_BIN(year); } if ((year  1900)  1970) year  100; return mktime(year, mon, day, hour, min, sec); } 对该函数的注释如下 1在从RTC中读取时间时由于RTC存在Update Cycle因此软件发出读操作的时机是很重要的。对此get_cmos_time()函数通过UIP标志位来解决这个问题第一个for循环不停地读取RTC频率选择寄存器中的UIP标志位并且只要读到UIP的值为1就马上退出这个for循环。第二个for循环同样不停地读取UIP标志位但他只要一读到UIP的值为0就马上退出这个for循环。这两个for循环的目的就是要在软件逻辑上同步RTC的Update Cycle显然第二个for循环最大可能需要2.228ms(TBUCmax(TUC)244us1984us2.228ms) (2)从第二个for循环退出后RTC的Update Cycle已经结束。此时我们就已经把当前时间逻辑定准在RTC的当前一秒时间间隔内。也就是说这是我们就可以开始从RTC寄存器中读取当前时间值。但是要注意读操作应该保证在244us内完成准确地说读操作要在RTC的下一个更新周期开始之前完成244us的限制是过分偏执的。所以get_cmos_time()函数接下来通过CMOS_READ()宏从RTC中依次读取秒、分钟、小时、日期、月份和年分。这里的do{}while(sec!CMOS_READ(RTC_SECOND))循环就是用来确保上述6个读操作必须在下一个Update Cycle开始之前完成。 3接下来判定时间的数据格式PC机中一般总是使用BCD格式的时间因此需要通过BCD_TO_BIN()宏把BCD格式转换为二进制格式。 4接下来对年分进行修正以将年份转换为“19XX”的格式如果是1970以前的年份则将其加上100。 5最后调用mktime()函数将当前时间与日期转换为相对于19700101 000000的秒数值并将其作为函数返回值返回。 函数mktime()定义在include/linux/time.h头文件中它用来根据Gauss算法将以year/mon/day/hour/min/sec如19801231 235959格式表示的时间转换为相对于19700101 000000这个UNIX时间基准以来的相对秒数。其源码如下 static inline unsigned long mktime (unsigned int year, unsigned int mon, unsigned int day, unsigned int hour, unsigned int min, unsigned int sec) { if (0  (int) (mon - 2)) { /* 1..12 - 11,12,1..10 */ mon  12; /* Puts Feb last since it has leap day */ year - 1; } return ((( (unsigned long) (year/4 - year/100  year/400  367*mon/12  day)  year*365 - 719499 )*24  hour /* now have hours */ )*60  min /* now have minutes */ )*60  sec; /* finally seconds */ } 3set_rtc_mmss()函数 该函数用来更新RTC中的时间它仅有一个参数nowtime是以秒数表示的当前时间其源码如下 static int set_rtc_mmss(unsigned long nowtime) { int retval  0; int real_seconds, real_minutes, cmos_minutes; unsigned char save_control, save_freq_select; /* gets recalled with irq locally disabled */ spin_lock(rtc_lock); save_control  CMOS_READ(RTC_CONTROL); /* tell the clock its being set */ CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL); save_freq_select  CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */ CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT); cmos_minutes  CMOS_READ(RTC_MINUTES); if (!(save_control  RTC_DM_BINARY) || RTC_ALWAYS_BCD) BCD_TO_BIN(cmos_minutes); /* * since were only adjusting minutes and seconds, * dont interfere with hour overflow. This avoids * messing with unknown time zones but requires your * RTC not to be off by more than 15 minutes */ real_seconds  nowtime % 60; real_minutes  nowtime / 60; if (((abs(real_minutes - cmos_minutes)  15)/30)  1) real_minutes  30; /* correct for half hour time zone */ real_minutes % 60; if (abs(real_minutes - cmos_minutes)  30) { if (!(save_control  RTC_DM_BINARY) || RTC_ALWAYS_BCD) { BIN_TO_BCD(real_seconds); BIN_TO_BCD(real_minutes); } CMOS_WRITE(real_seconds,RTC_SECONDS); CMOS_WRITE(real_minutes,RTC_MINUTES); } else { printk(KERN_WARNING set_rtc_mmss: cant update from %d to %d\n, cmos_minutes, real_minutes); retval  -1; } /* The following flags have to be released exactly in this order, * otherwise the DS12887 (popular MC146818A clone with integrated * battery and quartz) will not reset the oscillator and will not * update precisely 500 ms later. You wont find this mentioned in * the Dallas Semiconductor data sheets, but who believes data * sheets anyway ... -- Markus Kuhn */ CMOS_WRITE(save_control, RTC_CONTROL); CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT); spin_unlock(rtc_lock); return retval; } 对该函数的注释如下 1首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。 2接下来在RTC控制寄存器中设置SET标志位以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL寄存器的当前值读到变量save_control中然后再把值save_control | RTC_SET回写到寄存器RTC_CONTROL中。 3然后通过RTC_FREQ_SELECT寄存器中bit64重启RTC芯片内部的除法器。为此类似地先把RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中然后再把值save_freq_select  RTC_DIV_RESET2回写到RTC_FREQ_SELECT寄存器中。 4接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中并根据需要将它从BCD格式转化为二进制格式。 5从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意这里对于半小时区的情况要修正分钟数real_minutes的值。 6然后在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下将real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然在回写之前要记得把二进制转换为BCD格式。 7最后恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是先恢复RTC_CONTROL寄存器再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。 最后需要说明的一点是set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置也即500ms处左右被调用。此外Linux内核对每一次成功的更新RTC时间都留下时间轨迹它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间单位是秒数。该变量定义在arch/i386/kernel/time.c文件中 /* last time the cmos clock got updated */ static long last_rtc_update; 每一次成功地调用set_rtc_mmss()函数后内核都会马上将last_rtc_update更新为当前时间具体请见7.4.3节。dreamice 回复于2008-11-06 17:56:2173 Linux对时间的表示 通常操作系统可以使用三种方法来表示系统的当前时间与日期①最简单的一种方法就是直接用一个64位的计数器来对时钟滴答进行计数。②第二种方法就是用一个32位计数器来对秒进行计数同时还用一个32位的辅助计数器对时钟滴答计数之子累积到一秒为止。因为232超过136年因此这种方法直至22世纪都可以让系统工作得很好。③第三种方法也是按时钟滴答进行计数但是是相对于系统启动以来的滴答次数而不是相对于相对于某个确定的外部时刻当读外部后备时钟如RTC或用户输入实际时间时根据当前的滴答次数计算系统当前时间。 UNIX类操作系统通常都采用第三种方法来维护系统的时间与日期。 731 基本概念 首先有必要明确一些Linux内核时钟驱动中的基本概念。 1时钟周期clock cycle的频率82538254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率在PC机中这个值通常是1193180HZ该宏定义在include/asm-i386/timex.h头文件中 #define CLOCK_TICK_RATE 1193180 /* Underlying HZ */ 2时钟滴答clock tick我们知道当PIT通道0的计数器减到0值时它就在IRQ0上产生一次时钟中断也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断因此也就决定了一次时钟滴答的时间间隔长度。 3时钟滴答的频率HZ也即1秒时间内PIT所产生的时钟滴答次数。类似地这个值也是由PIT通道0的计数器初值决定的反过来说确定了时钟滴答的频率值后也就可以确定8254 PIT通道0的计数器初值。Linux内核用宏HZ来表示时钟滴答的频率而且在不同的平台上HZ有不同的定义值。对于ALPHA和IA62平台HZ的值是1024对于SPARC、MIPS、ARM和i386等平台HZ的值都是100。该宏在i386平台上的定义如下include/asm-i386/param.h #ifndef HZ #define HZ 100 #endif 根据HZ的值我们也可以知道一次时钟滴答的具体时间间隔应该是1000msHZ10ms。 4时钟滴答的时间间隔Linux用全局变量tick来表示时钟滴答的时间间隔长度该变量定义在kernel/timer.c文件中如下 long tick  (1000000  HZ/2) / HZ; /* timer interrupt period */ tick变量的单位是微妙μs由于在不同平台上宏HZ的值会有所不同因此方程式tick1000000÷HZ的结果可能会是个小数因此将其进行四舍五入成一个整数所以Linux将tick定义成1000000HZ2HZ其中被除数表达式中的HZ2的作用就是用来将tick值向上圆整成一个整型数。 另外Linux还用宏TICK_SIZE来作为tick变量的引用别名alias其定义如下archi386/kernel/time.c #define TICK_SIZE tick 5宏LATCHLinux用宏LATCH来定义要写到PIT通道0的计数器中的值它表示PIT将没隔多少个时钟周期产生一次时钟中断。显然LATCH应该由下列公式计算 LATCH1秒之内的时钟周期个数÷1秒之内的时钟中断次数CLOCK_TICK_RATE÷HZ 类似地上述公式的结果可能会是个小数应该对其进行四舍五入。所以Linux将LATCH定义为include/linux/timex.h /* LATCH is used in the interval timer and ftape setup. */ #define LATCH ((CLOCK_TICK_RATE  HZ/2) / HZ) /* For divider */ 类似地被除数表达式中的HZ2也是用来将LATCH向上圆整成一个整数。 732 表示系统当前时间的内核数据结构 作为一种UNIX类操作系统Linux内核显然采用本节一开始所述的第三种方法来表示系统的当前时间。Linux内核在表示系统当前时间时用到了三个重要的数据结构 ①全局变量jiffies这是一个32位的无符号整数用来表示自内核上一次启动以来的时钟滴答次数。每发生一次时钟滴答内核的时钟中断处理函数timer_interrupt都要将该全局变量jiffies加1。该变量定义在kernel/timer.c源文件中如下所示 unsigned long volatile jiffies; C语言限定符volatile表示jiffies是一个易该变的变量因此编译器将使对该变量的访问从不通过CPU内部cache来进行。 ②全局变量xtime它是一个timeval结构类型的变量用来表示当前时间距UNIX时间基准19700101 000000的相对秒数值。结构timeval是Linux内核表示时间的一种格式Linux内核对时间的表示有多种格式每种格式都有不同的时间精度其时间精度是微秒。该结构是内核表示时间时最常用的一种格式它定义在头文件include/linux/time.h中如下所示 struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; 其中成员tv_sec表示当前时间距UNIX时间基准的秒数值而成员tv_usec则表示一秒之内的微秒值且1000000tv_usec0。 Linux内核通过timeval结构类型的全局变量xtime来维持当前时间该变量定义在kernel/timer.c文件中如下所示 /* The current time */ volatile struct timeval xtime __attribute__ ((aligned (16))); 但是全局变量xtime所维持的当前时间通常是供用户来检索和设置的而其他内核模块通常很少使用它其他内核模块用得最多的是jiffies因此对xtime的更新并不是一项紧迫的任务所以这一工作通常被延迟到时钟中断的底半部分bottom half中来进行。由于bottom half的执行时间带有不确定性因此为了记住内核上一次更新xtime是什么时候Linux内核定义了一个类似于jiffies的全局变量wall_jiffies来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中 /* jiffies at the most recent update of wall time */ unsigned long wall_jiffies; ③全局变量sys_tz它是一个timezone结构类型的全局变量表示系统当前的时区信息。结构类型timezone定义在include/linux/time.h头文件中如下所示 struct timezone { int tz_minuteswest; /* minutes west of Greenwich */ int tz_dsttime; /* type of dst correction */ }; 基于上述结构Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处的时区信息如下所示 struct timezone sys_tz; 733 Linux对TSC的编程实现 Linux用定义在arch/i386/kernel/time.c文件中的全局变量use_tsc来表示内核是否使用CPU的TSC寄存器use_tsc1表示使用TSCuse_tsc0表示不使用TSC。该变量的值是在time_init()初始化函数中被初始化的详见下一节。该变量的定义如下 static int use_tsc; 宏cpu_has_tsc可以确定当前系统的CPU是否配置有TSC寄存器。此外宏CONFIG_X86_TSC也表示是否存在TSC寄存器。 7331 读TSC寄存器的宏操作 x86 CPU的rdtsc指令将TSC寄存器的高32位值读到EDX寄存器中、低32位读到EAX寄存器中。Linux根据不同的需要在rdtsc指令的基础上封装几个高层宏操作以读取TSC寄存器的值。它们均定义在include/asm-i386/msr.h头文件中如下 #define rdtsc(low,high) \ __asm__ __volatile__(rdtsc : a (low), d (high)) #define rdtscl(low) \ __asm__ __volatile__ (rdtsc : a (low) : : edx) #define rdtscll(val) \ __asm__ __volatile__ (rdtsc : A (val)) 宏rdtsc同时读取TSC的LSB与MSB并分别保存到宏参数low和high中。宏rdtscl则只读取TSC寄存器的LSB并保存到宏参数low中。宏rdtscll读取TSC的当前64位值并将其保存到宏参数val这个64位变量中。 7332 校准TSC 与可编程定时器PIT相比用TSC寄存器可以获得更精确的时间度量。但是在可以使用TSC之前它必须精确地确定1个TSC计数值到底代表多长的时间间隔也即到底要过多长时间间隔TSC寄存器才会加1。Linux内核用全局变量fast_gettimeoffset_quotient来表示这个值其定义如下arch/i386/kernel/time.c /* Cached *multiplier* to convert TSC counts to microseconds. * (see the equation below). * Equal to 2^32 * (1 / (clocks per usec) ). * Initialized in time_init. */ unsigned long fast_gettimeoffset_quotient; 根据上述定义的注释我们可以看出这个变量的值是通过下述公式来计算的 fast_gettimeoffset_quotient  (2^32) / (每微秒内的时钟周期个数) 定义在arch/i386/kernel/time.c文件中的函数calibrate_tsc()就是根据上述公式来计算fast_gettimeoffset_quotient的值的。显然这个计算过程必须在内核启动时完成因此函数calibrate_tsc()只被初始化函数time_init()所调用。 用TSC实现高精度的时间服务 在拥有TSCTimeStamp Counter的x86 CPU上Linux内核可以实现微秒级的高精度定时服务也即可以确定两次时钟中断之间的某个时刻的微秒级时间值。如下图所示 图77 TSC时间关系 从上图中可以看出要确定时刻x的微秒级时间值就必须确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值以微秒为单位。为此内核定义了以下两个变量 1中断服务执行延迟delay_at_last_interrupt由于从产生时钟中断的那个时刻到内核时钟中断服务函数timer_interrupt真正在CPU上执行的那个时刻之间是有一段延迟间隔的因此Linux内核用变量delay_at_last_interrupt来表示这一段时间延迟间隔其定义如下arch/i386/kernel/time.c /* Number of usecs that the last interrupt was delayed */ static int delay_at_last_interrupt; 关于delay_at_last_interrupt的计算步骤我们将在分析timer_interrupt函数时讨论。 2全局变量last_tsc_low它表示中断服务timer_interrupt真正在CPU上执行时刻的TSC寄存器值的低32位LSB。 显然通过delay_at_last_interrupt、last_tsc_low和时刻x处的TSC寄存器值我们就可以完全确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值。实现在arch/i386/kernel/time.c中的函数do_fast_gettimeoffset()就是这样计算时间间隔偏移的当然它仅在CPU配置有TSC寄存器时才被使用后面我们会详细分析这个函数。dreamice 回复于2008-11-06 17:56:4174 时钟中断的驱动 如前所述82538254 PIT的通道0通常被用来在IRQ0上产生周期性的时钟中断。对时钟中断的驱动是绝大数操作系统内核实现time-keeping的关键所在。不同的OS对时钟驱动的要求也不同但是一般都包含下列要求内容 1. 维护系统的当前时间与日期。 2. 防止进程运行时间超出其允许的时间。 3. 对CPU的使用情况进行记帐统计。 4. 处理用户进程发出的时间系统调用。 5. 对系统某些部分提供监视定时器。 其中第一项功能是所有OS都必须实现的基础功能它是OS内核的运行基础。通常有三种方法可用来维护系统的时间与日期1最简单的一种方法就是用一个64位的计数器来对时钟滴答进行计数。2第二种方法就是用一个32位计数器来对秒进行计数。用一个32位的辅助计数器来对时钟滴答计数直至累计一秒为止。因为232超过136年因此这种方法直至22世纪都可以工作得很好。3第三种方法也是按滴答进行计数但却是相对于系统启动以来的滴答次数而不是相对于一个确定的外部时刻。当读后备时钟如RTC或用户输入实际时间时根据当前的滴答次数计算系统当前时间。 UNIX类的OS通常都采用第三种方法来维护系统的时间与日期。 741 Linux对时钟中断的初始化 Linux对时钟中断的初始化是分为几个步骤来进行的1首先由init_IRQ()函数通过调用init_ISA_IRQ()函数对中断向量32256所对应的中断向量描述符进行初始化设置。显然这其中也就把IRQ0也即中断向量32的中断向量描述符初始化了。2然后init_IRQ()函数设置中断向量32256相对应的中断门。3init_IRQ()函数对PIT进行初始化编程4sched_init()函数对计数器、时间中断的Bottom Half进行初始化。5最后由time_init()函数对Linux内核的时钟中断机制进行初始化。这三个初始化函数都是由init/main.c文件中的start_kernel()函数调用的如下 asmlinkage void __init start_kernel() { … trap_init(); init_IRQ(); sched_init(); time_init(); softirq_init(); … } (1)init_IRQ()函数对8254 PIT的初始化编程 函数init_IRQ()函数在完成中断门的初始化后就对8254 PIT进行初始化编程设置设置的步骤如下1设置8254 PIT的控制寄存器端口0x43的值为“01100100”也即选择通道0、先读写LSB再读写MSB、工作模式2、二进制存储格式。2将宏LATCH的值写入通道0的计数器中端口0x40注意要先写LATCH的LSB再写LATCH的高字节。其源码如下所示arch/i386/kernel/i8259.c void __init init_IRQ(void) { …… /* * Set the clock to HZ Hz, we already have a valid * vector now: */ outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */ outb_p(LATCH  0xff , 0x40); /* LSB */ outb(LATCH  8 , 0x40); /* MSB */ …… } 2sched_init()对定时器机制和时钟中断的Bottom Half的初始化 函数sched_init()中与时间相关的初始化过程主要有两步1调用init_timervecs()函数初始化内核定时器机制2调用init_bh()函数将BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所对应的BH函数分别设置成timer_bh()、tqueue_bh()和immediate_bh()函数。如下所示kernel/sched.c void __init sched_init(void) { …… init_timervecs(); init_bh(TIMER_BH, timer_bh); init_bh(TQUEUE_BH, tqueue_bh); init_bh(IMMEDIATE_BH, immediate_bh); …… } 3time_init()函数对内核时钟中断机制的初始化 前面两个函数所进行的初始化步骤都是为时间中断机制做好准备而已。在执行完init_IRQ()函数和sched_init()函数后CPU已经可以为IRQ0上的时钟中断进行服务了因为IRQ0所对应的中断门已经被设置好指向中断服务函数IRQ0x20_interrupt()。但是由于此时中断向量0x20的中断向量描述符irq_desc0还是处于初始状态其status成员的值为IRQ_DISABLED并未挂接任何具体的中断服务描述符因此这时CPU对IRQ0的中断服务并没有任何具体意义而只是按照规定的流程空跑一趟。但是当CPU执行完time_init()函数后情形就大不一样了。 函数time_init()主要做三件事1从RTC中获取内核启动时的时间与日期2在CPU有TSC的情况下校准TSC以便为后面使用TSC做好准备3在IRQ0的中断请求描述符中挂接具体的中断服务描述符。其源码如下所示arch/i386/kernel/time.c void __init time_init(void) { extern int x86_udelay_tsc; xtime.tv_sec  get_cmos_time(); xtime.tv_usec  0; /* * If we have APM enabled or the CPU clock speed is variable * (CPU stops clock on HLT or slows clock to save power) * then the TSC timestamps may diverge by up to 1 jiffy from * real time but nothing will break. * The most frequent case is that the CPU is woken from a halt * state by the timer interrupt itself, so we get 0 error. In the * rare cases where a driver would wake the CPU and request a * timestamp, the maximum error is  1 jiffy. But timestamps are * still perfectly ordered. * Note that the TSC counter will be reset if APM suspends * to disk; this wont break the kernel, though, cuz were * smart. See arch/i386/kernel/apm.c. */ /* * Firstly we have to do a CPU check for chips with * a potentially buggy TSC. At this point we havent run * the ident/bugs checks so we must run this hook as it * may turn off the TSC flag. * * NOTE: this doesnt yet handle SMP 486 machines where only * some CPUs have a TSC. Thats never worked and nobody has * moaned if you have the only one in the world - you fix it! */ dodgy_tsc(); if (cpu_has_tsc) { unsigned long tsc_quotient  calibrate_tsc(); if (tsc_quotient) { fast_gettimeoffset_quotient  tsc_quotient; use_tsc  1; /* * We could be more selective here I suspect * and just enable this for the next intel chips ? */ x86_udelay_tsc  1; #ifndef do_gettimeoffset do_gettimeoffset  do_fast_gettimeoffset; #endif do_get_fast_time  do_gettimeofday; /* report CPU clock rate in Hz. * The formula is (10^6 * 2^32) / (2^32 * 1 / (clocks/us))  * clock/second. Our precision is about 100 ppm. */ { unsigned long eax0, edx1000; __asm__(divl %2 :a (cpu_khz), d (edx) :r (tsc_quotient), 0 (eax), 1 (edx)); printk(Detected %lu.%03lu MHz processor.\n, cpu_khz / 1000, cpu_khz % 1000); } } } #ifdef CONFIG_VISWS printk(Starting Cobalt Timer system clock\n); /* Set the countdown value */ co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ); /* Start the timer */ co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN); /* Enable (unmask) the timer interrupt */ co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL)  ~CO_CTRL_TIMEMASK); /* Wire cpu IDT entry to s/w handler (and Cobalt APIC to IDT) */ setup_irq(CO_IRQ_TIMER, irq0); #else setup_irq(0, irq0); #endif } 对该函数的注解如下 1调用函数get_cmos_time()从RTC中得到系统启动时的时间与日期它返回的是当前时间相对于19700101 000000这个UNIX时间基准的秒数值。因此这个秒数值就被保存在系统全局变量xtime的tv_sec成员中。而xtime的另一个成员tv_usec则被初始化为0。 2通过dodgy_tsc()函数检测CPU是否存在时间戳记数器BUGI know nothing about it 3通过宏cpu_has_tsc来确定系统中CPU是否存在TSC计数器。如果存在TSC那么内核就可以用TSC来获得更为精确的时间。为了能够用TSC来修正内核时间。这里必须作一些初始化工作①调用calibrate_tsc()来确定TSC的每一次计数真正代表多长的时间间隔单位为us也即一个时钟周期的真正时间间隔长度。②将calibrate_tsc()函数所返回的值保存在全局变量fast_gettimeoffset_quotient中该变量被用来快速地计算时间偏差同时还将另一个全局变量use_tsc设置为1表示内核可以使用TSC。这两个变量都定义在arch/i386/kernel/time.c文件中如下 /* Cached *multiplier* to convert TSC counts to microseconds. * (see the equation below). * Equal to 2^32 * (1 / (clocks per usec) ). * Initialized in time_init. */ unsigned long fast_gettimeoffset_quotient; …… static int use_tsc; ③接下来将系统全局变量x86_udelay_tsc设置为1表示可以通过TSC来实现微妙级的精确延时。该变量定义在arch/i386/lib/delay.c文件中。④将函数指针do_gettimeoffset强制性地指向函数do_fast_gettimeoffset()与之对应的是do_slow_gettimeoffset()函数从而使内核在计算时间偏差时可以用TSC这种快速的方法来进行。⑤将函数指针do_get_fast_time指向函数do_gettimeofday()从而可以让其他内核模块通过do_gettimeofday()函数来获得更精准的当前时间。⑥计算并报告根据TSC所算得的CPU时钟频率。 4不考虑CONFIG_VISWS的情况因此time_init()的最后一个步骤就是调用setup_irq()函数来为IRQ0挂接具体的中断服务描述符irq0。全局变量irq0是时钟中断请求的中断服务描述符其定义如下arch/i386/kernel/time.c static struct irqaction irq0  { timer_interrupt, SA_INTERRUPT, 0, timer, NULL, NULL}; 显然函数timer_interrupt()将成为时钟中断的服务程序ISR而SA_INTERRUPT标志也指定了timer_interrupt()函数将是在CPU关中断的条件下执行的。结构irq0中的next指针被设置为NULL因此IRQ0所对应的中断服务队列中只有irq0这唯一的一个元素且IRQ0不允许中断共享。 742 时钟中断服务例程timer_interrupt() 中断服务描述符irq0一旦被钩挂到IRQ0的中断服务队列中去后Linux内核就可以通过irq0-handler函数指针所指向的timer_interrupt()函数对时钟中断请求进行真正的服务而不是向前面所说的那样只是让CPU“空跑”一趟。此时Linux内核可以说是真正的“跳动”起来了。 在本节一开始所述的对时钟中断驱动的5项要求中通常只有第一项即timekeeping是最为迫切的因此必须在时钟中断服务例程中完成。而其余的几个要求可以稍缓因此可以放在时钟中断的Bottom Half中去执行。这样Linux内核就是timer_interrupt()函数的执行时间尽可能的短因为它是在CPU关中断的条件下执行的。 函数timer_interrupt()的源码如下arch/i386/kernel/time.c /* * This is the same as the above, except we _also_ save the current * Time Stamp Counter value at the time of the timer interrupt, so that * we later on can estimate the time of day more exactly. */ static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) { int count; /* * Here we are in the timer irq handler. We just have irqs locally * disabled but we dont know if the timer_bh is running on the other * CPU. We need to avoid to SMP race with it. NOTE: we don t need * the irq version of write_lock because as just said we have irq * locally disabled. -arca */ write_lock(xtime_lock); if (use_tsc) { /* * It is important that these two operations happen almost at * the same time. We do the RDTSC stuff first, since its * faster. To avoid any inconsistencies, we need interrupts * disabled locally. */ /* * Interrupts are just disabled locally since the timer irq * has the SA_INTERRUPT flag set. -arca */ /* read Pentium cycle counter */ rdtscl(last_tsc_low); spin_lock(i8253_lock); outb_p(0x00, 0x43); /* latch the count ASAP */ count  inb_p(0x40); /* read the latched count */ count | inb(0x40)  8; spin_unlock(i8253_lock); count  ((LATCH-1) - count) * TICK_SIZE; delay_at_last_interrupt  (count  LATCH/2) / LATCH; } do_timer_interrupt(irq, NULL, regs); write_unlock(xtime_lock); } 对该函数的注释如下 1由于函数执行期间要访问全局时间变量xtime因此一开就对自旋锁xtime_lock进行加锁。 2如果内核使用CPU的TSC寄存器use_tsc变量非0那么通过TSC寄存器来计算从时间中断的产生到timer_interrupt函数真正在CPU上执行这之间的时间延迟 l 调用宏rdtscl()将64位的TSC寄存器值中的低32位LSB读到变量last_tsc_low中以供do_fast_gettimeoffset()函数计算时间偏差之用。这一步的实质就是将CPU TSC寄存器的值更新到内核对TSC的缓存变量last_tsc_low中。 l 通过读8254 PIT的通道0的计数器的当前值来计算时间延迟为此首先对自旋锁i8253_lock进行加锁。自旋锁i8253_lock的作用就是用来串行化对8254 PIT的读写访问。其次向8254的控制寄存器端口0x43中写入值0x00以便对通道0的计数器进行锁存。最后通过端口0x40将通道0的计数器的当前值读到局部变量count中并解锁i8253_lock。 l 显然从时间中断的产生到timer_interrupt()函数真正执行这段时间内以一共流逝了LATCH-1-count个时钟周期因此这个延时长度可以用如下公式计算 delay_at_last_interruptLATCH-1count÷LATCHTICK_SIZE 显然上述公式的结果是个小数应对其进行四舍五入为此Linux用下述表达式来计算delay_at_last_interrupt变量的值 LATCH-1-countTICK_SIZELATCH/2LATCH 上述被除数表达式中的LATCH2就是用来将结果向上圆整成整数的。 3在计算出时间延迟后最后调用函数do_timer_interrupt()执行真正的时钟服务。 函数do_timer_interrupt()的源码如下arch/i386/kernel/time.c /* * timer_interrupt() needs to keep up the real-time clock, * as well as call the do_timer() routine every clocktick */ static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) { 。。。。。。 do_timer(regs); 。。。。。。。 /* * If we have an externally synchronized Linux clock, then update * CMOS clock accordingly every ~11 minutes. Set_rtc_mmss() has to be * called as close as possible to 500 ms before the new second starts. */ if ((time_status  STA_UNSYNC)  0  xtime.tv_sec  last_rtc_update  660  xtime.tv_usec  500000 - ((unsigned) tick) / 2  xtime.tv_usec  500000  ((unsigned) tick) / 2) { if (set_rtc_mmss(xtime.tv_sec)  0) last_rtc_update  xtime.tv_sec; else last_rtc_update  xtime.tv_sec - 600; /* do it again in 60 s */ } …… } 上述代码中省略了许多与SMP相关的代码因为我们不关心SMP。从上述代码我们可以看出do_timer_interrupt()函数主要作两件事 1调用do_timer()函数。 2判断是否需要更新CMOS时钟即RTC中的时间。Linux仅在下列三个条件同时成立时才更新CMOS时钟①系统全局时间状态变量time_status中没有设置STA_UNSYNC标志也即说明Linux有一个外部同步时钟。实际上全局时间状态变量time_status仅在一种情况下会被清除STA_SYNC标志那就是执行adjtimex()系统调用时这个syscall与NTP有关。②自从上次CMOS时钟更新已经过去了11分钟。全局变量last_rtc_update保存着上次更新CMOS时钟的时间。③由于RTC存在Update Cycle因此最好在一秒时间间隔的中间位置500ms左右调用set_rtc_mmss()函数来更新CMOS时钟。因此Linux规定仅当全局变量xtime的微秒数tv_usec在500000±tick/2微秒范围范围之内时才调用set_rtc_mmss()函数。如果上述条件均成立那就调用set_rtc_mmss()将当前时间xtime.tv_sec更新回写到RTC中。 如果上面是的set_rtc_mmss()函数返回0值则表明更新成功。于是就将“最近一次RTC更新时间”变量last_rtc_update更新为当前时间xtime.tv_sec。如果返回非0值说明更新失败于是就让last_rtc_updatextime.tv_sec-600相当于last_rtc_update60以便在在60秒之后再次对RTC进行更新。 函数do_timer()实现在kernel/timer.c文件中其源码如下 void do_timer(struct pt_regs *regs) { (*(unsigned long *)jiffies); #ifndef CONFIG_SMP /* SMP process accounting uses the local APIC timer */ update_process_times(user_mode(regs)); #endif mark_bh(TIMER_BH); if (TQ_ACTIVE(tq_timer)) mark_bh(TQUEUE_BH); } 该函数的核心是完成三个任务 1将表示自系统启动以来的时钟滴答计数变量jiffies加1。 2调用update_process_times()函数更新当前进程的时间统计信息。注意该函数的参数原型是“int user_tick”如果本次时钟中断即时钟滴答发生时CPU正处于用户态下执行则user_tick参数应该为1否则如果本次时钟中断发生时CPU正处于核心态下执行时则user_tick参数应改为0。所以这里我们以宏user_mode(regs)来作为update_process_times()函数的调用参数。该宏定义在include/asm-i386/ptrace.h头文件中它根据regs指针所指向的核心堆栈寄存器结构来判断CPU进入中断服务之前是处于用户态下还是处于核心态下。如下所示 #ifdef __KERNEL__ #define user_mode(regs) ((VM_MASK  (regs)-eflags) || (3  (regs)-xcs)) …… #endif 3调用mark_bh()函数激活时钟中断的Bottom Half向量TIMER_BH和TQUEUE_BH注意TQUEUE_BH仅在任务队列tq_timer不为空的情况下才会被激活。 至此内核对时钟中断的服务流程宣告结束下面我们详细分析一下update_process_times()函数的实现。 743 更新时间记帐信息——CPU分时的实现 函数update_process_times()被用来在发生时钟中断时更新当前进程以及内核中与时间相关的统计信息并根据这些信息作出相应的动作比如重新进行调度向当前进程发出信号等。该函数仅有一个参数user_tick取值为1或0其含义在前面已经叙述过。 该函数的源代码如下kernel/timer.c /* * Called from the timer interrupt handler to charge one tick to the current * process. user_tick is 1 if the tick is user time, 0 for system. */ void update_process_times(int user_tick) { struct task_struct *p  current; int cpu  smp_processor_id(), system  user_tick ^ 1; update_one_process(p, user_tick, system, cpu); if (p-pid) { if (--p-counter  0) { p-counter  0; p-need_resched  1; } if (p-nice  0) kstat.per_cpu_nice[cpu]  user_tick; else kstat.per_cpu_user[cpu]  user_tick; kstat.per_cpu_system[cpu]  system; } else if (local_bh_count(cpu) || local_irq_count(cpu)  1) kstat.per_cpu_system[cpu]  system; } 1首先用smp_processor_id()宏得到当前进程的CPU ID。 2然后让局部变量systemuser_tick^1表示当发生时钟中断时CPU是否正处于核心态下。因此如果user_tick1则system0如果user_tick0则system1。 3调用update_one_process()函数来更新当前进程的task_struct结构中的所有与时间相关的统计信息以及成员变量。该函数还会视需要向当前进程发送相应的信号signal。 4如果当前进程的PID非0则执行下列步骤来决定是否重新进行调度并更新内核时间统计信息 l 将当前进程的可运行时间片长度由task_struct结构中的counter成员表示其单位是时钟滴答次数减1。如果减到0值则说明当前进程已经用完了系统分配给它的的运行时间片因此必须重新进行调度。于是将当前进程的task_struct结构中的need_resched成员变量设置为1表示需要重新执行调度。 l 如果当前进程的task_struct结构中的nice成员值大于0那么将内核全局统计信息变量kstat中的per_cpu_nicecpu值将上user_tick。否则就将user_tick值加到内核全局统计信息变量kstat中的per_cpu_usercpu成员上。 l 将system变量值加到内核全局统计信息kstat.per_cpu_systemcpu上。 5否则就判断当前CPU在服务时钟中断前是否处于softirq软中断服务的执行中或则正在服务一次低优先级别的硬件中断中。如果是这样的话则将system变量的值加到内核全局统计信息kstat.per_cpu.systemcpu上。 l update_one_process()函数 实现在kernel/timer.c文件中的update_one_process()函数用来在时钟中断发生时更新一个进程的task_struc结构中的时间统计信息。其源码如下kernel/timer.c void update_one_process(struct task_struct *p, unsigned long user, unsigned long system, int cpu) { p-per_cpu_utime[cpu]  user; p-per_cpu_stime[cpu]  system; do_process_times(p, user, system); do_it_virt(p, user); do_it_prof(p); } 注释如下 1由于在一个进程的整个生命期Lifetime中它可能会在不同的CPU上执行也即一个进程可能一开始在CPU1上执行当它用完在CPU1上的运行时间片后它可能又会被调度到CPU2上去执行。另外当进程在某个CPU上执行时它可能又会在用户态和内核态下分别各执行一段时间。所以为了统计这些事件信息进程task_struct结构中的per_cpu_utimeNR_CPUS数组就表示该进程在各CPU的用户台下执行的累计时间长度per_cpu_stimeNR_CPUS数组就表示该进程在各CPU的核心态下执行的累计时间长度它们都以时钟滴答次数为单位。 所以update_one_process()函数的第一个步骤就是更新进程在当前CPU上的用户态执行时间统计per_cpu_utimecpu和核心态执行时间统计per_cpu_stimecpu。 2调用do_process_times()函数更新当前进程的总时间统计信息。 3调用do_it_virt()函数为当前进程的ITIMER_VIRTUAL软件定时器更新时间间隔。 4调用do_it_prof函数为当前进程的ITIMER_PROF软件定时器更新时间间隔。 l do_process_times()函数 函数do_process_times()将更新指定进程的总时间统计信息。每个进程task_struct结构中都有一个成员times它是一个tms结构类型include/linux/times.h struct tms { clock_t tms_utime;  本进程在用户台下的执行时间总和  clock_t tms_stime;  本进程在核心态下的执行时间总和  clock_t tms_cutime;  所有子进程在用户态下的执行时间总和  clock_t tms_cstime;  所有子进程在核心态下的执行时间总和  }; 上述结构的所有成员都以时钟滴答次数为单位。 函数do_process_times()的源码如下kernel/timer.c static inline void do_process_times(struct task_struct *p, unsigned long user, unsigned long system) { unsigned long psecs; psecs  (p-times.tms_utime  user); psecs  (p-times.tms_stime  system); if (psecs / HZ  p-rlim[RLIMIT_CPU].rlim_cur) { /* Send SIGXCPU every second.. */ if (!(psecs % HZ)) send_sig(SIGXCPU, p, 1); /* and SIGKILL when we go over max.. */ if (psecs / HZ  p-rlim[RLIMIT_CPU].rlim_max) send_sig(SIGKILL, p, 1); } } 注释如下 1根据参数user更新指定进程task_struct结构中的times.tms_utime值。根据参数system更新指定进程task_struct结构中的times.tms_stime值。 2将更新后的times.tms_utime值与times.tms_stime值的和保存到局部变量psecs中因此psecs就表示了指定进程p到目前为止已经运行的总时间长度以时钟滴答次数计。如果这一总运行时间长超过进程P的资源限额那就每隔1秒给进程发送一个信号SIGXCPU如果运行时间长度超过了进程资源限额的最大值那就发送一个SIGKILL信号杀死该进程。 l do_it_virt()函数 每个进程都有一个用户态执行时间的itimer软件定时器。进程任务结构task_struct中的it_virt_value成员是这个软件定时器的时间计数器。当进程在用户态下执行时每一次时钟滴答都使计数器it_virt_value减1当减到0时内核向进程发送SIGVTALRM信号并重置初值。初值保存在进程的task_struct结构的it_virt_incr成员中。 函数do_it_virt()的源码如下kernel/timer.c static inline void do_it_virt(struct task_struct * p, unsigned long ticks) { unsigned long it_virt  p-it_virt_value; if (it_virt) { it_virt - ticks; if (!it_virt) { it_virt  p-it_virt_incr; send_sig(SIGVTALRM, p, 1); } p-it_virt_value  it_virt; } } l do_it_prof函数 类似地每个进程也都有一个itimer软件定时器ITIMER_PROF。进程task_struct中的it_prof_value成员就是这个定时器的时间计数器。不管进程是在用户态下还是在内核态下运行每个时钟滴答都使it_prof_value减1。当减到0时内核就向进程发送SIGPROF信号并重置初值。初值保存在进程task_struct结构中的it_prof_incr成员中。 函数do_it_prof()就是用来完成上述功能的其源码如下kernel/timer.c static inline void do_it_prof(struct task_struct *p) { unsigned long it_prof  p-it_prof_value; if (it_prof) { if (--it_prof  0) { it_prof  p-it_prof_incr; send_sig(SIGPROF, p, 1); } p-it_prof_value  it_prof; } }dreamice 回复于2008-11-06 17:57:1075 时钟中断的Bottom Half 与时钟中断相关的Bottom Half向两主要有两个TIMER_BH和TQUEUE_BH。与TIMER_BH相对应的BH函数是timer_bh()与TQUEUE_BH对应的函数是tqueue_bh()。它们均实现在kernel/timer.c文件中。 751 TQUEUE_BH向量 TQUEUE_BH的作用是用来运行tq_timer这个任务队列中的任务。因此do_timer()函数仅仅在tq_timer任务队列不为空的情况才激活TQUEUE_BH向量。函数tqueue_bh()的实现非常简单它只是简单地调用run_task_queue()函数来运行任务队列tq_timer。如下所示 void tqueue_bh(void) { run_task_queue(tq_timer); } 任务对列tq_timer也是定义在kernel/timer.c文件中如下所示 DECLARE_TASK_QUEUE(tq_timer); 752 TIMER_BH向量 TIMER_BH这个Bottom Half向量是Linux内核时钟中断驱动的一个重要辅助部分。内核在每一次对时钟中断的服务快要结束时都会无条件地激活一个TIMER_BH向量以使得内核在稍后一段延迟后执行相应的BH函数——timer_bh()。该任务的源码如下 void timer_bh(void) { update_times(); run_timer_list(); } 从上述源码可以看出内核在时钟中断驱动的底半部分主要有两个任务1调用update_times()函数来更新系统全局时间xtime2调用run_timer_list()函数来执行定时器。关于定时器我们将在下一节讨论。本节我们主要讨论TIMER_BH的第一个任务——对内核时间xtime的更新。 我们都知道内核局部时间xtime是用来供用户程序通过时间syscall来检索或设置当前系统时间的而内核代码在大多数情况下都引用jiffies变量而很少使用xtime偶尔也会有引用xtime的情况比如更新inode的时间标记。因此对于时钟中断服务程序timer_interrupt而言jiffies变量的更新是最紧迫的而xtime的更新则可以延迟到中断服务的底半部分来进行。 由于Bottom Half机制在执行时间具有某些不确定性因此在timer_bh()函数得到真正执行之前期间可能又会有几次时钟中断发生。这样就会造成时钟滴答的丢失现象。为了处理这种情况Linux内核使用了一个辅助全局变量wall_jiffies来表示上一次更新xtime时的jiffies值。其定义如下kernel/timer.c /* jiffies at the most recent update of wall time */ unsigned long wall_jiffies; 而timer_bh()函数真正执行时的jiffies值与wall_jiffies的差就是在timer_bh()真正执行之前所发生的时钟中断次数。 函数update_times()的源码如下kernel/timer.c static inline void update_times(void) { unsigned long ticks; /* * update_times() is run from the raw timer_bh handler so we * just know that the irqs are locally enabled and so we dont * need to save/restore the flags of the local CPU here. -arca */ write_lock_irq(xtime_lock); ticks  jiffies - wall_jiffies; if (ticks) { wall_jiffies  ticks; update_wall_time(ticks); } write_unlock_irq(xtime_lock); calc_load(ticks); } 1首先根据jiffies和wall_jiffies的差值计算在此之前一共发生了几次时钟滴答并将这个值保存到局部变量ticks中。并在ticks值大于0的情况下ticks大于等于1一般情况下为1①更新wall_jiffies为jiffies变量的当前值wall_jiffiesticks等价于wall_jiffiesjiffies。②以参数ticks调用update_wall_time()函数去真正地更新全局时间xtime。 2调用calc_load()函数去计算系统负载情况。这里我们不去深究它。 函数update_wall_time()函数根据参数ticks所指定的时钟滴答次数相应地更新内核全局时间变量xtime。其源码如下kernel/timer.c /* * Using a loop looks inefficient, but ticks is * usually just one (we shouldnt be losing ticks, * were doing this this way mainly for interrupt * latency reasons, not because we think well * have lots of lost timer ticks */ static void update_wall_time(unsigned long ticks) { do { ticks--; update_wall_time_one_tick(); } while (ticks); if (xtime.tv_usec  1000000) { xtime.tv_usec - 1000000; xtime.tv_sec; second_overflow(); } } 对该函数的注释如下 1首先用一个do{}循环来根据参数ticks的值一次一次调用update_wall_time_one_tick()函数来为一次时钟滴答更新xtime中的tv_usec成员。 2根据需要调整xtime中的秒数成员tv_usec和微秒数成员tv_usec。如果微秒数成员tv_usec的值超过106则说明已经过了一秒钟。因此将tv_usec的值减去1000000并将秒数成员tv_sec的值加1然后调用second_overflow函数来处理微秒数成员溢出的情况。 函数update_wall_time_one_tick用来更新一次时钟滴答对系统全局时间xtime的影响。由于tick全局变量表示了一次时钟滴答的时间间隔长度以us为单位因此该函数的实现中最核心的代码就是将xtime的tv_usec成员增加tick微秒。这里我们不去关心函数实现中与NTPNetwork Time Protocol和系统调用adjtimex的相关部分。其源码如下kernel/timer.c /* in the NTP reference this is called hardclock() */ static void update_wall_time_one_tick(void) { if ( (time_adjust_step  time_adjust) ! 0 ) { /* We are doing an adjtime thing. * * Prepare time_adjust_step to be within bounds. * Note that a positive time_adjust means we want the clock * to run faster. * * Limit the amount of the step to be in the range * -tickadj .. tickadj */ if (time_adjust  tickadj) time_adjust_step  tickadj; else if (time_adjust  -tickadj) time_adjust_step  -tickadj; /* Reduce by this step the amount of time left */ time_adjust - time_adjust_step; } xtime.tv_usec  tick  time_adjust_step; /* * Advance the phase, once it gets to one microsecond, then * advance the tick more. */ time_phase  time_adj; if (time_phase  -FINEUSEC) { long ltemp  -time_phase  SHIFT_SCALE; time_phase  ltemp  SHIFT_SCALE; xtime.tv_usec - ltemp; } else if (time_phase  FINEUSEC) { long ltemp  time_phase  SHIFT_SCALE; time_phase - ltemp  SHIFT_SCALE; xtime.tv_usec  ltemp; } }dreamice 回复于2008-11-06 17:57:3376 内核定时器机制 Linux内核2.4版中去掉了老版本内核中的静态定时器机制而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的动态定时器是指内核的定时器队列是可以动态变化的然而就定时器本身而言二者并无本质的区别。考虑到静态定时器机制的能力有限因此Linux内核2.4版中完全去掉了以前的静态定时器机制。 761 Linux内核对定时器的描述 Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器 struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); }; 各数据成员的含义如下 1双向链表元素list用来将多个定时器连接成一条双向循环队列。 2expires指定定时器到期的时间这个时间被表示成自系统启动以来的时钟滴答计数也即时钟节拍数。当一个定时器的expires值小于或等于jiffies变量时我们就说这个定时器已经超时或到期了。在初始化一个定时器后通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值以时钟滴答次数计。 3函数指针function指向一个可执行函数。当定时器到期时内核就执行function所指定的函数。而data域则被内核用作function函数的调用参数。 内核函数init_timer()用来初始化一个定时器。实际上这个初始化函数仅仅将结构中的list成员初始化为空。如下所示include/linux/timer.h static inline void init_timer(struct timer_list * timer) { timer-list.next  timer-list.prev  NULL; } 由于定时器通常被连接在一个双向循环队列中等待执行此时我们说定时器处于pending状态。因此函数time_pending()就可以用list成员是否为空来判断一个定时器是否处于pending状态。如下所示include/linux/timer.h static inline int timer_pending (const struct timer_list * timer) { return timer-list.next ! NULL; } l 时间比较操作 在定时器应用中经常需要比较两个时间值以确定timer是否超时所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏include/linux/timer.h #define time_after(a,b) ((long)(b) - (long)(a)  0) #define time_before(a,b) time_after(b,a) #define time_after_eq(a,b) ((long)(a) - (long)(b)  0) #define time_before_eq(a,b) time_after_eq(b,a) 762 动态内核定时器机制的原理 Linux是怎样为其内核定时器机制提供动态扩展能力的呢其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列对列中的每一个元素都是一个timer_list结构对列中的所有定时器都在同一个时刻到期也即对列中的每一个timer_list结构都具有相同的expires值。显然可以用一个timer_list结构类型的指针来表示一个定时器向量。 显然定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义内核将至少要维护0xffffffff个timer_list结构类型的指针这显然是不现实的。 另一方面从内核本身这个角度看它所关心的定时器显然不是那些已经过期而被执行过的定时器这些定时器完全可以被丢弃也不是那些要经过很长时间才会到期的定时器而是那些当前已经到期或者马上就要到期的定时器注意时间间隔是以滴答次数为计数单位的。 基于上述考虑并假定一个定时器要经过interval个时钟滴答后才到期intervalexpiresjiffies则Linux采用了下列思想来实现其动态内核定时器机制对于那些0≤interval≤255的定时器Linux严格按照定时器向量的基本语义来组织这些定时器也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器由于他们离到期还有一段时间因此内核并不关心他们而是将它们以一种扩展的定时器向量语义或称为“松散的定时器向量语义”进行组织。所谓“松散的定时器向量语义”就是指各定时器的expires值可以互不相同的一个定时器队列。 具体的组织方案可以分为两大部分 1对于内核最关心的、interval值在0255之间的前256个定时器向量内核是这样组织它们的这256个定时器向量被组织在一起组成一个定时器向量数组并作为数据结构timer_vec_root的一部分该数据结构定义在kernel/timer.c文件中如下述代码段所示 /* * Event timer code */ #define TVN_BITS 6 #define TVR_BITS 8 #define TVN_SIZE (1  TVN_BITS) #define TVR_SIZE (1  TVR_BITS) #define TVN_MASK (TVN_SIZE - 1) #define TVR_MASK (TVR_SIZE - 1) struct timer_vec { int index; struct list_head vec[TVN_SIZE]; }; struct timer_vec_root { int index; struct list_head vec[TVR_SIZE]; }; static struct timer_vec tv5; static struct timer_vec tv4; static struct timer_vec tv3; static struct timer_vec tv2; static struct timer_vec_root tv1; static struct timer_vec * const tvecs[]  { (struct timer_vec *)tv1, tv2, tv3, tv4, tv5 }; #define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0])) 基于数据结构timer_vec_rootLinux定义了一个全局变量tv1以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时它就只从定时器向量数组tv1.vec256中的某个定时器向量内进行扫描。而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec256中的哪一个定时器向量也即该数组的索引其初值为0最大值为255以256为模。每个时钟节拍时index字段都会加1。显然index字段所指定的定时器向量tv1.vecindex中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vecindexk则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。 2而对于内核不关心的、interval值在0xff0xffffffff之间的定时器它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常定时器的interval值越小它所处的定时器向量的松散度也就越低也即向量中的各定时器的expires值相差越小而interval值越大它所处的定时器向量的松散度也就越大也即向量中的各定时器的expires值相差越大。 内核规定对于那些满足条件0x100≤interval≤0x3fff的定时器只要表达式interval8具有相同值的定时器都将被组织在同一个松散定时器向量中。因此为组织所有满足条件0x100≤interval≤0x3fff的定时器就需要2664个松散定时器向量。同样地为方便起见这64个松散定时器向量也放在一起形成数组并作为数据结构timer_vec的一部分。基于数据结构timer_vecLinux定义了全局变量tv2来表示这64条松散定时器向量。如上述代码段所示。 对于那些满足条件0x4000≤interval≤0xfffff的定时器只要表达式interval86的值相同的定时器都将被放在同一个松散定时器向量中。同样要组织所有满足条件0x4000≤interval≤0xfffff的定时器也需要2664个松散定时器向量。类似地这64个松散定时器向量也可以用一个timer_vec结构来描述相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。 对于那些满足条件0x100000≤interval≤0x3ffffff的定时器只要表达式interval866的值相同的定时器都将被放在同一个松散定时器向量中。同样要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器也需要2664个松散定时器向量。类似地这64个松散定时器向量也可以用一个timer_vec结构来描述相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。 对于那些满足条件0x4000000≤interval≤0xffffffff的定时器只要表达式interval8666的值相同的定时器都将被放在同一个松散定时器向量中。同样要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器也需要2664个松散定时器向量。类似地这64个松散定时器向量也可以用一个timer_vec结构来描述相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。 最后为了引用方便Linux定义了一个指针数组tvecs来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。 整个内核定时器机制的总体结构如下图78所示 763 内核动态定时器机制的实现 在内核动态定时器机制的实现中有三个操作时非常重要的1将一个定时器插入到它应该所处的定时器向量中。2定时器的迁移也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。3扫描并执行当前已经到期的定时器。 7631 动态定时器机制的初始化 函数init_timervecs()实现对动态定时器机制的初始化。该函数仅被sched_init()初始化例程所调用。动态定时器机制初始化过程的主要任务就是将tv1、tv2、…、tv5这5个结构变量中的定时器向量指针数组vec初始化为NULL。如下所示kernel/timer.c void init_timervecs (void) { int i; for (i  0; i  TVN_SIZE; i) { INIT_LIST_HEAD(tv5.vec  i); INIT_LIST_HEAD(tv4.vec  i); INIT_LIST_HEAD(tv3.vec  i); INIT_LIST_HEAD(tv2.vec  i); } for (i  0; i  TVR_SIZE; i) INIT_LIST_HEAD(tv1.vec  i); } 上述函数中的宏TVN_SIZE是指timer_vec结构类型中的定时器向量指针数组vec的大小值为64。宏TVR_SIZE是指timer_vec_root结构类型中的定时器向量数组vec的大小值为256。 7632 动态定时器的时钟滴答基准timer_jiffies 由于动态定时器是在时钟中断的Bottom Half中被执行的而从TIMER_BH向量被激活到其timer_bh()函数真正执行这段时间内可能会有几次时钟中断发生。因此内核必须记住上一次运行定时器机制是什么时候也即内核必须保存上一次运行定时器机制时的jiffies值。为此Linux在kernel/timer.c文件中定义了全局变量timer_jiffies来表示上一次运行定时器机制时的jiffies值。该变量的定义如下所示 static unsigned long timer_jiffies; 7633 对内核动态定时器链表的保护 由于内核动态定时器链表是一种系统全局共享资源为了实现对它的互斥访问Linux定义了专门的自旋锁timerlist_lock来保护。任何想要访问动态定时器链表的代码段都首先必须先持有该自旋锁并且在访问结束后释放该自旋锁。其定义如下kernel/timer.c /* Initialize both explicitly - lets try to have them in the same cache line */ spinlock_t timerlist_lock  SPIN_LOCK_UNLOCKED; 7634 将一个定时器插入到链表中 函数add_timer()用来将参数timer指针所指向的定时器插入到一个合适的定时器链表中。它首先调用timer_pending()函数判断所指定的定时器是否已经位于在某个定时器向量中等待执行。如果是则不进行任何操作只是打印一条内核告警信息就返回了如果不是则调用internal_add_timer()函数完成实际的插入操作。其源码如下kernel/timer.c void add_timer(struct timer_list *timer) { unsigned long flags; spin_lock_irqsave(timerlist_lock, flags); if (timer_pending(timer)) goto bug; internal_add_timer(timer); spin_unlock_irqrestore(timerlist_lock, flags); return; bug: spin_unlock_irqrestore(timerlist_lock, flags); printk(bug: kernel timer added twice at %p.\n, __builtin_return_address(0)); } 函数internal_add_timer()用于将一个不处于任何定时器向量中的定时器插入到它应该所处的定时器向量中去根据定时器的expires值来决定。如下所示kernel/timer.c static inline void internal_add_timer(struct timer_list *timer) { /* * must be cli-ed when calling this */ unsigned long expires  timer-expires; unsigned long idx  expires - timer_jiffies; struct list_head * vec; if (idx  TVR_SIZE) { int i  expires  TVR_MASK; vec  tv1.vec  i; } else if (idx  1  (TVR_BITS  TVN_BITS)) { int i  (expires  TVR_BITS)  TVN_MASK; vec  tv2.vec  i; } else if (idx  1  (TVR_BITS  2 * TVN_BITS)) { int i  (expires  (TVR_BITS  TVN_BITS))  TVN_MASK; vec  tv3.vec  i; } else if (idx  1  (TVR_BITS  3 * TVN_BITS)) { int i  (expires  (TVR_BITS  2 * TVN_BITS))  TVN_MASK; vec  tv4.vec  i; } else if ((signed long) idx  0) { /* can happen if you add a timer with expires  jiffies, * or you set a timer to go off in the past */ vec  tv1.vec  tv1.index; } else if (idx  0xffffffffUL) { int i  (expires  (TVR_BITS  3 * TVN_BITS))  TVN_MASK; vec  tv5.vec  i; } else { /* Can only get here on architectures with 64-bit jiffies */ INIT_LIST_HEAD(timer-list); return; } /* * Timers are FIFO! */ list_add(timer-list, vec-prev); } 对该函数的注释如下 1首先计算定时器的expires值与timer_jiffies的插值注意这里应该使用动态定时器自己的时间基准这个差值就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量idx保存这个差值。 2根据idx的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在7.6.2节已经说过了这里不再详述。最后定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部。 3最后调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。 7635 修改一个定时器的expires值 当一个定时器已经被插入到内核动态定时器链表中后我们还可以修改该定时器的expires值。函数mod_timer()实现这一点。如下所示kernel/timer.c int mod_timer(struct timer_list *timer, unsigned long expires) { int ret; unsigned long flags; spin_lock_irqsave(timerlist_lock, flags); timer-expires  expires; ret  detach_timer(timer); internal_add_timer(timer); spin_unlock_irqrestore(timerlist_lock, flags); return ret; } 该函数首先根据参数expires值更新定时器的expires成员。然后调用detach_timer()函数将该定时器从它原来所属的链表中删除。最后调用internal_add_timer()函数将该定时器根据它新的expires值重新插入到相应的链表中。 函数detach_timer()首先调用timer_pending()来判断指定的定时器是否已经处于某个链表中如果定时器原来就不处于任何链表中则detach_timer()函数什么也不做直接返回0值表示失败。否则就调用list_del()函数将定时器从它原来所处的链表中摘除。如下所示kernel/timer.c static inline int detach_timer (struct timer_list *timer) { if (!timer_pending(timer)) return 0; list_del(timer-list); return 1; } 7636 删除一个定时器 函数del_timer()用来将一个定时器从相应的内核定时器队列中删除。该函数实际上是对detach_timer()函数的高层封装。如下所示kernel/timer.c int del_timer(struct timer_list * timer) { int ret; unsigned long flags; spin_lock_irqsave(timerlist_lock, flags); ret  detach_timer(timer); timer-list.next  timer-list.prev  NULL; spin_unlock_irqrestore(timerlist_lock, flags); return ret; } 7637 定时器迁移操作 由于一个定时器的interval值会随着时间的不断流逝即jiffies值的不断增大而不断变小因此那些原本到期紧迫程度较低的定时器会随着jiffies值的不断增大而成为既将马上到期的定时器。比如定时器向量tv2.vec[0]中的定时器在经过256个时钟滴答后会成为未来256个时钟滴答内会到期的定时器。因此定时器在内核动态定时器链表中的位置也应相应地随着改变。改变的规则是当tv1.index重新变为0时意味着tv1中的256个定时器向量都已被内核扫描一遍了从而使tv1中的256个定时器向量变为空则用tv2.vecindex定时器向量中的定时器去填充tv1同时使tv2.index加1它以64为模。当tv2.index重新变为0意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了从而使得tv2变为空则用tv3.vecindex定时器向量中的定时器去填充tv2。如此一直类推下去直到tv5。 函数cascade_timers()完成这种定时器迁移操作该函数只有一个timer_vec结构类型指针的参数tv。这个函数将把定时器向量tv-vectv-index中的所有定时器重新填充到上一层定时器向量中去。如下所示kernel/timer.c static inline void cascade_timers(struct timer_vec *tv) { /* cascade all the timers from tv up one level */ struct list_head *head, *curr, *next; head  tv-vec  tv-index; curr  head-next; /* * We are removing _all_ timers from the list, so we dont have to * detach them individually, just clear the list afterwards. */ while (curr ! head) { struct timer_list *tmp; tmp  list_entry(curr, struct timer_list, list); next  curr-next; list_del(curr); // not needed internal_add_timer(tmp); curr  next; } INIT_LIST_HEAD(head); tv-index  (tv-index  1)  TVN_MASK; } 对该函数的注释如下 1首先用指针head指向定时器头部向量头部的list_head结构。指针curr指向定时器向量中的第一个定时器。 2然后用一个while{}循环来遍历定时器向量tv-vectv-index。由于定时器向量是一个双向循环队列因此循环的终止条件是currhead。对于每一个被扫描的定时器循环体都先调用list_del()函数将当前定时器从链表中摘除然后调用internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。 3当从while{}循环退出后定时器向量tv-vectv-index中所有的定时器都已被迁移到其它地方到它们该呆的地方因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来将定时器向量的表头结构初始化为空。 4最后将tv-index值加1当然它是以64为模。 7648 扫描并执行当前已经到期的定时器 函数run_timer_list()完成这个功能。如前所述该函数是被timer_bh()函数所调用的因此内核定时器是在时钟中断的Bottom Half中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函数的时间因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来期间一共发生了多少次时钟中断显然run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下kernel/timer.c static inline void run_timer_list(void) { spin_lock_irq(timerlist_lock); while ((long)(jiffies - timer_jiffies)  0) { struct list_head *head, *curr; if (!tv1.index) { int n  1; do { cascade_timers(tvecs[n]); } while (tvecs[n]-index  1  n  NOOF_TVECS); } repeat: head  tv1.vec  tv1.index; curr  head-next; if (curr ! head) { struct timer_list *timer; void (*fn)(unsigned long); unsigned long data; timer  list_entry(curr, struct timer_list, list); fn  timer-function; data timer-data; detach_timer(timer); timer-list.next  timer-list.prev  NULL; timer_enter(timer); spin_unlock_irq(timerlist_lock); fn(data); spin_lock_irq(timerlist_lock); timer_exit(); goto repeat; } timer_jiffies; tv1.index  (tv1.index  1)  TVR_MASK; } spin_unlock_irq(timerlist_lock); } 函数run_timer_list()的执行过程主要就是用一个大while{}循环来为时钟中断执行定时器服务每一次循环服务一次时钟中断。因此一共要执行jiffiestimer_jiffies1次循环。循环体所执行的服务步骤如下 1首先判断tv1.index是否为0如果为0则需要从tv2中补充定时器到tv1中来。但tv2也可能为空而需要从tv3中补充定时器因此用一个do{}while循环来调用cascade_timer()函数来依次视需要从tv2中补充tv1从tv3中补充tv2、…、从tv5中补充tv4。显然如果tvi.index02≤i≤5则对于tvi执行cascade_timers()函数后tvi.index肯定为1。反过来讲如果对tvi执行过cascade_timers()函数后tvi.index不等于1那么可以肯定在未对tvi执行cascade_timers()函数之前tvi.index值肯定不为0因此这时tvi不需要从tv(i1)中补充定时器这时就可以终止do{}while循环。 2接下来就要执行定时器向量tv1.vectv1.index中的所有到期定时器。因此这里用一个goto repeat循环从头到尾依次扫描整个定时器对列。由于在执行定时器的关联函数时并不需要关CPU中断所以在用detach_timer()函数将当前定时器从对列中摘除后就可以调用spin_unlock_irq()函数进行解锁和开中断然后在执行完当前定时器的关联函数后重新用spin_lock_irq函数加锁和关中断。 3当执行完定时器向量tv1.vec[tv1.index]中的所有到期定时器后tv1.vectv1.index应该是个空队列。至此这一次定时器服务也就宣告结束。 4最后将timer_jiffies值加1将tv1.index值加1当然它的模是256。然后回到while循环开始下一次定时器服务。dreamice 回复于2008-11-06 17:57:5177 进程间隔定时器itimer 所谓“间隔定时器Interval Timer简称itimer就是指定时器采用“间隔”值interval来作为计时方式当定时器启动后间隔值interval将不断减小。当interval值减到0时我们就说该间隔定时器到期。与上一节所说的内核动态定时器相比二者最大的区别在于定时器的计时方式不同。内核定时器是通过它的到期时刻expires值来计时的当全局变量jiffies值大于或等于内核动态定时器的expires值时我们说内核内核定时器到期。而间隔定时器则实际上是通过一个不断减小的计数器来计时的。虽然这两种定时器并不相同但却也是相互联系的。假如我们每个时钟节拍都使间隔定时器的间隔计数器减1那么在这种情形下间隔定时器实际上就是内核动态定时器下面我们会看到进程的真实间隔定时器就是这样通过内核定时器来实现的。 间隔定时器主要被应用在用户进程上。每个Linux进程都有三个相互关联的间隔定时器。其各自的间隔计数器都定义在进程的task_struct结构中如下所示include/linux/sched.h struct task_struct …… unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; …… } 1真实间隔定时器ITIMER_REAL这种间隔定时器在启动后不管进程是否运行每个时钟滴答都将其间隔计数器减1。当减到0值时内核向进程发送SIGALRM信号。结构类型task_struct中的成员it_real_incr则表示真实间隔定时器的间隔计数器的初始值而成员it_real_value则表示真实间隔定时器的间隔计数器的当前值。由于这种间隔定时器本质上与上一节的内核定时器时一样的因此Linux实际上是通过real_timer这个内嵌在task_struct结构中的内核动态定时器来实现真实间隔定时器ITIMER_REAL的。 2虚拟间隔定时器ITIMER_VIRT也称为进程的用户态间隔定时器。结构类型task_struct中成员it_virt_incr和it_virt_value分别表示虚拟间隔定时器的间隔计数器的初始值和当前值二者均以时钟滴答次数位计数单位。当虚拟间隔定时器启动后只有当进程在用户态下运行时一次时钟滴答才能使间隔计数器当前值it_virt_value减1。当减到0值时内核向进程发送SIGVTALRM信号虚拟闹钟信号并将it_virt_value重置为初值it_virt_incr。具体请见7.4.3节中的do_it_virt()函数的实现。 3PROF间隔定时器ITIMER_PROF进程的task_struct结构中的it_prof_value和it_prof_incr成员分别表示PROF间隔定时器的间隔计数器的当前值和初始值均以时钟滴答为单位。当一个进程的PROF间隔定时器启动后则只要该进程处于运行中而不管是在用户态或核心态下执行每个时钟滴答都使间隔计数器it_prof_value值减1。当减到0值时内核向进程发送SIGPROF信号并将it_prof_value重置为初值it_prof_incr。具体请见7.4.3节的do_it_prof()函数。 Linux在include/linux/time.h头文件中为上述三种进程间隔定时器定义了索引标识如下所示 #define ITIMER_REAL 0 #define ITIMER_VIRTUAL 1 #define ITIMER_PROF 2 771 数据结构itimerval 虽然在内核中间隔定时器的间隔计数器是以时钟滴答次数为单位但是让用户以时钟滴答为单位来指定间隔定时器的间隔计数器的初值显然是不太方便的因为用户习惯的时间单位是秒、毫秒或微秒等。所以Linux定义了数据结构itimerval来让用户以秒或微秒为单位指定间隔定时器的时间间隔值。其定义如下include/linux/time.h struct itimerval { struct timeval it_interval; /* timer interval */ struct timeval it_value; /* current value */ }; 其中it_interval成员表示间隔计数器的初始值而it_value成员表示间隔计数器的当前值。这两个成员都是timeval结构类型的变量因此其精度可以达到微秒级。 l timeval与jiffies之间的相互转换 由于间隔定时器的间隔计数器的内部表示方式与外部表现方式互不相同因此有必要实现以微秒为单位的timeval结构和为时钟滴答次数单位的jiffies之间的相互转换。为此Linux在kernel/itimer.c中实现了两个函数实现二者的互相转换——tvtojiffies()函数和jiffiestotv()函数。它们的源码如下 static unsigned long tvtojiffies(struct timeval *value) { unsigned long sec  (unsigned) value-tv_sec; unsigned long usec  (unsigned) value-tv_usec; if (sec  (ULONG_MAX / HZ)) return ULONG_MAX; usec  1000000 / HZ - 1; usec / 1000000 / HZ; return HZ*secusec; } static void jiffiestotv(unsigned long jiffies, struct timeval *value) { value-tv_usec  (jiffies % HZ) * (1000000 / HZ); value-tv_sec  jiffies / HZ; } 772 真实间隔定时器ITIMER_REAL的底层运行机制 间隔定时器ITIMER_VIRT和ITIMER_PROF的底层运行机制是分别通过函数do_it_virt函数和do_it_prof函数来实现的这里就不再重述可以参见7.4.3节。 由于间隔定时器ITIMER_REAL本质上与内核动态定时器并无区别。因此内核实际上是通过内核动态定时器来实现进程的ITIMER_REAL间隔定时器的。为此task_struct结构中专门设立一个timer_list结构类型的成员变量real_timer。动态定时器real_timer的函数指针function总是被task_struct结构的初始化宏INIT_TASK设置为指向函数it_real_fn()。如下所示include/linux/sched.h #define INIT_TASK(tsk) \ …… real_timer { function it_real_fn \ } \ …… } 而real_timer链表元素list和data成员总是被进程创建时分别初始化为空和进程task_struct结构的地址如下所示kernel/fork.c int do_fork(……) { …… p-it_real_value  p-it_virt_value  p-it_prof_value  0; p-it_real_incr  p-it_virt_incr  p-it_prof_incr  0; init_timer(p-real_timer); p-real_timer.data  (unsigned long)p; …… } 当用户通过setitimer()系统调用来设置进程的ITIMER_REAL间隔定时器时it_real_incr被设置成非零值于是该系统调用相应地设置好real_timer.expires值然后进程的real_timer定时器就被加入到内核动态定时器链表中这样该进程的ITIMER_REAL间隔定时器就被启动了。当real_timer定时器到期时它的关联函数it_real_fn()将被执行。注意所有进程的real_timer定时器的function函数指针都指向it_real_fn()这同一个函数因此it_real_fn()函数必须通过其参数来识别是哪一个进程为此它将unsigned long类型的参数p解释为进程task_struct结构的地址。该函数的源码如下kernel/itimer.c void it_real_fn(unsigned long __data) { struct task_struct * p  (struct task_struct *) __data; unsigned long interval; send_sig(SIGALRM, p, 1); interval  p-it_real_incr; if (interval) { if (interval  (unsigned long) LONG_MAX) interval  LONG_MAX; p-real_timer.expires  jiffies  interval; add_timer(p-real_timer); } } 函数it_real_fn()的执行过程大致如下 1首先将参数p通过强制类型转换解释为进程的task_struct结构类型的指针。 2向进程发送SIGALRM信号。 3在进程的it_real_incr非0的情况下继续启动real_timer定时器。首先计算real_timer定时器的expires值为jiffiesit_real_incr。然后调用add_timer()函数将real_timer加入到内核动态定时器链表中。 773 itimer定时器的系统调用 与itimer定时器相关的syscall有两个getitimer()和setitimer()。其中getitimer()用于查询调用进程的三个间隔定时器的信息而setitimer()则用来设置调用进程的三个间隔定时器。这两个syscall都是现在kernel/itimer.c文件中。 7731 getitimer()系统调用的实现 函数sys_getitimer()有两个参数1which指定查询调用进程的哪一个间隔定时器其取值可以是ITIMER_REAL、ITIMER_VIRT和ITIMER_PROF三者之一。2value指针指向用户空间中的一个itimerval结构用于接收查询结果。该函数的源码如下 /* SMP: Only we modify our itimer values. */ asmlinkage long sys_getitimer(int which, struct itimerval *value) { int error  -EFAULT; struct itimerval get_buffer; if (value) { error  do_getitimer(which, get_buffer); if (!error  copy_to_user(value, get_buffer, sizeof(get_buffer))) error  -EFAULT; } return error; } 显然sys_getitimer()函数主要通过do_getitimer()函数来查询当前进程的间隔定时器信息并将查询结果保存在内核空间的结构变量get_buffer中。然后调用copy_to_usr()宏将get_buffer中结果拷贝到用户空间缓冲区中。 函数do_getitimer()的源码如下kernel/itimer.c int do_getitimer(int which, struct itimerval *value) { register unsigned long val, interval; switch (which) { case ITIMER_REAL: interval  current-it_real_incr; val  0; /* * FIXME! This needs to be atomic, in case the kernel timer happens! */ if (timer_pending(current-real_timer)) { val  current-real_timer.expires - jiffies; /* look out for negative/zero itimer.. */ if ((long) val  0) val  1; } break; case ITIMER_VIRTUAL: val  current-it_virt_value; interval  current-it_virt_incr; break; case ITIMER_PROF: val  current-it_prof_value; interval  current-it_prof_incr; break; default: return(-EINVAL); } jiffiestotv(val, value-it_value); jiffiestotv(interval, value-it_interval); return 0; } 查询的过程如下 1首先用局部变量val和interval分别表示待查询间隔定时器的间隔计数器的当前值和初始值。 2如果whichITIMER_REAL则查询当前进程的ITIMER_REAL间隔定时器。于是从current-it_real_incr中得到ITIMER_REAL间隔定时器的间隔计数器的初始值并将其保存到interval局部变量中。而对于间隔计数器的当前值由于ITITMER_REAL间隔定时器是通过real_timer这个内核动态定时器来实现的因此不能通过current-it_real_value来获得ITIMER_REAL间隔定时器的间隔计数器的当前值而必须通过real_timer来得到这个值。为此先用timer_pending()函数来判断current-real_timer是否已被起动。如果未启动则说明ITIMER_REAL间隔定时器也未启动因此其间隔计数器的当前值肯定是0。因此将val变量简单地置0就可以了。如果已经启动则间隔计数器的当前值应该等于timer_real.expiresjiffies。 3如果whichITIMER_VIRT则查询当前进程的ITIMER_VIRT间隔定时器。于是简单地将计数器初值it_virt_incr和当前值it_virt_value分别保存到局部变量interval和val中。 4如果whichITIMER_PROF则查询当前进程的ITIMER_PROF间隔定时器。于是简单地将计数器初值it_prof_incr和当前值it_prof_value分别保存到局部变量interval和val中。 5最后通过转换函数jiffiestotv()将val和interval转换成timeval格式的时间值并保存到value-it_value和value-it_interval中作为查询结果返回。 7732 setitimer()系统调用的实现 函数sys_setitimer()不仅设置调用进程的指定间隔定时器而且还返回该间隔定时器的原有信息。它有三个参数1which含义与sys_getitimer()中的参数相同。2输入参数value指向用户空间中的一个itimerval结构含有待设置的新值。3输出参数ovalue指向用户空间中的一个itimerval结构用于接收间隔定时器的原有信息。 该函数的源码如下kernel/itimer.c /* SMP: Again, only we play with our itimers, and signals are SMP safe * now so that is not an issue at all anymore. */ asmlinkage long sys_setitimer(int which, struct itimerval *value, struct itimerval *ovalue) { struct itimerval set_buffer, get_buffer; int error; if (value) { if(copy_from_user(set_buffer, value, sizeof(set_buffer))) return -EFAULT; } else memset((char *) set_buffer, 0, sizeof(set_buffer)); error  do_setitimer(which, set_buffer, ovalue ? get_buffer : 0); if (error || !ovalue) return error; if (copy_to_user(ovalue, get_buffer, sizeof(get_buffer))) return -EFAULT; return 0; } 对该函数的注释如下 1在输入参数指针value非空的情况下调用copy_from_user()宏将用户空间中的待设置信息拷贝到内核空间中的set_buffer结构变量中。如果value指针为空则简单地将set_buffer结构变量全部置0。 2调用do_setitimer()函数完成实际的设置操作。如果输出参数ovalue指针有效则以内核变量get_buffer的地址作为do_setitimer()函数的第三那个调用参数这样当do_setitimer()函数返回时get_buffer结构变量中就将含有当前进程的指定间隔定时器的原来信息。Do_setitimer()函数返回0值表示成功非0值表示失败。 3在do_setitimer()函数返回非0值的情况下或者ovalue指针为空的情况下不需要输出间隔定时器的原有信息函数就可以直接返回了。 4如果ovalue指针非空调用copy_to_user()宏将get_buffer()结构变量中值拷贝到ovalue所指向的用户空间中去以便让用户得到指定间隔定时器的原有信息值。 函数do_setitimer()的源码如下kernel/itimer.c int do_setitimer(int which, struct itimerval *value, struct itimerval *ovalue) { register unsigned long i, j; int k; i  tvtojiffies(value-it_interval); j  tvtojiffies(value-it_value); if (ovalue  (k  do_getitimer(which, ovalue))  0) return k; switch (which) { case ITIMER_REAL: del_timer_sync(current-real_timer); current-it_real_value  j; current-it_real_incr  i; if (!j) break; if (j  (unsigned long) LONG_MAX) j  LONG_MAX; i  j  jiffies; current-real_timer.expires  i; add_timer(current-real_timer); break; case ITIMER_VIRTUAL: if (j) j; current-it_virt_value  j; current-it_virt_incr  i; break; case ITIMER_PROF: if (j) j; current-it_prof_value  j; current-it_prof_incr  i; break; default: return -EINVAL; } return 0; } 对该函数的注释如下 1首先调用tvtojiffies函数将timeval格式的初始值和当前值转换成以时钟滴答为单位的时间值。并分别保存在局部变量i和j中。 2如果ovalue指针非空则调用do_getitimer()函数查询指定间隔定时器的原来信息。如果do_getitimer()函数返回负值说明出错。因此就要直接返回错误值。否则继续向下执行开始真正地设置指定的间隔定时器。 3如果whichITITMER_REAL表示设置ITIMER_REAL间隔定时器。a调用del_timer_sync()函数该函数在单CPU系统中就是del_timer()函数将当前进程的real_timer定时器从内核动态定时器链表中删除。b将it_real_incr和it_real_value分别设置为局部变量i和j。c如果j0说明不必启动real_timer定时器因此执行break语句退出switch…case控制结构而直接返回。d将real_timer的expires成员设置成jiffies当前值j然后调用add_timer()函数将当前进程的real_timer定时器加入到内核动态定时器链表中从而启动该定时器。 4如果whichITIMER_VIRT则简单地用局部变量i和j的值分别更新it_virt_incr和it_virt_value就可以了。 5如果whichITIMER_PROF则简单地用局部变量i和j的值分别更新it_prof_incr和it_prof_value就可以了。 6最后返回0值表示成功。 7733 alarm系统调用 系统调用alarm可以让调用进程在指定的秒数间隔后收到一个SIGALRM信号。它只有一个参数seconds指定以秒数计的定时间隔。函数sys_alarm()的源码如下kernel/timer.c /* * For backwards compatibility? This can be done in libc so Alpha * and all newer ports shouldnt need it. */ asmlinkage unsigned long sys_alarm(unsigned int seconds) { struct itimerval it_new, it_old; unsigned int oldalarm; it_new.it_interval.tv_sec  it_new.it_interval.tv_usec  0; it_new.it_value.tv_sec  seconds; it_new.it_value.tv_usec  0; do_setitimer(ITIMER_REAL, it_new, it_old); oldalarm  it_old.it_value.tv_sec; /* ehhh.. We cant return 0 if we have an alarm pending.. */ /* And wed better return too much than too little anyway */ if (it_old.it_value.tv_usec) oldalarm; return oldalarm; } 这个系统调用实际上就是启动进程的ITIMER_REAL间隔定时器。因此它完全可放到用户空间的C函数库比如libc和glibc中来实现。但是为了保此内核的向后兼容性2.4.0版的内核仍然将这个syscall放在内核空间中来实现。函数sys_alarm的实现过程如下 1根据参数seconds的值构造一个itimerval结构变量it_new。注意由于alarm启动的ITIMER_REAL间隔定时器是一次性而不是循环重复的因此it_new变量中的it_interval成员一定要设置为0。 2调用函数do_setitimer()函数以新构造的定时器it_new来启动当前进程的ITIMER_REAL定时器同时将该间隔定时器的原定时间隔保存到局部变量it_old中。 3返回值oldalarm表示以秒数计的ITIMER_REAL间隔定时器的原定时间隔值。因此先把it_old.it_value.tv_sec赋给oldalarm并且在it_old.it_value.tv_usec非0的情况下将oldalarm的值加1也即不足1秒补足1秒。dreamice 回复于2008-11-06 17:58:1378 时间系统调用的实现 本节讲述与时间相关的syscall这些系统调用主要用来供用户进程向内核检索当前时间与日期因此他们是内核的时间服务接口。主要的时间系统调用共有5个time、stime和gettimeofday、settimeofday以及与网络时间协议NTP相关的adjtimex系统调用。这里我们不关心NTP因此仅分析前4个时间系统调用。前4个时间系统调用可以分为两组1time和stime是一组2gettimeofday和settimeofday是一组。 781 系统调用time和stime 系统调用time用于获取以秒数表示的系统当前时间即内核全局时间变量xtime中的tv_sec成员的值。它只有一个参数——整型指针tloc指向用户空间中的一个整数用来接收返回的当前时间值。函数sys_time的源码如下kernel/time.c asmlinkage long sys_time(int * tloc) { int i; /* SMP: This is fairly trivial. We grab CURRENT_TIME and stuff it to user space. No side effects */ i  CURRENT_TIME; if (tloc) { if (put_user(i,tloc)) i  -EFAULT; } return i; } 注释如下 1首先函数调用CURRENT_TIME宏来得到以秒数表示的内核当前时间值并将该值保存在局部变量i中。宏CURRENT_TIME定义在include/linux/sched.h头文件中它实际上就是内核全局时间变量xtime中的tv_sec成员。如下所示 #define CURRENT_TIME (xtime.tv_sec) 2然后在参数指针tloc非空的情况下将i的值通过put_user()宏传递到有tloc所指向的用户空间中去以作为函数的输出结果。 3最后将局部变量I的值——也即也秒数表示的系统当前时间值作为返回值返回。 系统调用stime()与系统调用time()刚好相反它可以让用户设置系统的当前时间以秒数为单位。它同样也只有一个参数——整型指针tptr指向用户空间中待设置的时间秒数值。函数sys_stime()的源码如下kernel/time.c asmlinkage long sys_stime(int * tptr) { int value; if (!capable(CAP_SYS_TIME)) return -EPERM; if (get_user(value, tptr)) return -EFAULT; write_lock_irq(xtime_lock); xtime.tv_sec  value; xtime.tv_usec  0; time_adjust  0; /* stop active adjtime() */ time_status | STA_UNSYNC; time_maxerror  NTP_PHASE_LIMIT; time_esterror  NTP_PHASE_LIMIT; write_unlock_irq(xtime_lock); return 0; } 注释如下 1首先检查调用进程的权限显然只有root用户才能有权限修改系统时间。 2调用get_user()宏将tptr指针所指向的用户空间中的时间秒数值拷贝到内核空间中来并保存到局部变量value中。 3将局部变量value的值更新到全局时间变量xtime的tv_sec成员中并将xtime的tv_usec成员清零。 4在相应地重置其它状态变量后函数就可以返回了返回值0表示成功。 782 系统调用gettimeofday 这个syscall用来供用户获取timeval格式的当前时间信息精确度为微秒级以及系统的当前时区信息timezone。结构类型timeval的指针参数tv指向接受时间信息的用户空间缓冲区参数tz是一个timezone结构类型的指针指向接收时区信息的用户空间缓冲区。这两个参数均为输出参数返回值0表示成功返回负值表示出错。函数sys_gettimeofday()的源码如下kernel/time.c asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz) { if (tv) { struct timeval ktv; do_gettimeofday(ktv); if (copy_to_user(tv, ktv, sizeof(ktv))) return -EFAULT; } if (tz) { if (copy_to_user(tz, sys_tz, sizeof(sys_tz))) return -EFAULT; } return 0; } 显然函数的实现主要分成两个大的方面 1如果tv指针有效则说明用户要以timeval格式来检索系统当前时间。为此先调用do_gettimeofday()函数来检索系统当前时间并保存到局部变量ktv中。然后再调用copy_to_user宏将保存在内核空间中的当前时间信息拷贝到由参数指针tv所指向的用户空间缓冲区中。 2如果tz指针有效则说明用户要检索当前时区信息因此调用copy_to_user()宏将全局变量sys_tz中的时区信息拷贝到参数指针tz所指向的用户空间缓冲区中。 3最后返回0表示成功。 函数do_gettimeofday()的源码如下arch/i386/kernel/time.c /* * This version of gettimeofday has microsecond resolution * and better than microsecond precision on fast x86 machines with TSC. */ void do_gettimeofday(struct timeval *tv) { unsigned long flags; unsigned long usec, sec; read_lock_irqsave(xtime_lock, flags); usec  do_gettimeoffset(); { unsigned long lost  jiffies - wall_jiffies; if (lost) usec  lost * (1000000 / HZ); } sec  xtime.tv_sec; usec  xtime.tv_usec; read_unlock_irqrestore(xtime_lock, flags); while (usec  1000000) { usec - 1000000; sec; } tv-tv_sec  sec; tv-tv_usec  usec; } 该函数的完成实际的当前时间检索工作。由于gettimeofday()系统调用要求时间精度要达到微秒级因此do_gettimeofday()函数不能简单地返回xtime中的值即可而必须精确地确定自从时钟驱动的Bottom Half上一次更新xtime的那个时刻由wall_jiffies变量表示参见7.3节到do_gettimeofday()函数的当前执行时刻之间的具体时间间隔长度,以便精确地修正xtime的值.如下图7-9所示 假定被do_gettimeofday()用来修正xtime的时间间隔为fixed_usec而从wall_jiffies到jiffies之间的时间间隔是lost_usec而从jiffies到do_gettimeofday()函数的执行时刻的时间间隔是offset_usec。则下列三个等式成立 fixed_useclost_usecoffset_usec lost_usecjiffieswall_jiffiesTICK_SIZEjiffieswall_jiffies1000000HZ 由于全局变量last_tsc_low表示上一次时钟中断服务函数timer_interrupt()执行时刻的CPU TSC寄存器的值因此我们可以用X86 CPU的TSC寄存器来计算offset_usec的值。也即 offset_usecdelay_at_last_interruptcurrent_tsc_lowlast_tsc_lowfast_gettimeoffset_quotient 其中delay_at_last_interrupt是从上一次发生时钟中断到timer_interrupt()服务函数真正执行时刻之间的时间延迟间隔。每一次timer_interrupt()被执行时都会计算这一间隔并利用TSC的当前值更新last_tsc_low变量可以参见7.4节。假定current_tsc_low是do_gettimeofday()函数执行时刻TSC的当前值全局变量fast_gettimeoffset_quotient则表示TSC寄存器每增加1所代表的时间间隔值它是由time_init()函数所计算的。 根据上述原理分析do_gettimeofday()函数的执行步骤如下 1调用函数do_gettimeoffset()计算从上一次时钟中断发生到执行do_gettimeofday()函数的当前时刻之间的时间间隔offset_usec。 2通过wall_jiffies和jiffies计算lost_usec的值。 3然后令secxtime.tv_secusecxtime.tv_useclost_usecoffset_usec。显然sec表示系统当前时间在秒数量级上的值而usec表示系统当前时间在微秒量级上的值。 4用一个while{}循环来判断usec是否已经溢出而超过106us1秒。如果溢出则将usec减去106us并相应地将sec增加1直到usec不溢出为止。 5最后用sec和usec分别更新参数指针所指向的timeval结构变量。至此整个查询过程结束。 函数do_gettimeoffset()根据CPU是否配置有TSC寄存器这一条件分别有不同的实现。其定义如下arch/i386/kernel/time.c #ifndef CONFIG_X86_TSC static unsigned long do_slow_gettimeoffset(void) { …… } static unsigned long (*do_gettimeoffset)(void)  do_slow_gettimeoffset; #else #define do_gettimeoffset() do_fast_gettimeoffset() #endif 显然在配置有TSC寄存器的i386平台上do_gettimeoffset函数实际上就是do_fast_gettimeoffset()函数。它通过TSC寄存器来计算do_fast_gettimeoffset()函数被执行的时刻到上一次时钟中断发生时的时间间隔值。其源码如下arch/i386/kernel/time.c static inline unsigned long do_fast_gettimeoffset(void) { register unsigned long eax, edx; /* Read the Time Stamp Counter */ rdtsc(eax,edx); /* .. relative to previous jiffy (32 bits is enough) */ eax - last_tsc_low; /* tsc_low delta */ /* * Time offset  (tsc_low delta) * fast_gettimeoffset_quotient *  (tsc_low delta) * (usecs_per_clock) *  (tsc_low delta) * (usecs_per_jiffy / clocks_per_jiffy) * * Using a mull instead of a divl saves up to 31 clock cycles * in the critical path. */ __asm__(mull %2 :a (eax), d (edx) :rm (fast_gettimeoffset_quotient), 0 (eax)); /* our adjusted time offset in microseconds */ return delay_at_last_interrupt  edx; } 对该函数的注释如下 1先调用rdtsc()函数读取当前时刻TSC寄存器的值并将其高32位保存在edx局部变量中低32位保存在局部变量eax中。 2让局部变量eaxΔtsc_loweaxlast_tsc_low也即计算当前时刻的TSC值与上一次时钟中断服务函数timer_interrupt()执行时的TSC值之间的差值。 3显然从上一次timer_interrupt()到当前时刻的时间间隔就是Δtsc_lowfast_gettimeoffset_quotient。因此用一条mul指令来计算这个乘法表达式的值。 4返回值delay_at_last_interruptΔtsc_lowfast_gettimeoffset_quotient就是从上一次时钟中断发生时到当前时刻之间的时间偏移间隔值。 783 系统调用settimeofday 这个系统调用与gettimeofday刚好相反它供用户设置当前时间以及当前时间信息。它也有两个参数1参数指针tv指向含有待设置时间信息的用户空间缓冲区2参数指针tz指向含有待设置时区信息的用户空间缓冲区。函数sys_settimeofday()的源码如下kernel/time.c asmlinkage long sys_settimeofday(struct timeval *tv, struct timezone *tz) { struct timeval new_tv; struct timezone new_tz; if (tv) { if (copy_from_user(new_tv, tv, sizeof(*tv))) return -EFAULT; } if (tz) { if (copy_from_user(new_tz, tz, sizeof(*tz))) return -EFAULT; } return do_sys_settimeofday(tv ? new_tv : NULL, tz ? new_tz : NULL); } 函数首先调用copy_from_user宏将保存在用户空间中的待设置时间信息和时区信息拷贝到内核空间中来并保存到局部变量new_tv和new_tz中。然后调用do_sys_settimeofday()函数完成实际的时间设置和时区设置操作。 函数do_sys_settimeofday()的源码如下kernel/time.c int do_sys_settimeofday(struct timeval *tv, struct timezone *tz) { static int firsttime  1; if (!capable(CAP_SYS_TIME)) return -EPERM; if (tz) { /* SMP safe, global irq locking makes it work. */ sys_tz  *tz; if (firsttime) { firsttime  0; if (!tv) warp_clock(); } } if (tv) { /* SMP safe, again the code in arch/foo/time.c should * globally block out interrupts when it runs. */ do_settimeofday(tv); } return 0; } 该函数的执行过程如下 1首先检查调用进程是否有相应的权限。如果没有则返回错误值EPERM。 2如果执政tz有效则用tz所指向的新时区信息更新全局变量sys_tz。并且如果是第一次设置时区信息则在tv指针不为空的情况下调用wrap_clock()函数来调整xtime中的秒数值。函数wrap_clock()的源码如下kernel/time.c inline static void warp_clock(void) { write_lock_irq(xtime_lock); xtime.tv_sec  sys_tz.tz_minuteswest * 60; write_unlock_irq(xtime_lock); } 3如果参数tv指针有效则根据tv所指向的新时间信息调用do_settimeofday()函数来更新内核的当前时间xtime。 4最后返回0值表示成功。 函数do_settimeofday()执行刚好与do_gettimeofday()相反的操作。这是因为全局变量xtime所表示的时间是与wall_jiffies相对应的那一个时刻。因此必须从参数指针tv所指向的新时间中减去时间间隔fixed_usec其含义见7.8.2节。函数源码如下arch/i386/kernel/time.c void do_settimeofday(struct timeval *tv) { write_lock_irq(xtime_lock); /* * This is revolting. We need to set xtime correctly. However, the * value in this location is the value at the most recent update of * wall time. Discover what correction gettimeofday() would have * made, and then undo it! */ tv-tv_usec - do_gettimeoffset(); tv-tv_usec - (jiffies - wall_jiffies) * (1000000 / HZ); while (tv-tv_usec  0) { tv-tv_usec  1000000; tv-tv_sec--; } xtime  *tv; time_adjust  0; /* stop active adjtime() */ time_status | STA_UNSYNC; time_maxerror  NTP_PHASE_LIMIT; time_esterror  NTP_PHASE_LIMIT; write_unlock_irq(xtime_lock); } 该函数的执行步骤如下 1调用do_gettimeoffset()函数计算上一次时钟中断发生时刻到当前时刻之间的时间间隔值。 2通过wall_jiffies与jiffies计算二者之间的时间间隔lost_usec。 3从tv-tv_usec中减去fixed_usec即tv-tv_useclost_usecoffset_usec。 4用一个while{}循环根据tv-tv_usec是否小于0来调整tv结构变量。如果tv-tv_usec小于0则将tv-tv_usec加上106us并相应地将tv-tv_sec减1。直到tv-tv_usec不小于0为止。 5用修正后的时间tv来更新内核全局时间变量xtime。 6最后重置其它时间状态变量。 至此我们已经完全分析了整个Linux内核的时钟机制
http://www.zqtcl.cn/news/944140/

相关文章:

  • 设计商城的网站建设电商网站建设与管理实践
  • 怎样建一个英文网站制作视频的手机软件
  • 昆明做网站费用被骗去国外做网站网站推广
  • 京东商城网站怎么做静态网页有什么特点
  • 网站上线准备工作网站源码运行
  • 视频剪辑自学网站wordpress怎样改头像
  • 女装网站模板青岛开发区网站
  • dede网站后台海外网络服务器
  • 三合一企业网站模板wordpress做的外贸网站
  • 常州做企业网站的公司亚马逊雨林有原始部落吗
  • 临沂网站设计哪家好qq浏览器网页版进入
  • seo资料站哔哩哔哩官方网站首页
  • 前端怎么做网站万网域名管理入口
  • asp.net 做网站实例特别酷炫网站
  • 个人网站的内容网页设计图片显示不出来怎么弄
  • 福建省建设人才与科技发展中心网站首页关于制作网站收费标准
  • 什么软件可以发帖子做推广中山优化网站
  • 中山网站建设开发网络营销的基本功能
  • 温州平阳县网站建设兼职免费下载简历模板
  • 导购网站 转化率wordpress 拓展
  • 美文分享网站源码互联网网站建设
  • 做网站用php还是python建设网站价格
  • 平台网站怎么做诱导网站怎么做
  • 网站建设人员构成网址申请域名
  • 微网站建设找哪家公司好郑州一凡网站建设
  • 江阴网站制作公司泉州网站建设论坛
  • 最新章节 62.一起来做网站吧时钟插件+wordpress
  • 惠州市建设规划局网站网页设计实训报告word
  • 大众汽车网站建设鳌江网站建设
  • 佛山外贸网站建设公司网站与网页区别