linux内核分析----定时器和时间管理

在这一次里,主要讲讲和时间相关的东西,这个我们都比较熟悉,我就直接如主题。       首先要明白两个概念:系统定时器和动态定时器。周期性产生的事件都是有系统定时器驱动的,这里的系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是定时器中断,它所对应的中断处理程序负责更新系统时间,也负责执行需要周期行运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。动态定时器是用来推迟执行程序的工具。内核可以动态创建或销毁动态定时器。
       内核必须在硬件的帮助下才能计算和管理时间。硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可看成是一个电子时间资源。系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定称为节拍率(tick rate).当时钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ.在系统启动时按照HZ值对硬件进行设置。体系结构不一样,HZ的值也不同,定义在asm/param.h中。刚提到的节拍率就是这个意思。周期是1/HZ秒。最后要说明的是这个HZ值在编写内核代码时,不是固定不变的,而是可调的。当然,对于操作系统而言,也并不是一定要这个固定的时钟中断。实际上,内核可以使用动态编程定时器操作挂起事件。这里就不多说了。
       在linux内核里,有一个叫jiffies的变量(定义在linux/jiffies)记录了自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ.系统运行时间以秒为单位计算,就等于jiffes/HZ.它作为在计算机表示的变量,就总存在大小,当这个变量增加到超出它的表示上限时,就要回绕到0.这个回绕看起来很简单,但实际上还是给我们编程造成了很大的麻烦,比如边界条件判断时。幸好,内核提供了四个宏来帮助比较节拍计数,这些宏定义在linux/jiffies.h可以很好的处理节拍回绕的情况:
           说明:unknown参数通常是jiffies,known参数是需要对比的值。
       如果改变内核中的HZ的值则会给用户空间中某些程序造成异常结果,这是因为内核是以节拍数/秒的形式给用户空间导出这个值的,在这个接口稳定了很长一段时间后,应用程序便逐渐依赖于这个特定的HZ的值了。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系----用户空间并不知道这个新的HZ的值。为了解决这个问题,内核必须更改所有导出的jiffies的值。内核定义了USER_HZ来代表用户空间看到的HZ值。内核可以使用宏jiffies_to_clock_t()将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍数。改宏的用法取决于USER_HZ是否为HZ的整数倍或相反。当是整数倍时,宏的形式相当简单:
1
#define jiffies_to_clock_t(x) ((x)/(HZ/USER_HZ));



       如果不是整数倍关系,那么该宏就得用更为复杂的算法了。同样的,如果是64位系统,内核使用函数jiffies_64_to_clock()将64位的jiffies值的单位从HZ转换为USER_HZ.
       体系结构提供了两种设备进行计时:系统定时器和实时时钟。系统定时器提供一种周期性触发中断机制。实时时钟(RTC)是用来持久存储系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保护系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中,实时时钟最主要的作用是在启动时初始化xtime变量。
       有了上面的概念基础,下面就分析时钟中断处理程序。它分为两个部分:体系结构相关部分和体系结构无关部分。相关的部分作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应地运行。执行的工作如下:
1.获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
2.需要时应答或重新设置系统时钟。
3.周期性地使用墙上时间更新实时时钟。
4.调用体系结构无关的时间例程:do_timer().
中断服务程序主要通过调用与体系结构无关的例程do_timer()执行下面的工作:
1.给jiffies_64变量加1.
2.更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
3.执行已经到期的动态定时器.
4.执行scheduler_tick()函数.
5.更新墙上时间,该时间存放在xtime变量中.
6.计算平均负载值.
       do_timer看起来还是很简单的,应为它的主要工作就是完成上面的框架,具体的让其它函数做就好了:
1
2
3
4
5
6
void do_timer(struct pt_regs *regs)
{
    jiffies_64++;
    update_process_times(user_mode(regs));
    update_times();
}



       上述user_mode()宏查询处理器寄存器regs的状态,如果时钟中断发生在用户空间,它返回1;如果发生在内核模式,则返回0.update_process_times()函数根据时钟中断产生的位置,对用户或对系统进行相应的时间更新:
1
2
3
4
5
6
7
8
9
void update_process_times(int user_tick)
{
    struct task_struct *p=current;
    int cpu=smp_processor_id();
    int system=user_tick^1;
    updata_one_process(p,user_tick,system,cpu);
    run_local_timers();
    scheduler_tick(user_tick,system);
}



       update_one_process()函数的作用是更新进程时间。它的实现是相当细致的。但注意,因为使用了XOR操作,所以user_tick和system两个变量只要其中有一个为1,则另外一个就必须为0,updates_one_process()函数可以通过判断分支,将user_tick和system加到进程相应的计数上:
1
2
p->utime = user;
p->stime = system;



       上述操作将适当的计数值增加1,而另外一个值保持不变。也许你已经发现了,这样做意味着内核对进程时间计数时,是根据中断发生时处理器所处的模式进行分类统计的,它把上一个tick全部算给进程。但是事实上进程在上一个节拍器间可能多次进入和退出内核模式,而在在上一个节拍期间,该进程也不一定是唯一一个运行进程,但是这没办法。接下来的run_lock_times() 函数标记了一个软中断去处理所有到期的定时器。最后,scheduler_tick()函数负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志,在SMP机器中中,该函数还要负责平衡每个处理器上的运行队列。当update_process_times()函数返回时,do_timer()函数接着会调用update_times()更新墙上时间。
1
2
3
4
5
6
7
8
9
10
void update_times(void)
{
    unsigned long ticks;
    if(ticks){
        wall_jiffies += ticks;
        update_wall_time(ticks);
    }
    last_time_offset = 0;
    calc_load(ticks);
}



       这里的ticks记录最近一次更新后新产生的节拍数。通常情况下ticks显然应该等于1.但是时钟中断也有可能丢失,因而节拍也会丢失。在中断长时间被禁止的情况下,就会出现这种现象(这种情况并不常见,往往是个BUG).wall_jiffies值随后被加上ticks----所以此刻wall_jiffies值就等于更新的墙上时间的更新值jiffies----接着调用update_wall_time()函数更新xtime,最后由calc_load()执行。do_timer()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。以上的工作每1/HZ都要发生一次。
       刚前边说的墙上时间就是我们常说的实际时间,指变量xtime,由结构体timespec定义(kernel/timer.c),如下:
1
2
3
4
struct  timespec{
    time_t tv_sec;  //秒,存放自1970年7月1日(UTC)以来经过的时间,1970年7月1日称为纪元
    long tv_nsec;   //纳秒,记录自上一秒开始经过的纳秒数
}



读写这个xtime变量需要xtime_lock锁,该锁是一个顺序锁(seqlock).关于内核读写就不说了,注意适当加解锁就好。回到用户空间,从用户空间取得墙上时间的主要接口是gettimeofday(),在内核中对应系统调用为sys_gettimeofday():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)
{
         if (likely(tv != NULL)) {
                 struct timeval ktv;
                 do_gettimeofday(&ktv);
                 if (copy_to_user(tv, &ktv, sizeof(ktv)))
                         return -EFAULT;
         }
         if (unlikely(tz != NULL)) {
                 if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
                         return -EFAULT;
         }
         return 0;