Linux中断处理之时钟中断(一) -电脑资料

电脑资料 时间:2019-01-01 我要投稿
【www.unjs.com - 电脑资料】

一:前言

时钟是整个操作系统的脉搏,它为进程的时间片调度,定时事件提供了依据.另外,用户空间的很多操作都依赖于时钟,例如select.poll,make.操作系统管理的时间为分两种,一种称为当前时间,也即我们日常生活所用的时间.这个时间一般保存在CMOS中.主板中有特定的芯片为其提供计时依据.另外一种时间称为相对时间.例如系统运行时间.显然对计算机而然,相对时间比当前时间更为重要.

二:与时钟有关的硬件处理.

1):实时时钟(RTC)

该时钟独立于CPU和其它芯片.即使PC断电,该时钟还是继续运行.该计时由一块单独的芯片处理,并把时钟值存放CMOS.该时间可参在IRQ8上周期性的产生时间信号.频率在2Hz ~ 8192Hz之间.但在linux中,只是用RTC来获取当前时间.

2):时间戳计时器(TSC)

CPU附带了一个64位的时间戳寄存器,当时钟信号到来的时候.该寄存器内容自动加1

3):可编程中断定时器(PIC)

该设备可以周期性的发送一个时间中断信号.发送中断信号的间隔可以对其进行编程控制.在linux系统中,该中断时间间隔由HZ表示.这个时间间隔也被称为一个节拍(tick).

4):CPU本地定时器

在处理器的本地APIC还提供了另外的一定定时设备.CPU本地定时器也可以单次或者周期性的产生中断信号.与上次描述的PIC相比.它有以下几点的区别:

APIC本地计时器是32位.而PIC是16位.由此APIC本地计时器可以提供更低频率的中断信号

本地APIC只把中断信号发送给本地CPU.而PIC发送的中断信号任何CPU都可以处理

APIC定时器是基于总线时钟信号的.而PIC有自己的内部时钟振荡器

5):高精度计时器(HPET)

在linux2.6中增加了对HPET的支持.HPET是一种由微软和intel联合开发的新型定时芯片.该设备有一组寄时器,每个寄时器对应有自己的时钟信号,时钟信号到来的时候就会自动加1.

实际上,在intel多理器系统与单处理器系统还有所不同:

在单处理系统中.所有计时活动过由PIC产生的时钟中断信号触发的

在多处理系统中,所有普通活动是由PIC产生的中断触发.所有具体的CPU活动,都由本地APIC触发的.

三:时钟中断相关代码分析

time_init()是时钟初始化函数,他由asmlinkage void __init start_kernel()调用.具体代码如下:

//时钟中断初始化
void __init time_init(void)
{
//如果定义了HPET
#ifdef CONFIG_HPET_TIMER
   if (is_hpet_capable()) {
     /*
     * HPET initialization needs to do memory-mapped io. So, let
     * us do a late initialization after mem_init().
     */
     late_time_init = hpet_time_init;
     return;
   }
#endif
   //从cmos 中取得实时时间
   xtime.tv_sec = get_cmos_time();
   //初始化wall_to_monotonic
   wall_to_monotonic.tv_sec = -xtime.tv_sec;
   xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);
   wall_to_monotonic.tv_nsec = -xtime.tv_nsec;
   //选择一个合适的定时器
   cur_timer = select_timer();
   printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);
   //注册时间中断信号处理函数
   time_init_hook();
}

该函数从cmos取得了当前时间.并为调整时间精度选择了合适的定时器

转入time_init_hook():

void __init time_init_hook(void)
{
   //注册中断处理函数
   setup_irq(0, &irq0);
}

Irq0定义如下:

static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL};

对应的中断处理函数为:timer_interrupt():

irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
   //因为该函数会修改xtime的值,为避免多处理器竞争.先加锁
   write_seqlock(&xtime_lock);
   //记录上一次时间中断的精确时间.做调准时钟用
   cur_timer->mark_offset();
   do_timer_interrupt(irq, NULL, regs);
   //解锁
   write_sequnlock(&xtime_lock);
   return IRQ_HANDLED;
}
核心处理函数为 do_timer_interrupt():
static inline void do_timer_interrupt(int irq, void *dev_id,
            struct pt_regs *regs)
{
#ifdef CONFIG_X86_IO_APIC
   if (timer_ack) {
     spin_lock(&i8259A_lock);
     outb(0x0c, PIC_MASTER_OCW3);
     /* Ack the IRQ; AEOI will end it automatically. */
     inb(PIC_MASTER_POLL);
     spin_unlock(&i8259A_lock);
   }
#endif
   do_timer_interrupt_hook(regs);
   //如果要进行时间同步,那就隔一段时间把当前时间写回coms
   if ((time_status & STA_UNSYNC) == 0 &&
      xtime.tv_sec > last_rtc_update + 660 &&
     (xtime.tv_nsec / 1000)
        >= USEC_AFTER - ((unsigned) TICK_SIZE) / 2 &&
      (xtime.tv_nsec / 1000)
        <= USEC_BEFORE + ((unsigned) TICK_SIZE) / 2) {
     /* horrible...FIXME */
     if (efi_enabled) {
        if (efi_set_rtc_mmss(xtime.tv_sec) == 0)
          last_rtc_update = xtime.tv_sec;
        else
          last_rtc_update = xtime.tv_sec - 600;
     } else 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 */
   }
#ifdef CONFIG_MCA
   if( MCA_bus ) {
     /* The PS/2 uses level-triggered interrupts. You can't
     turn them off, nor would you want to (any attempt to
     enable edge-triggered interrupts usually gets intercepted by a
     special hardware circuit). Hence we have to acknowledge
     the timer interrupt. Through some incredibly stupid
     design idea, the reset for IRQ 0 is done by setting the
     high bit of the PPI port B (0x61). Note that some PS/2s,
     notably the 55SX, work fine if this is removed. */
     irq = inb_p( 0x61 );  /* read the current state */
     outb_p( irq|0x80, 0x61 );  /* reset the IRQ */
   }
#endif
}

我们忽略选择编译部份,转到do_timer_interrupt_hook()

static inline void do_timer_interrupt_hook(struct pt_regs *regs)
{
   do_timer(regs);
/*
* In the SMP case we use the local APIC timer interrupt to do the
* profiling, except when we simulate SMP mode on a uniprocessor
* system, in that case we have to call the local interrupt handler.
*/
#ifndef CONFIG_X86_LOCAL_APIC
   //更新内核代码监管器,

Linux中断处理之时钟中断(一)

。在每次时钟中断的时候。取得每一次中断前的esp,进而可以得到运行的函//数地址。这样就可以统计运行时间最长的函内核函数区域。以便于内核管理者优化
   profile_tick(CPU_PROFILING, regs);
#else
   if (!using_apic_timer)
     smp_local_timer_interrupt(regs);
#endif
}

这里有几个重要的操作.先看do_timer():

void do_timer(struct pt_regs *regs)
{
   // 更新jiffies计数.jiffies_64与jiffies在链接的时候,实际是指向同一个区域
   jiffies_64++;
#ifndef CONFIG_SMP
   /* SMP process accounting uses the local APIC timer */
   //更新当前运行进程的与时钟相关的信息
   update_process_times(user_mode(regs));
#endif
   //更新当前时间.xtime的更新
   update_times();
}

Update_process_times()代码如下:

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);
   //激活时间软中断
   run_local_timers();
   //减少时间片。这个函数涉及到的东西过多,等到进程调度的时候再来分析。请关注本站更新*^_^*
   scheduler_tick(user_tick, system);
}

先看update_one_process():

static void update_one_process(struct task_struct *p, unsigned long user,
       unsigned long system, int cpu)
{
   do_process_times(p, user, system);
   //检查进程的定时器
   do_it_virt(p, user);
   do_it_prof(p);
}  
在这里简单介绍一下do_it_virt()与do_it_prof():

这两个函数主要检查用户空间的进程定时器是否到期.在进程的内存描述符有相关的字段.如下:

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()函数的实现。

(3)PROF间隔定时器ITIMER_PROF:进程的task_struct结构中的it_prof_value和it_prof_incr成员分别表示PROF间隔定时器的间隔计数器的当前值和初始值(均以时钟滴答为单位)。当一个进程的PROF间隔定时器启动后,则只要该进程处于运行中,而不管是在用户态或核心态下执行,每个时钟滴答都使间隔计数器it_prof_value值减1。当减到0值时,内核向进程发送SIGPROF信号,并将it_prof_value重置为初值it_prof_incr.

