MINIX 3 源码解读报告
—— MINIX 3.1.6 时钟任务
1.时钟的作用
操作系统的时钟任务有很多作用,如维护系统时间、提供定时器、检测系统性能等,还有非常重要的一
点就是防止一个进程独占 CPU 资源。MINIX 3 是一个分时的交互式系统,多个程序共享同一资源时,需要
将 CPU 时间划分成多个时间片,分配给不同的进程,使每个进程都能得到调度。一个进程的运行时间是
否超过最大运行时间,需要由时钟来控制。
2.MINIX 3 的时钟驱动程序
MINIX 3.1.6 的时钟任务管理部分在 kernel/clock.c 的 clock_task()函数中定义。在函数的开始,调用
init_clock()设置一个看门狗时钟,用来周期性地调整进程调度队列。随后,它循环运行,等待一个消
息,然后分析消息来源:如果是 HARD_INT,则进行进一步处理,如果是其他消息,提示出错,进入下一
个循环。
init_clock()程序调用 balance_queues 初始化时钟,除了产生周期性中断之外,还将中断处理程序的地
址放在合适的地方,以便时钟芯片向中断控制芯片触发了 8 号中断时能够找到,并使能它以响应输入的
中断。
时钟中断处理程序在 MINIX 3.1.6 中有两种,一种是用于 boot processor 的
bsp_timer_int_handler(),一种是只用于 non-boot processor 的 ap_timer_int_handler()。前者也调
用后者。它们的区别在于,bsp_timer_int_handler()更新 realtime,并且在必要的时候发消息通知
clock_task()。
在每次时钟中断产生之后,中断处理程序开始运行。clock.c 中不提供具体的系统启动时间的计算,只维
护节拍计数器,提供当前节拍数给系统调用计算真实时间,这符合 MINIX 微内核的特性:功能性的模块
都交由外层的服务器来做。
当中断被禁止时,时钟节拍会丢失,使用一个全局变量 lost_ticks 来计算丢失的节拍数,再加上主计时
器 ticks,每次中断处理程序被激活时将 lost_ticks 清零。这个全局变量本身是由
kernel/arch/i386/klib386.s 中的 int86 函数使用的,int86 使用引导程序监控程序来管理对于 BIOS 的
控制,而监控程序返回在返回到内核之前 BIOS 调用忙期间 ecx 寄存器内对时钟节拍的计数值。
bsp_timer_int_handler()运行时,得到当前的 lost_ticks 数,于是更新 ticks 并清零 lost_ticks 使它
在下一轮重新计算:
ticks = lost_ticks+1;
lost_ticks = 0;
然后更新 realtime:
realtime += ticks;
接下来才是真正调用时钟中断处理器 ap_timer_int_handler()。这个函数首先更新当前进程的用户时
间,如果它是一个用户进程,那么它将支付系统进程的时间。否则,它将从其他进程那里获得一些系统
时间。
p = proc_ptr;
billp = bill_ptr;
p->p_user_time += ticks;
if (priv(p)->s_flags & PREEMPTIBLE) {
p->p_ticks_left -= ticks;
}
if (! (priv(p)->s_flags & BILLABLE)) {
billp->p_sys_time += ticks;
billp->p_ticks_left -= ticks;
}
如果设置了定时器,就要计算新的剩余时间,并且检查是否到期。如果定时器的时间已经用完,则标记
变量 expired 为 1。
if ((p->p_misc_flags & MF_VIRT_TIMER) &&
(p->p_virt_left -= ticks) <= 0) expired = 1;
if ((p->p_misc_flags & MF_PROF_TIMER) &&
(p->p_prof_left -= ticks) <= 0) expired = 1;
if (! (priv(p)->s_flags & BILLABLE) &&
(billp->p_misc_flags & MF_PROF_TIMER) &&
(billp->p_prof_left -= ticks) <= 0) expired = 1;
接下来检查一个进程的虚拟定时器是否到期。当前进程和 bill_ptr 都要被检查,因为一个进程的
user_time 是另一个进程的 sys_time。
vtimer_check(p);
if (p != billp)
vtimer_check(billp);
平均负载被作为一队数组被保存在一个环形缓冲区内。
如果进程在 vtimer 更新后仍然是可调度的,检查它的剩余节拍数,以及它是否可抢占的。如果它的时钟
节拍数已经减小到 0,那么它就要被移出运行队列。
if (p->p_rts_flags == 0 && p->p_ticks_left <= 0 &&
priv(p)->s_flags & PREEMPTIBLE) {
/* this dequeues the process */
RTS_SET(p, RTS_NO_QUANTUM);
}
有时候用户进程需要设定一个时间,到达这个时间的时候由系统来通知该进程,这就要用到看门狗时
钟。在 MINIX 3 中,通知的方法是用一个同步警报将内核与用户空间联系起来。同步警报是以消息的形
式被传递的,只有当接收者执行了 receive 操作之后,才能被接收。如果该 notify 方法用来向一个接收
者通知一个警报消息,那么发送者不必阻塞,接收者也不必关心是否错过了一个警报消息,因为如果接
收者不是在等待该 notify 消息,那么它会被保存起来。
设置时间的函数是 set_timer(tp, exp_time, watchdog),其中参数 tp 是指向定时器的指针,exp_time
是一个 clock_t 变量,表示所定的期限;watchdog 是一个 tmr_func_t 变量,代表将被使用的看门狗时钟
函数。这个看门狗时钟是在其他地方定义的,它要做的就是实现像前面所述的那样,向设定时器的进程
发送一个消息。
在 kernel/system/do_setalarm.c 中定义了系统任务 do_setalarm,它有一个 timer_t 类型的指针 tp 指
向进程的定时器。执行 setalaram 的时候,tp 所指的 timer_t 结构中的 tmr_func 被设定为
cause_alarm,这就是一个看门狗时钟。它的代码仅仅包括两行:
int proc_nr_e = tmr_arg(tp)->ta_int;
lock_notify(CLOCK, proc_nr_e);
取得进程号,然后产生一个 notify,向该进程发送一个同步警报。do_setalarm 把 tp->tmr_exp_time 计
算出来,调用 kernel/clock.c 中的函数 set_timer,让时钟任务在定时器到点的时候调用
cause_alarm。