借助中断实现任务切换

image-20220709210520530

  • 可使用时钟中断打断任务(每个任务执行固定时间片)
  • 中断发生后立即转而执行中断服务程序 (ISR)
  • 在中断服务程序中完成任务上下文保存(寄存器)及任务切换

image-20220710112820375

解决方案的实现基础

  1. 建立并加载中断描述符表 (IDT)
  2. 编写时钟中断服务程序 (ISR)
  3. 初始化 8259A 并启动时钟中断(这种外部设备中断一般都要借助8259A)

初始工作-IDT及相应操作函数

loader.asm中,选择子是Code32Selector,默认处理函数DefaultHandler,之后需要用到中断的时候将offset修改为对应的中断处理函数即可
sfunc字段存储放上与中断相关的函数
gfunc存储C语言调用的函数,这些函数地址需要放到交换区,给kernel调用

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
[section .idt]
align 32
[bits 32]
IDT_ENTRY:
; IDT definition
; Selector, Offset, DCount, Attribute
%rep 256
Gate Code32Selector, DefaultHandler, 0, DA_386IGate + DA_DPL0
%endrep

IdtLen equ $ - IDT_ENTRY

[section .sfunc]
[bits 32]
; 放上与中断相关的函数
;
Delay:
%rep 5
nop
%endrep
ret

Init8259A:
push ax

; master
; ICW1
mov al, 00010001B
out MASTER_ICW1_PORT, al

call Delay

; ICW2
mov al, 0x20
out MASTER_ICW2_PORT, al

call Delay

; ICW3
mov al, 00000100B
out MASTER_ICW3_PORT, al

call Delay

; ICW4
mov al, 00010001B
out MASTER_ICW4_PORT, al

call Delay

; slave
; ICW1
mov al, 00010001B
out SLAVE_ICW1_PORT, al

call Delay

; ICW2
mov al, 0x28
out SLAVE_ICW2_PORT, al

call Delay

; ICW3
mov al, 00000010B
out SLAVE_ICW3_PORT, al

call Delay

; ICW4
mov al, 00000001B
out SLAVE_ICW4_PORT, al

call Delay

pop ax

ret

; al --> IMR register value
; dx --> 8259A port
WriteIMR:
out dx, al
call Delay
ret

; dx --> 8259A
; return:
; ax --> IMR register value
ReadIMR:
in ax, dx
call Delay
ret

;
; dx --> 8259A port
WriteEOI:
push ax

mov al, 0x20
out dx, al

call Delay

pop ax

ret

[section .gfunc]
[bits 32]
; 全局函数给C语言调用的,需要将函数地址传入到交换区
; parameter ===> Task* pt
RunTask:
push ebp
mov ebp, esp

mov esp, [ebp + 8]

lldt word [esp + 200]
ltr word [esp + 202]

pop gs
pop fs
pop es
pop ds

popad

add esp, 4

iret

;初始化 8259A
;
InitInterrupt:
push ebp
mov ebp, esp

push ax
push dx

call Init8259A

sti

mov ax, 0xFF
mov dx, MASTER_IMR_PORT

call WriteIMR

mov ax, 0xFF
mov dx, SLAVE_IMR_PORT

call WriteIMR

pop dx
pop ax

leave
ret

;
;打开时钟中断开关
EnableTimer:
push ebp
mov ebp, esp

push ax
push dx

mov dx, MASTER_IMR_PORT

call ReadIMR

and ax, 0xFE

call WriteIMR

pop dx
pop ax

leave
ret

; void SendEOI(uint port);
; port ==> 8259A port
SendEOI:
push ebp
mov ebp, esp

mov edx, [ebp + 8]

mov al, 0x20
out dx, al

call Delay

leave
ret

kernel.h中定义好中断门描述符和中断门描述符表的地址大小

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
ushort offset1; //ISR中断服务程序偏移地址第1部分
ushort selector; //段选择子
byte dcount; //
byte attr; //
ushort offset2; //ISR中断服务程序偏移地址第1部分
} Gate;

typedef struct {
Gate * const entry; //IDT的入口地址
const int size; //IDT的大小
} IdtInfo;

设置/获取中断服务的入口

kernel.c中添加用于修改全局中断描述符表的函数SetIntHandler和查看全局中断描述符表的函数GetIntHandler

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
int SetIntHandler(Gate* pGate, uint ifunc)
{
int ret = 0;

if( ret = (pGate != NULL) )
{
pGate->offset1 = ifunc & 0xFFFF;
pGate->selector = GDT_CODE32_FLAT_SELECTOR; //平坦模型代码段选择子,方便查找函数,"指哪打哪"
pGate->dcount = 0;
pGate->attr = DA_386IGate + DA_DPL0;
pGate->offset2 = (ifunc >> 16) & 0xFFFF;
}

return ret;
}