Do_process_times():
static inline void do_process_times(struct task_struct *p,
   unsigned long user, unsigned long system)
{
   unsigned long psecs;
   //p->utime:在用户空间所花的时间
   psecs = (p->utime += user);
   //p->stime:在系统空间所花的时间
   psecs += (p->stime += system);
   //如果运行的时间片到达
   if (psecs / HZ >= p->rlim[RLIMIT_CPU].rlim_cur) {
     /* Send SIGXCPU every second.. */
     //每秒发送一个SIGXCPU
     if (!(psecs % HZ))
        send_sig(SIGXCPU, p, 1);
     /* and SIGKILL when we go over max.. */
     //发送SIGKILL
     if (psecs / HZ >= p->rlim[RLIMIT_CPU].rlim_max)
        send_sig(SIGKILL, p, 1);
   }
}

该函数检查当前进程的时间片是否到达,如果到达就给当前进程发送SIGKILL和SIGXCPU

do_it_virt()/do_it_prof()检查过程的定时器是否到期.如果到期就给进程发送相应的信号:

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;
        //发送SIGVTALRM
        send_sig(SIGVTALRM, p, 1);
     }
     p->it_virt_value = it_virt;
   }
}

返回到update_process_times()的其它函数:

run_local_timers()
void run_local_timers(void)
{
   raise_softirq(TIMER_SOFTIRQ);
}

激活时间软中断.这个函数我们在IRQ中断中已经分析过了,不再赘述

我们在do_timer()还漏掉了一个函数:

static inline void update_times(void)
{
     unsigned long ticks;
     //wall_jiffies:上一次更新的值
     ticks = jiffies - wall_jiffies;
     if (ticks) {
          wall_jiffies += ticks;
          //更新xtime
          update_wall_time(ticks);
     }
     //统计TASK_RUNNING TASK_UNINTERRUPTIBLE进程数量
     calc_load(ticks);
}

四:定时器

在模块的编写过程中,我们经常使用定时器来等待一段时间之后再来执行某一个操作,

电脑资料

Linux中断处理之时钟中断(一)》(https://www.unjs.com)。为方便分析,写了下列一段测试程序:

#include
#include
#include
#include
#include
#include
MODULE_LICENSE("GPL");
void test_timerfuc(unsigned long x)
{
     printk("Eric xiao test ......\n");
}
//声明一个定个器
struct timer_list test_timer = TIMER_INITIALIZER(test_timerfuc, 0, 0);
int kernel_test_init()
{
     printk("test_init\n");
     //修改定时器到期时间。为3个HZ。一个HZ产生一个时钟中断
     mod_timer(&test_timer,jiffies+3*HZ);
     //把定时器加入时钟软中断处理链表
     add_timer(&test_timer);
}
int kernel_test_exit()
{
     printk("test_exit\n");
     return 0;
}
module_init(kernel_test_init);
module_exit(kernel_test_exit);

上面的例子程序比较简单,我们从这个例子开始研究linux下的定时器实现。

TIMER_INITIALIZER():

1):TIMER_INITIALIZER()用来声明一个定时器,它的定义如下:

#define TIMER_INITIALIZER(_function, _expires, _data) {             \<br />                   .function = (_function),                            \<br />                   .expires = (_expires),                                \<br />                   .data = (_data),                                \<br />                   .base = NULL,                                         \<br />                   .magic = TIMER_MAGIC,                              \<br />                   .lock = SPIN_LOCK_UNLOCKED,                         \<br />         }

Struct timer_list定义如下:

struct timer_list {
     //用来形成链表
     struct list_head entry;
     //定始器到达时间
     unsigned long expires;
     spinlock_t lock;
     unsigned long magic;
     //定时器时间到达后,所要运行的函数
     void (*function)(unsigned long);
     //定时器函数对应的参数
     unsigned long data;
     //挂载这个定时器的tvec_t_base_s.这个结构我们等会会看到
struct tvec_t_base_s *base; 
};

从上面的过程中我们可以看到TIMER_INITIALIZER()只是根据传入的参数初始化了struct timer_list结构.并把magic 成员初始化成TIMER_MAGIC

2): mod_timer():修改定时器的到时时间

int mod_timer(struct timer_list *timer, unsigned long expires)
{
     //如果该定时器没有定义fuction
     BUG_ON(!timer->function);
     //判断timer的magic是否为TIMER_MAGIC.如果不是,则将其修正为TIMER_MAGIC
     check_timer(timer);
     //如果要调整的时间就是定时器的定时时间而且已经被激活,则直接返回
     if (timer->expires == expires && timer_pending(timer))
          return 1;
     //调用_mod_timer().呆会再给出分析
     return __mod_timer(timer, expires);
}

3): add_timer()用来将定时器挂载到定时软中断队列,激活该定时器

static inline void add_timer(struct timer_list * timer)
{
     __mod_timer(timer, timer->expires);
}

