DCS218 - Operating Systems Lab 2021 Spring
前言
本文章介绍操作系统内核最重要的功能之一————时钟中断的实现。
编写内核
-
bootloader 中加载操作系统内核到地址
0x20000
,然后跳转到0x20000
。内核接管控制权后,输出 “19335025CYH”。 -
假设我们实现的内核很小,因此下面我们约定内核的大小是
200
个扇区,起始地址是0x20000
,内核存放在硬盘的起始位置是第6个扇区。bootloader 在进入保护模式后,从硬盘的第6个扇区中加载200个扇区到内存起始地址0x20000
处,然后跳转执行。 -
我们在 bootloader 的最后加上读取内核的代码,代码放置在
src/boot/bootloader.asm
下,如下所示。0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
... ; 进入保护模式并初始化的代码 mov eax, KERNEL_START_SECTOR mov ebx, KERNEL_START_ADDRESS mov ecx, KERNEL_SECTOR_COUNT load_kernel: push eax push ebx call asm_read_hard_disk ; 读取硬盘 add esp, 8 inc eax add ebx, 512 loop load_kernel jmp dword CODE_SELECTOR:KERNEL_START_ADDRESS ; 跳转到kernel jmp $ ; 死循环 ; asm_read_hard_disk(memory,block) ; 加载逻辑扇区号为block的扇区到内存地址memory ... ;省略
常量的定义放置在
include/boot.inc
下,新增的内容如下。0 1 2 3
; __________kernel_________ KERNEL_START_SECTOR equ 6 KERNEL_SECTOR_COUNT equ 200 KERNEL_START_ADDRESS equ 0x20000
-
首先,我们在
src/boot/entry.asm
下定义内核进入点。0 1 2
extern setup_kernel enter_kernel: jmp setup_kernel
我们会在链接阶段巧妙地将
entry.asm
的代码放在内核代码的最开始部份,使得bootloader在执行跳转到0x20000
后,即内核代码的起始指令,执行的第一条指令是jmp setup_kernel
。在jmp
指令执行后,我们便跳转到使用C++编写的函数setup_kernel
。此后,我们便可以使用C++来写内核了。setup_kernel
的定义在文件src/kernel/setup.cpp
中,内容如下。0 1 2 3 4 5 6 7 8 9
#include "asm_utils.h" extern "C" void setup_kernel() { // asm_hello_world(); asm_hello_world_cyh(); while(1) { } }
-
为了方便汇编代码的管理,我们将汇编函数放置在
src/utils/asm_utils.h
下,如下所示。这里我添加了打印自己学号的函数asm_hello_world_cyh
:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
global asm_hello_world_cyh asm_hello_world_cyh: push eax xor eax, eax mov ah, 0x03 ;青色 mov al, '1' mov [gs:2 * 0], ax ... mov al, 'H' mov [gs:2 * 10], ax pop eax ret
-
然后我们统一在文件
include/asm_utils.h
中声明所有的汇编函数,这样我们就不用单独地使用extern
来声明了,只需要#include "asm_utils.h"
即可,如下所示。0 1 2 3 4 5 6
#ifndef ASM_UTILS_H #define ASM_UTILS_H extern "C" void asm_hello_world(); extern "C" void asm_hello_world_cyh(); #endif
-
然后我们在
build
文件夹下开始编译,我们首先编译 MBR、bootloader。0 1
nasm -o mbr.bin -f bin -I../include/ ../src/boot/mbr.asm nasm -o bootloader.bin -f bin -I../include/ ../src/boot/bootloader.asm
其中,
-I
参数指定了头文件路径,-f
指定了生成的文件格式是二进制的文件。接着编译内核的代码。我们的方案是将所有的代码(C/C++,汇编代码)都同一编译成可重定位文件,然后再链接成一个可执行文件。
我们首先编译
src/boot/entry.asm
和src/utils/asm_utils.asm
。0 1
nasm -o entry.obj -f elf32 ../src/boot/entry.asm nasm -o asm_utils.o -f elf32 ../src/utils/asm_utils.asm
回忆一下,在Linux下的可重定位文件的格式是ELF文件格式。加上我们是32位保护模式,
-f
参数指定的生成文件格式是elf32
,而不再是bin
。接着编译
setup.cpp
:0
g++ -g -Wall -march=i386 -m32 -nostdlib -fno-builtin -ffreestanding -fno-pic -I../include -c ../src/kernel/setup.cpp
上面的参数:
-O0
告诉编译器不开启编译优化。-Wall
告诉编译器显示所有编译器警告信息-march=i386
告诉编译器生成i386处理器下的.o
文件格式。-m32
告诉编译器生成32位的二进制文件。-nostdlib -fno-builtin -ffreestanding -fno-pic
是告诉编译器不要包含C的任何标准库。-g
表示向生成的文件中加入debug信息供gdb使用。-I
指定了代码需要的头文件的目录。-c
表示生成可重定位文件。
最后我们链接生成的可重定位文件为两个文件:只包含代码的文件
kernel.bin
,可执行文件kernel.o
。0 1
ld -o kernel.o -melf_i386 -N entry.obj setup.o asm_utils.o -e enter_kernel -Ttext 0x00020000 ld -o kernel.bin -melf_i386 -N entry.obj setup.o asm_utils.o -e enter_kernel -Ttext 0x00020000 --oformat binary
这里面同样涉及很多参数,我们逐一来看。
-m
参数指定模拟器为i386。-N
参数告诉链接器不要进行页对齐。-Ttext
指定标号的起始地址。-e
参数指定程序进入点。--oformat
指定输出文件格式。
为什么要生成两个文件呢?注意到上面两条指令差别仅在于是否有
-oformat binary
。实际上,kernel.o
也是ELF32
格式的,其不仅包含代码和数据,还包含debug
信息和elf
文件信息等。特别地,kernel.o
开头并不是内核进入点,而是ELF
的文件头,因此我们需要解析ELF文件才能找到真正的内核进入点。为了简便起见,我们希望链接生成的文件只有内核的代码,不会包含其他的信息,即一开头就是可执行的指令。此时,我们加上了
-oformat binary
生成这样的文件。也就是说,kernel.bin
从头到尾都是我们编写的代码对应的机器指令,不再是ELF
格式的。此时,我们将其加载到内存后,跳转执行即可。kernel.o
仅用在gdb的debug过程中,通过kernel.o
,gdb就能知道每一个地址对应的C/C++代码或汇编代码是什么,这样为我们的debug过程带来了极大的方便。特别注意,输出的二进制文件的机器指令顺序和链接时给出的文件顺序相同。也就是说,如果我们按如下命令链接
0
ld -o kernel.bin -melf_i386 -N setup.o entry.obj asm_utils.o -e enter_kernel -Ttext 0x0 --oformat binary
那么
bootloader.bin
的第一条指令是setup.o
的第一条指令,这样就会导致错误。链接后我们使用dd命令将
mbr.bin bootloader.bin kernel.bin
写入硬盘即可,如下所示。0 1 2
dd if=mbr.bin of=../run/hd.img bs=512 count=1 seek=0 conv=notrunc dd if=bootloader.bin of=../run/hd.img bs=512 count=5 seek=1 conv=notrunc dd if=kernel.bin of=../run/hd.img bs=512 count=200 seek=6 conv=notrunc
在
run
目录下,启动。0
qemu-system-i386 -hda ../run/hd.img -serial null -parallel stdio -no-reboot
-
我们可以使用makefile的命令自动帮我们找到
.c
,.cpp
文件,然后编译生成.o
文件。然后我们又可以使用makefile找到所有生成的.o
文件,使用ld
链接生成二进制文件。这样做的好处是当我们新增一个.c
或.cpp
文件后,我们几乎不需要修改makefile,大大简化了编译过程:0 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
ASM_COMPILER = nasm C_COMPLIER = gcc CXX_COMPLIER = g++ CXX_COMPLIER_FLAGS = -g -Wall -march=i386 -m32 -nostdlib -fno-builtin -ffreestanding -fno-pic LINKER = ld SRCDIR = ../src RUNDIR = ../run BUILDDIR = build INCLUDE_PATH = ../include CXX_SOURCE += $(wildcard $(SRCDIR)/kernel/*.cpp) CXX_OBJ += $(CXX_SOURCE:$(SRCDIR)/kernel/%.cpp=%.o) ASM_SOURCE += $(wildcard $(SRCDIR)/utils/*.asm) ASM_OBJ += $(ASM_SOURCE:$(SRCDIR)/utils/%.asm=%.o) OBJ += $(CXX_OBJ) OBJ += $(ASM_OBJ) build : mbr.bin bootloader.bin kernel.bin kernel.o dd if=mbr.bin of=$(RUNDIR)/hd.img bs=512 count=1 seek=0 conv=notrunc dd if=bootloader.bin of=$(RUNDIR)/hd.img bs=512 count=5 seek=1 conv=notrunc dd if=kernel.bin of=$(RUNDIR)/hd.img bs=512 count=145 seek=6 conv=notrunc # nasm的include path有一个尾随/ mbr.bin : $(SRCDIR)/boot/mbr.asm $(ASM_COMPILER) -o mbr.bin -f bin -I$(INCLUDE_PATH)/ $(SRCDIR)/boot/mbr.asm bootloader.bin : $(SRCDIR)/boot/bootloader.asm $(ASM_COMPILER) -o bootloader.bin -f bin -I$(INCLUDE_PATH)/ $(SRCDIR)/boot/bootloader.asm entry.obj : $(SRCDIR)/boot/entry.asm $(ASM_COMPILER) -o entry.obj -f elf32 $(SRCDIR)/boot/entry.asm kernel.bin : entry.obj $(OBJ) $(LINKER) -o kernel.bin -melf_i386 -N entry.obj $(OBJ) -e enter_kernel -Ttext 0x00020000 --oformat binary kernel.o : entry.obj $(OBJ) $(LINKER) -o kernel.o -melf_i386 -N entry.obj $(OBJ) -e enter_kernel -Ttext 0x00020000 $(CXX_OBJ): $(CXX_COMPLIER) $(CXX_COMPLIER_FLAGS) -I$(INCLUDE_PATH) -c $(CXX_SOURCE) asm_utils.o : $(SRCDIR)/utils/asm_utils.asm $(ASM_COMPILER) -o asm_utils.o -f elf32 $(SRCDIR)/utils/asm_utils.asm clean: rm -f *.o* *.bin run: qemu-system-i386 -hda $(RUNDIR)/hd.img -serial null -parallel stdio -no-reboot debug: qemu-system-i386 -S -s -parallel stdio -hda $(RUNDIR)/hd.img -serial null& @sleep 1 gnome-terminal -e "gdb -q -tui -x $(RUNDIR)/gdbinit"
-
编译运行:
0 1 2
make clean make make run
中断的处理
-
以下代码在 assignment2 基础上修改。为了能够抽象地描述中断处理模块,我们不妨定义一个类,称为中断管理器
InterruptManager
,其定义放置在include/interrupt.h
中,如下所示:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#ifndef INTERRUPT_H #define INTERRUPT_H #include "os_type.h" class InterruptManager { private: // IDT起始地址 uint32 *IDT; public: InterruptManager(); // 初始化 void initialize(); // 设置中断描述符 // index 第index个描述符,index=0, 1, ..., 255 // address 中断处理程序的起始地址 // DPL 中断描述符的特权级 void setInterruptDescriptor(uint32 index, uint32 address, byte DPL); }; #endif
-
在使用中断之前,首先需要初始化IDT,在
boot/kernel/interrupt
中添加函数InterruptManager::initialize
,如下所示:默认函数0 1 2 3 4 5 6 7 8 9 10
void InterruptManager::initialize() { // 初始化IDT IDT = (uint32 *)IDT_START_ADDRESS; asm_lidt(IDT_START_ADDRESS, 256 * 8 - 1); for (uint i = 0; i < 256; ++i) { setInterruptDescriptor(i, (uint32)asm_interrupt_empty_handler, 0); } }
InterruptManager::initialize
先设置IDTR,然后再初始化256个中断描述符,将IDT设定在地址0x8880
处,即IDT_START_ADDRESS=0x8880
。为了使CPU能够找到IDT中的中断处理函数,我们需要将IDT的信息放置到寄存器IDTR中。当中断发生时,CPU会自动到IDTR中找到IDT的地址,然后根据中断向量号在IDT找到对应的中断描述符,最后跳转到中断描述符对应的函数中进行处理.由于我们只有256个中断描述符,每个中断描述符的大小均为8字节,因此我们有
$表界限=8*256-1=2047$
此时,IDTR的32位基地址是
0x8880
,表界限是2047
。确定了IDT的基地址和表界限后,我们就可以初始化IDTR了。IDTR的初始化需要用到指令
lidt
。lidt
实际上是将以tag
为起始地址的48字节放入到寄存器IDTR中。由于我们打算在C代码中初始化IDT,而C语言的语法并未提供lidt
语句。因此我们需要在汇编代码中实现能够将IDT的信息放入到IDTR的函数asm_lidt
,代码添加到src/utils/asm_utils.asm
中,如下所示:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
; void asm_lidt(uint32 start, uint16 limit) asm_lidt: push ebp mov ebp, esp push eax mov eax, [ebp + 4 * 3] mov [ASM_IDTR], ax mov eax, [ebp + 4 * 2] mov [ASM_IDTR + 2], eax lidt [ASM_IDTR] pop eax pop ebp ret ASM_IDTR dw 0 dd 0
-
将IDT的信息放入到IDTR后,我们就可以插入256个默认的中断处理描述符到IDT中。实际上在我们的实验中,对于中断描述符,有几个值是定值:
- P=1表示存在。
- D=1表示32位代码。
- DPL=0表示特权级0.
- 代码段选择子等于bootloader中的代码段选择子,也就是寻址4GB空间的代码段选择子。
因此,从目前来看,不同的中断描述符间变化的只是中断处理程序在目标代码段中的偏移。由于我们的程序运行在平坦模式下,也就是段起始地址从内存地址0开始,长度为4GB。此时,函数名就是中断处理程序在目标代码段中的偏移。
将段描述符的设置定义在函数
InterruptManager::setInterruptDescriptor
中,如下所示:0 1 2 3 4 5 6 7 8
// 设置中断描述符 // index 第index个描述符,index=0, 1, ..., 255 // address 中断处理程序的起始地址 // DPL 中断描述符的特权级 void InterruptManager::setInterruptDescriptor(uint32 index, uint32 address, byte DPL) { IDT[index * 2] = (CODE_SELECTOR << 16) | (address & 0xffff); IDT[index * 2 + 1] = (address & 0xffff0000) | (0x1 << 15) | (DPL << 13) | (0xe << 8); }
其中,
IDT
是中断描述符表的起始地址指针,实际上我们可以认为中断描述符表就是一个数组。在InterruptManager
中,我们将变量IDT
视作是一个uint32
类型的数组。由于每个中断描述符的大小是两个uint32
,第index
个中断描述符是IDT[2 * index],IDT[2 * index + 1]
。 -
接下来,我们定义一个默认的中断处理函数是
asm_interrupt_empty_handler
,放置在src/utils/asm_utils.asm
中,如下所示。0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
ASM_UNHANDLED_INTERRUPT_INFO db 'Unhandled interrupt happened, halt...' db 0 ; void asm_unhandled_interrupt() asm_unhandled_interrupt: cli mov esi, ASM_UNHANDLED_INTERRUPT_INFO xor ebx, ebx mov ah, 0x03 .output_information: cmp byte[esi], 0 je .end mov al, byte[esi] mov word[gs:bx], ax inc esi add ebx, 2 jmp .output_information .end: jmp $
asm_interrupt_empty_handler
首先关中断,然后输出提示字符串,最后做死循环。 -
在
InterruptManager::initialize
最后,我们调用setInterruptDescriptor
放入256个默认的中断描述符即可,这256个默认的中断描述符对应的中断处理函数是asm_unhandled_interrupt
。0 1 2
for (uint i = 0; i < 256; ++i) { setInterruptDescriptor(i, (uint32)asm_unhandled_interrupt, 0); }
-
在函数
src/kernel/setup.cpp
中定义并初始化中断处理器。注意,只定义一个InterruptManager
的实例,因为中断管理器有且只有一个。0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#include "asm_utils.h" #include "interrupt.h" // 中断管理器 InterruptManager interruptManager; extern "C" void setup_kernel() { asm_hello_world_cyh(); // asm_hello_world(); // while(1) { // } // 中断处理部件 interruptManager.initialize(); // 尝试触发除0错误 int a = 1 / 0; // 死循环 asm_halt(); }
然后在
include/os_modules.h
中声明这个实例,以便在其他.cpp
文件中使用。0 1 2 3 4 5 6 7
#ifndef OS_MODULES_H #define OS_MODULES_H #include "interrupt.h" extern InterruptManager interruptManager; #endif
-
最后将一些常量统一定义在文件
include/os_constant.h
下:0 1 2 3 4 5 6
#ifndef OS_CONSTANT_H #define OS_CONSTANT_H #define IDT_START_ADDRESS 0x8880 #define CODE_SELECTOR 0x20 #endif
-
更新
include/asm_utils.h
:0 1 2 3 4 5 6 7 8 9 10 11 12
#ifndef ASM_UTILS_H #define ASM_UTILS_H #include "os_type.h" extern "C" void asm_hello_world_cyh(); extern "C" void asm_hello_world(); extern "C" void asm_lidt(uint32 start, uint16 limit); extern "C" void asm_unhandled_interrupt(); extern "C" void asm_halt(); #endif
在
include/utils/assm_utils.asm
中添加halt
函数:0 1
asm_halt: jmp $
-
编译,以debug模式运行
0 1
make make debug
把默认函数更改为自己的函数:
时钟中断
-
为中断控制器
InterruptManager
加入如下成员变量和函数。0 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
class InterruptManager { private: uint32 *IDT; // IDT起始地址 uint32 IRQ0_8259A_MASTER; // 主片中断起始向量号 uint32 IRQ0_8259A_SLAVE; // 从片中断起始向量号 public: InterruptManager(); void initialize(); // 设置中断描述符 // index 第index个描述符,index=0, 1, ..., 255 // address 中断处理程序的起始地址 // DPL 中断描述符的特权级 void setInterruptDescriptor(uint32 index, uint32 address, byte DPL); // 开启时钟中断 void enableTimeInterrupt(); // 禁止时钟中断 void disableTimeInterrupt(); // 设置时钟中断处理函数 void setTimeInterrupt(void *handler); private: // 初始化8259A芯片 void initialize8259A(); };
-
初始化8259A
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
void InterruptManager::initialize8259A() { // ICW 1 asm_out_port(0x20, 0x11); asm_out_port(0xa0, 0x11); // ICW 2 IRQ0_8259A_MASTER = 0x20; IRQ0_8259A_SLAVE = 0x28; asm_out_port(0x21, IRQ0_8259A_MASTER); asm_out_port(0xa1, IRQ0_8259A_SLAVE); // ICW 3 asm_out_port(0x21, 4); asm_out_port(0xa1, 2); // ICW 4 asm_out_port(0x21, 1); asm_out_port(0xa1, 1); // OCW 1 屏蔽主片所有中断,但主片的IRQ2需要开启 asm_out_port(0x21, 0xfb); // OCW 1 屏蔽从片所有中断 asm_out_port(0xa1, 0xff); }
初始化8259A芯片的过程是通过设置一系列的ICW字来完成的。由于并未建立处理8259A中断的任何函数,因此在初始化的最后,需要屏蔽主片和从片的所有中断。
其中,
asm_out_port
是对out
指令的封装,放在asm_utils.asm
中,如下所示:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
; void asm_out_port(uint16 port, uint8 value) asm_out_port: push ebp mov ebp, esp push edx push eax mov edx, [ebp + 4 * 2] ; port mov eax, [ebp + 4 * 3] ; value out dx, al pop eax pop edx pop ebp ret
-
接下来处理主片的IRQ0中断。在计算机中,有一个称为8253的芯片,其能够以一定的频率来产生时钟中断。当其产生了时钟中断后,信号会被8259A截获,从而产生IRQ0中断。处理时钟中断并不需要了解8253芯片,只需要对8259A芯片产生的时钟中断进行处理即可,步骤如下。
- 编写中断处理函数。
- 设置主片IRQ0中断对应的中断描述符。
- 开启时钟中断。
- 开中断。
我们首先编写中断处理的函数。
此时需要对屏幕进行输出,之前只是单纯地往显存地址上赋值来显示字符。但是这样做并不太方便。因此简单封装一个能够处理屏幕输出的类
STDIO
,声明放置在文件include/stdio.h
0 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
#ifndef STDIO_H #define STDIO_H #include "os_type.h" class STDIO { private: uint8 *screen; public: STDIO(); // 初始化函数 void initialize(); // 打印字符c,颜色color到位置(x,y) void print(uint x, uint y, uint8 c, uint8 color); // 打印字符c,颜色color到光标位置 void print(uint8 c, uint8 color); // 打印字符c,颜色默认到光标位置 void print(uint8 c); // 移动光标到一维位置 void moveCursor(uint position); // 移动光标到二维位置 void moveCursor(uint x, uint y); // 获取光标位置 uint getCursor(); public: // 滚屏 void rollUp(); }; #endif
代码实现放置在
src/kernel/stdio.cpp
,此处略。 -
接下来定义中断处理函数
c_time_interrupt_handler
。由于我们需要显示中断发生的次数,我们需要在src/kernel/interrupt.cpp
中定义一个全局变量来充当计数变量,如下所示。0
int times = 0;
中断处理函数
c_time_interrupt_handler
如下所示:0 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
// 中断处理函数 extern "C" void c_time_interrupt_handler() { // 清空屏幕 for (int i = 0; i < 80; ++i) { stdio.print(0, i, ' ', 0x07); } // 输出中断发生的次数 ++times; char str[] = "interrupt happend: "; char number[10]; int temp = times; // 将数字转换为字符串表示 for(int i = 0; i < 10; ++i ) { if(temp) { number[i] = temp % 10 + '0'; } else { number[i] = '0'; } temp /= 10; } // 移动光标到(0,0)输出字符 stdio.moveCursor(0); for(int i = 0; str[i]; ++i ) { stdio.print(str[i]); } // 输出中断发生的次数 for( int i = 9; i > 0; --i ) { stdio.print(number[i]); } }
-
由于C语言缺少可以编写一个完整的中断处理函数的语法,因此当中断发生后,CPU首先跳转到汇编实现的代码,然后使用汇编代码保存寄存器的内容。保存现场后,汇编代码调用
call
指令来跳转到C语言编写的中断处理函数主体。C语言编写的函数返回后,指令的执行流程会返回到call
指令的下一条汇编代码。此时,我们使用汇编代码恢复保存的寄存器的内容,最后使用iret
返回。一个完整的时钟中断处理函数如下所示,代码保存在
asm_utils.asm
中。0 1 2 3 4 5 6 7 8 9 10 11 12
asm_time_interrupt_handler: pushad nop ; 否则断点打不上去 ; 发送EOI消息,否则下一次中断不发生 mov al, 0x20 out 0x20, al out 0xa0, al call c_time_interrupt_handler popad iret
其中,
pushad
指令是将EAX
,ECX
,EDX
,EBX
,ESP
,EBP
,ESI
,EDI
依次入栈,popad
则相反。注意,对于8259A芯片产生的中断,我们需要在中断返回前发送EOI消息。否则,8259A不会产生下一次中断。编写好了中断处理函数后,我们就可以设置时钟中断的中断描述符,也就是主片IRQ0中断对应的描述符,如下所示。
0 1 2 3
void InterruptManager::setTimeInterrupt(void *handler) { setInterruptDescriptor(IRQ0_8259A_MASTER, (uint32)handler, 0); }
-
然后封装一下开启和关闭时钟中断的函数。关于8259A上的中断开启情况,可以通过读取OCW1来得知;如果要修改8259A上的中断开启情况就需要先读取再写入对应的OCW1。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void InterruptManager::enableTimeInterrupt() { uint8 value; // 读入主片OCW asm_in_port(0x21, &value); // 开启主片时钟中断,置0开启 value = value & 0xfe; asm_out_port(0x21, value); } void InterruptManager::disableTimeInterrupt() { uint8 value; asm_in_port(0x21, &value); // 关闭时钟中断,置1关闭 value = value | 0x01; asm_out_port(0x21, value); }
-
最后在
setup_kernel
中定义STDIO
的实例stdio
,最后初始化内核的组件,然后开启时钟中断和开中断。0 1 2 3 4 5 6 7 8 9 10
extern "C" void setup_kernel() { // 中断处理部件 interruptManager.initialize(); // 屏幕IO处理部件 stdio.initialize(); interruptManager.enableTimeInterrupt(); interruptManager.setTimeInterrupt((void *)asm_time_interrupt_handler); asm_enable_interrupt(); asm_halt(); }
在
include/os_modules.h
声明这个实例。0 1 2 3 4 5 6 7 8
#ifndef OS_MODULES_H #define OS_MODULES_H #include "interrupt.h" extern InterruptManager interruptManager; extern STDIO stdio; #endif
开中断需要使用
sti
指令,如果不开中断,那么CPU不会响应可屏蔽中断。也就是说,即使8259A芯片发生了时钟中断,CPU也不会处理。开中断指令被封装在函数asm_enable_interrupt
中,如下所示:0 1 2 3
; void asm_enable_interrupt() asm_enable_interrupt: sti ret
-
现在编译运行代码:
0
make && make run
最后加载qemu运行,效果如下:
-
实现跑马灯,在中断函数添加:
0 1 2 3 4 5 6 7
// 跑马灯 for(int i = 0; i<times%40;i++) { stdio.print(' '); } for(int i = 0; str1[i]; ++i) { stdio.print(str1[i]); }
-
编译运行,效果如下:
总结
此次实验学习并实现了保护模式下的中断处理,并了解了IDT的机制和中断处理芯片8259A的使用方法。最后使用混合编程实现了时钟中断。在实验过程中体会到了保护模式下中断的精妙和混合编程的便利,收获良多。
参考
https://gitee.com/nelsoncheung/sysu-2021-spring-operating-system/tree/main
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.