Linux 时钟处理机制
2010-09-14 11:59
369 查看
在Linux操作系统中,很多活动都和时间有关,例如:进程调度和网络处理等等。所以说,了解Linux操作系统中的时钟处理机制有助于更好地了解Linux操作系统的运作方式。本文分析了Linux2.6.25内核的时钟处理机制,首先介绍了在计算机系统中的一些硬件计时器,然后重点介绍了Linux操作系统中的硬件时钟和软件时钟的处理过程以及软件时钟的应用。最后对全文进行了总结。
1计算机系统中的计时器
在计算机系统中存在着许多硬件计时器,例如RealTimerClock(RTC)、TimeStampCounter(TSC)和ProgrammableIntervalTimer(PIT)等等。
这部分内容不是本文的中点,这里仅仅简单介绍几种,更多内容参见参考文献:
●RealTimerClock(RTC):
独立于整个计算机系统(例如:CPU和其他chip)
内核利用其获取系统当前时间和日期
●TimeStampCounter(TSC):
从Pentium起,提供一个寄存器TSC,用来累计每一次外部振荡器产生的时钟信号
通过指令rdtsc访问这个寄存器
比起PIT,TSC可以提供更精确的时间测量
●ProgrammableIntervalTimer(PIT):
时间测量设备
内核使用的产生时钟中断的设备,产生的时钟中断依赖于硬件的体系结构,慢的为10ms一次,快的为1ms一次
HighPrecisionEventTimer(HPET):
PIT和RTC的替代者,和之前的计时器相比,HPET提供了更高的时钟频率(至少10MHz)以及更宽的计数器宽度(64位)
一个HPET包括了一个固定频率的数值增加的计数器以及3到32个独立的计时器,这每一个计时器有包涵了一个比较器和一个寄存器(保存一个数值,表示触发中断的时机)。每一个比较器都比较计数器中的数值和寄存器中的数值,当这两个数值相等时,将产生一个中断
2硬件时钟处理
这里所说的硬件时钟处理特指的是硬件计时器时钟中断的处理过程。
2.1数据结构
和硬件计时器(本文又称作硬件时钟,区别于软件时钟)相关的数据结构主要有两个:
structclocksource:对硬件设备的抽象,描述时钟源信息
structclock_event_device:时钟的事件信息,包括当硬件时钟中断发生时要执行那些操作(实际上保存了相应函数的指针)。本文将该结构称作为“时钟事件设备”。
上述两个结构内核源代码中有较详细的注解,分别位于文件clocksource.h和clockchips.h中。需要特别注意的是结构clock_event_device的成员event_handler,它指定了当硬件时钟中断发生时,内核应该执行那些操作,也就是真正的时钟中断处理函数。在2.3节“时钟初始化”部分会介绍它真正指向哪个函数。
Linux内核维护了两个链表,分别存储了系统中所有时钟源的信息和时钟事件设备的信息。这两个链表的表头在内核中分别是clocksource_list和clockevent_devices。图2-1显示了这两个链表。
图2-1时钟源链表和时钟事件链表
2.2通知链技术(notificationchain)
在时钟处理这部分中,内核用到了所谓的“通知链(notificationchain)”技术。所以在介绍时钟处理过程之前先来了解下“通知链”技术。
在Linux内核中,各个子系统之间有很强的相互关系,一些被一个子系统生成或者被探测到的事件,很可能是另一个或者多个子系统感兴趣的,也就是说这个事件的获取者必须能够通知所有对该事件感兴趣的子系统,并且还需要这种通知机制具有一定的通用性。基于这些,Linux内核引入了“通知链”技术。
2.2.1数据结构:
通知链有四种类型,
1原子通知链(Atomicnotifierchains):通知链元素的回调函数(当事件发生时要执行的函数)只能在中断上下文中运行,不允许阻塞
2可阻塞通知链(Blockingnotifierchains):通知链元素的回调函数在进程上下文中运行,允许阻塞
3原始通知链(Rawnotifierchains):对通知链元素的回调函数没有任何限制,所有锁和保护机制都由调用者维护
4SRCU通知链(SRCUnotifierchains):可阻塞通知链的一种变体
所以对应了四种通知链头结构:
●structatomic_notifier_head:原子通知链的链头
●structblocking_notifier_head:可阻塞通知链的链头
●structraw_notifier_head:原始通知链的链头
●structsrcu_notifier_head:SRCU通知链的链头
通知链元素的类型:
structnotifier_block:通知链中的元素,记录了当发出通知时,应该执行的操作(即回调函数)
链头中保存着指向元素链表的指针。通知链元素结构则保存着回调函数的类型以及优先级,参见notifier.h文件。
2.2.2运作机制
通知链的运作机制包括两个角色:
1被通知者:对某一事件感兴趣一方。定义了当事件发生时,相应的处理函数,即回调函数。但需要事先将其注册到通知链中(被通知者注册的动作就是在通知链中增加一项)。
2通知者:事件的通知者。当检测到某事件,或者本身产生事件时,通知所有对该事件感兴趣的一方事件发生。他定义了一个通知链,其中保存了每一个被通知者对事件的处理函数(回调函数)。通知这个过程实际上就是遍历通知链中的每一项,然后调用相应的事件处理函数。
包括以下过程:
1通知者定义通知链
2被通知者向通知链中注册回调函数
3当事件发生时,通知者发出通知(执行通知链中所有元素的回调函数)
整个过程可以看作是“发布——订阅”模型(参见参考资料)
被通知者调用notifier_chain_register函数注册回调函数,该函数按照优先级将回调函数加入到通知链中。注销回调函数则使用notifier_chain_unregister函数,即将回调函数从通知链中删除。2.2.1节讲述的4种通知链各有相应的注册和注销函数,但是他们最终都是调用上述两个函数完成注册和注销功能的。有兴趣的读者可以自行查阅内核代码。
通知者调用notifier_call_chain函数通知事件的到达,这个函数会遍历通知链中所有的元素,然后依次调用每一个的回调函数(即完成通知动作)。2.2.1节讲述的4种通知链也都有其对应的通知函数,这些函数也都是最终调用notifier_call_chain函数完成事件的通知。
更多关于通知链的内容,参见参考文献。
由以上的叙述,“通知链”技术可以概括为:事件的被通知者将事件发生时应该执行的操作通过函数指针方式保存在链表(通知链)中,然后当事件发生时通知者依次执行链表中每一个元素的回调函数完成通知。
2.3时钟初始化
内核初始化部分(start_kernel函数)和时钟相关的过程主要有以下几个:
1tick_init()
2init_timers()
3hrtimers_init()
4time_init()
其中函数hrtimers_init()和高精度时钟相关(本文暂不介绍这部分内容)。下面将详细介绍剩下三个函数。
2.3.1tick_init函数
函数tick_init()很简单,调用clockevents_register_notifier函数向clockevents_chain通知链注册元素:tick_notifier。这个元素的回调函数指明了当时钟事件设备信息发生变化(例如新加入一个时钟事件设备等等)时,应该执行的操作,该回调函数为tick_notify(参见2.4节)。
2.3.2init_timers函数
注:本文中所有代码均来自于Linux2.6.25源代码
函数init_timers()的实现如清单2-1(省略了部分和
主要功能无关的内容,以后代码同样方式处理)
清单2-1init_timers函数
代码解释:
●初始化本CPU上的软件时钟相关的数据结构,参见3.2节
●向cpu_chain通知链注册元素timers_nb,该元素的回调函数用于初始化指定CPU上的软件时钟相关的数据结构
●初始化时钟的软中断处理函数
2.3.3time_init函数
函数time_init的实现如清单2-2
清单2-2time_init函数
函数init_tsc_clocksource初始化tsc时钟源。choose_time_init实际是函数hpet_time_init,其代码清单2-3
清单2-3hpet_time_init函数
函数hpet_enable检测系统是否可以使用hpet时钟,如果可以则初始化hpet时钟。否则初始化pit时钟。最后设置硬件时钟发生时的处理函数(参见2.4节)。
初始化硬件时钟这个过程主要包括以下两个过程(参见hpet_enable的实现):
1初始化时钟源信息(structclocksource类型的变量),并将其添加到时钟源链表中,即clocksource_list链表(参见图2-1)。
2初始化时钟事件设备信息(structclock_event_device类型的变量),并向通知链clockevents_chain发布通知:一个时钟事件设备要被添加到系统中。在通知(执行回调函数)结束后,该时钟事件设备被添加到时钟事件设备链表中,即clockevent_devices链表(参见图2-1)。有关通知链的内容参见2.2节。
需要注意的是在初始化时钟事件设备时,全局变量global_clock_event被赋予了相应的值。该变量保存着系统中当前正在使用的时钟事件设备(保存了系统当前使用的硬件时钟中断发生时,要执行的中断处理函数的指针)。
2.4硬件时钟处理过程
由2.3.3可知硬件时钟中断的处理函数保存在静态变量irq0中,其定义如清单2-4
清单2-4变量irq0定义
由定义可知:函数timer_event_interrupt为时钟中断处理函数,其定义如清单2-5
清单2-5timer_event_interrupt函数
从代码中可以看出:函数timer_event_interrupt实际上调用的是global_clock_event变量的event_handler成员。那event_handler成员指向哪里呢?
为了说明这个问题,不妨假设系统中使用的是hpet时钟。由2.3.3节可知global_clock_event指向hpet时钟事件设备(hpet_clockevent)。查看hpet_enable函数的代码并没有发现有对event_handler成员的赋值。所以继续查看时钟事件设备加入事件的处理函数tick_notify,该函数记录了当时钟事件设备发生变化(例如,新时钟事件设备的加入)时,执行那些操作(参见2.3.1节),代码如清单2-6
清单2-6tick_notify函数
由代码可知:对于新加入时钟事件设备这个事件,将会调用函数tick_check_new_device。顺着该函数的调用序列向下查找。tick_set_periodic_handler函数将时钟事件设备的event_handler成员赋值为tick_handle_periodic函数的地址。由此可知,函数tick_handle_periodic为硬件时钟中断发生时,真正的运行函数。
函数tick_handle_periodic的处理过程分成了以下两个部分:
1全局处理:整个系统中的信息处理
2局部处理:局部于本地CPU的处理
●总结一下,一次时钟中断发生后,OS主要执行的操作(tick_handle_periodic):
●全局处理(仅在一个CPU上运行):
1更新jiffies_64
2更新xtimer和当前时钟源信息等
3根据tick计算avenrun负载
●局部处理(每个CPU都要运行):
1根据当前在用户态还是核心态,统计当前进程的时间:用户态时间还是核心态时间
2唤醒TIMER_SOFTIRQ软中断
3唤醒RCU软中断
4调用scheduler_tick(更新进程时间片等等操作,更多内容参见参考文献)
5profile_tick函数调用
以上就介绍完了硬件时钟的处理过程,下面来看软件时钟。
3软件时钟处理
这里所说“软件时钟”指的是软件定时器(SoftwareTimers),是一个软件上的概念,是建立在硬件时钟基础之上的。它记录了未来某一时刻要执行的操作(函数),并使得当这一时刻真正到来时,这些操作(函数)能够被按时执行。举个例子说明:它就像生活中的闹铃,给闹铃设定振铃时间(未来的某一时间)后,当时间(相当于硬件时钟)更新到这个振铃时间后,闹铃就会振铃。这个振铃时间好比软件时钟的到期时间,振铃这个动作好比软件时钟到期后要执行的函数,而闹铃时间更新好比硬件时钟的更新。
实现软件时钟原理也比较简单:每一次硬件时钟中断到达时,内核更新的jiffies,然后将其和软件时钟的到期时间进行比较。如果jiffies等于或者大于软件时钟的到期时间,内核就执行软件时钟指定的函数。
接下来的几节会详细介绍Linux2.6.25是怎么实现软件时钟的。
3.1相关数据结构
structtimer_list:软件时钟,记录了软件时钟的到期时间以及到期后要执行的操作。具体的成员以及含义见表3-1。
structtvec_base:用于组织、管理软件时钟的结构。在SMP系统中,每个CPU有一个。具体的成员以及含义参见表3-2。
表3-1structtimer_list主要成员
表3-2structtvec_base类型的成员
其中tv1的类型为structtvec_root,tv2~tv5的类型为structtvec,清单3-1显示它们的定义
清单3-1structtvec_root和structtvec的定义
可见它们实际上就是类型为structlist_head的数组,其中TVN_SIZE和TVR_SIZE在系统没有配置宏CONFIG_BASE_SMALL时分别被定义为64和256。
3.2数据结构之间的关系
图3-1显示了以上数据结构之间的关系:
从图中可以清楚地看出:软件时钟(structtimer_list,在图中由timer表示)以双向链表(structlist_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
(0<=n<=255)保存着下n个tick到达时,到期的所有软件时钟。而tv2[0]则保存着下256到511个tick之间到期所有软件时钟,tv2[1]保存着下512到767个tick之间到期的所有软件时钟,tv2
(0<=n<=63)保存着下256*(n+1)到256*(n+2)-1个tick之间到达的所有软件时钟。tv3~tv5依次类推。
注:一个tick的长度指的是两次硬件时钟中断发生之间的时间间隔
从上面的说明中可以看出:软件时钟是按照其到期时间相对于当前正在处理的软件时钟的到期时间(timer_jiffies的数值)保存在structtvec_base变量中的。而且这个到期时间的最大相对值(到期时间-timer_jiffies)为0xffffffffUL(tv5最后一个元素能够表示的相对到期时间的最大值)。
还需要注意的是软件时钟的处理是局部于CPU的,所以在SMP系统中每一个CPU都保存一个类型为structtvec_base的变量,用来组织、管理本CPU的软件时钟。从图中也可以看出structtvec_base变量是per-CPU的(关于per-CPU的变量原理和使用参见参考资料)。
由于以后的讲解经常要提到每个CPU相关的structtvec_base变量,所以为了方便,称保存软件时钟的structtvec_base变量为该软件时钟的base,或称CPU的base。
3.3添加或删除软件时钟
在了解了软件时钟的数据组织关系之后,现在来看一下如何添加以及删除一个软件时钟。
3.3.1添加软件时钟
在Linux内核中要添加一个软件时钟,首先必须分配structtimer_list类型的变量,然后调用函数add_timer()将该软件时钟添加到相应调用add_timer函数的CPU的base中。Add_timer是对函数__mod_timer()的一层包装。函数__mod_timer()的代码如清单3-2:
清单3-2__mod_timer函数
代码解释:
注:卸载软件时钟的意思是指将软件时钟从软件时钟所在base中删除,以后所说的卸载软件时钟也都是这个意思
取得软件时钟所在base上的同步锁(structtvec_base变量中的自旋锁),并返回该软件时钟的base,保存在base变量中
如果该软件时钟处在pending状态(在base中,准备执行),则卸载该软件时钟
取得本CPU上的base指针(类型为structtvec_base*),保存在new_base中
如果base和new_base不一样,也就是说软件时钟发生了迁移(从一个CPU中移到了另一个CPU上),那么如果该软件时钟的处理函数当前没有在迁移之前的那个CPU上运行,则先将软件时钟的base设置为NULL,然后再将该软件时钟的base设置为new_base。否则,跳到5。
设置软件时钟的到期时间
调用internal_add_timer函数将软件时钟添加到软件时钟的base中(本CPU的base)
释放锁
这里有必要详细说明一下软件时钟如何被添加到软件时钟的base中的(添加到本CPUbase的tv1~tv5里面),因为这是软件时钟处理的基础。来看函数internal_add_timer函数的实现,如清单3-3
清单3-3internal_add_timer函数
代码解释:
计算该软件时钟的到期时间和timer_jiffies(当前正在处理的软件时钟的到期时间)的差值,作为索引保存到idx变量中。
判断idx所在的区间,在
[0,]或者(,0)(该软件时钟已经到期),则将要添加到tv1中
[,],则将要添加到tv2中
[,],则将要添加到tv3中
[,],则将要添加到tv4中
[,),则将要添加到tv5中,但实际上最大值为0xffffffffUL
计算所要加入的具体位置(哪个链表中,即tv1~tv5的哪个子链表,参考图3-1)
最后将其添加到相应的链表中
从这个函数可以得知,内核中是按照软件时钟到期时间的相对值(相对于timer_jiffies的值)将软件时钟添加到软件时钟所在的base中的。
3.3.2删除软件时钟
内核可调用del_timer函数删除软件时钟,del_timer的代码如清单3-4
清单3-4del_timer函数
代码解释:
检测该软件时钟是否处在pending状态(在base中,准备运行),如果不是则直接函数返回
如果处于pending状态,则获得锁
再次检测软件时钟是否处于pending状态(该软件时钟可能被卸载了),不是则释放锁然后函数返回
如果还是pending状态,则将其卸载,之后释放锁,函数返回
如果在SMP系统中,则需使用del_timer_sync函数来删除软件时钟。在讲解del_timer_sync函数之前,先来看下try_to_del_timer_sync函数的实现(该函数被del_timer_sync函数使用),其代码如清单3-5
清单3-5try_to_del_timer_sync函数
该函数检测当前运行的软件时钟是不是该软件时钟,如果是,则函数返回-1,表明目前不能删除该软件时钟;如果不是检测该软件时钟是否处于pending状态,如果不是,则函数返回0,表明软件时钟已经被卸载,如果处于pending状态再把软件时钟卸载,函数返回1,表明成功卸载该软件时钟。
接下来,再来看看函数del_timer_sync定义,如清单3-6
清单3-6del_timer_sync函数
del_timer_sync函数无限循环试图卸载该软件时钟,直到该软件时钟能够被成功卸载。从其实现中可以看出:如果一个软件时钟的处理函数正在执行时,对其的卸载操作将会失败。一直等到软件时钟的处理函数运行结束后,卸载操作才会成功。这样避免了在SMP系统中一个CPU正在执行软件时钟的处理函数,而另一个CPU则要将该软件时钟卸载所引发的问题。
3.3时钟的软中断处理
软件时钟的处理是在时钟的软中断中进行的。
3.3.1软中断初始化
软中断的一个重要的处理时机是在每个硬件中断处理完成后(参见irq_exit函数),且由2.4节的内容可知:在硬件时钟中断处理中,会唤醒时钟的软中断,所以每次硬件时钟中断处理函数执行完成后都要进行时钟的软中断处理。和时钟相关的软中断是TIMER_SOFTIRQ,其处理函数为run_timer_softirq,该函数用来处理所有的软件时钟。这部分初始化代码在函数init_timers中进行,如清单3-7
清单3-7init_timers函数
3.3.2处理过程
函数run_timer_softirq所作的工作就是找出所有到期的软件时钟,然后依次执行其处理函数。其代码如清单3-8
清单3-8run_timer_softirq函数
函数首先获得到本地CPU的base。然后检测如果jiffies
注:hrtimer_run_pending()函数是高精度时钟的处理。本文暂没有涉及高精度时钟相关的内容。
大于等于timer_jiffies,说明可能已经有软件时钟到期了,此
时就要进行软件时钟的处理,调用函数__run_timers进行处
理。如果jiffies小于timer_jiffies,表明没有软件时钟到期,
则不用对软件时钟进行处理。函数返回。
接下来看一下函数__run_timers都作了些什么,如清单3-9
清单3-9__run_timers函数
代码解释:
获得base的同步锁
如果jiffies大于等于timer_jiffies(当前正要处理的软件时钟的到期时间,说明可能有软件时钟到期了),就一直运行3~7,否则跳转至8
计算得到tv1的索引,该索引指明当前到期的软件时钟所在tv1中的链表(结构参见3.2节),代码:
调用cascade函数对软件时钟进行必要的调整(稍后会介绍调整的过程)
使得timer_jiffies的数值增加1
取出相应的软件时钟链表
遍历该链表,对每个元素进行如下操作
设置当前软件时钟为base中正在运行的软件时钟(即保存当前软件时钟到base->running_timer成员中)
将当前软件时钟从链表中删除,即卸载该软件时钟
释放锁,执行软件时钟处理程序
再次获得锁
设置当前base中不存在正在运行的软件时钟
释放锁
3.3.3软件时钟调整过程
函数cascade用于调整软件时钟(这个调整过程是指:将马上就要到期的软件时钟从其所在的链表中删除,重新计算到期时间的相对值(到期时间-timer_jiffies),然后根据该值重新插入到base中)。注意到在软件时钟处理过程中,每次都是从tv1中取出一个链表进行处理,而不是从tv2~tv5中取,所以对软件时钟就要进行必要的调整。
在讲解cascade函数之前,再从直观上理解下为什么需要进行调整。所有软件时钟都是按照其到期时间的相对值(相对于timer_jiffies)被调加到base中的。但是timer_jiffies的数值都会在处理中增加1(如3.3.2节所示),也就是说这个相对值会随着处理发生变化,当这个相对值小于等于256时,就要将软件时钟从tv2~tv5中转移到tv1中(tv1中保存着下256个tick内到期的所有软件时钟)。
函数cascade的实现如清单3-10
清单3-10cascade函数
该函数根据索引,取出相应的tv(tv2~tv5)中的链表,然后遍历链表每一个元素。按照其到期时间重新将软件时钟加入到软件时钟的base中。该函数返回tv中被调整的链表索引值(参见图3-1)。
清单3-9中调整软件时钟的代码如下:
这部分代码表明:如果index有0再到0时(index是对timer_jiffies取模),说明时间已经过了256个tick,这时要把tv2中软件时钟转移到tv1中。如果index和第一个cascade函数的返回值都从0再到到0时,说明时间已经过了256*64个tick,这时要把tv3中软件时钟转移到tv1或者tv2中。之后的调整过程依次类推。
3.4自我激活
软件时钟可分为两种类型:
仅仅激活一次
激活多次或者周期性激活
多次激活的实现机制就是要在软件时钟处理函数中重新设置软件时钟的到期时间为将来的一个时间,这个过程通过调用mod_timer函数来实现。该函数的实现如清单3-11
清单3-11mod_timer函数
从代码中可以看出,该函数实际上调用__mod_timer函数(参见3.3.1节)来调整软件时钟的到期时间。
3.5软件时钟的应用
软件时钟的处理是在处理软中断时触发的,而软中断的处理又会紧接着硬件中断处理结束而进行,并且系统会周期地产生时钟中断(硬件中断),这样,软件时钟的处理至少会在系统每一次时钟中断处理完成后触发(如果软件时钟的到期时间大于系统当前的jiffies,表明时间未到期,则不会调用保存在软件时钟中的函数,但此时的确提供了处理软件时钟的时机)。从这点上看,软件时钟会有较快的相应——一旦时间到期,保存在软件时钟中的函数会将快地被调用(在时钟软中断中被调用,参见3.3.2节)。所以内核中凡是需要隔一段时间间隔后作指定操作的过程都通过软件时钟完成。例如大部分设备驱动程序使用软件时钟探测异常条件、软盘驱动程序利用软件时钟关闭有一段时间没有被访问软盘的设备马达、进程的定时睡眠(schedule_timeout函数)和网络超时重传等等。
本节主要通过介绍进程的定时睡眠(schedule_timeout函数)和网络超时重传来说明软件时钟的应用。
3.5.1进程的定时睡眠
函数schedule_timeout的代码如清单3-12
清单3-12函数schedule_timeout
函数schedule_timeout定义了一个软件时钟变量timer,在计算到期时间后初始化这个软件时钟:设置软件时钟当时间到期时的处理函数为process_timeout,参数为当前进程描述符,设置软件时钟的到期时间为expire。之后调用schedule()函数。此时当前进程睡眠,交出执行权,内核调用其它进程运行。但内核在每一个时钟中断处理结束后都要检测这个软件时钟是否到期。如果到期,将调用process_timeout函数,参数为睡眠的那个进程描述符。process_timeout函数的代码如清单3-13。
清单3-13函数process_timeout
函数process_timeout直接调用wake_up_process将进程唤醒。当内核重新调用该进程执行时,该进程继续执行schedule_timeout函数,执行流则从schedule函数中返回,之后调用del_singleshot_timer_sync函数将软件时钟卸载,然后函数schedule_timeout结束。函数del_singleshot_timer_sync是实际上就是函数del_timer_sync(参见3.3.2节),如清单3-14
清单3-14函数del_singleshot_timer_sync
以上就是进程定时睡眠的实现过程。接下来介绍的是软件时钟在网络超时重传上的应用。
3.5.2网路超时重传
对于TCP协议而言,如果某次发送完数据包后,并超过一定的时间间隔还没有收到这次发送数据包的ACK时,TCP协议规定要重新发送这个数据包。
在Linux2.6.25的内核中,这种数据的重新发送使用软件时钟来完成。这个软件时钟保存在面向连接的套接字(对应内核中inet_connection_sock结构)中。对这个域的初始在函数tcp_init_xmit_timers中,如清单3-15
清单3-15函数tcp_init_xmit_timers、函数inet_csk_init_xmit_timers和函数setup_timer
在函数inet_csk_init_xmit_timers中,变量icsk就是前面提到的面向连接的套接字,其成员icsk_retransmit_timer则为实现超时重传的软件时钟。该函数调用setup_timer函数将函数tcp_write_timer(参考函数tcp_init_xmit_timers)设置为软件时钟icsk->icsk_retransmit_timer当时间到期后的处理函数。初始化的时候并没有设置该软件时钟的到期时间。
在TCP协议具体的一次数据包发送中,函数tcp_write_xmit用来将数据包从TCP层发送到网络层,如清单3-16。
清单3-16tcp_write_xmit函数
注意该函数中加粗的函数,其中tcp_transmit_skb函数是真正将数据包由TCP层发送到网络层中的函数。数据发送后,将调用函数tcp_event_new_data_sent,而后者又会调用函数inet_csk_reset_xmit_timer来设置超时软件时钟的到期时间。
当函数tcp_event_new_data_sent结束之后,处理超时的软件时钟已经设置好了。内核会在每一次时钟中断处理完成后检测该软件时钟是否到期。如果网络真的超时,没有ACK返回,那么当该软件时钟到期后内核就会执行函数tcp_write_timer。函数tcp_write_timer将进行数据包的重新发送,并重新设置超时重传软件时钟的到期时间。
4总结
本文介绍了Linux内核的时钟处理机制。首先简单介绍了系统的硬件计时器,然后重点介绍了硬件时钟的处理过程和软件时钟的处理过程以及软件时钟的应用。
由于linux还不是一个实时的操作系统,因此如果需要更高精度,或者更精确的定时的话,可能就需要打一些实时的补丁,或者用商用版的实时linux,.
这里内的定时器最小间隔也就是1个tick.
这里还有一个要注意的,我这里的分析并没有分析内核新的hrt定时器.这个定时器是MontaVista加入到内核的一个高精度的定时器的实现.
先来看几个相关的数据结构.
///这个是一个最主要的数据结构,表示一个完整的定时器级联表
Java代码
structtvec_base{
///自旋锁
spinlock_tlock;
///表示由本地cpu正在处理的定时器链表
structtimer_list*running_timer;
///这个表示当前的定时器级联表中最快要超时的定时器的jiffer
unsignedlongtimer_jiffies;
///下面表示了5级的定时器级联表.
structtvec_roottv1;
structtvectv2;
structtvectv3;
structtvectv4;
structtvectv5;
}____cacheline_aligned;
下面来看tvec和tvec_root的结构:
Java代码
structtvec{
structlist_headvec[TVN_SIZE];
};
structtvec_root{
structlist_headvec[TVR_SIZE];
};
可以看到这两个结构也就是hash链表.每次通过超时jiffies来计算slot,然后插入到链表.这里链表是FIFO的.这里除了tv5外其他几个都是简单的与TVR_MASK按位与计算.
Java代码
structtimer_list{
structlist_headentry;
///超时节拍数
unsignedlongexpires;
///定时器将要执行的回调函数
void(*function)(unsignedlong);
///传递给回调函数的参数
unsignedlongdata;
///从属于那个base
structtvec_base*base;
};
///定义了一个percpu变量.这里要知道定时器的注册和触发执行一定是在相同的cpu上的.
structtvec_baseboot_tvec_bases;
staticDEFINE_PER_CPU(structtvec_base*,tvec_bases)=&boot_tvec_bases;
内核注册定时器最终都会通过调用internal_add_timer来实现.具体的工作方式是这样的:
1如果定时器在接下来的0~255个jiffies中到期,则将定时器添加到tv1.
2如果定时器是在接下来的256*64个jiffies中到期,则将定时器添加到tv2.
3如果定时器是在接下来的256*64*64个jiffies中到期,则将定时器添加到tv3.
4如果定时器是在接下来的256*64*64*64个jiffies中到期,则将定时器添加到tv4.
5如果更大的超时,则利用0xffffffff来计算hash,然后插入到tv5(这个只会出现在64的系统).
看下面的图就能比较清晰了:
接下来看源码:
Java代码
staticvoidinternal_add_timer(structtvec_base*base,structtimer_list*timer)
{
///取出超时jiffies
unsignedlongexpires=timer->expires;
///得到定时器还有多长时间到期(这里是相比于最短的那个定时器)
unsignedlongidx=expires-base->timer_jiffies;
structlist_head*vec;
///开始判断该把定时器加入到那个队列.依次为tv1到tv5
if(idx<TVR_SIZE){
inti=expires&TVR_MASK;
vec=base->tv1.vec+i;
}elseif(idx<1<<(TVR_BITS+TVN_BITS)){
inti=(expires>>TVR_BITS)&TVN_MASK;
vec=base->tv2.vec+i;
}elseif(idx<1<<(TVR_BITS+2*TVN_BITS)){
inti=(expires>>(TVR_BITS+TVN_BITS))&TVN_MASK;
vec=base->tv3.vec+i;
}elseif(idx<1<<(TVR_BITS+3*TVN_BITS)){
inti=(expires>>(TVR_BITS+2*TVN_BITS))&TVN_MASK;
vec=base->tv4.vec+i;
}elseif((signedlong)idx<0){
/*
*Canhappenifyouaddatimerwithexpires==jiffies,
*oryousetatimertogooffinthepast
*/
vec=base->tv1.vec+(base->timer_jiffies&TVR_MASK);
}else{
inti;
/*Ifthetimeoutislargerthan0xffffffffon64-bit
*architecturesthenweusethemaximumtimeout:
*/
if(idx>0xffffffffUL){
idx=0xffffffffUL;
expires=idx+base->timer_jiffies;
}
i=(expires>>(TVR_BITS+3*TVN_BITS))&TVN_MASK;
vec=base->tv5.vec+i;
}
/*
*TimersareFIFO:
*/
///最终加入链表
list_add_tail(&timer->entry,vec);
}
这里要知道内核中的软定时器是用软中断来实现的,软中断的注册以及实现可以看我前面的blog,这里就不介绍了.我们来看timer模块的初始化:
Java代码
void__initinit_timers(void)
{
///主要是初始化boot_tvec_bases(如果是smp,则会初始化所有cpu上的boot_tvec_bases)
interr=timer_cpu_notify(&timers_nb,(unsignedlong)CPU_UP_PREPARE,
(void*)(long)smp_processor_id());
init_timer_stats();
BUG_ON(err==NOTIFY_BAD);
///注册到cpu的notifychain(这个我前面的blog也有介绍)
register_cpu_notifier(&timers_nb);
///注册软中断
open_softirq(TIMER_SOFTIRQ,run_timer_softirq);
}
ok,接下来我们就来看timer_cpu_notify这个函数,其实这个函数还是定时器注册的cpu的notifychain的action:
Java代码
staticstructnotifier_block__cpuinitdatatimers_nb={
.notifier_call=timer_cpu_notify,
};
staticint__cpuinittimer_cpu_notify(structnotifier_block*self,
unsignedlongaction,void*hcpu)
{
longcpu=(long)hcpu;
switch(action){
caseCPU_UP_PREPARE:
caseCPU_UP_PREPARE_FROZEN:
///模块初始化的时候就会调用这个函数
if(init_timers_cpu(cpu)<0)
returnNOTIFY_BAD;
break;
....................................
returnNOTIFY_OK;
}
其他的部分我们忽略,我们就发现定时器模块会调用init_timers_cpu来初始化.我们来分析这个函数.
这个函数最主要的功能就是初始化boot_tvec_bases,也就是全局的定时器表:
Java代码
staticint__cpuinitinit_timers_cpu(intcpu)
{
intj;
structtvec_base*base;
///可以看到这个是一个静态变量.它保存了每个cpu上的那个boot_tvec_bases.
staticchar__cpuinitdatatvec_base_done[NR_CPUS];
///如果为空,说明这个cpu上的定时器表还没有初始化,因此需要初始化
if(!tvec_base_done[cpu]){
/*这个也是一个静态变量.它表示了cpu是否初始化完毕.这个函数有一个宏__cpuinit,这个将*这个函数放置到cpuinit这个段,因此也就是说这个函数会先在cpu初始化时调用,也就是第一**次会先给boot_done赋值,然后再调用这个函数才会进入kmalloc.
*/
staticcharboot_done;
if(boot_done){
/*
*TheAPsusethispathlaterinboot
*/
///malloc一个tvec_base
base=kmalloc_node(sizeof(*base),
GFP_KERNEL|__GFP_ZERO,
cpu_to_node(cpu));
if(!base)
return-ENOMEM;
/*Makesurethattvec_baseis2bytealigned*/
if(tbase_get_deferrable(base)){
WARN_ON(1);
kfree(base);
return-ENOMEM;
}
///由于在percpu的变量中类型为tvec_bases的,只有boot_tvec_bases,因此,也就是将base这个指针付给boot_tvec_bases.
per_cpu(tvec_bases,cpu)=base;
}else{
///cpu初始化完毕后会进入这里,标记了cpu已经boot完毕.此时内存初始化完毕.
boot_done=1;
base=&boot_tvec_bases;
}
tvec_base_done[cpu]=1;
}else{
///取出tvec_base付给base
base=per_cpu(tvec_bases,cpu);
}
///开始初始化
spin_lock_init(&base->lock);
///开始初始化5个定时器表
for(j=0;j<TVN_SIZE;j++){
INIT_LIST_HEAD(base->tv5.vec+j);
INIT_LIST_HEAD(base->tv4.vec+j);
INIT_LIST_HEAD(base->tv3.vec+j);
INIT_LIST_HEAD(base->tv2.vec+j);
}
for(j=0;j<TVR_SIZE;j++)
INIT_LIST_HEAD(base->tv1.vec+j);
///默认值为初始化时的jiffes
base->timer_jiffies=jiffies;
return0;
}
通过上面的定时器初始化函数我们知道定时器软中断所对应的action是run_timer_softirq,也就是当时钟中断到来,软中断启动时,就会调用这个函数,因此我们来看这个函数:
这个函数功能很简单,它的最关键就是调用__run_timers,这个函数才是真正处理定时器的函数.
Java代码
staticvoidrun_timer_softirq(structsoftirq_action*h)
{
structtvec_base*base=__get_cpu_var(tvec_bases);
///这个函数应该是提供给2.6.31内核的新特性PerformanceCounters.
perf_counter_do_pending();
///处理hrttimer
hrtimer_run_pending();
///判断当前的jiffies是否大于等于最小的那个超时jiffies.是的话就进入定时器处理
if(time_after_eq(jiffies,base->timer_jiffies))
__run_timers(base);
}
__run_timers这个函数的主要功能是运行所有超时的定时器:
1
Java代码
staticinlinevoid__run_timers(structtvec_base*base)
{
structtimer_list*timer;
///关闭中断并且开启自旋锁
spin_lock_irq(&base->lock);
///然后遍历定时器级联表
while(time_after_eq(jiffies,base->timer_jiffies)){
///这里的head和work_list其实表示的就是已经超时的定时器,也就是我们将要处理的定时器.
structlist_headwork_list;
structlist_head*head=&work_list;
///从timer_jiffies得到所在index,其实也就是在tv1中的index
intindex=base->timer_jiffies&TVR_MASK;
///开始处理层叠定时器,这里的这个cascade是一个关键的函数,我们下面会分析,这里只需要知道这个函数其实也就是用来一层层的得到这个定时器处于哪个级别中.
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));
///更新timer_jiffies.
++base->timer_jiffies;
///用work_list替换掉base->tv1.vec+index.这里因为上面的处理中,就算定时器不在base->tv1中,可是通过cascade的调节,会将base->tv2加入到base->tv1中,或者说base->tv3,以此类推.
list_replace_init(base->tv1.vec+index,&work_list);
///如果这个值不为空说明有已经超时的定时器.这里head也就是work_list,也就是base->tv1
while(!list_empty(head)){
void(*fn)(unsignedlong);
unsignedlongdata;
///取出定时器.
timer=list_first_entry(head,structtimer_list,entry);
fn=timer->function;
data=timer->data;
timer_stats_account_timer(timer);
///设置当前正在处理的定时器为timer(这个主要是针对smp的架构),因为我们是在软中断中进行的,因此要防止多个cpu的并发.
set_running_timer(base,timer);
///删除这个定时器.
detach_timer(timer,1);
spin_unlock_irq(&base->lock);
{
intpreempt_count=preempt_count();
lock_map_acquire(&lockdep_map);
///执行定时器回调函数
fn(data);
.............................................
}
spin_lock_irq(&base->lock);
}
}
///修改base->running_timer为空
set_running_timer(base,NULL);
spin_unlock_irq(&base->lock);
}
ok我们接下来来看下定时器超时的机制,关键在这段代码:
Java代码
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));
index为0就说明当前要处理的定时器不在base->tv1中.因此我们需要cascade来进行调解.
Java代码
///得到在N级(也就是tv2,tv3...)的定时器表中的slot.这里可以对照我们前面的internal_add_timer加入定时器的情况.
#defineINDEX(N)((base->timer_jiffies>>(TVR_BITS+(N)*TVN_BITS))&TVN_MASK)
staticintcascade(structtvec_base*base,structtvec*tv,intindex)
{
/*cascadeallthetimersfromtvuponelevel*/
structtimer_list*timer,*tmp;
structlist_headtv_list;
///这里实例化tv_list为我们将要处理的链表.并将老的list重新初始化为空.
list_replace_init(tv->vec+index,&tv_list);
/*
*Weareremoving_all_timersfromthelist,sowe
*don'thavetodetachthemindividually.
*/
list_for_each_entry_safe(timer,tmp,&tv_list,entry){
BUG_ON(tbase_get_base(timer->base)!=base);
///重新加入定时器,也就是加入到自己对应的位置
internal_add_timer(base,timer);
}
///然后返回index,这里可以看到如果index为空则说明这个级别的定时器也已经都处理过了,因此我们需要再处理下一个级别.
returnindex;
}
可以看到定时器处理始终都是在处理tv1,如果tv1已经处理完了,则将tv2添加到tv1,以此类推.
而定时器软中断如何触发呢,是用update_process_times来触发的,这个函数比较简单,主要是调用run_local_timers来触发软中断:
Java代码
voidrun_local_timers(void)
{
hrtimer_run_queues();
///触发软中断.
raise_softirq(TIMER_SOFTIRQ);
softlockup_tick();
}
1计算机系统中的计时器
在计算机系统中存在着许多硬件计时器,例如RealTimerClock(RTC)、TimeStampCounter(TSC)和ProgrammableIntervalTimer(PIT)等等。
这部分内容不是本文的中点,这里仅仅简单介绍几种,更多内容参见参考文献:
●RealTimerClock(RTC):
独立于整个计算机系统(例如:CPU和其他chip)
内核利用其获取系统当前时间和日期
●TimeStampCounter(TSC):
从Pentium起,提供一个寄存器TSC,用来累计每一次外部振荡器产生的时钟信号
通过指令rdtsc访问这个寄存器
比起PIT,TSC可以提供更精确的时间测量
●ProgrammableIntervalTimer(PIT):
时间测量设备
内核使用的产生时钟中断的设备,产生的时钟中断依赖于硬件的体系结构,慢的为10ms一次,快的为1ms一次
HighPrecisionEventTimer(HPET):
PIT和RTC的替代者,和之前的计时器相比,HPET提供了更高的时钟频率(至少10MHz)以及更宽的计数器宽度(64位)
一个HPET包括了一个固定频率的数值增加的计数器以及3到32个独立的计时器,这每一个计时器有包涵了一个比较器和一个寄存器(保存一个数值,表示触发中断的时机)。每一个比较器都比较计数器中的数值和寄存器中的数值,当这两个数值相等时,将产生一个中断
2硬件时钟处理
这里所说的硬件时钟处理特指的是硬件计时器时钟中断的处理过程。
2.1数据结构
和硬件计时器(本文又称作硬件时钟,区别于软件时钟)相关的数据结构主要有两个:
structclocksource:对硬件设备的抽象,描述时钟源信息
structclock_event_device:时钟的事件信息,包括当硬件时钟中断发生时要执行那些操作(实际上保存了相应函数的指针)。本文将该结构称作为“时钟事件设备”。
上述两个结构内核源代码中有较详细的注解,分别位于文件clocksource.h和clockchips.h中。需要特别注意的是结构clock_event_device的成员event_handler,它指定了当硬件时钟中断发生时,内核应该执行那些操作,也就是真正的时钟中断处理函数。在2.3节“时钟初始化”部分会介绍它真正指向哪个函数。
Linux内核维护了两个链表,分别
图2-1时钟源链表和时钟事件链表
2.2通知链技术(notificationchain)
在时钟处理这部分中,内核用到了所谓的“通知链(notificationchain)”技术。所以在介绍时钟处理过程之前先来了解下“通知链”技术。
在Linux内核中,各个子系统之间有很强的相互关系,一些被一个子系统生成或者被探测到的事件,很可能是另一个或者多个子系统感兴趣的,也就是说这个事件的获取者必须能够通知所有对该事件感兴趣的子系统,并且还需要这种通知机制具有一定的通用性。基于这些,Linux内核引入了“通知链”技术。
2.2.1数据结构:
通知链有四种类型,
1原子通知链(Atomicnotifierchains):通知链元素的回调函数(当事件发生时要执行的函数)只能在中断上下文中运行,不允许阻塞
2可阻塞通知链(Blockingnotifierchains):通知链元素的回调函数在进程上下文中运行,允许阻塞
3原始通知链(Rawnotifierchains):对通知链元素的回调函数没有任何限制,所有锁和保护机制都由调用者维护
4SRCU通知链(SRCUnotifierchains):可阻塞通知链的一种变体
所以对应了四种通知链头结构:
●structatomic_notifier_head:原子通知链的链头
●structblocking_notifier_head:可阻塞通知链的链头
●structraw_notifier_head:原始通知链的链头
●structsrcu_notifier_head:SRCU通知链的链头
通知链元素的类型:
structnotifier_block:通知链中的元素,记录了当发出通知时,应该执行的操作(即回调函数)
链头中保存着指向元素链表的指针。通知链元素结构则保存着回调函数的类型以及优先级,参见notifier.h文件。
2.2.2运作机制
通知链的运作机制包括两个角色:
1被通知者:对某一事件感兴趣一方。定义了当事件发生时,相应的处理函数,即回调函数。但需要事先将其注册到通知链中(被通知者注册的动作就是在通知链中增加一项)。
2通知者:事件的通知者。当检测到某事件,或者本身产生事件时,通知所有对该事件感兴趣的一方事件发生。他定义了一个通知链,其中保存了每一个被通知者对事件的处理函数(回调函数)。通知这个过程实际上就是遍历通知链中的每一项,然后调用相应的事件处理函数。
包括以下过程:
1通知者定义通知链
2被通知者向通知链中注册回调函数
3当事件发生时,通知者发出通知(执行通知链中所有元素的回调函数)
整个过程可以看作是“发布——订阅”模型(参见参考资料)
被通知者调用notifier_chain_register函数注册回调函数,该函数按照优先级将回调函数加入到通知链中。注销回调函数则使用notifier_chain_unregister函数,即将回调函数从通知链中删除。2.2.1节讲述的4种通知链各有相应的注册和注销函数,但是他们最终都是调用上述两个函数完成注册和注销功能的。有兴趣的读者可以自行查阅内核代码。
通知者调用notifier_call_chain函数通知事件的到达,这个函数会遍历通知链中所有的元素,然后依次调用每一个的回调函数(即完成通知动作)。2.2.1节讲述的4种通知链也都有其对应的通知函数,这些函数也都是最终调用notifier_call_chain函数完成事件的通知。
更多关于通知链的内容,参见参考文献。
由以上的叙述,“通知链”技术可以概括为:事件的被通知者将事件发生时应该执行的操作通过函数指针方式保存在链表(通知链)中,然后当事件发生时通知者依次执行链表中每一个元素的回调函数完成通知。
2.3时钟初始化
内核初始化部分(start_kernel函数)和时钟相关的过程主要有以下几个:
1tick_init()
2init_timers()
3hrtimers_init()
4time_init()
其中函数hrtimers_init()和高精度时钟相关(本文暂不介绍这部分内容)。下面将详细介绍剩下三个函数。
2.3.1tick_init函数
函数tick_init()很简单,调用clockevents_register_notifier函数向clockevents_chain通知链注册元素:tick_notifier。这个元素的回调函数指明了当时钟事件设备信息发生变化(例如新加入一个时钟事件设备等等)时,应该执行的操作,该回调函数为tick_notify(参见2.4节)。
2.3.2init_timers函数
注:本文中所有代码均来自于Linux2.6.25源代码
函数init_timers()的实现如清单2-1(省略了部分和
主要功能无关的内容,以后代码同样方式处理)
清单2-1init_timers函数
void__initinit_timers(void) { interr=timer_cpu_notify(&timers_nb,(unsignedlong)CPU_UP_PREPARE, (void*)(long)smp_processor_id()); …… register_cpu_notifier(&timers_nb); open_softirq(TIMER_SOFTIRQ,run_timer_softirq,NULL); } |
●初始化本CPU上的软件时钟相关的数据结构,参见3.2节
●向cpu_chain通知链注册元素timers_nb,该元素的回调函数用于初始化指定CPU上的软件时钟相关的数据结构
●初始化时钟的软中断处理函数
2.3.3time_init函数
函数time_init的实现如清单2-2
清单2-2time_init函数
void__inittime_init(void) { …… init_tsc_clocksource(); late_time_init=choose_time_init(); } |
清单2-3hpet_time_init函数
void__inithpet_time_init(void) { if(!hpet_enable()) setup_pit_timer(); setup_irq(0,&irq0); } |
初始化硬件时钟这个过程主要包括以下两个过程(参见hpet_enable的实现):
1初始化时钟源信息(structclocksource类型的变量),并将其添加到时钟源链表中,即clocksource_list链表(参见图2-1)。
2初始化时钟事件设备信息(structclock_event_device类型的变量),并向通知链clockevents_chain发布通知:一个时钟事件设备要被添加到系统中。在通知(执行回调函数)结束后,该时钟事件设备被添加到时钟事件设备链表中,即clockevent_devices链表(参见图2-1)。有关通知链的内容参见2.2节。
需要注意的是在初始化时钟事件设备时,全局变量global_clock_event被赋予了相应的值。该变量保存着系统中当前正在使用的时钟事件设备(保存了系统当前使用的硬件时钟中断发生时,要执行的中断处理函数的指针)。
2.4硬件时钟处理过程
由2.3.3可知硬件时钟中断的处理函数保存在静态变量irq0中,其定义如清单2-4
清单2-4变量irq0定义
staticstructirqactionirq0={ .handler=timer_event_interrupt, .flags=IRQF_DISABLED|IRQF_IRQPOLL|IRQF_NOBALANCING, .mask=CPU_MASK_NONE, .name="timer" }; |
清单2-5timer_event_interrupt函数
staticirqreturn_ttimer_event_interrupt(intirq,void*dev_id) { add_pda(irq0_irqs,1); global_clock_event->event_handler(global_clock_event); returnIRQ_HANDLED; } |
为了说明这个问题,不妨假设系统中使用的是hpet时钟。由2.3.3节可知global_clock_event指向hpet时钟事件设备(hpet_clockevent)。查看hpet_enable函数的代码并没有发现有对event_handler成员的赋值。所以继续查看时钟事件设备加入事件的处理函数tick_notify,该函数记录了当时钟事件设备发生变化(例如,新时钟事件设备的加入)时,执行那些操作(参见2.3.1节),代码如清单2-6
清单2-6tick_notify函数
staticinttick_notify(structnotifier_block*nb,unsignedlongreason,void*dev) { switch(reason){ caseCLOCK_EVT_NOTIFY_ADD: returntick_check_new_device(dev); …… returnNOTIFY_OK; } |
函数tick_handle_periodic的处理过程分成了以下两个部分:
1全局处理:整个系统中的信息处理
2局部处理:局部于本地CPU的处理
●总结一下,一次时钟中断发生后,OS主要执行的操作(tick_handle_periodic):
●全局处理(仅在一个CPU上运行):
1更新jiffies_64
2更新xtimer和当前时钟源信息等
3根据tick计算avenrun负载
●局部处理(每个CPU都要运行):
1根据当前在用户态还是核心态,统计当前进程的时间:用户态时间还是核心态时间
2唤醒TIMER_SOFTIRQ软中断
3唤醒RCU软中断
4调用scheduler_tick(更新进程时间片等等操作,更多内容参见参考文献)
5profile_tick函数调用
以上就介绍完了硬件时钟的处理过程,下面来看软件时钟。
3软件时钟处理
这里所说“软件时钟”指的是软件定时器(SoftwareTimers),是一个软件上的概念,是建立在硬件时钟基础之上的。它记录了未来某一时刻要执行的操作(函数),并使得当这一时刻真正到来时,这些操作(函数)能够被按时执行。举个例子说明:它就像生活中的闹铃,给闹铃设定振铃时间(未来的某一时间)后,当时间(相当于硬件时钟)更新到这个振铃时间后,闹铃就会振铃。这个振铃时间好比软件时钟的到期时间,振铃这个动作好比软件时钟到期后要执行的函数,而闹铃时间更新好比硬件时钟的更新。
实现软件时钟原理也比较简单:每一次硬件时钟中断到达时,内核更新的jiffies,然后将其和软件时钟的到期时间进行比较。如果jiffies等于或者大于软件时钟的到期时间,内核就执行软件时钟指定的函数。
接下来的几节会详细介绍Linux2.6.25是怎么实现软件时钟的。
3.1相关数据结构
structtimer_list:软件时钟,记录了软件时钟的到期时间以及到期后要执行的操作。具体的成员以及含义见表3-1。
structtvec_base:用于组织、管理软件时钟的结构。在SMP系统中,每个CPU有一个。具体的成员以及含义参见表3-2。
表3-1structtimer_list主要成员
域名 | 类型 | 描述 |
entry | structlist_head | 所在的链表 |
expires | unsignedlong | 到期时间,以tick为单位 |
function | void(*)(unsignedlong) | 回调函数,到期后执行的操作 |
data | unsignedlong | 回调函数的参数 |
base | structtvec_base* | 记录该软件时钟所在的structtvec_base变量 |
域名 | 类型 | 描述 |
lock | spinlock_t | 用于同步操作 |
running_timer | structtimer_list* | 正在处理的软件时钟 |
timer_jiffies | unsignedlong | 当前正在处理的软件时钟到期时间 |
tv1 | structtvec_root | 保存了到期时间从timer_jiffies到timer_jiffies+之间(包括边缘值)的所有软件时钟 |
tv2 | structtvec | 保存了到期时间从timer_jiffies+到timer_jiffies+之间(包括边缘值)的所有软件时钟 |
tv3 | structtvec | 保存了到期时间从timer_jiffies+到timer_jiffies+之间(包括边缘值)的所有软件时钟 |
tv4 | structtvec | 保存了到期时间从timer_jiffies+到timer_jiffies+之间(包括边缘值)的所有软件时钟 |
tv5 | structtvec | 保存了到期时间从timer_jiffies+到timer_jiffies+之间(包括边缘值)的所有软件时钟 |
清单3-1structtvec_root和structtvec的定义
structtvec{ structlist_headvec[TVN_SIZE]; }; structtvec_root{ structlist_headvec[TVR_SIZE]; }; |
3.2数据结构之间的关系
图3-1显示了以上数据结构之间的关系:
从图中可以清楚地看出:软件时钟(structtimer_list,在图中由timer表示)以双向链表(structlist_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
(0<=n<=255)保存着下n个tick到达时,到期的所有软件时钟。而tv2[0]则保存着下256到511个tick之间到期所有软件时钟,tv2[1]保存着下512到767个tick之间到期的所有软件时钟,tv2
(0<=n<=63)保存着下256*(n+1)到256*(n+2)-1个tick之间到达的所有软件时钟。tv3~tv5依次类推。
注:一个tick的长度指的是两次硬件时钟中断发生之间的时间间隔
从上面的说明中可以看出:软件时钟是按照其到期时间相对于当前正在处理的软件时钟的到期时间(timer_jiffies的数值)保存在structtvec_base变量中的。而且这个到期时间的最大相对值(到期时间-timer_jiffies)为0xffffffffUL(tv5最后一个元素能够表示的相对到期时间的最大值)。
还需要注意的是软件时钟的处理是局部于CPU的,所以在SMP系统中每一个CPU都保存一个类型为structtvec_base的变量,用来组织、管理本CPU的软件时钟。从图中也可以看出structtvec_base变量是per-CPU的(关于per-CPU的变量原理和使用参见参考资料)。
由于以后的讲解经常要提到每个CPU相关的structtvec_base变量,所以为了方便,称保存软件时钟的structtvec_base变量为该软件时钟的base,或称CPU的base。
3.3添加或删除软件时钟
在了解了软件时钟的数据组织关系之后,现在来看一下如何添加以及删除一个软件时钟。
3.3.1添加软件时钟
在Linux内核中要添加一个软件时钟,首先必须分配structtimer_list类型的变量,然后调用函数add_timer()将该软件时钟添加到相应调用add_timer函数的CPU的base中。Add_timer是对函数__mod_timer()的一层包装。函数__mod_timer()的代码如清单3-2:
清单3-2__mod_timer函数
int__mod_timer(structtimer_list*timer,unsignedlongexpires) { structtvec_base*base,*new_base; unsignedlongflags; intret=0; …… base=lock_timer_base(timer,&flags); if(timer_pending(timer)){ detach_timer(timer,0); ret=1; } new_base=__get_cpu_var(tvec_bases); if(base!=new_base){ if(likely(base->running_timer!=timer)){ /*Seethecommentinlock_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); returnret; } |
注:卸载软件时钟的意思是指将软件时钟从软件时钟所在base中删除,以后所说的卸载软件时钟也都是这个意思
取得软件时钟所在base上的同步锁(structtvec_base变量中的自旋锁),并返回该软件时钟的base,保存在base变量中
如果该软件时钟处在pending状态(在base中,准备执行),则卸载该软件时钟
取得本CPU上的base指针(类型为structtvec_base*),保存在new_base中
如果base和new_base不一样,也就是说软件时钟发生了迁移(从一个CPU中移到了另一个CPU上),那么如果该软件时钟的处理函数当前没有在迁移之前的那个CPU上运行,则先将软件时钟的base设置为NULL,然后再将该软件时钟的base设置为new_base。否则,跳到5。
设置软件时钟的到期时间
调用internal_add_timer函数将软件时钟添加到软件时钟的base中(本CPU的base)
释放锁
这里有必要详细说明一下软件时钟如何被添加到软件时钟的base中的(添加到本CPUbase的tv1~tv5里面),因为这是软件时钟处理的基础。来看函数internal_add_timer函数的实现,如清单3-3
清单3-3internal_add_timer函数
staticvoidinternal_add_timer(structtvec_base*base,structtimer_list*timer) { unsignedlongexpires=timer->expires; unsignedlongidx=expires-base->timer_jiffies; structlist_head*vec; if(idx<TVR_SIZE){ inti=expires&TVR_MASK; vec=base->tv1.vec+i; }elseif(idx<1<<(TVR_BITS+TVN_BITS)){ inti=(expires>>TVR_BITS)&TVN_MASK; vec=base->tv2.vec+i; }elseif(idx<1<<(TVR_BITS+2*TVN_BITS)){ inti=(expires>>(TVR_BITS+TVN_BITS))&TVN_MASK; vec=base->tv3.vec+i; }elseif(idx<1<<(TVR_BITS+3*TVN_BITS)){ inti=(expires>>(TVR_BITS+2*TVN_BITS))&TVN_MASK; vec=base->tv4.vec+i; }elseif((signedlong)idx<0){ vec=base->tv1.vec+(base->timer_jiffies&TVR_MASK); }else{ inti; if(idx>0xffffffffUL){ idx=0xffffffffUL; expires=idx+base->timer_jiffies; } i=(expires>>(TVR_BITS+3*TVN_BITS))&TVN_MASK; vec=base->tv5.vec+i; } list_add_tail(&timer->entry,vec); } |
计算该软件时钟的到期时间和timer_jiffies(当前正在处理的软件时钟的到期时间)的差值,作为索引保存到idx变量中。
判断idx所在的区间,在
[0,]或者(,0)(该软件时钟已经到期),则将要添加到tv1中
[,],则将要添加到tv2中
[,],则将要添加到tv3中
[,],则将要添加到tv4中
[,),则将要添加到tv5中,但实际上最大值为0xffffffffUL
计算所要加入的具体位置(哪个链表中,即tv1~tv5的哪个子链表,参考图3-1)
最后将其添加到相应的链表中
从这个函数可以得知,内核中是按照软件时钟到期时间的相对值(相对于timer_jiffies的值)将软件时钟添加到软件时钟所在的base中的。
3.3.2删除软件时钟
内核可调用del_timer函数删除软件时钟,del_timer的代码如清单3-4
清单3-4del_timer函数
intdel_timer(structtimer_list*timer) { structtvec_base*base; unsignedlongflags; intret=0; …… if(timer_pending(timer)){ base=lock_timer_base(timer,&flags); if(timer_pending(timer)){ detach_timer(timer,1); ret=1; } spin_unlock_irqrestore(&base->lock,flags); } returnret; } |
检测该软件时钟是否处在pending状态(在base中,准备运行),如果不是则直接函数返回
如果处于pending状态,则获得锁
再次检测软件时钟是否处于pending状态(该软件时钟可能被卸载了),不是则释放锁然后函数返回
如果还是pending状态,则将其卸载,之后释放锁,函数返回
如果在SMP系统中,则需使用del_timer_sync函数来删除软件时钟。在讲解del_timer_sync函数之前,先来看下try_to_del_timer_sync函数的实现(该函数被del_timer_sync函数使用),其代码如清单3-5
清单3-5try_to_del_timer_sync函数
inttry_to_del_timer_sync(structtimer_list*timer) { structtvec_base*base; unsignedlongflags; intret=-1; base=lock_timer_base(timer,&flags); if(base->running_timer==timer) gotoout; ret=0; if(timer_pending(timer)){ detach_timer(timer,1); ret=1; } out: spin_unlock_irqrestore(&base->lock,flags); returnret; } |
接下来,再来看看函数del_timer_sync定义,如清单3-6
清单3-6del_timer_sync函数
intdel_timer_sync(structtimer_list*timer) { for(;;){ intret=try_to_del_timer_sync(timer); if(ret>=0) returnret; cpu_relax(); } } |
3.3时钟的软中断处理
软件时钟的处理是在时钟的软中断中进行的。
3.3.1软中断初始化
软中断的一个重要的处理时机是在每个硬件中断处理完成后(参见irq_exit函数),且由2.4节的内容可知:在硬件时钟中断处理中,会唤醒时钟的软中断,所以每次硬件时钟中断处理函数执行完成后都要进行时钟的软中断处理。和时钟相关的软中断是TIMER_SOFTIRQ,其处理函数为run_timer_softirq,该函数用来处理所有的软件时钟。这部分初始化代码在函数init_timers中进行,如清单3-7
清单3-7init_timers函数
void__initinit_timers(void) { …… open_softirq(TIMER_SOFTIRQ,run_timer_softirq,NULL); } |
函数run_timer_softirq所作的工作就是找出所有到期的软件时钟,然后依次执行其处理函数。其代码如清单3-8
清单3-8run_timer_softirq函数
staticvoidrun_timer_softirq(structsoftirq_action*h) { structtvec_base*base=__get_cpu_var(tvec_bases); hrtimer_run_pending(); if(time_after_eq(jiffies,base->timer_jiffies)) __run_timers(base); } |
注:hrtimer_run_pending()函数是高精度时钟的处理。本文暂没有涉及高精度时钟相关的内容。
大于等于timer_jiffies,说明可能已经有软件时钟到期了,此
时就要进行软件时钟的处理,调用函数__run_timers进行处
理。如果jiffies小于timer_jiffies,表明没有软件时钟到期,
则不用对软件时钟进行处理。函数返回。
接下来看一下函数__run_timers都作了些什么,如清单3-9
清单3-9__run_timers函数
staticinlinevoid__run_timers(structtvec_base*base) { …… spin_lock_irq(&base->lock); while(time_after_eq(jiffies,base->timer_jiffies)){ …… intindex=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,structtimer_list,entry); fn=timer->function; data=timer->data; …… set_running_timer(base,timer); detach_timer(timer,1); spin_unlock_irq(&base->lock); { intpreempt_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中的链表(结构参见3.2节),代码:
intindex=base->timer_jiffies&TVR_MASK; |
使得timer_jiffies的数值增加1
取出相应的软件时钟链表
遍历该链表,对每个元素进行如下操作
设置当前软件时钟为base中正在运行的软件时钟(即保存当前软件时钟到base->running_timer成员中)
将当前软件时钟从链表中删除,即卸载该软件时钟
释放锁,执行软件时钟处理程序
再次获得锁
设置当前base中不存在正在运行的软件时钟
释放锁
3.3.3软件时钟调整过程
函数cascade用于调整软件时钟(这个调整过程是指:将马上就要到期的软件时钟从其所在的链表中删除,重新计算到期时间的相对值(到期时间-timer_jiffies),然后根据该值重新插入到base中)。注意到在软件时钟处理过程中,每次都是从tv1中取出一个链表进行处理,而不是从tv2~tv5中取,所以对软件时钟就要进行必要的调整。
在讲解cascade函数之前,再从直观上理解下为什么需要进行调整。所有软件时钟都是按照其到期时间的相对值(相对于timer_jiffies)被调加到base中的。但是timer_jiffies的数值都会在处理中增加1(如3.3.2节所示),也就是说这个相对值会随着处理发生变化,当这个相对值小于等于256时,就要将软件时钟从tv2~tv5中转移到tv1中(tv1中保存着下256个tick内到期的所有软件时钟)。
函数cascade的实现如清单3-10
清单3-10cascade函数
staticintcascade(structtvec_base*base,structtvec*tv,intindex) { structtimer_list*timer,*tmp; structlist_headtv_list; list_replace_init(tv->vec+index,&tv_list); list_for_each_entry_safe(timer,tmp,&tv_list,entry){ …… internal_add_timer(base,timer); } returnindex; } |
清单3-9中调整软件时钟的代码如下:
intindex=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)); |
3.4自我激活
软件时钟可分为两种类型:
仅仅激活一次
激活多次或者周期性激活
多次激活的实现机制就是要在软件时钟处理函数中重新设置软件时钟的到期时间为将来的一个时间,这个过程通过调用mod_timer函数来实现。该函数的实现如清单3-11
清单3-11mod_timer函数
intmod_timer(structtimer_list*timer,unsignedlongexpires) { …… if(timer->expires==expires&&timer_pending(timer)) return1; return__mod_timer(timer,expires); } |
3.5软件时钟的应用
软件时钟的处理是在处理软中断时触发的,而软中断的处理又会紧接着硬件中断处理结束而进行,并且系统会周期地产生时钟中断(硬件中断),这样,软件时钟的处理至少会在系统每一次时钟中断处理完成后触发(如果软件时钟的到期时间大于系统当前的jiffies,表明时间未到期,则不会调用保存在软件时钟中的函数,但此时的确提供了处理软件时钟的时机)。从这点上看,软件时钟会有较快的相应——一旦时间到期,保存在软件时钟中的函数会将快地被调用(在时钟软中断中被调用,参见3.3.2节)。所以内核中凡是需要隔一段时间间隔后作指定操作的过程都通过软件时钟完成。例如大部分设备驱动程序使用软件时钟探测异常条件、软盘驱动程序利用软件时钟关闭有一段时间没有被访问软盘的设备马达、进程的定时睡眠(schedule_timeout函数)和网络超时重传等等。
本节主要通过介绍进程的定时睡眠(schedule_timeout函数)和网络超时重传来说明软件时钟的应用。
3.5.1进程的定时睡眠
函数schedule_timeout的代码如清单3-12
清单3-12函数schedule_timeout
signedlong__schedschedule_timeout(signedlongtimeout) { structtimer_listtimer; unsignedlongexpire; …… expire=timeout+jiffies; setup_timer(&timer,process_timeout,(unsignedlong)current); __mod_timer(&timer,expire); schedule(); del_singleshot_timer_sync(&timer); timeout=expire-jiffies; out: returntimeout<0?0:timeout; } |
清单3-13函数process_timeout
staticvoidprocess_timeout(unsignedlong__data) { wake_up_process((structtask_struct*)__data); } |
清单3-14函数del_singleshot_timer_sync
#definedel_singleshot_timer_sync(t)del_timer_sync(t) |
3.5.2网路超时重传
对于TCP
在Linux2.6.25的内核中,这种数据的重新发送使用软件时钟来完成。这个软件时钟保存在面向连接的套接字(对应内核中inet_connection_sock结构)中。对这个域的初始在函数tcp_init_xmit_timers中,如清单3-15
清单3-15函数tcp_init_xmit_timers、函数inet_csk_init_xmit_timers和函数setup_timer
voidtcp_init_xmit_timers(structsock*sk) { inet_csk_init_xmit_timers(sk, &tcp_write_timer,&tcp_delack_timer, &tcp_keepalive_timer); } voidinet_csk_init_xmit_timers(structsock*sk, void(*retransmit_handler)(unsignedlong), void(*delack_handler)(unsignedlong), void(*keepalive_handler)(unsignedlong)) { structinet_connection_sock*icsk=inet_csk(sk); setup_timer(&icsk->icsk_retransmit_timer,retransmit_handler, (unsignedlong)sk); …… } staticinlinevoidsetup_timer(structtimer_list*timer, void(*function)(unsignedlong), unsignedlongdata) { timer->function=function; timer->data=data; init_timer(timer); } |
在TCP
清单3-16tcp_write_xmit函数
staticinttcp_write_xmit(structsock*sk,unsignedintmss_now,intnonagle) { structtcp_sock*tp=tcp_sk(sk); structsk_buff*skb; …… if(unlikely(tcp_transmit_skb(sk,skb,1,GFP_ATOMIC))) break; tcp_event_new_data_sent(sk,skb); …… return!tp->packets_out&&tcp_send_head(sk); } |
当函数tcp_event_new_data_sent结束之后,处理超时的软件时钟已经设置好了。内核会在每一次时钟中断处理完成后检测该软件时钟是否到期。如果网络真的超时,没有ACK返回,那么当该软件时钟到期后内核就会执行函数tcp_write_timer。函数tcp_write_timer将进行数据包的重新发送,并重新设置超时重传软件时钟的到期时间。
4总结
本文介绍了Linux内核的时钟处理机制。首先简单介绍了系统的硬件计时器,然后重点介绍了硬件时钟的处理过程和软件时钟的处理过程以及软件时钟的应用。
linux内核定时器的实现
由于linux还不是一个实时的操作系统,因此如果需要更高精度,或者更精确的定时的话,可能就需要打一些实时的补丁,或者用商用版的实时linux,.这里内的定时器最小间隔也就是1个tick.
这里还有一个要注意的,我这里的分析并没有分析内核新的hrt定时器.这个定时器是MontaVista加入到内核的一个高精度的定时器的实现.
先来看几个相关的数据结构.
///这个是一个最主要的数据结构,表示一个完整的定时器级联表
Java代码
structtvec_base{
///自旋锁
spinlock_tlock;
///表示由本地cpu正在处理的定时器链表
structtimer_list*running_timer;
///这个表示当前的定时器级联表中最快要超时的定时器的jiffer
unsignedlongtimer_jiffies;
///下面表示了5级的定时器级联表.
structtvec_roottv1;
structtvectv2;
structtvectv3;
structtvectv4;
structtvectv5;
}____cacheline_aligned;
structtvec_base{
///自旋锁
spinlock_tlock;
///表示由本地cpu正在处理的定时器链表
structtimer_list*running_timer;
///这个表示当前的定时器级联表中最快要超时的定时器的jiffer
unsignedlongtimer_jiffies;
///下面表示了5级的定时器级联表.
structtvec_roottv1;
structtvectv2;
structtvectv3;
structtvectv4;
structtvectv5;
}____cacheline_aligned;
下面来看tvec和tvec_root的结构:
Java代码
structtvec{
structlist_headvec[TVN_SIZE];
};
structtvec_root{
structlist_headvec[TVR_SIZE];
};
structtvec{
structlist_headvec[TVN_SIZE];
};
structtvec_root{
structlist_headvec[TVR_SIZE];
};
可以看到这两个结构也就是hash链表.每次通过超时jiffies来计算slot,然后插入到链表.这里链表是FIFO的.这里除了tv5外其他几个都是简单的与TVR_MASK按位与计算.
Java代码
structtimer_list{
structlist_headentry;
///超时节拍数
unsignedlongexpires;
///定时器将要执行的回调函数
void(*function)(unsignedlong);
///传递给回调函数的参数
unsignedlongdata;
///从属于那个base
structtvec_base*base;
};
structtimer_list{
structlist_headentry;
///超时节拍数
unsignedlongexpires;
///定时器将要执行的回调函数
void(*function)(unsignedlong);
///传递给回调函数的参数
unsignedlongdata;
///从属于那个base
structtvec_base*base;
};
///定义了一个percpu变量.这里要知道定时器的注册和触发执行一定是在相同的cpu上的.
structtvec_baseboot_tvec_bases;
staticDEFINE_PER_CPU(structtvec_base*,tvec_bases)=&boot_tvec_bases;
内核注册定时器最终都会通过调用internal_add_timer来实现.具体的工作方式是这样的:
1如果定时器在接下来的0~255个jiffies中到期,则将定时器添加到tv1.
2如果定时器是在接下来的256*64个jiffies中到期,则将定时器添加到tv2.
3如果定时器是在接下来的256*64*64个jiffies中到期,则将定时器添加到tv3.
4如果定时器是在接下来的256*64*64*64个jiffies中到期,则将定时器添加到tv4.
5如果更大的超时,则利用0xffffffff来计算hash,然后插入到tv5(这个只会出现在64的系统).
看下面的图就能比较清晰了:
接下来看源码:
Java代码
staticvoidinternal_add_timer(structtvec_base*base,structtimer_list*timer)
{
///取出超时jiffies
unsignedlongexpires=timer->expires;
///得到定时器还有多长时间到期(这里是相比于最短的那个定时器)
unsignedlongidx=expires-base->timer_jiffies;
structlist_head*vec;
///开始判断该把定时器加入到那个队列.依次为tv1到tv5
if(idx<TVR_SIZE){
inti=expires&TVR_MASK;
vec=base->tv1.vec+i;
}elseif(idx<1<<(TVR_BITS+TVN_BITS)){
inti=(expires>>TVR_BITS)&TVN_MASK;
vec=base->tv2.vec+i;
}elseif(idx<1<<(TVR_BITS+2*TVN_BITS)){
inti=(expires>>(TVR_BITS+TVN_BITS))&TVN_MASK;
vec=base->tv3.vec+i;
}elseif(idx<1<<(TVR_BITS+3*TVN_BITS)){
inti=(expires>>(TVR_BITS+2*TVN_BITS))&TVN_MASK;
vec=base->tv4.vec+i;
}elseif((signedlong)idx<0){
/*
*Canhappenifyouaddatimerwithexpires==jiffies,
*oryousetatimertogooffinthepast
*/
vec=base->tv1.vec+(base->timer_jiffies&TVR_MASK);
}else{
inti;
/*Ifthetimeoutislargerthan0xffffffffon64-bit
*architecturesthenweusethemaximumtimeout:
*/
if(idx>0xffffffffUL){
idx=0xffffffffUL;
expires=idx+base->timer_jiffies;
}
i=(expires>>(TVR_BITS+3*TVN_BITS))&TVN_MASK;
vec=base->tv5.vec+i;
}
/*
*TimersareFIFO:
*/
///最终加入链表
list_add_tail(&timer->entry,vec);
}
staticvoidinternal_add_timer(structtvec_base*base,structtimer_list*timer)
{
///取出超时jiffies
unsignedlongexpires=timer->expires;
///得到定时器还有多长时间到期(这里是相比于最短的那个定时器)
unsignedlongidx=expires-base->timer_jiffies;
structlist_head*vec;
///开始判断该把定时器加入到那个队列.依次为tv1到tv5
if(idx<TVR_SIZE){
inti=expires&TVR_MASK;
vec=base->tv1.vec+i;
}elseif(idx<1<<(TVR_BITS+TVN_BITS)){
inti=(expires>>TVR_BITS)&TVN_MASK;
vec=base->tv2.vec+i;
}elseif(idx<1<<(TVR_BITS+2*TVN_BITS)){
inti=(expires>>(TVR_BITS+TVN_BITS))&TVN_MASK;
vec=base->tv3.vec+i;
}elseif(idx<1<<(TVR_BITS+3*TVN_BITS)){
inti=(expires>>(TVR_BITS+2*TVN_BITS))&TVN_MASK;
vec=base->tv4.vec+i;
}elseif((signedlong)idx<0){
/*
*Canhappenifyouaddatimerwithexpires==jiffies,
*oryousetatimertogooffinthepast
*/
vec=base->tv1.vec+(base->timer_jiffies&TVR_MASK);
}else{
inti;
/*Ifthetimeoutislargerthan0xffffffffon64-bit
*architecturesthenweusethemaximumtimeout:
*/
if(idx>0xffffffffUL){
idx=0xffffffffUL;
expires=idx+base->timer_jiffies;
}
i=(expires>>(TVR_BITS+3*TVN_BITS))&TVN_MASK;
vec=base->tv5.vec+i;
}
/*
*TimersareFIFO:
*/
///最终加入链表
list_add_tail(&timer->entry,vec);
}
这里要知道内核中的软定时器是用软中断来实现的,软中断的注册以及实现可以看我前面的blog,这里就不介绍了.我们来看timer模块的初始化:
Java代码
void__initinit_timers(void)
{
///主要是初始化boot_tvec_bases(如果是smp,则会初始化所有cpu上的boot_tvec_bases)
interr=timer_cpu_notify(&timers_nb,(unsignedlong)CPU_UP_PREPARE,
(void*)(long)smp_processor_id());
init_timer_stats();
BUG_ON(err==NOTIFY_BAD);
///注册到cpu的notifychain(这个我前面的blog也有介绍)
register_cpu_notifier(&timers_nb);
///注册软中断
open_softirq(TIMER_SOFTIRQ,run_timer_softirq);
}
void__initinit_timers(void)
{
///主要是初始化boot_tvec_bases(如果是smp,则会初始化所有cpu上的boot_tvec_bases)
interr=timer_cpu_notify(&timers_nb,(unsignedlong)CPU_UP_PREPARE,
(void*)(long)smp_processor_id());
init_timer_stats();
BUG_ON(err==NOTIFY_BAD);
///注册到cpu的notifychain(这个我前面的blog也有介绍)
register_cpu_notifier(&timers_nb);
///注册软中断
open_softirq(TIMER_SOFTIRQ,run_timer_softirq);
}
ok,接下来我们就来看timer_cpu_notify这个函数,其实这个函数还是定时器注册的cpu的notifychain的action:
Java代码
staticstructnotifier_block__cpuinitdatatimers_nb={
.notifier_call=timer_cpu_notify,
};
staticint__cpuinittimer_cpu_notify(structnotifier_block*self,
unsignedlongaction,void*hcpu)
{
longcpu=(long)hcpu;
switch(action){
caseCPU_UP_PREPARE:
caseCPU_UP_PREPARE_FROZEN:
///模块初始化的时候就会调用这个函数
if(init_timers_cpu(cpu)<0)
returnNOTIFY_BAD;
break;
....................................
returnNOTIFY_OK;
}
staticstructnotifier_block__cpuinitdatatimers_nb={
.notifier_call=timer_cpu_notify,
};
staticint__cpuinittimer_cpu_notify(structnotifier_block*self,
unsignedlongaction,void*hcpu)
{
longcpu=(long)hcpu;
switch(action){
caseCPU_UP_PREPARE:
caseCPU_UP_PREPARE_FROZEN:
///模块初始化的时候就会调用这个函数
if(init_timers_cpu(cpu)<0)
returnNOTIFY_BAD;
break;
....................................
returnNOTIFY_OK;
}
其他的部分我们忽略,我们就发现定时器模块会调用init_timers_cpu来初始化.我们来分析这个函数.
这个函数最主要的功能就是初始化boot_tvec_bases,也就是全局的定时器表:
Java代码
staticint__cpuinitinit_timers_cpu(intcpu)
{
intj;
structtvec_base*base;
///可以看到这个是一个静态变量.它保存了每个cpu上的那个boot_tvec_bases.
staticchar__cpuinitdatatvec_base_done[NR_CPUS];
///如果为空,说明这个cpu上的定时器表还没有初始化,因此需要初始化
if(!tvec_base_done[cpu]){
/*这个也是一个静态变量.它表示了cpu是否初始化完毕.这个函数有一个宏__cpuinit,这个将*这个函数放置到cpuinit这个段,因此也就是说这个函数会先在cpu初始化时调用,也就是第一**次会先给boot_done赋值,然后再调用这个函数才会进入kmalloc.
*/
staticcharboot_done;
if(boot_done){
/*
*TheAPsusethispathlaterinboot
*/
///malloc一个tvec_base
base=kmalloc_node(sizeof(*base),
GFP_KERNEL|__GFP_ZERO,
cpu_to_node(cpu));
if(!base)
return-ENOMEM;
/*Makesurethattvec_baseis2bytealigned*/
if(tbase_get_deferrable(base)){
WARN_ON(1);
kfree(base);
return-ENOMEM;
}
///由于在percpu的变量中类型为tvec_bases的,只有boot_tvec_bases,因此,也就是将base这个指针付给boot_tvec_bases.
per_cpu(tvec_bases,cpu)=base;
}else{
///cpu初始化完毕后会进入这里,标记了cpu已经boot完毕.此时内存初始化完毕.
boot_done=1;
base=&boot_tvec_bases;
}
tvec_base_done[cpu]=1;
}else{
///取出tvec_base付给base
base=per_cpu(tvec_bases,cpu);
}
///开始初始化
spin_lock_init(&base->lock);
///开始初始化5个定时器表
for(j=0;j<TVN_SIZE;j++){
INIT_LIST_HEAD(base->tv5.vec+j);
INIT_LIST_HEAD(base->tv4.vec+j);
INIT_LIST_HEAD(base->tv3.vec+j);
INIT_LIST_HEAD(base->tv2.vec+j);
}
for(j=0;j<TVR_SIZE;j++)
INIT_LIST_HEAD(base->tv1.vec+j);
///默认值为初始化时的jiffes
base->timer_jiffies=jiffies;
return0;
}
staticint__cpuinitinit_timers_cpu(intcpu)
{
intj;
structtvec_base*base;
///可以看到这个是一个静态变量.它保存了每个cpu上的那个boot_tvec_bases.
staticchar__cpuinitdatatvec_base_done[NR_CPUS];
///如果为空,说明这个cpu上的定时器表还没有初始化,因此需要初始化
if(!tvec_base_done[cpu]){
/*这个也是一个静态变量.它表示了cpu是否初始化完毕.这个函数有一个宏__cpuinit,这个将*这个函数放置到cpuinit这个段,因此也就是说这个函数会先在cpu初始化时调用,也就是第一**次会先给boot_done赋值,然后再调用这个函数才会进入kmalloc.
*/
staticcharboot_done;
if(boot_done){
/*
*TheAPsusethispathlaterinboot
*/
///malloc一个tvec_base
base=kmalloc_node(sizeof(*base),
GFP_KERNEL|__GFP_ZERO,
cpu_to_node(cpu));
if(!base)
return-ENOMEM;
/*Makesurethattvec_baseis2bytealigned*/
if(tbase_get_deferrable(base)){
WARN_ON(1);
kfree(base);
return-ENOMEM;
}
///由于在percpu的变量中类型为tvec_bases的,只有boot_tvec_bases,因此,也就是将base这个指针付给boot_tvec_bases.
per_cpu(tvec_bases,cpu)=base;
}else{
///cpu初始化完毕后会进入这里,标记了cpu已经boot完毕.此时内存初始化完毕.
boot_done=1;
base=&boot_tvec_bases;
}
tvec_base_done[cpu]=1;
}else{
///取出tvec_base付给base
base=per_cpu(tvec_bases,cpu);
}
///开始初始化
spin_lock_init(&base->lock);
///开始初始化5个定时器表
for(j=0;j<TVN_SIZE;j++){
INIT_LIST_HEAD(base->tv5.vec+j);
INIT_LIST_HEAD(base->tv4.vec+j);
INIT_LIST_HEAD(base->tv3.vec+j);
INIT_LIST_HEAD(base->tv2.vec+j);
}
for(j=0;j<TVR_SIZE;j++)
INIT_LIST_HEAD(base->tv1.vec+j);
///默认值为初始化时的jiffes
base->timer_jiffies=jiffies;
return0;
}
通过上面的定时器初始化函数我们知道定时器软中断所对应的action是run_timer_softirq,也就是当时钟中断到来,软中断启动时,就会调用这个函数,因此我们来看这个函数:
这个函数功能很简单,它的最关键就是调用__run_timers,这个函数才是真正处理定时器的函数.
Java代码
staticvoidrun_timer_softirq(structsoftirq_action*h)
{
structtvec_base*base=__get_cpu_var(tvec_bases);
///这个函数应该是提供给2.6.31内核的新特性PerformanceCounters.
perf_counter_do_pending();
///处理hrttimer
hrtimer_run_pending();
///判断当前的jiffies是否大于等于最小的那个超时jiffies.是的话就进入定时器处理
if(time_after_eq(jiffies,base->timer_jiffies))
__run_timers(base);
}
staticvoidrun_timer_softirq(structsoftirq_action*h)
{
structtvec_base*base=__get_cpu_var(tvec_bases);
///这个函数应该是提供给2.6.31内核的新特性PerformanceCounters.
perf_counter_do_pending();
///处理hrttimer
hrtimer_run_pending();
///判断当前的jiffies是否大于等于最小的那个超时jiffies.是的话就进入定时器处理
if(time_after_eq(jiffies,base->timer_jiffies))
__run_timers(base);
}
__run_timers这个函数的主要功能是运行所有超时的定时器:
1
Java代码
staticinlinevoid__run_timers(structtvec_base*base)
{
structtimer_list*timer;
///关闭中断并且开启自旋锁
spin_lock_irq(&base->lock);
///然后遍历定时器级联表
while(time_after_eq(jiffies,base->timer_jiffies)){
///这里的head和work_list其实表示的就是已经超时的定时器,也就是我们将要处理的定时器.
structlist_headwork_list;
structlist_head*head=&work_list;
///从timer_jiffies得到所在index,其实也就是在tv1中的index
intindex=base->timer_jiffies&TVR_MASK;
///开始处理层叠定时器,这里的这个cascade是一个关键的函数,我们下面会分析,这里只需要知道这个函数其实也就是用来一层层的得到这个定时器处于哪个级别中.
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));
///更新timer_jiffies.
++base->timer_jiffies;
///用work_list替换掉base->tv1.vec+index.这里因为上面的处理中,就算定时器不在base->tv1中,可是通过cascade的调节,会将base->tv2加入到base->tv1中,或者说base->tv3,以此类推.
list_replace_init(base->tv1.vec+index,&work_list);
///如果这个值不为空说明有已经超时的定时器.这里head也就是work_list,也就是base->tv1
while(!list_empty(head)){
void(*fn)(unsignedlong);
unsignedlongdata;
///取出定时器.
timer=list_first_entry(head,structtimer_list,entry);
fn=timer->function;
data=timer->data;
timer_stats_account_timer(timer);
///设置当前正在处理的定时器为timer(这个主要是针对smp的架构),因为我们是在软中断中进行的,因此要防止多个cpu的并发.
set_running_timer(base,timer);
///删除这个定时器.
detach_timer(timer,1);
spin_unlock_irq(&base->lock);
{
intpreempt_count=preempt_count();
lock_map_acquire(&lockdep_map);
///执行定时器回调函数
fn(data);
.............................................
}
spin_lock_irq(&base->lock);
}
}
///修改base->running_timer为空
set_running_timer(base,NULL);
spin_unlock_irq(&base->lock);
}
staticinlinevoid__run_timers(structtvec_base*base)
{
structtimer_list*timer;
///关闭中断并且开启自旋锁
spin_lock_irq(&base->lock);
///然后遍历定时器级联表
while(time_after_eq(jiffies,base->timer_jiffies)){
///这里的head和work_list其实表示的就是已经超时的定时器,也就是我们将要处理的定时器.
structlist_headwork_list;
structlist_head*head=&work_list;
///从timer_jiffies得到所在index,其实也就是在tv1中的index
intindex=base->timer_jiffies&TVR_MASK;
///开始处理层叠定时器,这里的这个cascade是一个关键的函数,我们下面会分析,这里只需要知道这个函数其实也就是用来一层层的得到这个定时器处于哪个级别中.
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));
///更新timer_jiffies.
++base->timer_jiffies;
///用work_list替换掉base->tv1.vec+index.这里因为上面的处理中,就算定时器不在base->tv1中,可是通过cascade的调节,会将base->tv2加入到base->tv1中,或者说base->tv3,以此类推.
list_replace_init(base->tv1.vec+index,&work_list);
///如果这个值不为空说明有已经超时的定时器.这里head也就是work_list,也就是base->tv1
while(!list_empty(head)){
void(*fn)(unsignedlong);
unsignedlongdata;
///取出定时器.
timer=list_first_entry(head,structtimer_list,entry);
fn=timer->function;
data=timer->data;
timer_stats_account_timer(timer);
///设置当前正在处理的定时器为timer(这个主要是针对smp的架构),因为我们是在软中断中进行的,因此要防止多个cpu的并发.
set_running_timer(base,timer);
///删除这个定时器.
detach_timer(timer,1);
spin_unlock_irq(&base->lock);
{
intpreempt_count=preempt_count();
lock_map_acquire(&lockdep_map);
///执行定时器回调函数
fn(data);
.............................................
}
spin_lock_irq(&base->lock);
}
}
///修改base->running_timer为空
set_running_timer(base,NULL);
spin_unlock_irq(&base->lock);
}
ok我们接下来来看下定时器超时的机制,关键在这段代码:
Java代码
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));
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));
index为0就说明当前要处理的定时器不在base->tv1中.因此我们需要cascade来进行调解.
Java代码
///得到在N级(也就是tv2,tv3...)的定时器表中的slot.这里可以对照我们前面的internal_add_timer加入定时器的情况.
#defineINDEX(N)((base->timer_jiffies>>(TVR_BITS+(N)*TVN_BITS))&TVN_MASK)
staticintcascade(structtvec_base*base,structtvec*tv,intindex)
{
/*cascadeallthetimersfromtvuponelevel*/
structtimer_list*timer,*tmp;
structlist_headtv_list;
///这里实例化tv_list为我们将要处理的链表.并将老的list重新初始化为空.
list_replace_init(tv->vec+index,&tv_list);
/*
*Weareremoving_all_timersfromthelist,sowe
*don'thavetodetachthemindividually.
*/
list_for_each_entry_safe(timer,tmp,&tv_list,entry){
BUG_ON(tbase_get_base(timer->base)!=base);
///重新加入定时器,也就是加入到自己对应的位置
internal_add_timer(base,timer);
}
///然后返回index,这里可以看到如果index为空则说明这个级别的定时器也已经都处理过了,因此我们需要再处理下一个级别.
returnindex;
}
///得到在N级(也就是tv2,tv3...)的定时器表中的slot.这里可以对照我们前面的internal_add_timer加入定时器的情况.
#defineINDEX(N)((base->timer_jiffies>>(TVR_BITS+(N)*TVN_BITS))&TVN_MASK)
staticintcascade(structtvec_base*base,structtvec*tv,intindex)
{
/*cascadeallthetimersfromtvuponelevel*/
structtimer_list*timer,*tmp;
structlist_headtv_list;
///这里实例化tv_list为我们将要处理的链表.并将老的list重新初始化为空.
list_replace_init(tv->vec+index,&tv_list);
/*
*Weareremoving_all_timersfromthelist,sowe
*don'thavetodetachthemindividually.
*/
list_for_each_entry_safe(timer,tmp,&tv_list,entry){
BUG_ON(tbase_get_base(timer->base)!=base);
///重新加入定时器,也就是加入到自己对应的位置
internal_add_timer(base,timer);
}
///然后返回index,这里可以看到如果index为空则说明这个级别的定时器也已经都处理过了,因此我们需要再处理下一个级别.
returnindex;
}
可以看到定时器处理始终都是在处理tv1,如果tv1已经处理完了,则将tv2添加到tv1,以此类推.
而定时器软中断如何触发呢,是用update_process_times来触发的,这个函数比较简单,主要是调用run_local_timers来触发软中断:
Java代码
voidrun_local_timers(void)
{
hrtimer_run_queues();
///触发软中断.
raise_softirq(TIMER_SOFTIRQ);
softlockup_tick();
}
voidrun_local_timers(void)
{
hrtimer_run_queues();
///触发软中断.
raise_softirq(TIMER_SOFTIRQ);
softlockup_tick();
}
相关文章推荐
- Linux 时钟处理机制
- Linux 时钟处理机制
- 【转】Linux 时钟处理机制-4
- Linux时钟处理机制
- 【转】Linux 时钟处理机制-5
- Linux 时钟处理机制
- Linux内部的时钟处理机制全面剖析
- Linux内部的时钟处理机制全面剖析
- 【转】Linux 时钟处理机制-6
- Linux 时钟处理机制
- 【转】linux内核时钟处理机制-定时器管理接口数据结构
- linux时钟处理机制(一)
- Linux 时钟处理机制
- Linux 时钟处理机制
- Linux 时钟处理机制
- Linux 时钟处理机制
- 【转】Linux 时钟处理机制-1
- Linux内部的时钟处理机制全面剖析
- Linux 时钟处理机制
- Linux 时钟处理机制