可以看到mod_timer与add_timer 最后都会调用__mod_timer().为了分析这个函数,我们先来了解一下定时系统相关的数据结构.

tvec_bases: per cpu变量,它的定义如下:

static DEFINE_PER_CPU(tvec_base_t, tvec_bases) = { SPIN_LOCK_UNLOCKED };

由此可以看到tves_bases的数型数据为teves_base_t.数据结构的定义如下:

typedef struct tvec_t_base_s tvec_base_t;

struct tvec_t_base_s的定义:

struct tvec_t_base_s {
     spinlock_t lock;
     //上一次运行计时器的jiffies 值
     unsigned long timer_jiffies;
     struct timer_list *running_timer;
     //tv1 tv2 tv3 tv4 tv5是五个链表数组
     tvec_root_t tv1;
     tvec_t tv2;
     tvec_t tv3;
     tvec_t tv4;
     tvec_t tv5;
} ____cacheline_aligned_in_smp;
Tves_root_t与tvec_t的定义如下:
#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)
typedef struct tvec_s {
     struct list_head vec[TVN_SIZE];
} tvec_t;
typedef struct tvec_root_s {
     struct list_head vec[TVR_SIZE];
} tvec_root_t;

系统规定定时器最大超时时间间隔为0xFFFFFFFF.即为一个32位数.即使在64位系统上.如果超过此值也会将其强制设这oxFFFFFFFF(这在后面的代码分析中可以看到).内核最关心的就是间隔在0~255个HZ之间的定时器.次重要的是间隔在255~1<<(8+6)之间的定时器.第三重要的是间隔在1<<(8+6) ~ 1<<(8+6+6)之间的定器.依次往下推.也就是把32位的定时间隔为份了五个部份.1个8位.4个6位.所以内核定义了五个链表数组.第一个链表数组大小为8位大小,也即上面定义的 #define TVR_SIZE (1 << TVR_BITS).其它的四个数组大小为6位大小.即上面定义的#define TVN_SIZE (1 << TVN_BITS)

在加入定时器的时候,按照时间间隔把定时器加入到相应的数组即可.了解这点之后,就可以来看__mod_timer()的代码了:

//修改timer或者新增一个timer都会调用此接口

int __mod_timer(struct timer_list *timer, unsigned long expires)
{
     tvec_base_t *old_base, *new_base;
     unsigned long flags;
     int ret = 0;
//入口参数检测
     BUG_ON(!timer->function);
     check_timer(timer);
     spin_lock_irqsave(&timer->lock, flags);
     //取得当前CPU对应的tvec_bases
     new_base = &__get_cpu_var(tvec_bases);
repeat:
     //该定时器所在的tvec_bases.对于新增的timer.它的base字段为NULL
     old_base = timer->base;
     /*
     * Prevent deadlocks via ordering by old_base < new_base.
     */
     //在把timer从当前tvec_bases摘下来之前,要充分考虑好竞争的情况
     if (old_base && (new_base != old_base)) {
          //按次序获得锁
          if (old_base < new_base) {
               spin_lock(&new_base->lock);
               spin_lock(&old_base->lock);
          } else {
               spin_lock(&old_base->lock);
               spin_lock(&new_base->lock);
          }
          /*
          * The timer base might have been cancelled while we were
          * trying to take the lock(s):
          */
          //如果timer->base != old_base.那就是说在Lock的时候.其它CPU更改它的值
          //那就解锁.重新判断
          if (timer->base != old_base) {
               spin_unlock(&new_base->lock);
               spin_unlock(&old_base->lock);
               goto repeat;
          }
     } else {
          //old_base == NULl 或者是 new_base==old_base的情况
          //获得锁
          spin_lock(&new_base->lock);
          //同理,在Lock的时候timer会生了改变
          if (timer->base != old_base) {
               spin_unlock(&new_base->lock);
               goto repeat;
          }
     }
     /*
     * Delete the previous timeout (if there was any), and install
     * the new one:
     */
     //将其从其它的tvec_bases上删除.注意运行到这里的话,说话已经被Lock了
     if (old_base) {
          list_del(&timer->entry);
          ret = 1;
     }
     //修改它的定时器到达时间
     timer->expires = expires;
     //将其添加到new_base中
     internal_add_timer(new_base, timer);
     //修改base字段
     timer-base = new_base;
//操作完了,解锁
if (old_base && (new_base != old_base))
spin_unlock(&old_base->lock);
spin_unlock(&new_base->lock);
spin_unlock_irqrestore(&timer->lock, flags);
return ret;
}

最新文章