首先是定时器的初始化,在 Linux-0.11 版本中,使用的是 8253/8254 中的定时器 0,初始化部分代码如下:
void sched_init(void) {
/* ... */
outb_p(0x36, 0x43);
/* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff, 0x40);
/* LSB */
outb(LATCH >> 8, 0x40);
/* MSB */
set_intr_gate(0x20, &timer_interrupt);
/* ... */
}
定时器 0 的端口地址是 0x40,而控制寄存器的端口地址是 0x43.那么首先是配置定时器 0,定时器 0 的工作模式为模式 3,二进制计数,然后是写入定时器初值,由于 8253/8254 定时器的数据总线只有 8 位,所以需要分两次写入,这里是先写入低 8 位,再写入高 8 位,那么写入的初值是多少呢,就是这里的 LATCH,LATCH 为 (1193180/HZ),HZ 定义为 100,而 8253/8254 的输入时钟为 1.193180MHz,那么定时的时间约为 11931/1.1931=10000us=10ms,也就是定时器 10 毫秒产生一次中断,而中断处理函数为 timer_interrupt,代码如下:
_timer_interrupt: push % ds#save ds,
es and put kernel data space push % es#into them. % fs is used by _system_call push % fs pushl % edx#we save % eax,
%ecx,
%edx as gcc doesn 't
pushl %ecx # save those across function calls. %ebx
pushl %ebx # is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax
mov %ax,%fs
incl _jiffies
movb $0x20,%al # EOI to interrupt controller #1
outb %al,$0x20
movl CS(%esp),%eax
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax
call _do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call'
注意在 seched_init 函数中,设置定时器的中断处理函数为 timer_interrupt,而这里变成了_timer_interrupt,即多了个'_'前缀,这是为什么呢?在老版本的 gcc 在编译过程中会给全局变量和函数加上'_'前缀,而新版本的 gcc 则不会加上这个'_'前缀,因为新版本的 gcc 默认选项是 - fnoleading-underscore,如果需要加上'_'前缀,可以使用 - fleading-underscore 选项,大家可以试一下(只编译生成. s 文件,不汇编).
那么定时器中断处理函数到底做了哪些事情呢?一是 jiffies 值加 1,jiffies 变量记录系统启动之后的时钟滴答值.二是调用 do_timer 函数,do_timer 函数是干什么的呢,代码如下:
void do_timer(long cpl) {
extern int beepcount;
extern void sysbeepstop(void);
if (beepcount) if (!--beepcount) sysbeepstop();
if (cpl) current - >utime++;
else current - >stime++;
if (next_timer) {
next_timer - >jiffies--;
while (next_timer && next_timer - >jiffies <= 0) {
void( * fn)(void);
fn = next_timer - >fn;
next_timer - >fn = NULL;
next_timer = next_timer - >next; (fn)();
}
}
if (current_DOR & 0xf0) do_floppy_timer();
if ((--current - >counter) > 0) return;
current - >counter = 0;
if (!cpl) return;
schedule();
}
beepcount 为蜂鸣器的鸣叫时间,如果为 0,需要停止蜂鸣器.
而参数 cpl 取值有 0 或 3,如果是 0,表示任务处于内核态被定时器中断,如果是 3 表示任务处于用户态被定时器中断,从前面的定时器中断处理函数来看,该参数值为 3,即用户态被中断.如果是用户态被中断 utime++,如果是内核态中断 stime++,而 current 表示当前正在被执行的任务.
接下来的 if (next_timer) 这部分是内核中的软定时器,后面再来看.
do_floppy_timer 和软盘相关的,这里也不去看了.
counter 值如果大于 0,直接返回.counter 是任务或进程的时间片计数,如果大于 0,说明时间片还没有用完,则该任务还会继续占用 CPU,否则有可能会发生任务调度,为什么是有可能呢?因为这里还对 cpl 值做了判断,如果 cpl 值为 0,即内核任务则直接返回,即内核任务是不可被抢占的,如果是处于用户态的任务,则调用 schedule 函数,该函数即任务调度函数.
void schedule(void) {
int i,
next,
c;
struct task_struct * *p;
/* check alarm, wake up any interruptible tasks that have got a signal */
for (p = &LAST_TASK; p > &FIRST_TASK; --p) if ( * p) {
if (( * p) - >alarm && ( * p) - >alarm < jiffies) { ( * p) - >signal |= (1 << (SIGALRM - 1)); ( * p) - >alarm = 0;
}
if ((( * p) - >signal & ~ (_BLOCKABLE & ( * p) - >blocked)) && ( * p) - >state == TASK_INTERRUPTIBLE)( * p) - >state = TASK_RUNNING;
}
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p) continue;
if (( * p) - >state == TASK_RUNNING && ( * p) - >counter > c) c = ( * p) - >counter,
next = i;
}
if (c) break;
for (p = &LAST_TASK; p > &FIRST_TASK; --p) if ( * p)( * p) - >counter = (( * p) - >counter >> 1) + ( * p) - >priority;
}
switch_to(next);
}
首先是在 for 循环中,检查每个任务的 alarm 值,如果设置了 alarm 值,并且 alarm 值小于 jiffies 值(即已经过期),则在任务的 signal 位图中设置 SIGALRM 信号(即向任务发送 SIGALRM 信号),并将 alarm 值清零.
如果 signal 位图中除了被阻塞信号外还有其它信号,并且任务状态是可中断状态(TASK_INTERRUPTIBLE),则将任务的状态置成就绪态(TASK_RUNNING).
接下来才是任务调度的核心部分.在 while (1) 中又有两个循环,在内核中有个任务数组,该数组长度为 64,即最多同时支持 64 个任务,那么每个任务就会对应数组中的一个索引值,该值的范围是 0~63.那么在 while (--i) 中首先是遍历任务数组,注意它是从后往前遍历的,如果为 NULL,即没有任务,则跳过,否则将所有处于就绪态任务中的 counter 值(时间片)最大的那个任务选取出来,然后切换到改任务去执行.如果所有任务的 counter 值都为 0,即所有任务的时间片已经执行完毕,则将重新分配时间片,分配的算法是根据任务的优先级(priority)来分配的,分配公式是 counter = counter / 2 + priority,即优先级越高的任务 / 进程,分配的时间片也就越长,也就越先被执行.然后重新检查处于就绪态任务的时间片值,选取时间片最大的那个任务,最后调用 switch_to 进行任务切换.
总结:从上面可以看到,任务调度的核心是 jiffies,而 jiffies 又由定时器 0 来更新,所以定时器在任务调度中处于核心位置.
来源: http://lib.csdn.net/article/linux/34522