分类: LINUX
2014-02-18 11:50:59
走到这里,大家肯定对Linux的中断处理有概念了,下面我们通过一个具体的实例,来了解Linux内核处理中断的全过程,那就是定时器中断。在详细分析这个过程之前,我们把Linux时间管理的概念先缕一缕。
在当前的80x86体系结构上,内核显式地与几种时钟和定时器电路打交道,其主要分为了时钟和定时器两大类:
- 时钟电路同时用于跟踪当前时间和产生精确的时间度量。
- 定时器电路由内核编程,所以它们以固定的、预先定义的频率发出中断。
1、实时时钟(RTC)
所有的PC都包含一个叫实时时钟(Renl Time Clock RTC)的时钟,它是独立于CPU和所有其他芯片的。即使当PC被切断电源,RTC还继续工作,因为它靠一个小电池或蓄电池供电。CMOS RAM和RTC被集成在一个芯片上。
RTC能在IRQ8上发出周期性的中断,频率在2~8192 Hz之间。我们可以对RTC进行编程以使当RTC到达某个特定的值时激活IRQ8线,也就是作为一个闹钟来工作。
Linux只用RTC来获取时间和日期
,不过,通过对/dev/rtc设备文件进行操作,也允许透程对RTC编程。内核通过0x70和Ox71
I/O端口访问RTC。系统管理员通过执行Unix系统时钟程序(直接作用于这两个I/O端口)可以设置时钟。MC146818
RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上产生周期性的中断,中断的频率在2HZ~8192HZ之间。与MC146818
RTC对应的设备驱动程序实现在include/linux/rtc.h和drivers/char/rtc.c文件中,对应的设备文件是/dev
/rtc(major=10,minor=135,只读字符设备)。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活IRQ8
线,从而将RTC当作一个闹钟来用。
而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时,它从RTC中读取时间与日期的基准值。然后再运行期间内核就完全抛开RTC,从而以软件的形式维护系统的当前时间与日期,并在需要时将时间回写到RTC芯片中。所以,RTC时钟只是个为后面我们介绍的那些时钟起一个初始化的作用,仅此而已!
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义
了mc146818
RTC芯片各寄存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件
中。
2、时间戳计数器(TSC)
时间戳计数器与实时时钟配合使用,用来将时钟和定时器调整得更精确。所有的80x86微处理器都包含一条CLK输入引线,它接收外部振荡器的时钟信 号。从Pentium开始,80x86微处理器就都包含一个计数器,它在每次外部振荡器的时钟信号到来时加1。该计数器是利用64位的时间戳计数器 (Time Stamp Counter TSC)寄存器来实现的,可以通过汇编语言指令rdtsc读这个寄存器 。
为什么要出现一个时间戳计数器呢?Linux利用这个寄存器可获得更精确的时间量,以便计算进程用户态与内核态的运行时间及等待(睡眠)时间。为了 做到这点,Linux在初始化系统的时候必须确定时钟信号的频率。事实上,因为编译内核时并不声明这个频率,所以同一内核映像可以运行在产生任何时钟频率 的CPU上,即这个频率由CPU决定。
算出CPU实际频率的任务是在系统初始化期间完成的。calibrate_tsc()函数通过计算一个大约在5ms的时间间隔内所产生的时钟信号的个数来算出CPU实际频率。通过适当地设置可编程间隔定时器(PIT)的一个通道来产生这个时间常量。
3、可编程间隔定时器(PIT),重点!
可编程间隔定时器(Programmable Interval Timer PIT)的作用类似于微波炉的闹钟,即让用户意识到烹调的时间间隔已经过了。所不同的是,这个设备不是通过振铃,而是发出一个特殊的中断,叫做时钟中断 (timer interrupt)来通知内核又一个时间间隔过去了。每个IBM兼容PC都至少包含一个PIT,PIT通常是使用0x40~0x43 I/O端口的一个8254 CMOS芯片,该芯片作为I/O设备与中断控制器的IRQ0相连,向量号为32。
Linux给PC的PIT进行编程,使它以1000 Hz的频率向IRQ0发出时钟中断,即每1ms产生一次时钟中断。这个时间间隔叫做一个节拍(tick),它的长度以纳秒为单位存放在tick_nsec 变量中。在PC上,tick_nsec被初始化为999848ns(产生的时钟信号频率大约为1000.15 Hz),但是如果计算机被外部时钟同步的话,它的值可能被内核自动调整。
时钟中断的频率可以通过编译内核前对一些参数的设定来满足于具体硬件体系结构的要求。较慢的机器,其节拍大约为lOms(每秒产生100次时钟中断),而较快的机器的节拍为大约1ms(每秒产生1000或1024次时钟中断)。
在Linux的代码中,有几个宏产生决定时钟中断频率的常量,对此讨论如下:
- HZ产生每秒时钟中断的近似个数,也就是时钟中断的频率。在IBM PC上,这个值设置为1000。
- CLOCK_TICK_RATE产生的值为1193182,这个值是8254芯片的内部振荡器频率。
- LATCH产生CLOCK_TICK_RATE和HZ的比值再四舍五入后的整数值。这个值用来对PIT编程。
PIT由setup_pit_timer()进行如下初始化:
spin_lock_irqsave(&i8253_lock, flags);
outb_p(0x34,0x43);
udelay(10);
outb_p(LATCH & 0xff, 0x40);
udelay(10);
outb(LATCH >> 8, 0x40);
spin_unlock_irqrestore(&i8253_lock, flags);
outb()C函数等价于outb汇编语言指令:它把第一个操作数拷贝到由第二个操作数指定的I/O端口。outb_p()函数类似于outb(),不过,它会通过一个空操作而产生一个暂停,以避免硬件难以分辨。udelay()宏函数引入了一个更短的延迟。
第一条outb_p()语句让PIT以新的频率产生中断。接下来的两条outb_p()和outb()语句为设备提供新的中断频率。把16位 LATCH常量作为两个连续的字节发送到设备的8位I/O端口0x40。结果,PIT将以(大约)1000Hz的频率产生时钟中断,也就是说,每1ms产 生一次时钟中断。
4、其他定时器
除了PIT,80x86体系还有其他几个定时器,这里只简单提一提:
- CPU本地定时器:CPU本地定时器是一种能够产生单步中断或周期性中断的设备,向量范围是239 (0xef),比PIT更灵活地编程。
- 高精度事件定时器(HPET):可以通过映射到内存空间的寄存器来对HPET芯片编程的定时器。
- ACPI电源管理定时器:时钟信号拥有大约为3.58 MHz的固定频率,专门针对ACPI电源的定时器。
本博文,我们重点讨论第三项,即可编程间隔定时器,它是整个Linux内核的心脏,驱动着若干进程的运行。PS,这里只讨论单CPU的计时体系。
定时器中断相关的数据结构
定时器对象
前面讲了那么多定时器,为了使用一种统一的方法来处理可能存在的定时器资源,内核使用了“定时器对象”,它是类型为timer_opts的描述符,该类型由定时器名称和四个标准的方法组成,如下表所示。
字段名 |
说明 |
name |
标识定时器源的一个字符串 |
mark_offset |
记录上一个节拍的准确时间,由时钟中断处理程序调用 |
get_offset |
返回自上一个节拍开始所经过的时间 |
monotonic_clock |
返回自内核初始化开始所经过的纳秒数 |
delay |
等待指定数目的“循环” |
定时器对象中最重要的方法是mark_offset和get_offset。mark_offset方法由时钟中断处理程序调用,并以适当的数据结 构记录每个节拍到来时的准确时间。get_offset方法使用已记录的值来计算自上一次时钟中断(节拍)以来经过的时间(以纳秒为单位)。由于这两种方 法,使得Linux计时体系结构能够达到子节拍的分辨度,也就是说,内核能够以比节拍周期更高的精度来测定当前的时间。这种操作被称作“定时插补 (time interpolation)”
全局变量cur_timer存放了某个定时器对象的地址,该定时器是系统可利用的定时器资源中“最好的”。最初,cur_timer指向 timer_none,这个timer_none是一个虚拟的定时器资源对象,内核在初始化的时候使用它。在内核初始化期 间,select_timer()函数设置cur_timer指向适当定时器对象的地址。我们看到,select_timer()将优先选择HPET(如 果可以使用);否则,将选择ACPI电源管理定时器(如果可以使用);再次之是TSC作为最后的方案。但是不管前面是什么方案,select_timer()总是会选择PIT配合使用,因为前面的那些时钟主要是 用来维护jiffies_64变量的,我们后面会提及 。看下面的表,“定时插补”一列列出了定时器对象的mark_offset方法和get_offset方法所使用的定时器源,“延迟”一列列出了delay方法使用的定时器源。
定时器对象名称 |
说明 |
定时插补 |
延迟 |
timer_hpet |
高精度事件定时器(HPET ) |
HPET |
HPET |
timer_pmtmr |
ACPI 电源管理定时器(ACPI PMT ) |
ACPI PMT |
TSC |
timer_tsc |
时间戳计数器(TSC ) |
TSC |
TSC |
timer_pit |
可编程间隔定时器(PIT) |
PIT |
紧致循环 |
timer_none |
普通虚拟定时器资源(内核初始化时使用) |
( 无) |
紧致循环 |
jiffies变量
jiffies变量是一个计数器,用来记录自系统启动以来产生的总的节拍数 。每次时钟中断发生时(每个节拍)它便加1。在80x86体系结构中,jiffies是一个32位的变量,因此每隔大约50天它的值会回绕 (wraparound)到0,这对Linux服务器来说是一个相对较短的时间间隔。不过,由于使用了time_after、 time_after_eq、time_before和time_before_eq四个宏(即使发生回绕它们也能产生正确的值),内核干净利索地处理了 jiffies变量的溢出。
注意,jiffies桩初始化为Oxfffb6c20,并不是0,它是一个32位的有符号值,正好等于-300000。因此,计数器将会在系统启动 后的5分钟内处于溢出状态。这样做是有目的的,使得那些不对jiffies作溢出检测的有缺陷的内核代码在开发阶段被及时地发现,从而不再出现在稳定的内 核版本中。
但是在某些情况下,不管jiffies是否溢出,内核都需要取得自系统启动以来产生的系统节拍的真实数目。因此,在80x86系统 中,jiffies变量通过连接器被换算成一个64位计数器的低32位,这个64位的计数器被称作jiffies_64。在1ms为一个节拍的情况 下,jiffies_64变量将会在数十亿年后才发生回绕,所以我们可以放心地假定它不会溢出。
你可能要问为什么在80x86体系结构中jiffies不直接被声明成64位无符号的长整型数。答案是:在32位的体系结构中不能自动地对64位的 变量进行访问。因此,在每次数行对64位数的读操作时,需要一些同步机制来保证当两个32位的计数器(由这两个32位的计数器组成的64位计数器)的值在 被读取时这个64位的计数器不会被更新,结果是,每个64位的读操作明显比32位的读操作更慢。
get_jiffies_64( ) 函数来读取 jiffies_64 的值并返回该值。
xtime变量
xtime变量存放当前时间和日期 ;它是一个timespec类型的数据结构,该结构有两个字段:
tv_sec:存放自1970年1月1日(UTC)午夜以来经过的秒数
tv_nsec:存放自上一秒开始经过的纳秒数(它的值域范围在0 - 999999999之间)
xtime变量通常是每个节拍更新一次,也就是说,大约每秒更新1000次。用户程序从xtime变量获得当前时间和日期。内核也经常引用它,例如,在更新节点时间戳时引用。
定时器中断的上半部分
在单处理器系统上,所有与定时有关的活动都是由IRQ线0上的可编程间隔定时器产生的中断触发的。还是那句老话,在Linux中,某些活动都尽可能在中断产生后立即执行,而其余的活动延迟。
初始化阶段
在内核初始化期间,time_init()函数被调用来建立计时体系结构,它通常执行如下操作
1.初始化xtime变量。利用get_cmos_time()函数从实时时钟上读取自1970年1月1日(UTC)午夜以来经过的秒数。设置xtime的tv_nsec字段,它将落到秒的范围内。
2.初始化wall_to_monotonic变量。这个变量同xtime一样是timespec类型,只不过它存放将被加到xtime上的秒数和 纳秒数,以此来获得单向(只增)的时间流。其实,外部时钟的闰秒和同步都有可能突发地改变xtime的tv_sec和tv_nsec字段,这样使得它们不 再是单向递增的。
3.如果内核支持HPET,它将调用hpet_enable()函数来确认ACPI固件是否探测到了该芯片并将它的寄存器映射到了内存地址空间中。 如果结果是肯定的,那么hpet_enable()将对HPET芯片的第一个定时器编程使其以每秒1000次的频率引发IRQ 0处的中断。否则,如果不能获得HPET芯片,内核将使用PIT:该芯片已经被init_IRQ()函数编程,使得它以每秒1000次的频率引发IRQ 0处的中断,我们这里主要就讨论这种情况。
4. 调用select_timer()来挑选系统中可利用的最好的定时器资源,并设置cur_timer变量指向该定时器资源对应的定时器对象的地址,我们这里假设使用的是TSC和PIT,上面的第三步未执行。
5. 调用setup_irq(0, &irq0)来创建与IRQ0相应的中断门,IRQ0引脚线连接着系统时钟中断源(PIT或HPET)。irq0变量被静态定义如下:
struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL };
从现在起,timer_interrupt()函数将会在每个节拍到来时被调用,而中断被禁止,因为IRQ0主描述符的状态字段中的SA_INTERRUPT标志被置位。
定时器中断总体过程
当CPU接收一个定时器(PIT)中断时,就会马上跳到common_interrupt汇编程序段中,开始执行相应的中断处理程序代码,该代码的 地址存放在IDT的相应门中,那么针对定时器中断,这个门的向量就是32号。于是,与其他上下文切换一样,Linux需要保留当前寄存器的内容以便保存和 恢复当前指令。
保存寄存器是中断处理程序做的第一件事情。每个IRQ的中断处理程序地址存放于interrupt数组中,即IRQ0中断处理程序的地址开始存在于interrupt[0],然后复制到IDT相应表项的中断门中。
保存寄存器的值以后,栈顶的地址被存放到eax寄存器中,然后中断处理程序调用do_IRQ()函数。到执行到do_IRQ()的ret指令时(即函数结束时),控制转到ret_from_intr()(从中断和异常返回)。
do_IRQ()函数不用多说了,因为前几篇博文说得很多了,这里只提一点,我们假设系统式两片8259A级联出来的PIC,那么 __do_IRQ()函数获得自旋锁后,就调用主IRQ描述符的ack方法,把IRQ号传给他。ack方法就是mask_and_ack_8259A() 函数来应答PIT的中断,注意禁用这条IRQ线 ,确保在这个中断处理程序结束前,CPU不进一步接受这种中断的出现:handle_IRQ_event(irq, regs, irq_desc[irq].action);这里的irq_desc[irq].action就是0号中断服务程序timer_interrupt。之 后,__do_IRQ()函数执行irq_desc[irq].handler->end(irq);即调用i8259A_irq_type的 end_8259A_irq()函数重新开启这条IRQ线 ,也就是0号线。
定时器中断处理程序
timer_interrupt()函数是PIT或HPET的中断服务例程(ISR),它执行以下步骤:
1. 在xtime_lock顺序锁上产生一个write_seqlock()来保护与定时相关的内核变量。
2. 执行cur_timer定时器对象的mark_offset方法。正如前面的“时钟中断的数据结构”一节解释的那样,有四种可能的情况:
a) cur_timer指向timer_hpet对象:这种情况下,HPET芯片作为时钟中断源。mark_offset方法检查自上一个节拍以来是否
丢失时钟中断,在这种不太可能发生的情况下,它会相应地更新jiffies_64。接着,该方法记录下HPET周期计数器的当前值。
b) cur_timer指向timer_pmtmr对象:这种情况下,PIT芯片作为时钟中断源,但是内核使用APIC电源管理定时器以更高的分辨度来
测量时间。mark_offset方法检查自上一个节拍以来是否丢失时钟中断,如果丢失则更新jiffies_64。然后,它记录APIC电源管理定时器
计数器的当前值。
c) cur_timer指向timer_tsc对象
:这种情况下,PIT芯片作为时钟中断源,但是内核使用时间戳计数器以更高的分辨度来测量时间。mark_offset方法执行与上一种情况相同的操作:检查自上一个节拍以来是否丢失时钟中断,如果丢失则更新jiffies_64。
然后,它记录TSC计数器的当前值。
d) cur_timer指向timer_pit对象(大多数情况下是这样):这种情况下,PIT芯片作为时钟中断源,除此之外没有别的定时器电路。mark_offset方法什么也不做。
3. 调用do_timer_interrupt( )函数,do_timer_interrupt( )函数执行以下操作:
a) 使jiffies_64的值增1。注意,这样做是安全的,因为内核控制路径仍然为写操作保持着xtime_lock顺序锁。
b) 调用update_times()函数来更新系统日期和时间,并计算当前系统负载。
c) 调用update_process_times()函数为本地CPU上运行链表的进程执行几个与定时相关的计数操作,参见博文“scheduler_tick函数
”。
d) 调用profile_tick()函数。
e) 如果使用外部时钟来同步系统时钟(以前已发出过adjtimex()系统调用),则每隔660秒(每隔11分钟)调用一次set_rtc_mmss()函数来调整实时时钟。这个特性用来帮助网络中的系统同步它们的时钟。
4. 调用write_sequnlock()释放xtime_lock顺序锁。
5. 返回值1,报告中断已经被有效地处理了。
这里我们再进一步深入分析一下do_timer_interrupt函数。在这里,我们这里暂不讨论SMP结构中采用APIC时的特殊处理 (CONFIG_X86_IO_APIC),也不讨论SGI工作站和PS/2的“Micro chanel”的特殊情况,此外我们也不关心时钟的精度(time_status变量的条件语句)。所以我们重点关注定时中断的核心步 骤:update_times和update_process_times,其通过函数do_timer_interrupt_hook被触发。
update_times函数
先来看update_times():
static inline void update_times(void)
{
unsigned long ticks;
ticks = jiffies - wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
update_wall_time(ticks);
}
calc_load(ticks);
}
这里做了两件事。第一件是update_wall_time(),目的是处理所谓“实时时钟”,或者说“墙上时间”xtime中的数值 ,包括计数,进位,以及为精度目的而做的校正。我们暂时不深入了。这里的wall_jiffies也像jiffies一样是个全局量,它代表着与当前xtime中的数值相对应的jiffies值,表示“挂钟”当前的读数已经校准到了时轴上的哪一点。
第二件事是calc_load(),目的是计算和积累关于CPU负荷统计信息。内核每隔5秒钟计算、累积和更新一次系统在过去15分钟、10分钟以及1分钟内平均有多少个进程处于可执行状态,作为衡量系统负荷轻重的指标。由于涉及的主要是数值计算,我们就不深入进去了。
profile_tick函数
do_timer_interrupt函数调用profile_tick()函数,内核进行性能分析。通过该函数在每次系统时钟中断时的采样,并对采样结果进行分析,可以获知系统的性能瓶颈在什么地方。
update_process_times函数(重点)
函数update_process_times()则对当前被中断进程进行记账,减小进程剩余可用的时间片。然后激活其他内核模块的处理函数,如同 步机制RCU的处理函数、内核定时器的处理函数。首先,do_timer_interrupt_hook()调用该函数时,传递进来一个实际参数 user_mode(regs),该参数使用函数 user_mode()来判断被中断时系统处于用户态还是内核态。该函数根据被打断的上下文所使用的指令段选择子寄存器cs的CPL的字段是否为3,如果 为3则返回1,表示是用户态;否则返回0,表示为内核态。
随后通过宏定义current、smp_processor_id()获得当前进程描述符指针和当前处理器编号,并将其分别保存到指针变量p和整型cpu中。
然后,根据被中断进程运行于用户态还是系统态分别调用account_user_time()、account_system_time()对被中 断进程进行记账。它们分别将进程描述符中用户态时间字段utime、内核态时间字段stime的值加上jiffies_to_cputime(1),表示 进程在用户态或者内核又运行了一个系统时钟滴答。然后更新处理器历史统计信息。其中jiffies_to_cputime()是一个宏定义,该宏定义用于 将一个时钟中断转换为处理器时间。
之后,调用函数run_local_timers()设置时钟中断处理的下半部处理标记 、激活时钟中断处理的下半部。该下半部负责维护、更新内核定时器链表,对于超时的内核定时器执行相应的超时处理函数,并将超时的定时器移出内核定时器链表。其中函数 run_local_timers()在文件src/kernel/timer.c中定义如下:
void run_local_timers(void)
{
raise_softirq(TIMER_SOFTIRQ);
}
定时器中断的下半部分
当执行完run_local_timers()函数以后,整个定时器中断进入了下半部分。好了,我们又来回忆下半部分的基础知识。定时器中断的下半部属于软中断,没有涉及tasklet,前面讲过,软中断的数据结果如图所示:
初始化的时候有个init_timers 函数 ,该函数执行一条命令:open_softirq(TIMER_SOFTIRQ,run_timer_softirq, NULL);,也就是优先级最高的软中断TIMER_SOFTIRQ对应的处理函数为run_timer_softirq,即把 SOFTIRQ_VEC[0]的action指向run_timer_softirq,data当然就是空的NULL了。所以,当执行 raise_softirq(TIMER_SOFTIRQ); 函数时,就会告诉首先设置每个CPU的irq_cpustat_t数据结构的 __softirq_pending字段对应定时器软中断的那个位,这个操作被称为“挂起软中断”,即挂起0号软中断(定时器优先级最高,为0),实现代 码如下:
local_softirq_pending() |= 1UL << (nr); //对于定时器软中断来说,这个nr就是0.
注意,前一篇博文说过了,内核使用宏local_softirq_pending(),获得本地CPU的软中断位掩码__softirq_pending。
raise_softirq(TIMER_SOFTIRQ)还要调用in_interrupt()函数来做一个判断,判断两种情况,一种是对应的 raise_softirq被其他中断上下文调用了,一种是当前系统禁用了软中断,注意!是软中断,不是eflags寄存器的IF位。
继续往下走,raise_softirq(TIMER_SOFTIRQ)调用wakeup_softirqd()以唤醒本地CPU的 ksoftirqd内核线程,该线程周期性地调用do_softirq( );只要local_softirq_pending()返回不为0,就调用,因为说明有某个或多个软中断是挂着的,待处理的。
当然,我们这里肯定不为0,所以调用do_softirq,执行local_irq_save以保存IF标志的状态值,并禁用本地CPU上的中断 。
然后调用__do_softirq()函数,该函数主要执行这么一个循环:
pending =local_softirq_pending;
do {
if (pending & 1) {
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
__do_softirq()函数读取本地CPU的软中断掩码并执行与每个设置位相关的可延迟函数。由于正在执行一个软中断函数时可能出现新挂起的软中断,所以为了保证可延迟函数的低延迟性,__do_softirq()一直运行到执行完所有挂起的软中断。
最后回到do_softirq()函数中,执行local_irq_restore以恢之前保存的IF标志(表示本地是关中断还是开中断)的状态值并返回。
讲到这里,我们才进入软中断的真正处理函数h->action(h);,也就是跳到前边提到的run_timer_softirq()。该函 数作为后半部在合适的时刻开始运行负责对内核定时器链表中的定时器进行处理。接下来我们从定时器中断下半部的入口函数 run_timer_softirq()开始做一个简单的分析,看一看定时器中断下半部都具体完成了哪些工作。
该函数是定时器中断处理的下半部,负责判断当前处理器上是否有定时器超时,并在有定时器超时的情况下进行超时处理。
run_timer_softirq()首先声明并初始化指针变量base指向每处理器变量tvec_bases的本地拷贝的地址。 随后使用宏定义time_after_eq()判断当前处理器上的待处理内核定时器链表中是否有超时的内核定时器,如果有,调用函数 __run_timers()进行处理。
__run_timers()函数负责调用超时内核定时器对应的超时处理函数,完成超时处理。在必要时对内核定时器核心数据结构tvec_base_t进行更新和维护,以保证能够快速访问超时定时器所在的链表。
回到update_process_times函数当中,当下半部主要函数run_timer_softirq()执行完毕后,有一个重要的收尾工作,也就是执行scheduler_tick()。
当每次时钟节拍到来时,scheduler_tick()负责执行以下步骤:
1. 把转换为纳秒的TSC的当前值存入本地运行队列的timestamp_last_tick字段,即记录最近一次定时器中断的时间戳 。这个时间戳是从函数sched_clock()获得的。
2. 检查当前进程是否是本地CPU的swapper进程,如果是,执行下面的子步骤:
a)
如果本地运行队列除了swapper进程外,还包括另外一个可运行的进程,就设置当前进程的TIF_NEED_RESCHED字段,以强迫进行重新调度。
就像我们稍后在讲schedule函数所看到的,如果内核支持超线程技术,那么,只要一个逻辑CPU运行队列中的所有进程都有比另一个逻辑CPU(两个逻
辑 CPU对应同一个物理CPU)上已经在执行的进程有低得多的优先级,前一个逻辑CPU就可能空闲,即使它的运行队列中有可运行的进程。
b) 跳转到第7步(没有必要更新swapper进程的时间片计数器)
3. 检查current->array是否指向本地运行队列的活动链表。如果不是,说明进程已经过期但还没有被替换:设置TIF_NEED_RESCHED字段,以强迫进行重新调度并跳转到第7步。
4. 获得this_rq()->lock自旋锁。
5. 递减当前进程的时间片计数器,并检查是否已经用完时间片。由于进程的调度类型不同,函数所执行的这一步操作也有很大的差别,我们马上将会讨论它。
6. 释放this_rq()->lock自旋锁。
7. 调用rebalance_tick()函数,该函数应该保证不同CPU的运行队列包含数量基本相同的可运行进程。
第5步中,我们分情况请进行说明:
更新实时进程的时间片
如果当前进程是先进先出(FIFO)的实时进程,函数scheduler_tick()什么都不做。实际上在这种情况下,current所表示的当 前进程想占用CPU多久就占用多久,而且不可能比其他优先级低或其他优先级相等的进程所抢占,因此,维持当前进程的最新时间片计数器是没有意义的。
如果current表示基于时间片轮转的实时进程,scheduler_tick()就递减它的时间片计数器并检查时间片是否被用完:
if (current->policy == SCHED_RR && !--current->time_slice) {
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
set_tsk_need_resched(current);
list_del(¤t->run_list);
list_add_tail(¤t->run_list,
this_rq( )->active->queue+current->prio);
}
如果函数确定时间片确实用完了,就执行一系列操作以达到抢占当前进程的目的,如果必要的话,就尽快抢占:
第一步操作包括调用task_timeslice()来重填进程的时间片计数器。该函数检查进程的静态优先级,并根据前面“普通进程的调度”公式返 回相应的基本时间片。此外,current的first_time_slice字段被清零:该标志被fork系统调用例程中的 copy_process()设置,并在进程的第一个时间片刚用完时立刻清零。
第二步,scheduler_tick()函数调用函数set_tsk_need_resched()设置进程的 TIF_NEED_RESCHED标志。该标志强制调用schedule()函数,以便current指向的进程能被另外一个有相同优先级或更高优先级的 实时进程所取代。
scheduler_tick()的最后一步操作包括把进程描述符移到与当前进程优先级相应的运行队列活动链表的尾部。把current指向的进程 放到链表的尾部,可以保证每个优先级与它相同的可运行实时进程获得CPU时间片以前,它不会再次被选择来执行。这是基于时间片轮转的调度策略。进程描述符 的移动是通过两个步骤完成的:先调用list_del()把进程从运行队列的活动链表中删除,然后调用list_add_tail()把进程重新插入到同 一个活动链表的尾部。
更新普通进程的时间片
如果当前进程是普通进程,函数scheduler_tick()执行下列操作:
1. 递减时间片计数器(current->time_slice)。
2. 检查时间片计数器。如果时间片用完,函数执行下列操作:
a) 调用dequeue_task()从可运行进程的this_rq()->active集合中删除current指向的进程。
b) 调用set_tsk_need_resched( )设置TIF_NEED_RESCHED标志。
c) 更新current指向的进程的动态优先级:current->prio =
effective_prio(current);。函数effective_prio()读current的static_prio和
sleep_avg字段,并根据前面的公式计算出进程的动态优先级。
d) 重填进程的时间片:
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
e) 如果本地运行队列数据结构中的expired_timestamp字段等于0(即过期进程集合为空),就把当前时钟节拍值赋给expired_timestamp:
if (!this_rq( )->expired_timestamp)
this_rq( )->expired_timestamp = jiffies;
f) 把当前进程插入活动进程集合或过期进程集合:
if (!TASK_INTERACTIVE(current) || EXPIRED_STARVING(this_rq( )) {
enqueue_task(current, this_rq( )->expired);
if (current->static_prio < this_rq( )->best_expired_prio)
this_rq( )->best_expired_prio = current->static_prio;
} else
enqueue_task(current, this_rq( )->active);
如果用前面列出的公式(3)识别出进程是一个交互式进程,TASK_INTERACTIVE宏就产生1。宏EXPIRED_STARVING检查运行队列
中的第一个过期进程的等待时间是否已经超过1000个时钟节拍乘以运行队列中的可运行进程数加1,如果是,宏产生1。如果当前进程的静态优先级大于一个过
期进程的静态优先级,EXPIRED_STARVING宏也产生1。
3. 否则,如果时间片没有用完(current->time_slice不等于0),检查当前进程的剩余时间片是否太长:
if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -
p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
(p->time_slice >= TIMESLICE_GRANULARITY(p)) &&
(p->array == rq->active)) {
list_del(¤t->run_list);
list_add_tail(¤t->run_list,
this_rq( )->active->queue+current->prio);
set_tsk_need_resched(p);
}
宏TIMESLICE_GRANULARITY产生两个数的乘积给当前进程的bonus,其中一个数为系统中的CPU数量,另一个为成比例的常量。基本
上,具有高静态优先级的交互式进程,其时间片被分成大小为TIMESLICE_GRANULARITY的几个片段,以使这些进程不会独占CPU。