1 /*
2 * linux/kernel/sched.c
3 *
4 * (C) 1991 Linus Torvalds
5 */
6
7 /*
8 * 'sched.c' is the main kernel file. It contains scheduling primitives
9 * (sleep_on, wakeup, schedule etc) as well as a number of simple system
10 * call functions (type getpid(), which just extracts a field from
11 * current-task
12 */
/*
* 'sched.c'是主要的内核文件。其中包括有关调度的基本函数(sleep_on、wakeup、schedule等)
* 以及一些简单的系统调用函数(比如getpid(),仅从当前任务中获取一个字段)。
*/
// 下面是调度程序头文件。定义了任务结构task_struct、第1个初始任务的数据。还有一些以宏
// 的形式定义的有关描述符参数设置和获取的嵌入式汇编函数程序。
13 #include <linux/sched.h>
14 #include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。
15 #include <linux/sys.h> // 系统调用头文件。含有82个系统调用C函数程序,以'sys_'开头。
16 #include <linux/fdreg.h> // 软驱头文件。含有软盘控制器参数的一些定义。
17 #include <asm/system.h> // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。
18 #include <asm/io.h> // io头文件。定义硬件端口输入/输出宏汇编语句。
19 #include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。
20
21 #include <signal.h> // 信号头文件。定义信号符号常量,sigaction结构,操作函数原型。
22
// 该宏取信号nr在信号位图中对应位的二进制数值。信号编号1-32。比如信号5的位图数值等于
// 1<<(5-1) = 16 = 00010000b。
23 #define _S(nr) (1<<((nr)-1))
// 除了SIGKILL和SIGSTOP信号以外其他信号都是可阻塞的(…1011,1111,1110,1111,1111b)。
24 #define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP)))
25
// 内核调试函数。显示任务号nr的进程号、进程状态和内核堆栈空闲字节数(大约)。
26 void show_task(int nr,struct task_struct * p)
27 {
28 int i,j = 4096-sizeof(struct task_struct);
29
30 printk("%d: pid=%d, state=%d, father=%d, child=%d, ",nr,p->pid,
31 p->state, p->p_pptr->pid, p->p_cptr ? p->p_cptr->pid : -1);
32 i=0;
33 while (i<j && !((char *)(p+1))[i]) // 检测指定任务数据结构以后等于0的字节数。
34 i++;
35 printk("%d/%d chars free in kstack\n\r",i,j);
36 printk(" PC=%08X.", *(1019 + (unsigned long *) p));
37 if (p->p_ysptr || p->p_osptr)
38 printk(" Younger sib=%d, older sib=%d\n\r",
39 p->p_ysptr ? p->p_ysptr->pid : -1,
40 p->p_osptr ? p->p_osptr->pid : -1);
41 else
42 printk("\n\r");
43 }
44
// 显示所有任务的任务号、进程号、进程状态和内核堆栈空闲字节数(大约)。
// NR_TASKS是系统能容纳的最大进程(任务)数量(64个),定义在include/kernel/sched.h 第6行。
45 void show_state(void)
46 {
47 int i;
48
49 printk("\rTask-info:\n\r");
50 for (i=0;i<NR_TASKS;i++)
51 if (task[i])
52 show_task(i,task[i]);
53 }
54
// PC机8253定时芯片的输入时钟频率约为1.193180MHz。Linux内核希望定时器发出中断的频率是
// 100Hz,也即每10ms发出一次时钟中断。因此这里LATCH是设置8253芯片的初值,参见438行。
55 #define LATCH (1193180/HZ)
56
57 extern void mem_use(void); // [??]没有任何地方定义和引用该函数。
58
59 extern int timer_interrupt(void); // 时钟中断处理程序(kernel/system_call.s,176)。
60 extern int system_call(void); // 系统调用中断处理程序(kernel/system_call.s,80)。
61
// 每个任务(进程)在内核态运行时都有自己的内核态堆栈。这里定义了任务的内核态堆栈结构。
// 这里定义任务联合(任务结构成员和stack字符数组成员)。因为一个任务的数据结构与其内核
// 态堆栈放在同一内存页中,所以从堆栈段寄存器ss可以获得其数据段选择符。
62 union task_union {
63 struct task_struct task;
64 char stack[PAGE_SIZE];
65 };
66
// 设置初始任务的数据。初始数据在include/kernel/sched.h中,第156行开始。
67 static union task_union init_task = {INIT_TASK,};
68
// 从开机开始算起的滴答数时间值全局变量(10ms/滴答)。系统时钟中断每发生一次即一个滴答。
// 前面的限定符 volatile,英文解释是易改变的、不稳定的意思。这个限定词的含义是向编译器
// 指明变量的内容可能会由于被其他程序修改而变化。通常在程序中申明一个变量时, 编译器会
// 尽量把它存放在通用寄存器中,例如 ebx,以提高访问效率。当CPU把其值放到 ebx中后一般
// 就不会再关心该变量对应内存位置中的内容。若此时其他程序(例如内核程序或一个中断过程)
// 修改了内存中该变量的值,ebx中的值并不会随之更新。为了解决这种情况就创建了volatile
// 限定符,让代码在引用该变量时一定要从指定内存位置中取得其值。这里即是要求 gcc不要对
// jiffies 进行优化处理,也不要挪动位置,并且需要从内存中取其值。因为时钟中断处理过程
// 等程序会修改它的值。
69 unsigned long volatile jiffies=0;
70 unsigned long startup_time=0; // 开机时间。从1970:0:0:0开始计时的秒数。
// 这个变量用于累计需要调整地时间嘀嗒数。
71 int jiffies_offset = 0; /* # clock ticks to add to get "true
72 time". Should always be less than
73 1 second's worth. For time fanatics
74 who like to syncronize their machines
75 to WWV :-) */
/* 为调整时钟而需要增加的时钟嘀嗒数,以获得“精确时间”。这些调整用嘀嗒数
* 的总和不应该超过1秒。这样做是为了那些对时间精确度要求苛刻的人,他们喜
* 欢自己的机器时间与WWV同步 :-)
*/
76
77 struct task_struct *current = &(init_task.task); // 当前任务指针(初始化指向任务0)。
78 struct task_struct *last_task_used_math = NULL; // 使用过协处理器任务的指针。
79
// 定义任务指针数组。第1项被初始化指向初始任务(任务0)的任务数据结构。
80 struct task_struct * task[NR_TASKS] = {&(init_task.task), };
81
// 定义用户堆栈,共1K项,容量4K字节。在内核初始化操作过程中被用作内核栈,初始化完成
// 以后将被用作任务0的用户态堆栈。在运行任务0之前它是内核栈,以后用作任务0和1的用
// 户态栈。下面结构用于设置堆栈ss:esp(数据段选择符,指针),见head.s,第23行。
// ss被设置为内核数据段选择符(0x10),指针esp指在 user_stack数组最后一项后面。这是
// 因为Intel CPU执行堆栈操作时是先递减堆栈指针sp值,然后在sp指针处保存入栈内容。
82 long user_stack [ PAGE_SIZE>>2 ] ;
83
84 struct {
85 long * a;
86 short b;
87 } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
88 /*
89 * 'math_state_restore()' saves the current math information in the
90 * old math state array, and gets the new ones from the current task
91 */
/*
* 将当前协处理器内容保存到老协处理器状态数组中,并将当前任务的协处理器
* 内容加载进协处理器。
*/
// 当任务被调度交换过以后,该函数用以保存原任务的协处理器状态(上下文)并恢复新调度进
// 来的当前任务的协处理器执行状态。
92 void math_state_restore()
93 {
// 如果任务没变则返回(上一个任务就是当前任务)。这里"上一个任务"是指刚被交换出去的任务。
94 if (last_task_used_math == current)
95 return;
// 在发送协处理器命令之前要先发WAIT指令。如果上个任务使用了协处理器,则保存其状态。
96 __asm__("fwait");
97 if (last_task_used_math) {
98 __asm__("fnsave %0"::"m" (last_task_used_math->tss.i387));
99 }
// 现在,last_task_used_math指向当前任务,以备当前任务被交换出去时使用。此时如果当前
// 任务用过协处理器,则恢复其状态。否则的话说明是第一次使用,于是就向协处理器发初始化
// 命令,并设置使用了协处理器标志。
100 last_task_used_math=current;
101 if (current->used_math) {
102 __asm__("frstor %0"::"m" (current->tss.i387));
103 } else {
104 __asm__("fninit"::); // 向协处理器发初始化命令。
105 current->used_math=1; // 设置使用已协处理器标志。
106 }
107 }
108
109 /*
110 * 'schedule()' is the scheduler function. This is GOOD CODE! There
111 * probably won't be any reason to change this, as it should work well
112 * in all circumstances (ie gives IO-bound processes good response etc).
113 * The one thing you might take a look at is the signal-handler code here.
114 *
115 * NOTE!! Task 0 is the 'idle' task, which gets called when no other
116 * tasks can run. It can not be killed, and it cannot sleep. The 'state'
117 * information in task[0] is never used.
118 */
/*
* 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为
* 它可以在所有的环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件
* 事值得留意,那就是这里的信号处理代码。
*
* 注意!!任务0是个闲置('idle')任务,只有当没有其他任务可以运行时才调用
* 它。它不能被杀死,也不能睡眠。任务0中的状态信息'state'是从来不用的。
*/
119 void schedule(void)
120 {
121 int i,next,c;
122 struct task_struct ** p; // 任务结构指针的指针。
123
124 /* check alarm, wake up any interruptible tasks that have got a signal */
/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */
125
// 从任务数组中最后一个任务开始循环检测alarm。在循环时跳过空指针项。
126 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
127 if (*p) {
// 如果设置过任务超时定时timeout,并且已经超时,则复位超时定时值,并且如果任务处于可
// 中断睡眠状态TASK_INTERRUPTIBLE下,将其置为就绪状态(TASK_RUNNING)。
128 if ((*p)->timeout && (*p)->timeout < jiffies) {
129 (*p)->timeout = 0;
130 if ((*p)->state == TASK_INTERRUPTIBLE)
131 (*p)->state = TASK_RUNNING;
132 }
// 如果设置过任务的定时值alarm,并且已经过期(alarm<jiffies),则在信号位图中置SIGALRM
// 信号,即向任务发送SIGALARM信号。然后清alarm。该信号的默认操作是终止进程。jiffies
// 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h第139行。
133 if ((*p)->alarm && (*p)->alarm < jiffies) {
134 (*p)->signal |= (1<<(SIGALRM-1));
135 (*p)->alarm = 0;
136 }
// 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则置任务为就绪
// 状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL和SIGSTOP
// 不能被阻塞。
137 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
138 (*p)->state==TASK_INTERRUPTIBLE)
139 (*p)->state=TASK_RUNNING; //置为就绪(可执行)状态。
140 }
141
142 /* this is the scheduler proper: */
/* 这里是调度程序的主要部分 */
143
144 while (1) {
145 c = -1;
146 next = 0;
147 i = NR_TASKS;
148 p = &task[NR_TASKS];
// 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个
// 就绪状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,
// next就指向哪个的任务号。
149 while (--i) {
150 if (!*--p)
151 continue;
152 if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
153 c = (*p)->counter, next = i;
154 }
// 如果比较得出有counter值不等于0的结果,或者系统中没有一个可运行的任务存在(此时c
// 仍然为-1,next=0),则退出144行开始的循环,执行161行上的任务切换操作。否则就根据
// 每个任务的优先权值,更新每一个任务的counter值,然后回到125行重新比较。counter 值
// 的计算方式为 counter = counter /2 + priority。注意,这里计算过程不考虑进程的状态。
155 if (c) break;
156 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
157 if (*p)
158 (*p)->counter = ((*p)->counter >> 1) +
159 (*p)->priority;
160 }
// 用下面宏(定义在sched.h中)把当前任务指针current指向任务号为next的任务,并切换
// 到该任务中运行。在146行上next被初始化为0。因此若系统中没有任何其他任务可运行时,
// 则 next 始终为0。因此调度函数会在系统空闲时去执行任务0。 此时任务0仅执行pause()
// 系统调用,并又会调用本函数。
161 switch_to(next); // 切换到任务号为next的任务,并运行之。
162 }
163
//// pause()系统调用。转换当前任务的状态为可中断的等待状态,并重新调度。
// 该系统调用将导致进程进入睡眠状态,直到收到一个信号。该信号用于终止进程或者使进程
// 调用一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()才
// 会返回。此时 pause()返回值应该是 -1,并且 errno被置为 EINTR。这里还没有完全实现
// (直到0.95版)。
164 int sys_pause(void)
165 {
166 current->state = TASK_INTERRUPTIBLE;
167 schedule();
168 return 0;
169 }
170
// 把当前任务置为指定的睡眠状态(可中断的或不可中断的),并让睡眠队列头指针指向当前任务。
// 函数参数p是等待任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的
// 指针形式 '**p',这是因为C函数参数只能传值,没有直接的方式让被调用函数改变调用该函数
// 程序中变量的值。但是指针'*p'指向的目标(这里是任务结构)会改变,因此为了能修改调用该
// 函数程序中原来就是指针变量的值,就需要传递指针'*p'的指针,即'**p'。参见程序前示例图中
// p指针的使用情况。
// 参数state是任务睡眠使用的状态:TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE。处于不可
// 中断睡眠状态(TASK_UNINTERRUPTIBLE)的任务需要内核程序利用wake_up()函数明确唤醒之。
// 处于可中断睡眠状态(TASK_INTERRUPTIBLE)可以通过信号、任务超时等手段唤醒 (置为就绪
// 状态TASK_RUNNING)。
// *** 注意,由于本内核代码不是很成熟,因此下列与睡眠相关的代码存在一些问题,不宜深究。
171 static inline void __sleep_on(struct task_struct **p, int state)
172 {
173 struct task_struct *tmp;
174
// 若指针无效,则退出。(指针所指的对象可以是NULL,但指针本身不会为0)。
// 如果当前任务是任务0,则死机(impossible!)。
175 if (!p)
176 return;
177 if (current == &(init_task.task))
178 panic("task[0] trying to sleep");
// 让tmp指向已经在等待队列上的任务(如果有的话),例如inode->i_wait。并且将睡眠队列头
// 的等待指针指向当前任务。这样就把当前任务插入到了 *p的等待队列中。然后将当前任务置
// 为指定的等待状态,并执行重新调度。
179 tmp = *p;
180 *p = current;
181 current->state = state;
182 repeat: schedule();
// 只有当这个等待任务被唤醒时,程序才又会返回到这里,表示进程已被明确地唤醒并执行。
// 如果等待队列中还有等待任务,并且队列头指针 *p 所指向的任务不是当前任务时,说明
// 在本任务插入等待队列后还有任务进入等待队列。于是我们应该也要唤醒这个任务,而我
// 们自己应按顺序让这些后面进入队列的任务唤醒,因此这里将等待队列头所指任务先置为
// 就绪状态,而自己则置为不可中断等待状态,即自己要等待这些后续进队列的任务被唤醒
// 而执行时来唤醒本任务。然后重新执行调度程序。
183 if (*p && *p != current) {
184 (**p).state = 0;
185 current->state = TASK_UNINTERRUPTIBLE;
186 goto repeat;
187 }
// 执行到这里,说明本任务真正被唤醒执行。此时等待队列头指针应该指向本任务,若它为
// 空,则表明调度有问题,于是显示警告信息。最后我们让头指针指向在我们前面进入队列
// 的任务(*p = tmp)。 若确实存在这样一个任务,即队列中还有任务(tmp不为空),就
// 唤醒之。最先进入队列的任务在唤醒后运行时最终会把等待队列头指针置成NULL。
188 if (!*p)
189 printk("Warning: *P = NULL\n\r");
190 if (*p = tmp)
191 tmp->state=0;
192 }
193
// 将当前任务置为可中断的等待状态(TASK_INTERRUPTIBLE),并放入头指针*p指定的等待
// 队列中。
194 void interruptible_sleep_on(struct task_struct **p)
195 {
196 __sleep_on(p,TASK_INTERRUPTIBLE);
197 }
198
// 把当前任务置为不可中断的等待状态(TASK_UNINTERRUPTIBLE),并让睡眠队列头指针指向
// 当前任务。只有明确地唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。
199 void sleep_on(struct task_struct **p)
200 {
201 __sleep_on(p,TASK_UNINTERRUPTIBLE);
202 }
203
// 唤醒 *p指向的任务。*p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针
// 处的,因此唤醒的是最后进入等待队列的任务。若该任务已经处于停止或僵死状态,则显示
// 警告信息。
204 void wake_up(struct task_struct **p)
205 {
206 if (p && *p) {
207 if ((**p).state == TASK_STOPPED) // 处于停止状态。
208 printk("wake_up: TASK_STOPPED");
209 if ((**p).state == TASK_ZOMBIE) // 处于僵死状态。
210 printk("wake_up: TASK_ZOMBIE");
211 (**p).state=0; // 置为就绪状态TASK_RUNNING。
212 }
213 }
214
215 /*
216 * OK, here are some floppy things that shouldn't be in the kernel
217 * proper. They are here because the floppy needs a timer, and this
218 * was the easiest way of doing it.
219 */
/*
* 好了,从这里开始是一些有关软盘的子程序,本不应该放在内核的主要部分
* 中的。将它们放在这里是因为软驱需要定时处理,而放在这里是最方便的。
*/
// 下面220 -- 281行代码用于处理软驱定时。在阅读这段代码之前请先看一下块设备一章中
// 有关软盘驱动程序(floppy.c)后面的说明, 或者到阅读软盘块设备驱动程序时在来看这
// 段代码。其中时间单位:1个滴答 = 1/100秒。
// 下面数组wait_motor[]用于存放等待软驱马达启动到正常转速的进程指针。数组索引0-3
// 分别对应软驱A--D。数组 mon_timer[]存放各软驱马达启动所需要的滴答数。程序中默认
// 启动时间为50个滴答(0.5秒)。数组 moff_timer[] 存放各软驱在马达停转之前需维持
// 的时间。程序中设定为10000个滴答(100秒)。
220 static struct task_struct * wait_motor[4] = {NULL,NULL,NULL,NULL};
221 static int mon_timer[4]={0,0,0,0};
222 static int moff_timer[4]={0,0,0,0};
// 下面变量对应软驱控制器中当前数字输出寄存器。该寄存器每位的定义如下:
// 位7-4:分别控制驱动器D-A马达的启动。1 - 启动;0 - 关闭。
// 位3 :1 - 允许DMA和中断请求;0 - 禁止DMA和中断请求。
// 位2 :1 - 启动软盘控制器; 0 - 复位软盘控制器。
// 位1-0:00 - 11,用于选择控制的软驱A-D。
// 这里设置初值为:允许DMA和中断请求、启动FDC。
223 unsigned char current_DOR = 0x0C;
224
// 指定软驱启动到正常运转状态所需等待时间。
// 参数nr -- 软驱号(0--3),返回值为滴答数。
// 局部变量selected是选中软驱标志(blk_drv/floppy.c,123行)。mask是所选软驱对应的
// 数字输出寄存器中启动马达比特位。mask高4位是各软驱启动马达标志。
225 int ticks_to_floppy_on(unsigned int nr)
226 {
227 extern unsigned char selected;
228 unsigned char mask = 0x10 << nr;
229
// 系统最多有4个软驱。首先预先设置好指定软驱nr停转之前需要经过的时间(100秒)。然后
// 取当前DOR寄存器值到临时变量mask中,并把指定软驱的马达启动标志置位。
230 if (nr>3)
231 panic("floppy_on: nr>3");
232 moff_timer[nr]=10000; /* 100 s = very big :-) */ // 停转维持时间。
233 cli(); /* use floppy_off to turn it off */ // 关中断。
234 mask |= current_DOR;
// 如果当前没有选择软驱,则首先复位其他软驱的选择位,然后置指定软驱选择位。
235 if (!selected) {
236 mask &= 0xFC;
237 mask |= nr;
238 }
// 如果数字输出寄存器的当前值与要求的值不同,则向FDC数字输出端口输出新值(mask),并且
// 如果要求启动的马达还没有启动,则置相应软驱的马达启动定时器值(HZ/2 = 0.5秒或 50个
// 滴答)。若已经启动,则再设置启动定时为2个滴答,能满足下面 do_floppy_timer()中先递
// 减后判断的要求。执行本次定时代码的要求即可。此后更新当前数字输出寄存器current_DOR。
239 if (mask != current_DOR) {
240 outb(mask,FD_DOR);
241 if ((mask ^ current_DOR) & 0xf0)
242 mon_timer[nr] = HZ/2;
243 else if (mon_timer[nr] < 2)
244 mon_timer[nr] = 2;
245 current_DOR = mask;
246 }
247 sti(); // 开中断。
248 return mon_timer[nr]; // 最后返回启动马达所需的时间值。
249 }
250
// 等待指定软驱马达启动所需的一段时间,然后返回。
// 设置指定软驱的马达启动到正常转速所需的延时,然后睡眠等待。在定时中断过程中会一直
// 递减判断这里设定的延时值。当延时到期,就会唤醒这里的等待进程。
251 void floppy_on(unsigned int nr)
252 {
// 关中断。如果马达启动定时还没到,就一直把当前进程置为不可中断睡眠状态并放入等待马达
// 运行的队列中。然后开中断。
253 cli();
254 while (ticks_to_floppy_on(nr))
255 sleep_on(nr+wait_motor);
256 sti();
257 }
258
// 置关闭相应软驱马达停转定时器(3秒)。
// 若不使用该函数明确关闭指定的软驱马达,则在马达开启100秒之后也会被关闭。
259 void floppy_off(unsigned int nr)
260 {
261 moff_timer[nr]=3*HZ;
262 }
263
// 软盘定时处理子程序。更新马达启动定时值和马达关闭停转计时值。该子程序会在时钟定时
// 中断过程中被调用,因此系统每经过一个滴答(10ms)就会被调用一次,随时更新马达开启或
// 停转定时器的值。如果某一个马达停转定时到,则将数字输出寄存器马达启动位复位。
264 void do_floppy_timer(void)
265 {
266 int i;
267 unsigned char mask = 0x10;
268
269 for (i=0 ; i<4 ; i++,mask <<= 1) {
270 if (!(mask & current_DOR)) // 如果不是DOR指定的马达则跳过。
271 continue;
272 if (mon_timer[i]) { // 如果马达启动定时到则唤醒进程。
273 if (!--mon_timer[i])
274 wake_up(i+wait_motor);
275 } else if (!moff_timer[i]) { // 如果马达停转定时到则
276 current_DOR &= ~mask; // 复位相应马达启动位,并且
277 outb(current_DOR,FD_DOR); // 更新数字输出寄存器。
278 } else
279 moff_timer[i]--; // 否则马达停转计时递减。
280 }
281 }
282
// 下面是关于定时器的代码。最多可有64个定时器。
283 #define TIME_REQUESTS 64
284
// 定时器链表结构和定时器数组。该定时器链表专用于供软驱关闭马达和启动马达定时操作。
// 这种类型定时器类似现代Linux系统中的动态定时器(Dynamic Timer),仅供内核使用。
285 static struct timer_list {
286 long jiffies; // 定时滴答数。
287 void (*fn)(); // 定时处理程序。
288 struct timer_list * next; // 链接指向下一个定时器。
289 } timer_list[TIME_REQUESTS], * next_timer = NULL; // next_timer是定时器队列头指针。
290
// 添加定时器。输入参数为指定的定时值(滴答数)和相应的处理程序指针。
// 软盘驱动程序(floppy.c)利用该函数执行启动或关闭马达的延时操作。
// 参数jiffies – 以10毫秒计的滴答数;*fn()- 定时时间到时执行的函数。
291 void add_timer(long jiffies, void (*fn)(void))
292 {
293 struct timer_list * p;
294
// 如果定时处理程序指针为空,则退出。否则关中断。
295 if (!fn)
296 return;
297 cli();
// 如果定时值<=0,则立刻调用其处理程序。并且该定时器不加入链表中。
298 if (jiffies <= 0)
299 (fn)();
300 else {
// 否则从定时器数组中,找一个空闲项。
301 for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++)
302 if (!p->fn)
303 break;
// 如果已经用完了定时器数组,则系统崩溃J。否则向定时器数据结构填入相应信息,并链入
// 链表头。
304 if (p >= timer_list + TIME_REQUESTS)
305 panic("No more time requests free");
306 p->fn = fn;
307 p->jiffies = jiffies;
308 p->next = next_timer;
309 next_timer = p;
// 链表项按定时值从小到大排序。在排序时减去排在前面需要的滴答数,这样在处理定时器时
// 只要查看链表头的第一项的定时是否到期即可。[[?? 这段程序好象没有考虑周全。如果新
// 插入的定时器值小于原来头一个定时器值时则根本不会进入循环中,但此时还是应该将紧随
// 其后面的一个定时器值减去新的第1个的定时值。即如果第1个定时值<=第2个,则第2个
// 定时值扣除第1个的值即可,否则进入下面循环中进行处理。]]
310 while (p->next && p->next->jiffies < p->jiffies) {
311 p->jiffies -= p->next->jiffies;
312 fn = p->fn;
313 p->fn = p->next->fn;
314 p->next->fn = fn;
315 jiffies = p->jiffies;
316 p->jiffies = p->next->jiffies;
317 p->next->jiffies = jiffies;
318 p = p->next;
319 }
320 }
321 sti();
322 }
323
//// 时钟中断C函数处理程序,在sys_call.s中的_timer_interrupt(189行)被调用。
// 参数cpl是当前特权级0或3,是时钟中断发生时正被执行的代码选择符中的特权级。
// cpl=0时表示中断发生时正在执行内核代码;cpl=3时表示中断发生时正在执行用户代码。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
324 void do_timer(long cpl)
325 {
326 static int blanked = 0;
327
// 首先判断是否经过了一定时间而让屏幕黑屏(blankout)。如果blankcount计数不为零,
// 或者黑屏延时间隔时间blankinterval为0的话,那么若已经处于黑屏状态(黑屏标志
// blanked = 1),则让屏幕恢复显示。若blankcount计数不为零,则递减之,并且复位
// 黑屏标志。
328 if (blankcount || !blankinterval) {
329 if (blanked)
330 unblank_screen();
331 if (blankcount)
332 blankcount--;
333 blanked = 0;
// 否则的话若黑屏标志未置位,则让屏幕黑屏,并且设置黑屏标志。
334 } else if (!blanked) {
335 blank_screen();
336 blanked = 1;
337 }
// 接着处理硬盘操作超时问题。如果硬盘超时计数递减之后为0,则进行硬盘访问超时处理。
338 if (hd_timeout)
339 if (!--hd_timeout)
340 hd_times_out(); // 硬盘访问超时处理(blk_drv/hdc,318行)。
341
// 如果发声计数次数到,则关闭发声。(向0x61口发送命令,复位位0和1。位0控制8253
// 计数器2的工作,位1控制扬声器)。
342 if (beepcount) // 扬声器发声时间滴答数(chr_drv/console.c,950行)。
343 if (!--beepcount)
344 sysbeepstop();
345
// 如果当前特权级(cpl)为0(最高,表示是内核程序在工作),则将内核代码运行时间stime
// 递增;[ Linus把内核程序统称为超级用户(supervisor)的程序,见sys_call.s,207行
// 上的英文注释。这种称呼来自于Intel CPU 手册。] 如果cpl > 0,则表示是一般用户程序
// 在工作,增加utime。
346 if (cpl)
347 current->utime++;
348 else
349 current->stime++;
350
// 如果有定时器存在,则将链表第1个定时器的值减1。如果已等于0,则调用相应的处理程序,
// 并将该处理程序指针置为空。然后去掉该项定时器。next_timer是定时器链表的头指针。
351 if (next_timer) {
352 next_timer->jiffies--;
353 while (next_timer && next_timer->jiffies <= 0) {
354 void (*fn)(void); // 这里插入了一个函数指针定义!!L
355
356 fn = next_timer->fn;
357 next_timer->fn = NULL;
358 next_timer = next_timer->next;
359 (fn)(); // 调用定时处理函数。
360 }
361 }
// 如果当前软盘控制器FDC的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序。
362 if (current_DOR & 0xf0)
363 do_floppy_timer();
// 如果进程运行时间还没完,则退出。否则置当前任务运行计数值为0。并且若发生时钟中断时
// 正在内核代码中运行则返回,否则调用执行调度函数。
364 if ((--current->counter)>0) return;
365 current->counter=0;
366 if (!cpl) return; // 对于内核态程序,不依赖counter值进行调度。
367 schedule();
368 }
369
// 系统调用功能 - 设置报警定时时间值(秒)。
// 若参数seconds大于0,则设置新定时值,并返回原定时时刻还剩余的间隔时间。否则返回0。
// 进程数据结构中报警定时值alarm的单位是系统滴答(1滴答为10毫秒),它是系统开机起到
// 设置定时操作时系统滴答值jiffies和转换成滴答单位的定时值之和,即'jiffies + HZ*定时
// 秒值'。而参数给出的是以秒为单位的定时值,因此本函数的主要操作是进行两种单位的转换。
// 其中常数HZ = 100,是内核系统运行频率。定义在include/sched.h第4行上。
// 参数seconds是新的定时时间值,单位是秒。
370 int sys_alarm(long seconds)
371 {
372 int old = current->alarm;
373
374 if (old)
375 old = (old - jiffies) / HZ;
376 current->alarm = (seconds>0)?(jiffies+HZ*seconds):0;
377 return (old);
378 }
379
// 取当前进程号pid。
380 int sys_getpid(void)
381 {
382 return current->pid;
383 }
384
// 取父进程号ppid。
385 int sys_getppid(void)
386 {
387 return current->p_pptr->pid;
388 }
389
// 取用户号uid。
390 int sys_getuid(void)
391 {
392 return current->uid;
393 }
394
// 取有效的用户号euid。
395 int sys_geteuid(void)
396 {
397 return current->euid;
398 }
399
// 取组号gid。
400 int sys_getgid(void)
401 {
402 return current->gid;
403 }
404
// 取有效的组号egid。
405 int sys_getegid(void)
406 {
407 return current->egid;
408 }
409
// 系统调用功能 -- 降低对CPU的使用优先权(有人会用吗?J)。
// 应该限制increment为大于0的值,否则可使优先权增大!!
410 int sys_nice(long increment)
411 {
412 if (current->priority-increment>0)
413 current->priority -= increment;
414 return 0;
415 }
416
// 内核调度程序的初始化子程序。
417 void sched_init(void)
418 {
419 int i;
420 struct desc_struct * p; // 描述符表结构指针。
421
// Linux系统开发之初,内核不成熟。内核代码会被经常修改。Linus怕自己无意中修改了这些
// 关键性的数据结构,造成与POSIX标准的不兼容。这里加入下面这个判断语句并无必要,纯粹
// 是为了提醒自己以及其他修改内核代码的人。
422 if (sizeof(struct sigaction) != 16) // sigaction是存放有关信号状态的结构。
423 panic("Struct sigaction MUST be 16 bytes");
// 在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
// FIRST_TSS_ENTRY和FIRST_LDT_ENTRY的值分别是4和5,定义在include/linux/sched.h
// 中;gdt 是一个描述符表数组(include/linux/head.h ),实际上对应程序 head.s 中
// 第234行上的全局描述符表基址(_gdt)。因此 gdt + FIRST_TSS_ENTRY 即为
// gdt[FIRST_TSS_ENTRY](即是gdt[4]),也即 gdt 数组第4项的地址。 参见
// include/asm/system.h,第65行开始。
424 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
425 set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
// 清任务数组和描述符表项(注意i=1开始,所以初始任务的描述符还在)。描述符项结构
// 定义在文件include/linux/head.h中。
426 p = gdt+2+FIRST_TSS_ENTRY;
427 for(i=1;i<NR_TASKS;i++) {
428 task[i] = NULL;
429 p->a=p->b=0;
430 p++;
431 p->a=p->b=0;
432 p++;
433 }
434 /* Clear NT, so that we won't have troubles with that later on */
/* 清除标志寄存器中的位NT,这样以后就不会有麻烦 */
// EFLAGS中的NT标志位用于控制任务的嵌套调用。当NT位置位时,那么当前中断任务执行
// IRET指令时就会引起任务切换。NT指出TSS中的back_link字段是否有效。NT=0时无效。
435 __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位NT标志。
// 将任务0 的 TSS段选择符加载到任务寄存器tr。将局部描述符表段选择符加载到局部描述
// 符表寄存器ldtr中。注意!!是将GDT中相应LDT描述符的选择符加载到ldtr。只明确加
// 这一次,以后新任务LDT的加载,是CPU根据TSS中的LDT项自动加载。
436 ltr(0); // 定义在include/linux/sched.h 第157-158行。
437 lldt(0); // 其中参数(0)是任务号。
// 下面代码用于初始化 8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的
// 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始
// 定时计数值。
438 outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
439 outb_p(LATCH & 0xff , 0x40); /* LSB */ // 定时值低字节。
440 outb(LATCH >> 8 , 0x40); /* MSB */ // 定时值高字节。
// 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。
// 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符的宏定义在文件
// include/asm/system.h中第33、39行处。两者的区别参见system.h文件开始处的说明。
441 set_intr_gate(0x20,&timer_interrupt);
442 outb(inb_p(0x21)&~0x01,0x21);
443 set_system_gate(0x80,&system_call);
444 }
445