这一部分来看 main 中调用的 time_init 与 sched_init 函数。

首先是 time_init,其定义在 main.c 第 76 行,CMOS_READ 与 BCD_TO_BIN 在 main.c 的注释里已经提及,该函数在最后调用了 kernel_mktime 来计算从 1970 年 1 月 1 日 0 时至现在的开机时间,kernel_mktime 定义在 kernel/mktime.c 中

mktime.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Line 7
#include <time.h> // 该头文件定义了关于时间的 tm 结构体

#define MINUTE 60 // 定义一分钟 60 秒,一个小时 60 分钟
#define HOUR (60*MINUTE) // 一天 24 小时,一年 365 天
#define DAY (24*HOUR)
#define YEAR (365*DAY)

static int month[12] = { // 该数组成员是每个月 1 号前过了多少秒
0,
DAY*(31),
DAY*(31+29), // 先假设是闰年,2 月按 29 天来算
DAY*(31+29+31),
DAY*(31+29+31+30),
DAY*(31+29+31+30+31),
DAY*(31+29+31+30+31+30),
DAY*(31+29+31+30+31+30+31),
DAY*(31+29+31+30+31+30+31+31),
DAY*(31+29+31+30+31+30+31+31+30),
DAY*(31+29+31+30+31+30+31+31+30+31),
DAY*(31+29+31+30+31+30+31+31+30+31+30)
};

long kernel_mktime(struct tm * tm) // 返回自 1970 年 1 月 1 日 0 时至今的时间(s)
{
long res;
int year;
if (tm->tm_year < 70 ) tm->tm_year += 100; // 1970 年以下的年份不考虑,考虑 2000 年以上
year = tm->tm_year - 70; // 年份减去 70
// (year+1)/4 计算经过了多少个闰年,乘 DAY 表示所有闰年多的那一天的总秒数
res = YEAR*year + DAY*((year+1)/4);
res += month[tm->tm_mon]; // 加上当前月份 1 号前过的秒数
if (tm->tm_mon>1 && ((year+2)%4)) // 如果不是闰年且是 3 月以上,需要减去多算的那一天
res -= DAY;
res += DAY*(tm->tm_mday-1); // 加上当前月份(已过的天数-1)的总秒数
res += HOUR*tm->tm_hour; // 加上当前小时的秒数
res += MINUTE*tm->tm_min; // 加上当前分钟的秒数
res += tm->tm_sec; // 加上当前的秒数
return res;
}

sched.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Line 13
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/sys.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
#include <asm/segment.h>

#include <signal.h> // 包含一些头文件

#define _S(nr) (1<<((nr)-1)) // 取信号 nr 在信号位图中对应位的二进制数值,n 的范围是 1 ~ 32
#define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP))) // 除了 SIGKILL 与 SIGSTOP 外的信号都是可阻塞的

void show_task(int nr,struct task_struct * p) // 内核调试函数,nr 表示任务号,p 是任务结构指针
{
int i,j = 4096-sizeof(struct task_struct); // j 表示内核栈最大容量及最低顶端位置

printk("%d: pid=%d, state=%d, ",nr,p->pid,p->state); // 输出任务号 nr 的进程号及进程状态
i=0;
while (i<j && !((char *)(p+1))[i]) // 检测任务数据结构后为空的空闲字节并输出
i++;
printk("%d (of %d) chars free in kernel stack\n\r",i,j);
}

void show_stat(void) // 显示所有任务的状态信息
{
int i;

for (i=0;i<NR_TASKS;i++) // 遍历任务数组,R_TASKS 为 64,定义在 include/linux/sched.h
if (task[i])
show_task(i,task[i]);
}

#define LATCH (1193180/HZ) // 8253 计数/定时芯片的初值,HZ 为 100

extern void mem_use(void); // 未在其它地方定义及使用过

extern int timer_interrupt(void); // 引用 timer_interrupt 及 system_call 子程序
extern int system_call(void);

union task_union { // 定义任务的内核态堆栈结构
struct task_struct task;
char stack[PAGE_SIZE]; // PAGE_SIZE 值为 4096,定义在 include/linux/mm.h
};

static union task_union init_task = {INIT_TASK,}; // 定义初始任务
// INIT_TASK 在 include/linux/sched.h
long volatile jiffies=0; // 该变量用于计数经过了多少个时钟中断,称为时钟滴答数
long startup_time=0; // 开机时间
struct task_struct *current = &(init_task.task); // 当前任务的任务结构体指针
struct task_struct *last_task_used_math = NULL; // 使用过协处理器任务的指针

