问题
创建两个任务:Task A 和 Task B,并执行 Task A;那么,什么时候由谁进行任务切换执行?
问题分析
课程任务:打断任务的执行 (中断机制)
可使用时钟中断打断任务 (每个任务执行固定时间片)
中断发生后立即转而执行中断服务程序 (ISR)
在中断服务程序中完成任务上下文保存及任务切换
解决方案
解决方案的实现基础
- 建立并加载中断描述符表 (IDT)
- 编写时钟中断服务程序 (ISR)
- 初始化8259A并开启时钟中断
内核的中断实现 (上)
loader.asm
%include "blfunc.asm"
%include "common.asm"
org BaseOfLoader
interface:
BaseOfStack equ BaseOfLoader
BaseOfTarget equ BaseOfKernel
Target db "KERNEL "
TarLen equ ($-Target)
[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_32 + DA_C + DA_DPL0
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_32 + DA_DRWA + DA_DPL0
CODE32_FLAT_DESC : Descriptor 0, 0xFFFFF, DA_32 + DA_C + DA_DPL0
DATA32_FLAT_DESC : Descriptor 0, 0xFFFFF, DA_32 + DA_DRW + DA_DPL0
TASK_LDT_DESC : Descriptor 0, 0, 0
TASK_TSS_DESC : Descriptor 0, 0, 0
; GDT end
GdtLen equ $ - GDT_ENTRY
GdtPtr:
dw GdtLen - 1
dd 0
; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0
Code32FlatSelector equ (0x0003 << 3) + SA_TIG + SA_RPL0
Data32FlatSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0
; end of [section .gdt]
[section .idt]
align 32
[bits 32]
IDT_ENTRY:
; IDT definition
; Selector, Offset, DCount, Attribut
%rep 256
Gate Code32FlatSelector, DefaultHandler, 0, DA_386IGate + DA_DPL0
%endrep
; IDT end
IdtLen equ $ - IDT_ENTRY
IdtPtr:
dw IdtLen - 1
dd 0
[section .s16]
[bits 16]
BLMain:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, SPInitValue
; initialize GDT for 32 bits code segment
mov esi, CODE32_SEGMENT
mov edi, CODE32_DESC
call InitDescItem
; initalize GDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, GDT_ENTRY
mov dword [GdtPtr + 2], eax
; initialize IDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, IDT_ENTRY
mov dword [IdtPtr + 2], eax
call LoadTarget
cmp dx, 0
jz Output
call StoreGlobal
; 1. load GDT
lgdt [GdtPtr]
; 2. close interrupt
; load IDT
; set IOPL = 3
cli
lidt [IdtPtr]
pushf
pop eax
or eax, 0x3000
push eax
popf
; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al
; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax
; 5. jump to 32 bits code
jmp dword Code32Selector : 0
Output:
mov bp, ErrStr
mov cx, ErrLen
call Print
jmp $
; esi --> code segment labelBACK_ENTRY_SEGMENT
; edi --> descriptor label
InitDescItem:
push eax
mov eax, 0
mov ax, cs
shl eax, 4
add eax, esi
mov word [edi + 2], ax
shr eax, 16
mov byte [edi + 4], al
mov byte [edi + 7], ah
pop eax
ret
;
;
StoreGlobal:
push eax
mov dword [RunTaskEntry], RunTask
mov dword [InitInterruptEntry], InitInterrupt
mov dword [EnableTimerEntry], EnableTimer
mov dword [SendEOIEntry], SendEOI
mov eax, dword [GdtPtr + 2]
mov dword [GdtEntry], eax
mov dword [GdtSize], GdtLen / 8
mov eax, dword [IdtPtr + 2]
mov dword [IdtEntry], eax
mov dword [IdtSize], IdtLen / 8
pop eax
ret
[section .sfunc]
[bits 32]
DefaultHandlerFunc:
iret
DefaultHandler equ DefaultHandlerFunc - $$
; void InitInterrupt()
;
InitInterrupt:
push ebp
mov ebp, esp
push ax
push dx
call Init8259A
sti
mov al, 0xFF
mov dx, MASTER_IMR_PORT
call WriteIMR
mov al, 0xFF
mov dx, SLAVE_IMR_PORT
call WriteIMR
pop dx
pop ax
leave
ret
; void EnableTimer()
;
EnableTimer:
push ebp
mov ebp, esp
push ax
push dx
mov dx, MASTER_IMR_PORT
call ReadIMR
and ax, 0xFE
call WriteIMR
pop ax
pop dx
leave
ret
; SendEOI(uint port)
;
SendEOI:
push ebp
mov ebp, esp
mov edx, [esp + 8];
mov al, 0x20
out dx, al
call Delay
leave
ret
;
;
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 port
; return:
; al --> IMR register value
ReadIMR:
in al, 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]
;
; 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
[section .s32]
[bits 32]
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax
mov ax, Data32FlatSelector
mov ds, ax
mov es, ax
mov fs, ax
mov ax, Data32FlatSelector
mov ss, ax
mov esp, BaseOfLoader
jmp dword Code32FlatSelector : BaseOfKernel
Code32SegLen equ $ - CODE32_SEGMENT
ErrStr db "No Kernel"
ErrLen equ ($-ErrStr)
Buffer:
db 0x00
loader.asm里,我们初始化了中断描述符表,并把它加载进了内存,这256个中断向量默认的中断服务程序为 DefaultHandlerFunc;我们给内核提供了3个接口,这3个函数都需要遵循调用规则;InitInterrupt 函数用来初始化中断,EnableTimer 函数用来初始化时钟,SendEOI 函数用来发送EOI控制字,表示该中断已经结束;在 StoreGlobal 函数中我们将这3个内核函数的入口地址写到了共享交换区上。
kernel.h
#ifndef KERNEL_H
#define KERNEL_H
#include "type.h"
#include "const.h"
typedef struct
{
ushort limit1;
ushort base1;
byte base2;
byte attr1;
byte attr2_limit2;
byte base3;
} Descriptor;
typedef struct
{
Descriptor* const entry;
const int size;
} GdtInfo;
typedef struct
{
ushort offset1;
ushort selector;
byte dcount;
byte attr;
ushort offset2;
} Gate;
typedef struct
{
Gate* const entry;
const int size;
} IdtInfo;
typedef struct
{
uint gs;
uint fs;
uint es;
uint ds;
uint edi;
uint esi;
uint ebp;
uint kesp;
uint ebx;
uint edx;
uint ecx;
uint eax;
uint raddr;
uint eip;
uint cs;
uint eflags;
uint esp;
uint ss;
} RegValue;
typedef struct
{
uint previous;
uint esp0;
uint ss0;
uint unused[22];
ushort reserved;
ushort iomb;
} TSS;
typedef struct
{
RegValue rv;
Descriptor ldt[3];
TSS tss;
ushort ldtSelector;
ushort tssSelector;
uint id;
char name[8];
byte stack[512];
} Task;
int SetDescValue(Descriptor* pDesc, uint base, uint limit, ushort attr);
int GetDescValue(Descriptor* pDesc, uint* pBase, uint* pLimit, ushort* pAttr);
int SetIntHandler(Gate* pGate, uint ifunc);
int GetIntHandler(Gate* pGate, uint* pIfunc);
#endif
kernel.c
#include "kernel.h"
int SetDescValue(Descriptor* pDesc, uint base, uint limit, ushort attr)
{
int ret = 0;
if(ret = (pDesc != NULL))
{
pDesc->base1 = base & 0xFFFF;
pDesc->limit1 = limit & 0xFFFF;
pDesc->base2 = (base >> 16) & 0xFF;
pDesc->attr1 = attr & 0xFF;
pDesc->attr2_limit2 = ((attr >> 8) & 0xF0) | ((limit >> 16) & 0x0F);
pDesc->base3 = (base >> 24) & 0xFF;
}
return ret;
}
int GetDescValue(Descriptor* pDesc, uint* pBase, uint* pLimit, ushort* pAttr)
{
int ret = 0;
if(ret = (pDesc) && (pBase) && (pLimit) && (pAttr))
{
*pBase = (pDesc->base3 << 24) | (pDesc->base2 << 16) | pDesc->base1;
*pLimit = ((pDesc->attr2_limit2 & 0x0F) << 16) | pDesc->limit1;
*pAttr = ((pDesc->attr2_limit2 & 0xF0) << 8) | pDesc->attr1;
}
return ret;
}
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;
}
我们定义了 Gate 结构体,里面存储着门描述符的相关信息;定义了 IdtInfo 结构体,里面存储着中断描述符表和和它的大小。
int SetIntHandler(Gate* pGate, uint ifunc) 用来将 ifunc 函数入口地址作为一个中断描述符的中断服务程序。
int GetIntHandler(Gate* pGate, uint* pIfunc) 用来读取一个中断描述符的中断服务程序的入口地址,存储在 pIfunc中。
kentry.asm
%include "common.asm"
global _start
extern KMain
extern ClearScreen
extern gGdtInfo
extern gIdtInfo
extern RunTask
extern InitInterrupt
extern EnableTimer
extern SendEOI
[section .text]
[bits 32]
_start:
mov ebp, 0
call InitGlobal
call ClearScreen
call KMain
jmp $
;
;
InitGlobal:
push ebp
mov ebp, esp
mov eax, dword [GdtEntry]
mov dword [gGdtInfo], eax
mov eax, dword [GdtSize]
mov dword [gGdtInfo + 4], eax
mov eax, dword [IdtEntry]
mov dword [gIdtInfo], eax
mov eax, dword [IdtSize]
mov dword [gIdtInfo + 4], eax
mov eax, dword [RunTaskEntry]
mov dword [RunTask], eax
mov eax, dword [InitInterruptEntry]
mov dword [InitInterrupt], eax
mov eax, dword [EnableTimerEntry]
mov dword [EnableTimer], eax
mov eax, dword[SendEOIEntry]
mov dword [SendEOI], eax
leave
ret
InitGlobal 用来初始化中断描述符表的信息和3个接口函数的入口地址给内核使用。
kmain.c
#include "kernel.h"
#include "screen.h"
#include "global.h"
void (* const InitInterrupt)() = NULL;
void (* const EnableTimer)() = NULL;
void (* const SendEOI)() = NULL;
Task t = {0};
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);
asm volatile("leave\n" "iret\n");
}
Delay(int n)
{
while(n > 0)
{
int i = 0, j = 0;
for(i = 0; i < 1000; i++)
{
for(j = 0; j < 1000; j++)
{
asm volatile("nop\n");
}
}
n--;
}
}
void TaskA()
{
int i = 0;
SetPrintPos(0, 12);
PrintString("TaskA: ");
while(1)
{
SetPrintPos(8, 12);
PrintChar('A' + i);
i = (i + 1) % 26;
Delay(1);
}
}
void KMain()
{
PrintString("D.T.OS\n");
uint base = 0;
uint limit = 0;
uint attr = 0;
int i = 0;
PrintString("Gdt Entry: ");
PrintIntHex((uint)gGdtInfo.entry);
PrintChar('\n');
PrintString("Gdt Size: ");
PrintIntDec((uint)gGdtInfo.size);
PrintChar('\n');
PrintString("Idt Entry: ");
PrintIntHex((uint)gIdtInfo.entry);
PrintChar('\n');
PrintString("Idt Size: ");
PrintIntDec((uint)gIdtInfo.size);
PrintChar('\n');
t.rv.gs = LDT_VIDEO_SELECTOR;
t.rv.cs = LDT_CODE32_SELECTOR;
t.rv.ds = LDT_DATA32_SELECTOR;
t.rv.es = LDT_DATA32_SELECTOR;
t.rv.fs = LDT_DATA32_SELECTOR;
t.rv.ss = LDT_DATA32_SELECTOR;
t.rv.esp = (uint)(t.stack + sizeof(t.stack));
t.rv.eip = (uint)TaskA;
t.rv.eflags = 0x3202;
t.tss.ss0 = GDT_DATA32_FLAT_SELECTOR;
t.tss.esp0 = 0x9000;
t.tss.iomb = sizeof(t.tss);
SetDescValue(t.ldt + LDT_VIDEO_INDEX, 0xB8000, 0X07FFF, DA_DRWA + DA_32 + DA_DPL3);
SetDescValue(t.ldt + LDT_CODE32_INDEX, 0x00, 0XFFFFF, DA_C + DA_32 + DA_DPL3);
SetDescValue(t.ldt + LDT_DATA32_INDEX, 0x00, 0XFFFFF, DA_DRW + DA_32 + DA_DPL3);
t.ldtSelector = GDT_TASK_LDT_SELECTOR;
t.tssSelector = GDT_TASK_TSS_SELECTOR;
SetDescValue(&gGdtInfo.entry[GDT_TASK_LDT_INDEX], (uint)&t.ldt, sizeof(t.ldt) - 1, DA_LDT + DA_DPL0);
SetDescValue(&gGdtInfo.entry[GDT_TASK_TSS_INDEX], (uint)&t.tss, sizeof(t.tss) - 1, DA_386TSS + DA_DPL0);
SetIntHandler(gIdtInfo.entry + 0x20, (uint)TimerHandler);
PrintString("Stack Bottom: ");
PrintIntHex((uint)t.stack);
PrintString(" Stack Top: ");
PrintIntHex((uint)t.stack + sizeof(t.stack));
InitInterrupt();
EnableTimer();
RunTask(&t);
}
全局的3个函数指针被用来初始化3个内核函数的入口地址,TimerHandler 函数是时钟的中断服务程序。
117行,我们将时钟中断关联到了 TimerHandler 函数。
124 - 125行,我们初始化了中断并使能了时钟中断。
101行,我们将 eflags 的值设为 0x3202,把 IF 位置位1,打开了处理器的中断;如果这里不把 IF 位置1的话,Task A在执行的时候,IF 位将一直为0,处理器将接收不到中断。
104行我们将 esp0 的值设置为 0x9000,是因为Task A任务的特权级为3,在Task A任务执行期间,会因为时钟中断的到来,从而升高特权级去执行中断服务程序,中断服务程序的特权级为0,因此这里要将设置0特权级的 esp 值,从而能正常的从3特权级跳转到0特权级执行。
TimerHandler 时钟中断服务程序中,我们需要在中断服务程序结束时,手动清除EOI标志位,表明中断已经结束,可以接受下一个中断;如果我们不清除EOI,则中断服务程序只会执行一次;在30行,我们内嵌汇编,用 iret 指令进行返回,在中断服务程序中,我们需要指定用 iret 指令返回,否则就是用普通的 ret 指令返回。
程序成功的执行了!