时钟是整个操作系统的脉搏,它为进程的时间片调度,定时事件提供了依据.另外,用户空间的很多操作都依赖于时钟,例如select.poll,make. 操作系统管理的时间为分两种,一种称为当前时间,也即我们日常生活所用的时间.这个时间一般保存在CMOS中.主板中有特定的芯片为其提供计时依据.另外一种时间称为相对时间.例如系统运行时间.显然对计算机而然,相对时间比当前时间更为重要.
二:与时钟有关的硬件处理.
1):实时时钟(RTC)
该时钟独立于CPU和其它芯片.即使PC断电,该时钟还是继续运行.该计时由一块单独的芯片处理,并把时钟值存放CMOS.该时间可参在IRQ8上周期性的产生时间信号.频率在2Hz ~ 8192Hz之间.但在linux中,只是用RTC来获取当前时间.
2):时间戳计时器(TSC)
CPU附带了一个64位的时间戳寄存器,当时钟信号到来的时候.该寄存器内容自动加1
3):可编程中断定时器(PIC)
该设备可以周期性的发送一个时间中断信号.发送中断信号的间隔可以对其进行编程控制.在linux系统中,该中断时间间隔由HZ表示.这个时间间隔也被称为一个节拍(tick).
在 ./include/asm-i386/param.h 定义
10#ifndef HZ
11#define HZ 100
12#endif
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触发的.
6)内核在2.6引入hrtimer,增强了clock event模型。 在当前内核里,有3种中断源将自己注册为一个clock_event_device结构:
struct clock_event_device { const char *name; unsigned int features; unsigned long max_delta_ns; unsigned long min_delta_ns; unsigned long mult; int shift; int rating; int irq; cpumask_t cpumask; int (*set_next_event)(unsigned long evt, struct clock_event_device *); void (*set_mode)(enum clock_event_mode mode, struct clock_event_device *); void (*event_handler)(struct clock_event_device *); void (*broadcast)(cpumask_t mask); struct list_head list; enum clock_event_mode mode; ktime_t next_event; }; |
最重要的是set_next_event(), event_handler(). 前者是设置下一个clock事件的触发条件, 一般就是往clock device里重设一下定时器. 后者是event handler, 事件处理函数. 该处理函数会在时钟中断ISR里被调用. 如果这个clock用来做为ticker时钟, 那么handler的执行和之前kernel的时钟中断ISR基本相同, 类似timer_tick(). 事件处理函数可以在运行时动态替换, 这就给kernel一个改变整个时钟中断处理方式的机会, 也就给highres tick及dynamic tick一个动态挂载的机会.
1) LAPIC Timer (每个CPU一个lapic_events)
arch/x86/kernel/apic_32.c, setup_APIC_timer():
clockevents_register_device(levt);
嗯,lapic_clockevent是每个lapic_events的默认值,每个CPU都在setup_APIC_Timer中用它对lapic_events
进行初始化。
2) HPET
arch/x86/kernel/hpet.c, hpet_legacy_clockevent_register():
clockevents_register_device(&hpet_clockevent);
3) PIT
arch/x86/kernel/i8253.c, setup_pit_timer():
clockevents_register_device(&pit_clockevent);
同时,系统中有一个唯一的global_clock_event指针,从代码上来看,它或者被赋值为&pit_clockevent,或者
被赋值为hpet_clockevent。 我的理解是,只要CONFIG_HPET_TIMER=y,那么就是后者。 而且,LAPIC Timer只
能是一种Local Interrupt Source,因此不可能用来给global_clock_event赋值。
7)
和硬件计时器(相关的数据结构主要有两个:
- struct clocksource :对硬件设备的抽象,描述时钟源信息
- struct clock_event_device :时钟的事件信息,包括当硬件时钟中断发生时要执行那些操作(实际上保存了相应函数的指针)。本文将该结构称作为“时钟事件设备”。
上述两个结构内核源代码中有较详细的注解,分别位于文件 clocksource.h 和 clockchips.h 中。需要特别注意的是结构 clock_event_device 的成员 event_handler ,它指定了当硬件时钟中断发生时,内核应该执行那些操作,也就是真正的时钟中断处理函数。
Linux 内核维护了两个链表,分别存储了系统中所有时钟源的信息和时钟事件设备的信息。这两个链表的表头在内核中分别是 clocksource_list 和 clockevent_devices 。图2-1显示了这两个链表。
图2-1 时钟源链表和时钟事件链表
三:时钟中断相关代码分析
time_init()是时钟初始化函数,他由asmlinkage void __init start_kernel()调用.具体代码如下:
//时钟中断初始化
/* * This is called directly from init code; we must delay timer setup in the * HPET case as we can't make the decision to turn on HPET this early in the * boot process. * * The chosen time_init function will usually be hpet_time_init, above, but * in the case of virtual hardware, an alternative function may be substituted. */ void __init time_init(void) { tsc_init(); late_time_init = choose_time_init(); } |
内核中检查0号中断的中断源为HPET还是PIT的代码流程是:
start_kernel() > time_init() > choose_time_init() == hpet_time_init:
void __init hpet_time_init(void)
{
if (!hpet_enable())
setup_pit_timer();
//注册时钟中断处理函数
time_init_hook();
}
注意,如果HPET失败,则使用PIT。 做完这个选择之后进行0号中断的相关设置。
-> HPET的情况:
hpet_enable() > hpet_legacy_clockevent_register() :
clockevents_register_device(&hpet_clockevent);
global_clock_event = &hpet_clockevent;
-> PIT的情况:
setup_pit_timer() :
clockevents_register_device(&pit_clockevent);
global_clock_event = &pit_clockevent;
-> 设置完中断源后:
time_init_hook():
/**
* static struct irqaction irq0 = {
* .handler = timer_interrupt,
* .flags = IRQF_DISABLED | IRQF_NOBALANCING | IRQF_IRQPOLL,
* .mask = CPU_MASK_NONE,
* .name = "timer"
* };
*/
irq0.mask = cpumask_of_cpu(0);
setup_irq(0, &irq0);
可见, 不管是其中断源是HPET还是PIT, 0号中断只能由CPU0来处理,其处理例程为timer_interrupt。
注:在start_kernel()>timekeeping_init()已队系统时间进行了初始化,而在2.6以前版本中初始化时间是在time_init()函数中初始化.
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():
/* * 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. */ irqreturn_t timer_interrupt(int irq, void *dev_id) { #ifdef CONFIG_X86_IO_APIC if (timer_ack) { /* * Subtle, when I/O APICs are used we have to ack timer IRQ * manually to reset the IRR bit for do_slow_gettimeoffset(). * This will also deassert NMI lines for the watchdog if run * on an 82489DX-based system. */ 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(); 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. */ u8 irq_v = inb_p( 0x61 ); /* read the current state */ outb_p( irq_v|0x80, 0x61 ); /* reset the IRQ */ } return IRQ_HANDLED; } |
核心处理函数为 do_timer_interrupt_hook():
static inline void do_timer_interrupt_hook(void)
{
global_clock_event->event_handler(global_clock_event);
}
而global_clock_event在前面提过了,中断处理函数初始化通过如下调用:
static inline void tick_set_periodic_handler(struct clock_event_device *dev,
int broadcast)
{
dev->event_handler = tick_handle_periodic;
}
初始化dev->event_handler函数的过程: clockevents_register_device() > clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev) > raw_notifier_call_chain(&clockevents_chain, reason, dev) > __raw_notifier_call_chain(nh, val, v, -1, NULL) > notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls) : ... ret = nb->notifier_call(nb, val, v); /* 注意这个参数v,指向clockevents_register_device * 的参数clock_event_device *global_clock_event; * 而参数nb,就是&clockevents_chain */ 然而,这个notifier_call函数指针赋值在start_kernel() > tick_init() : clockevents_register_notifier(&tick_notifier); 而tick_notifier的定义: static struct notifier_block tick_notifier = { .notifier_call = tick_notify, }; 还没有完呢, tick_notify> tick_check_new_device()> tick_setup-device()> tick_setup_periodic()> tick_set_periodic_handler() 这个dev->event_handler()处理函数终于初始化了 |
直接转到tick_handle_periodic():
void tick_handle_periodic(struct clock_event_device *dev) { int cpu = smp_processor_id(); ktime_t next; tick_periodic(cpu); if (dev->mode != CLOCK_EVT_MODE_ONESHOT) return; /* * Setup the next period for devices, which do not have * periodic mode: */ next = ktime_add(dev->next_event, tick_period); for (;;) { if (!clockevents_program_event(dev, next, ktime_get())) return; tick_periodic(cpu); next = ktime_add(next, tick_period); } } 其中tick_periodic调用就是以前的一系列更新操作,包括更新进程时间片等等. static void tick_periodic(int cpu) { if (tick_do_timer_cpu == cpu) { write_seqlock(&xtime_lock); /* Keep track of the next tick event */ tick_next_period = ktime_add(tick_next_period, tick_period); do_timer(1); write_sequnlock(&xtime_lock); } //更新当前运行进程的与时钟相关的信息 update_process_times(user_mode(get_irq_regs())); profile_tick(CPU_PROFILING); } |
我们忽略选择编译部份,转到do_timer()
void do_timer(unsigned long ticks)
{
// 更新jiffies计数.jiffies_64与jiffies在链接的时候,实际是指向同一个区域
jiffies_64 += ticks;
更新当前时间.xtime的更新
update_times(ticks);
}
Update_process_times()代码如下:
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id();
//这里判断时钟中断发生用户空间,还是发生在内核模式,然后计数值加1
/* Note: this timer irq context must be accounted for as well. */
if (user_tick)
account_user_time(p, jiffies_to_cputime(1));
else
account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1));
//激活时间软中断
run_local_timers();
if (rcu_pending(cpu))
rcu_check_callbacks(cpu, user_tick);
//减少时间片。
scheduler_tick();
run_posix_cpu_timers(p);
}
run_local_timers()
void run_local_timers(void)
{
raise_softirq(TIMER_SOFTIRQ);
}
而该中断的处理函数__run_timers():
static inline void __run_timers(tvec_base_t *base) { struct timer_list *timer; spin_lock_irq(&base->lock); /*这里进入定时器处理循环,利用系统全局jiffies与定时器基准jiffies进行对比,如果前者大,则表明某些定时器进行处理了,否则表示所有的 定时器都没有超时.因为CPU可能关闭中断,引起时钟中断信号丢失.可能jiffies要大base->timer_jiffies while (time_after_eq(jiffies, base->timer_jiffies)) { //定义并初始化一个链表 struct list_head work_list; struct list_head *head = &work_list; int index = base->timer_jiffies & TVR_MASK; /* * Cascade timers: */ //当index == 0时,说明已经循环了一个周期 //则将tv2填充tv1.如果tv2为空,则用tv3填充tv2.依次类推...... if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3)); //更新base->timer_jiffies ++base->timer_jiffies; //将base->tv1.vec项移至work_list.并将base->tv1.vec置空 list_replace_init(base->tv1.vec + index, &work_list); /*如果当前找到的时间数组对应的列表不为空,则表明该列表上串连的所有定时器都已经超时,循环调用每个定时器的处理 函数,并将其从列表中删除,直到列表为空为止。*/ while (!list_empty(head)) { void (*fn)(unsigned long); unsigned long data; //遍历链表中的每一项.运行它所对应的函数,并将定时器从链表上脱落 timer = list_first_entry(head, struct timer_list,entry); fn = timer->function; data = timer->data; timer_stats_account_timer(timer); set_running_timer(base, timer); detach_timer(timer, 1); spin_unlock_irq(&base->lock); { int preempt_count = preempt_count(); fn(data); if (preempt_count != preempt_count()) { printk(KERN_WARNING "huh, entered %p " "with preempt_count %08x, exited" " with %08x?\n", fn, preempt_count, preempt_count()); BUG(); } } spin_lock_irq(&base->lock); } } set_running_timer(base, NULL); spin_unlock_irq(&base->lock); } |
硬件定时器计完一个jiffies之后,会引起硬件中断,在硬件中断服务程序中会触发软中断,在定时器软中断服务程序中会调用 __run_timers()完成定时器多级hash table的处理,并且处理定时时间到的所有timer。__run_timers算法实现描述如下:
1、根据当前jiffes和base->timer_jiffies循环判断多级hash table扫描条件,如果满足条件,那么继续(2),否则退出循环。
2、通过base->timer_jiffies计算得到V1 table中需要处理的索引项。并且将索引高层hash table中的具体项,将该项中的timer分散到低层table中去。
3、增加base->timer_jiffies值,提取出V1中索引得到的定时器链表。
4、如果该定时器链表不为空,那么依次处理链表中的定时器,处理过程为调用定时器的处理函数timer->function。
下面对scheduler_tick()函数做解析吧.
void scheduler_tick(void) { int cpu = smp_processor_id(); struct rq *rq = cpu_rq(cpu); struct task_struct *curr = rq->curr; u64 next_tick = rq->tick_timestamp + TICK_NSEC; spin_lock(&rq->lock); __update_rq_clock(rq); /* * Let rq->clock advance by at least TICK_NSEC: */ if (unlikely(rq->clock < next_tick)) rq->clock = next_tick; rq->tick_timestamp = rq->clock; update_cpu_load(rq); if (curr != rq->idle) /* FIXME: needed? */ curr->sched_class->task_tick(rq, curr); spin_unlock(&rq->lock); #ifdef CONFIG_SMP rq->idle_at_tick = idle_cpu(cpu); trigger_load_balance(rq, cpu); #endif } |
scheduler_tick()函数负责减少运行进程的时间片计数值并且在需要时设置need_resched标志.该函数还负责平衡每个处理器的运行队列.
我们在do_timer()还漏掉了一个函数:
static inline void update_times(unsigned long ticks)
{
//更新xtime
update_wall_time();
//统计TASK_RUNNING TASK_UNINTERRUPTIBLE进程数量
calc_load(ticks);
}
实现软件时钟原理也比较简单:每一次硬件时钟中断到达时,内核更新的 jiffies ,然后将其和软件时钟的到期时间进行比较。如果 jiffies 等于或者大于软件时钟的到期时间,内核就执行软件时钟指定的函数。
接下来的几节会详细介绍 Linux2.6.23 是怎么实现软件时钟的。
- struct timer_list :软件时钟,记录了软件时钟的到期时间以及到期后要执行的操作。
-
- struct tvec_base :用于组织、管理软件时钟的结构。在 SMP 系统中,每个 CPU 有一个。
struct timer_list 主要成员
|
struct tvec_base 类型的成员
|
其中 tv1 的类型为 struct tvec_root ,tv 2~ tv 5的类型为 struct tvec .
struct tvec { |
可见它们实际上就是类型为 struct list_head 的数组,其中 TVN_SIZE 和 TVR_SIZE 在系统没有配置宏 CONFIG_BASE_SMALL 时分别被定义为64和256。
从图中可以清楚地看出:软件时钟( struct timer_list ,在图中由 timer 表示)以双向链表( struct list_head )的形式,按照它们的到期时间保存相应的( tv1~tv5 )中。tv1 中保存了相对于 timer_jiffies 下256个 tick 时间内到期的所有软件时钟; tv2 中保存了相对于 timer_jiffies 下256*64个 tick 时间内到期的所有软件时钟; tv3 中保存了相对于 timer_jiffies 下256*64*64个 tick 时间内到期的所有软件时钟; tv4 中保存了相对于 timer_jiffies 下256*64*64*64个 tick 时间内到期的所有软件时钟; tv5 中保存了相对于 timer_jiffies 下256*64*64*64*64个 tick 时间内到期的所有软件时钟。具体的说,从静态的角度看,假设 timer_jiffies 为0,那么 tv1[0] 保存着当前到期(到期时间等于 timer_jiffies )的软件时钟(需要马上被处理), tv1[1] 保存着下一个 tick 到达时,到期的所有软件时钟, tv1[n] (0<= n <=255)保存着下 n 个 tick 到达时,到期的所有软件时钟。而 tv2[0] 则保存着下256到511个 tick 之间到期所有软件时钟, tv2[1] 保存着下512到767个 tick 之间到期的所有软件时钟, tv2[n] (0<= n <=63)保存着下256*(n+1)到256*(n+2)-1个 tick 之间到达的所有软件时钟。 tv3~tv5 依次类推。
注:本章主要摘自IBM资料,下章主要对软时钟中断代码分析
资料:http://www.ibm.com/developerworks/cn/linux/l-cn-clocks/index.html
1):TIMER_INITIALIZER()用来声明一个定时器,它的定义如下:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = &boot_tvec_bases, \
}
注:在上章对里面的数据结构已经解释过了
2): mod_timer():修改定时器的到时时间
int mod_timer(struct timer_list *timer, unsigned long expires) { //如果该定时器没有定义fuction BUG_ON(!timer->function); timer_stats_timer_set_start_info(timer); /* * This is a common optimization triggered by the * networking code - if the timer is re-modified * to be the same thing then just return: */ //如果要调整的时间就是定时器的定时时间而且已经被激活,则直接返回 if (timer->expires == expires && timer_pending(timer)) return 1; //调用__mod_timer().呆会再给出分析 return __mod_timer(timer, expires); } |
从代码可以看出,如果所给的要修改的时间等于定时器原来的时间并且定时器现在正处于活动状态,则不修改,返回1,否则修改定时器时间,返回0。mod_timer()是一个非有效的更新处于活动状态的定时器的时间的方法,如果定时器处于非活动状态,则会激活定时器。在功能上,mod_timer()等价于:
3): add_timer()用来将定时器挂载到定时软中断队列,激活该定时器
static inline void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
__mod_timer(timer, timer->expires);
}
可以看到mod_timer与add_timer 最后都会调用__mod_timer().为了分析这个函数。
下面转入__mod_timer()的代码了:
int __mod_timer(struct timer_list *timer, unsigned long expires) { tvec_base_t *base, *new_base; unsigned long flags; int ret = 0; timer_stats_timer_set_start_info(timer); BUG_ON(!timer->function); base = lock_timer_base(timer, &flags); if (timer_pending(timer)) { detach_timer(timer, 0); ret = 1; } //取得当前CPU对应的tvec_bases new_base = __get_cpu_var(tvec_bases); if (base != new_base) { if (likely(base->running_timer != timer)) { /* See the comment in lock_timer_base() */ timer_set_base(timer, NULL); spin_unlock(&base->lock); base = new_base; spin_lock(&base->lock); timer_set_base(timer, base); } } timer->expires = expires; internal_add_timer(base, timer); spin_unlock_irqrestore(&base->lock, flags); return ret; } |
代码解析:
取得软件时钟所在 base 上的同步锁( struct tvec_base 变量中的自旋锁),并返回该软件时钟的 base ,保存在 base 变量中
如果该软件时钟处在 pending 状态(在 base 中,准备执行),则调用detach_timer()函数将该定时器从它原来所属的链表中删除。
取得本 CPU 上的 base 指针(类型为 struct tvec_base* ),保存在 new_base 中
如果 base 和 new_base 不一样,也就是说软件时钟发生了迁移(从一个 CPU 中移到了另一个 CPU 上),那么如果该软件时钟的处理函数当前没有在迁移之前的那个 CPU 上运行,则先将软件时钟的 base 设置为 NULL ,然后再将该软件时钟的 base 设置为 new_base 。否则,跳到5。
设置软件时钟的到期时间。
调用 internal_add_timer 函数将软件时钟添加到软件时钟的 base 中(本 CPU 的 base )
4)internal_add_timer()的代码如下:
static void internal_add_timer(tvec_base_t *base, struct timer_list *timer) { unsigned long expires = timer->expires; unsigned long idx = expires - base->timer_jiffies; struct list_head *vec; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; vec = base->tv1.vec + i; } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >> TVR_BITS) & TVN_MASK; vec = base->tv2.vec + i; } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; vec = base->tv3.vec + i; } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; vec = base->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 = base->tv1.vec + (base->timer_jiffies & TVR_MASK); } else { int i; /* If the timeout is larger than 0xffffffff on 64-bit * architectures then we use the maximum timeout: */ if (idx > 0xffffffffUL) { idx = 0xffffffffUL; expires = idx + base->timer_jiffies; } i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; vec = base->tv5.vec + i; } /* * Timers are FIFO: */ list_add_tail(&timer->entry, vec); } |
* 计算该软件时钟的到期时间和 timer_jiffies (当前正在处理的软件时钟的到期时间)的差值,作为索引保存到 idx 变量中。
* 判断 idx 所在的区间,在
o [0, 对象12]或者( 对象13, 0)(该软件时钟已经到期),则将要添加到 tv1 中
o [对象14, 对象15],则将要添加到 tv2 中
o [对象16, 对象17],则将要添加到 tv3 中
o [对象18, 对象19],则将要添加到 tv4 中
o [对象20, 对象21),则将要添加到 tv5 中,但实际上最大值为 0xffffffffUL
* 计算所要加入的具体位置(哪个链表中,即 tv1~tv5 的哪个子链表)
* 最后将其添加到相应的链表中
从这个函数可以得知,内核中是按照软件时钟到期时间的相对值(相对于 timer_jiffies 的值)将软件时钟添加到软件时钟所在的 base 中的。
5):定时器更新
每过一个HZ,就会检查当前是否有定时器的定时器时间到达.如果有,运行它所注册的函数,再将其删除.为了分析这一过程,我们先从定时器系统的初始化看起.
asmlinkage void __init start_kernel(void)
{
……
init_timers();
……
}
Init_timers()的定义如下:
void __init init_timers(void)
{
int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
init_timer_stats();
BUG_ON(err == NOTIFY_BAD);
register_cpu_notifier(&timers_nb);
//注册TIMER_SOFTIRQ软中断
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
}
我们在前面分析过,每当时钟当断函数到来的时候,就会打开定时器的软中断.运行其软中断函数.run_timer_softirq()
代码如下:
static void run_timer_softirq(struct softirq_action *h)
{
取得当于CPU的tvec_base_t结构
tvec_base_t *base = __get_cpu_var(tvec_bases);
hrtimer_run_queues();
//如果jiffies > base->timer_jiffies
if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}
__run_timers()代码如下:
static inline void __run_timers(struct tvec_base *base) { …… spin_lock_irq(&base->lock); while (time_after_eq(jiffies, base->timer_jiffies)) { …… int index = base->timer_jiffies & TVR_MASK; if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3)); ++base->timer_jiffies; list_replace_init(base->tv1.vec + index, &work_list); while (!list_empty(head)) { …… timer = list_first_entry(head, struct timer_list,entry); fn = timer->function; data = timer->data; …… set_running_timer(base, timer); detach_timer(timer, 1); spin_unlock_irq(&base->lock); { int preempt_count = preempt_count(); fn(data); …… } spin_lock_irq(&base->lock); } } set_running_timer(base, NULL); spin_unlock_irq(&base->lock); } |
代码解析:
获得 base 的同步锁
如果 jiffies 大于等于 timer_jiffies (当前正要处理的软件时钟的到期时间,说明可能有软件时钟到期了),就一直运行3~7,否则跳转至8
计算得到 tv1 的索引,该索引指明当前到期的软件时钟所在 tv1 中的链表,代码:
int index = base->timer_jiffies & TVR_MASK;
调用 cascade 函数对软件时钟进行必要的调整(稍后会介绍调整的过程)
使得 timer_jiffies 的数值增加1
取出相应的软件时钟链表
遍历该链表,对每个元素进行如下操作
* 设置当前软件时钟为 base 中正在运行的软件时钟(即保存当前软件时钟到 base-> running_timer 成员中)
* 将当前软件时钟从链表中删除,即卸载该软件时钟
* 释放锁,执行软件时钟处理程序
* 再次获得锁
设置当前 base 中不存在正在运行的软件时钟
释放锁
static int cascade(struct tvec_base *base, struct tvec *tv, int index)
{
struct timer_list *timer, *tmp;
struct list_head tv_list;
list_replace_init(tv->vec + index, &tv_list);
list_for_each_entry_safe(timer, tmp, &tv_list, entry) {
……
internal_add_timer(base, timer);
}
return index;
}
该函数根据索引,取出相应的 tv ( tv2~tv5 )中的链表,然后遍历链表每一个元素。按照其到期时间重新将软件时钟加入到软件时钟的 base 中。该函数返回 tv 中被调整的链表索引值
6):del_timer()删除定时器
//删除一个timer
int del_timer(struct timer_list *timer)
{
tvec_base_t *base;
unsigned long flags;
int ret = 0;
timer_stats_timer_clear_start_info(timer);
if (timer_pending(timer)) {
base = lock_timer_base(timer, &flags);
if (timer_pending(timer))
{
//将timer从链表中删除
detach_timer(timer, 1);
ret = 1;
}
spin_unlock_irqrestore(&base->lock, flags);
}
return ret;
}
被激活或未被激活的定时器都可以用该函数,如果定时器未被激活,该函数返回0;否则返回1。
注意,不需为已经超时定时器调用该函数,因为会被自动调用。删除定时器时需要等待其他cpu运行该定时器处理器程序都退出。
7): del_timer_sync()有竞争情况下的定时器删除
int del_timer_sync(struct timer_list *timer)
{
for (;;) {
int ret = try_to_del_timer_sync(timer);
if (ret >= 0)
return ret;
cpu_relax();
}
}
del_timer_sync 函数无限循环试图卸载该软件时钟,直到该软件时钟能够被成功卸载。从其实现中可以看出:如果一个软件时钟的处理函数正在执行时,对其的卸载操作将会失败。一直等到软件时钟的处理函数运行结束后,卸载操作才会成功。这样避免了在 SMP 系统中一个 CPU 正在执行软件时钟的处理函数,而另一个 CPU 则要将该软件时钟卸载所引发的问题。
定时器部份到这里就介绍完了.为了管理定时器.内核用了一个很巧妙的数据结构.值得好好的体会.
注:下章,介绍定时器的延迟。
schedule_timeout()函数,会使需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。
用法如下:
//将任务设置为可中断睡眠状态
set_current_state(TASK_INTERRUPTIBLE);
//小睡,“S”秒后醒
schedule_timeout(s*HZ)
fastcall signed long __sched schedule_timeout(signed long timeout) { struct timer_list timer; unsigned long expire; switch (timeout) { case MAX_SCHEDULE_TIMEOUT: /* * These two special cases are useful to be comfortable * in the caller. Nothing more. We could take * MAX_SCHEDULE_TIMEOUT from one of the negative value * but I' d like to return a valid offset (>=0) to allow * the caller to do everything it want with the retval. */ schedule(); goto out; default: /* * Another bit of PARANOID. Note that the retval will be * 0 since no piece of kernel is supposed to do a check * for a negative retval of schedule_timeout() (since it * should never happens anyway). You just have the printk() * that will tell you if something is gone wrong and where. */ if (timeout < 0) { printk(KERN_ERR "schedule_timeout: wrong timeout " "value %lx\n", timeout); dump_stack(); current->state = TASK_RUNNING; goto out; } } expire = timeout + jiffies; setup_timer(&timer, process_timeout, (unsigned long)current); __mod_timer(&timer, expire); schedule(); del_singleshot_timer_sync(&timer); timeout = expire - jiffies; out: return timeout < 0 ? 0 : timeout; } |
函数 schedule_timeout 定义了一个软件时钟变量 timer ,在计算到期时间后初始化这个软件时钟:设置软件时钟当时间到期时的处理函数为 process_timeout ,参数为当前进程描述符,设置软件时钟的到期时间为 expire 。之后调用 schedule() 函数。此时当前进程睡眠,交出执行权,内核调用其它进程运行。但内核在每一个时钟中断处理结束后都要检测这个软件时钟是否到期。如果到期,将调用 process_timeout 函数,参数为睡眠的那个进程描述符。
其中MAX_SCHEDULE_TIMEOUT是用来检查任务是否无限期睡眠,如果是,函数不会为它设置定时器,这时调度会立即执行。
setup_timer()函数对定时器的初始化
static inline void setup_timer(struct timer_list * timer,
void (*function)(unsigned long),
unsigned long data)
{
timer->function = function;
timer->data = data;
init_timer(timer);
}
其中对base字段的赋值是调用了init_timer()函数。
process_timeout()函数
static void process_timeout(unsigned long __data)
{
wake_up_process((struct task_struct *)__data);
}
函数 process_timeout 直接调用 wake_up_process 将进程唤醒。当内核重新调用该进程执行时,该进程继续执行 schedule_timeout 函数,执行流则从 schedule 函数中返回,之后调用 del_singleshot_timer_sync 函数将软件时钟卸载,然后函数 schedule_timeout 结束。
注:下章,做个总结。毕竟看了那么多,得总结下。看来我有个好习惯也,呵呵。
时间管理在内核占有非常重要的地位。相对于事件驱动而言,内核中大量的函数都是基于时间驱动的。
其中有些函数是周期执行的,像调度程序中的运行队列进行平衡调整。
时钟是整个操作系统的脉搏,它为进程的时间片调度,定时事件提供了依据.另外,用户空间的很多操作都依赖于时钟,例如select.poll,make. 操作系统管理的时间为分两种,一种称为当前时间,也即我们日常生活所用的时间.这个时间一般保存在CMOS中.主板中有特定的芯片为其提供计时依据.另外一种时间称为相对时间.例如系统运行时间.显然对计算机而然,相对时间比当前时间更为重要.
内核通过控制时钟中断维护时间,且时钟中断对于管理操作系统相当的重要,大量内核函数的生命周期
都离不开流逝的时间的控制。时钟中断的作用:
1.更新系统运行时间
2.更新实际时间
3.在SMP系统中,均衡调度程序中各处理器上的运行队列。如果运行队列负载不均衡的话,尽量使
它们均衡
4.检查当前进程是否耗尽自己的时间片。如果用尽,就重新进行调度
5.运行超时的动态定时器
6.更新资源消耗和处理器的时间统计值
时钟中断处理程序具体执行的工作:
1.获得xtime_lock锁,以便对访问jiffies_64和墙上时间进行更新<xtime_lock防止多cpu的竞态访问>
2.需要时应答或重新设置系统时钟
3.调用do_timer(),执行以下工作:
1>给jiffies_64变量增加1;
2>更新资源消耗统计值,比如当前进程所消耗的系统时间和用户时间
3>执行已经到期的动态定时器
4>执行scheduler_tick(),函数减少当前运行进程的时间片计数值并且
设置need_resched标志
5>更新实际时间,存放在xtime变量中,计算平均负载值。