int GetIntHandler(Gate* pGate, uint* pIFunc)
{
int ret = 0;

if( ret = (pGate && pIFunc) )
{
*pIFunc = (pGate->offset2 << 16) | pGate->offset1;
}

return ret;
}

时钟中断处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void TimerHandler()
{
static uint i = 0;

i = (i + 1) % 10;

if( i == 0 )
{
static uint j = 0;

SetPrintPos(0, 13);

PrintString("Timer: ");

SetPrintPos(8, 13);

PrintIntDec(j++);
}

SendEOI(MASTER_EOI_PORT); //必须手动发送EOI结束本次中断,才可以继续响应下一次时钟中断

asm volatile("leave\n""iret\n"); //高特权级返回低特权级需要嵌入汇编语句iret返回
}

kmain.c中定义一个时钟中断处理函数并且注册到IDT全局中断描述符表里面去

注意eflags的IF位需要置为1,中断总开关打开才可以让中断能够触发

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
void KMain()
{
//...

p.rv.esp = (uint)p.stack + sizeof(p.stack);
p.rv.eip = (uint)TaskA;
p.rv.eflags = 0x3202; //eflag 0x3202打开中断总开关 IOPL = 3, IF = 1

p.tss.ss0 = GDT_DATA32_FLAT_SELECTOR;
p.tss.esp0 = 0x9000;
//p.tss.esp0 = (uint)&p.rv + sizeof(p.rv); 指向RegValue
p.tss.iomb = sizeof(p.tss);

//...

SetIntHandler(gIdtInfo.entry + 0x20, (uint)TimerHandlerEntry);

InitInterrupt();

EnableTimer();

gCTaskAddr = &p;

RunTask(gCTaskAddr);
}

上下文恢复

中断服务程序仅完成了逻辑功能:在中断发生时并没有保存上下文 (所有寄存器的值),中断结束时也没有恢复上下文

时钟中断过程:

  • 外部设备会发送一个中断请求
  • 先去查找是否注册了TSS全局段描述符,如果注册了从TSS中取出esp和ss0的值(内核栈信息),sp会转移到高特权级的栈Stack_DPL0(内核栈)
  • Stack_DPL0(注意是内核栈)压入ss,esp,eflag,cs,ip等寄存器用于执行完中断服务程序后iret返回

注意我们将内核栈设置为0x9000位置,将ss,esp,eflag,cs,ip等寄存器保存到了内核栈,那么之前设置的结构体RegValue结构体意义又何在呢?

中断发生时可以这样实现:

  1. 将 TSS 中的 esp0 指向任务数据结构中 RegValue 的末尾处。即: 将 RegValue 成员当作中断栈使用
  2. 中断发生时(ss,esp,eflag,cs,ip会自动压入,剩下的寄存器手动压入), 直接进行寄存器压钱操作。即: 使用 RegValue 成员保存上下文
  3. 重新指定 esp 的值, 并完成中断服务程序的逻辑功能。即: 重新指定内核栈(为函数调用做准备,比如指向0x9000避免破坏RegValue其他成员)

中断结束返回时:

  1. 中断返回前, 将esp 指向任务数据结构起始位置.即: esp 指向 RegValue 成员的起始位置
  2. 执行寄存器出栈操作。即: 使用 RegValue 成员恢复上下文
  3. 使 用 iret 指令进行中断返回。即: 使用 RegValue 成员恢复任务执行

image-20220710210752876

添加一个TimerHandlerEntry函数用于保存上下文(所有寄存器)

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
; 中断操作前 寄存器压栈操作用于保存上下文(寄存器的值)
; 中断操作前esp指向regvalue末尾, ss,esp,eflag,cs,ip自动压入后esp指向raddr
%macro BeginISR 0
cli

sub esp, 4 ;绕过raddr

pushad

push ds
push es
push fs
push gs

mov si, ss ; 进入中断服务程序后,数据段ds与附加段es的选择子与堆栈段的选择子一致
mov ds, si
mov es, si

mov esp, BaseOfLoader ; 重新指定中断服务程序使用的内核栈
%endmacro

; 中断操作结束后 esp指向目标任务的起始位置
%macro EndISR 0
mov esp, [gCTaskAddr]

pop gs ; 恢复上下文
pop fs
pop es
pop ds

popad

add esp, 4 ; 跳过raddr

iret ; 必须要iret返回, 高特权级跳回低特权级
%endmacro

; 0x20时钟中断调用TimerHandlerEntry
TimerHandlerEntry:
BeginISR
call TimerHandler
EndISR
  • 如果任务执行时发生中断, 必须保存任务上下文
  • 将任务数据结构中的RegValue 成员当作初始内核栈(中断栈)
  • 中断发生时, 上下文信息直接保存到 RegValue 成员
  • 保存成功后, 切换内核栈进行其它函数调用
  • 中断返回时, 通过 RegValue 成员恢复上下文