struct task_struct * task[NR_TASKS] = {&(init_task.task), }; // 所有任务的任务结构体数组

long user_stack [ PAGE_SIZE>>2 ] ; // 设置用户栈,大小为 4K 字节

struct { // 设置 ss:esp(0x10 是内核选择符,esp指向用户栈的末尾)
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

// 该函数用于在任务调度时将旧任务的协处理器状态保存,并还原当前任务的协处理器状态
void math_state_restore()
{
if (last_task_used_math == current) // 如果选择出来的任务和上次一样,则不作处理
return;
__asm__("fwait"); // 保存状态前需要先执行 fwait 指令
if (last_task_used_math) { // 如果上一个任务使用了协处理器,则将其状态保存在相应 tss 字段中
__asm__("fnsave %0"::"m" (last_task_used_math->tss.i387));
}
last_task_used_math=current; // 上一个使用协处理器的任务就指向了当前任务
if (current->used_math) { // 如果当前任务使用过协处理器,则恢复其状态
__asm__("frstor %0"::"m" (current->tss.i387));
} else { // 否则初始化协处理器,并将 used_math 字段置 1
__asm__("fninit"::);
current->used_math=1;
}
}

接下来是进程调度函数,schedule 会选出 counter 值最大的任务作为 next 任务来运行。如果系统中只有 0 号进程在运行时,则切换到 0 号进程,其又执行 pause 来调用 schedule 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Line 104
void schedule(void)
{
int i,next,c;
struct task_struct ** p;

// 该循环检测 alarm,唤醒已经得到信号的可中断任务
// 从任务数组的最后一个任务开始往前遍历
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) { // 跳过空指针
if ((*p)->alarm && (*p)->alarm < jiffies) { // 如果设置过任务 alarm 值且已经过期
(*p)->signal |= (1<<(SIGALRM-1)); // 就向任务发送 SIGALRM 信号
(*p)->alarm = 0;
}
// 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING; // 则将任务设置为就绪态
}

// 下面这个循环是调度程序的主要部分,同样从任务数组的最后一个任务开始遍历
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p) // 跳过空指针
continue;
// 从数组中选出 counter 值最大的任务
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) // 否则就根据每个任务的 priority 更新 counter 值
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next); // 调用 switch_to 函数切换任务
}

switch_to 定义在 include/linux/sched.h,_TSS 也定义在该文件中。

ljmp 长跳转的指令格式为:ljmp 16 位段选择子:32 位偏移值,这里定义的 _tmp.a 就是 32 位偏移值,_tmp.b 低 2 字节是 16 位段选择子(高 2 字节不用)。跳转到 TSS 描述符的选择子会导致任务切换到该 TSS 对应的进程,且对于造成任务切换的长跳转 _tmp.a 的值无用,因此这里没有对 _tmp.a 进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// include/linux/sched.h Line 171
#define switch_to(n) {\ // n 为选择出来的任务在任务数组中的索引
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \ // 判断选出的任务是否就是当前任务
"je 1f\n\t" \ // 如果是,则什么都不做
"movw %%dx,%1\n\t" \ // 否则将新任务的 TSS 选择子存入 __tmp.b 中
"xchgl %%ecx,current\n\t" \ // current 指向新任务,ecx 指向旧任务
"ljmp *%0\n\t" \ // 长跳转至 TSS 选择子导致任务切换,以下代码在切换回来后执行
"cmpl %%ecx,last_task_used_math\n\t" \ // 判断原任务上次是否使用过协处理器
"jne 1f\n\t" \ // 如果没有则返回
"clts\n" \ // 否则清除 CR0 中的 TS 位
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
// 新任务的 TSS 描述符的选择子放在 dx 中,任务结构体指针放在 ecx 中
"d" (_TSS(n)),"c" ((long) task[n])); \
}

// Line 155
// 计算 GDT 中第 n 个任务的 TSS 描述符的选择子,FIRST_TSS_ENTRY 值为 4,
// 因为每个描述符 8 字节,左移三位表示 FIRST_TSS 描述符在 GDT 中的起始偏移地址,
// 每个任务需要一个 TSS 描述符与一个 LDT 描述符,共 16 字节,故 n 要左移 4 位
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))

继续看 sched.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Line 144
int sys_pause(void) // 导致进程进入睡眠状态直到接收到一个信号
{
current->state = TASK_INTERRUPTIBLE; // 将当前任务状态切换为可中断的等待状态
schedule(); // 重新执行调度函数
return 0;
}

void sleep_on(struct task_struct **p) // 将当前任务置为不可中断的睡眠状态(需要 wake_up 唤醒)
{ // p 是等待任务队列的头指针
struct task_struct *tmp;

if (!p) // 为空指针则返回
return;
if (current == &(init_task.task)) // 0 号进程不可以进入该状态
panic("task[0] trying to sleep");
tmp = *p; // tmp 指向已经在等待队列上的任务
*p = current; // 等待队列头指针指向当前任务
current->state = TASK_UNINTERRUPTIBLE;
schedule(); // 重新调度
if (tmp) // 当原进程被 wake_up 函数唤醒才会执行此处代码
tmp->state=0; // 如果早期进入队列的任务没有被唤醒,将其状态置为就绪态
}

void interruptible_sleep_on(struct task_struct **p)
{ // 将当前任务置为可中断的睡眠状态,可通过信号、任务超时等手段唤醒
struct task_struct *tmp;

if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp=*p;
*p=current;
repeat: current->state = TASK_INTERRUPTIBLE; // 将当前任务置为可中断的睡眠状态
schedule(); // 重新调度
if (*p && *p != current) { // 原任务被唤醒时,若头指针指向的任务不是当前任务,
(**p).state=0; // 说明本任务之后还有任务进入队列,需要将它们都唤醒
goto repeat;
}
*p=NULL; // 对于最先进入队列的任务,最终会将头指针置为空
if (tmp)
tmp->state=0;
}

void wake_up(struct task_struct **p) // 唤醒不可中断等待任务
{
if (p && *p) { // 如果两个指针都非空
(**p).state=0; // 将任务置位就绪态
*p=NULL;
}
}

从 201 行到 262 行的几个函数用于软驱定时处理,等看到块设备部分再记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Line 264
#define TIME_REQUESTS 64 // 定义最多有 64 个内核定时器

static struct timer_list { // 定时器链表的结构及定时器链表数组
long jiffies; // 滴答数
void (*fn)(); // 定时处理程序指针
struct timer_list * next; // next 指针
} timer_list[TIME_REQUESTS], * next_timer = NULL; // next_timer 为定时器队列的头指针

void add_timer(long jiffies, void (*fn)(void)) // 添加定时器
{
struct timer_list * p;

if (!fn) // 如果定时处理程序指针为空,则返回
return;
cli(); // 关闭外中断
if (jiffies <= 0) // 如果定时值小于 0,则立即执行处理程序,并且定时器不加入链表
(fn)();
else { // 否则从数组中取空闲项
for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++)
if (!p->fn)
break;
if (p >= timer_list + TIME_REQUESTS) // 如果没有空闲项,系统 GG
panic("No more time requests free");
p->fn = fn;
p->jiffies = jiffies; // 给结构体赋值
p->next = next_timer;
next_timer = p; // 链入表头
while (p->next && p->next->jiffies < p->jiffies) { // 将该定时器插入链表中的适当位置
p->jiffies -= p->next->jiffies; // (从小到大),减去前面需要的滴答数
fn = p->fn; // 这样处理定时器时只需要看表头第一项是否到期
p->fn = p->next->fn;
p->next->fn = fn;
jiffies = p->jiffies;
p->jiffies = p->next->jiffies;
p->next->jiffies = jiffies;
p = p->next;
}
}
sti();
}

void do_timer(long cpl) // 由 timer_interrupt 调用,参数时间中断前运行程序的 cpl
{
extern int beepcount; // 扬声器发声时间滴答数(定义在kernel/chr_drv/console.c)
extern void sysbeepstop(void); // 关闭扬声器(同上)

if (beepcount) // 如果发声次数到,关闭发声
if (!--beepcount)
sysbeepstop();

if (cpl) // 如果 cpl = 0,内核程序运行时间增加
current->utime++;
else // 否则内核程序运行时间增加
current->stime++;

if (next_timer) { // 如果有定时器存在
next_timer->jiffies--; // 将表头定时器值减一
while (next_timer && next_timer->jiffies <= 0) { // 如果已经等于 0
void (*fn)(void);

fn = next_timer->fn;
next_timer->fn = NULL; // 处理程序的指针置空
next_timer = next_timer->next; // 表头指针向后指
(fn)(); // 调用相应的处理程序
}
}
if (current_DOR & 0xf0) // 如果当前软盘控制器 FDC 的数字输出寄存器中马达启动位置位
do_floppy_timer(); // 执行软盘定时程序
if ((--current->counter)>0) return; // 如果当前进程时间片没有用完,则返回
current->counter=0;
if (!cpl) return; // 对于内核程序,直接返回(不依赖counter值调度)
schedule(); // 对于用户程序,执行调度函数
}

int sys_alarm(long seconds) // 系统调用功能,设置报警定时器。参数单位是秒,所以需要单位转换
{
int old = current->alarm;

if (old)
old = (old - jiffies) / HZ;
// 将当前任务报警计时设置为 seconds 秒后的滴答数
current->alarm = (seconds>0)?(jiffies+HZ*seconds):0;
return (old);
}

int sys_getpid(void) // 系统调用功能,返回当前进程号 PID
{
return current->pid;
}

int sys_getppid(void) // 系统调用功能,返回当前进程父进程号 PPID
{
return current->father;
}

int sys_getuid(void) // 系统调用功能,返回当前进程用户号 UID
{
return current->uid;
}

int sys_geteuid(void) // 系统调用功能,返回当前进程有效用户号 EUID
{
return current->euid;
}

int sys_getgid(void) // 系统调用功能,返回当前进程组号 GID
{
return current->gid;
}

int sys_getegid(void) // 系统调用功能,返回当前进程有效组号 EGID
{
return current->egid;
}

int sys_nice(long increment) // 系统调用功能,调整进程对 CPU 的使用优先权
{
if (current->priority-increment>0)
current->priority -= increment; // 当 increment 为负数时,可以增加优先权
return 0;
}

最后是 main.c 中调用的 sched_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Line 385
void sched_init(void)
{
int i;
struct desc_struct * p; // 描述符表结构体指针
// sigaction 结构体长度必须为 16,保持对 POSIX 标准的兼容
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
// 设置初始任务的 TSS 与 LDT 描述符,宏定义在下面给出
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY; // p = &gdt[6],从 gdt 中第 6 项开始清空
for(i=1;i<NR_TASKS;i++) { // 初始任务的 TSS 与 LDT 描述符在 GDT 中的索引为 4 和 5
task[i] = NULL; // i 从 1 开始,不对初始任务做清空操作
p->a=p->b=0; // 将任务 TSS 描述符清空
p++;
p->a=p->b=0; // 将任务 LDT 描述符清空
p++;
}

__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位 NT 标志位
ltr(0); // 将任务 0 的 tss 描述符的选择子加载入 TR 任务寄存器,定义在 include/linux/sched.h
lldt(0); // 将任务 0 的 LDT 描述符的选择子加载入 LDTR 任务寄存器
outb_p(0x36,0x43);
outb_p(LATCH & 0xff , 0x40);
outb(LATCH >> 8 , 0x40); // 初始化 8253 定时器,使其每 10 ms 发出一个 IRQ0 请求
set_intr_gate(0x20,&timer_interrupt); // 设置时钟中断门
outb(inb_p(0x21)&~0x01,0x21); // 允许定时器中断
set_system_gate(0x80,&system_call); // 设置系统调用中断门
}

// include/asm/system.h Line 52
#define _set_tssldt_desc(n,addr,type) \ // 以 TSS 为例,n 为描述符项的地址,addr 为 TSS 所在的地址
__asm__ ("movw $104,%1\n\t" \ // 将 TSS 长度放入第 1-2 字节
"movw %%ax,%2\n\t" \ // 将 TSS 地址低 16 位放在 3-4 字节
"rorl $16,%%eax\n\t" \ // 将 TSS 地址高 16 位移至 ax 中
"movb %%al,%3\n\t" \ // 将 TSS 地址高 16 中的低字节放入第 5 字节
"movb $" type ",%4\n\t" \ // 填充 type 字段
"movb $0x00,%5\n\t" \ // 第 7 字节为 0
"movb %%ah,%6\n\t" \ // 将 TSS 地址高 16 中的高字节放入第 8 字节
"rorl $16,%%eax" \ // 再次循环右移,恢复 eax 的值
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")