代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.概述
这里我们实现调度器和任务切换,调度器的工作就是根据任务的状态将其从处理器上换上换下。
调度器主要任务就是读写就绪队列,增删里面的结点,结点是线程PCB中的general_tag,相当于线程的PCB,从队列中将其取出时一定要还原成PCB才行。
线程每次在处理器上的执行时间是由其 ticks 决定的,我们在初始化线程的时候,已经将线程 PCB 中的ticks赋值为prio,优先级越高,ticks越大。 每发生一次时钟中断,时钟中断的处理程序便将当前运行线程的ticks减1。 当ticks为0时,时钟的中断处理程序调用调度器schedule,也就是该把当前线程换下处理器了,让调度器选择另一个线程上处理器。
调度器是从就绪队列thread_ready_list中取出上处理器运行的线程,所有待执行的线程都在thread_ready_list中,就是Round-Robin Scheduling,即轮询调度,按先进先出的顺序始终调度队头的线程。就绪队列thread_ready_list中的线程都属于运行条件已具备,但还在等待被调度运行的线程,因此thread_ready_list中的线程的状态都是TASK_READY。 而当前运行线程的状态为TASK_RUNNING,它仅保存在全部队列thread_all_list当中。
调度器schedule并不仅由时钟中断处理程序来调用,它还有被其他函数调用的情况,比如后面要介绍的函数thread_block。因此,在schedule中要判断当前线程是出于什么原因才要被换下处理器的地步,就是查看线程的状态,如果线程的状态为TASK_RUNNING,这说明时间片到期了,将其ticks重新赋值为它的优先级prio,将其状态由TASK_RUNNING置为TASK_READY,并将其加入到就绪队列的末尾。如果状态为其他,这不需要任何操作,因为调度器是从就绪队列中取出下一个线程,而当前运行的线程并不在就绪队列中。
调度器按照队列先进先出的顺序,把就绪队列中的第1个结点作为下一个要运行的新线程,将该线程的状态置为TASK_RUNNING,之后通过函数switch_to将新线程的寄存器环境恢复,这样新线程便开始执行。 完整的调度过程:
- 时钟中断处理函数。
- 调度器 schedule。
- 任务切换函数switch_to。
2.注册时钟中断处理函数
对kernel/interrupt.c的全部修改代码如下:
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //目前共支持的中断数,33
#define PIC_M_CTRL 0x20 //主片的控制端口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
#define EFLAGS_IF 0x00000200 //eflags寄存器中的if=1
#define GET_FLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
//"=g" 指示编译器将结果放在任意通用寄存器中,并将其赋值给 EFLAG_VAR。
//pushfl 指令将标志寄存器 EFLAGS 的值入栈,然后 popl %0 指令将栈顶的值弹出到指定的操作数 %0 中。
//当调用 GET_FLAGS(EFLAG_VAR) 宏时,它将 EFLAGS 寄存器的值存储到 EFLAG_VAR 变量中。
/*中断门描述符结构体*/
struct gate_desc{
uint16_t func_offset_low_word; //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
uint16_t selector; //低32位——16~31位:目标CS段选择子
uint8_t dcount; //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
uint8_t attribute; //高32位——8~15位:P+DPL+S+TYPE
uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};
//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //IDT是中描述符表,实际上是中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //指针格式应该与数组类型一致,这里intr_entry_table中的元素类型就是function,就是.text的地址。所以用intr_handle来引用。声明引用定义在kernel.S中的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //idt_table为函数数组,里面保持了中断处理函数的指针
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址
//中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
/* 通用的中断处理请求 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) {
// IRQ7 IRQ15 会产生伪中断, 无需处理
// 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项
return;
}
// 将光标置为屏幕左上角, 清理一块区域
set_cursor(0); // 设置光标位置
int cursor_pos = 0;
while(cursor_pos < 320) {
// 清空四行
put_char(' ');
cursor_pos++;
}
// 将光标重新置为屏幕左上角
set_cursor(0);
put_str("!!!!! exception message begin !!!!!\n");
set_cursor(88); // 从第 2 行第 8 个字符开始打印
put_str(intr_name[vec_nr]);
if(vec_nr == 14) {
// 若为 Pagefault, 将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
// cr2 存放造成 page_fault 的地址
asm("movl %%cr2, %0" : "=r"(page_fault_vaddr));
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("\n!!!!! exception message end !!!!!\n");
// 已经进入中断处理程序就表示已经处在关中断情况下
// 不会出现线程调度的情况, 故下面的死循环不会再被中断
// 将程序悬停在此, 便于观察报错信息
while(1);
}
/*初始化idt_table*/
static void exception_init(void){
//将idt_table的元素都指向通用的中断处理函数,名称为unknown
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
idt_table[i] = general_intr_handler; //默认为general_intr_handler,以后会由register_handler来注册具体函数
intr_name[i] = "unknown";
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
//intr_name[15]是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/*完成所有有关中断的初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
exception_init();
pic_init(); //初始化8259A
/*加载idt*/
// uint64_t idt_operand = ((sizeof(idt)-1) | (uint64_t)((uint32_t)idt << 16)); //书上是错的
uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)(uint32_t)idt << 16)); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
asm volatile("lidt %0" : : "m" (idt_operand)); //加载IDT,IDT的0~15位是表界限,16~47位是表基址
put_str("idt_init done\n");
}
/*开中断,并返回开中断前的状态*/
enum intr_status intr_enable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
return old_status;
}else{
old_status = INTR_OFF;
asm volatile("sti"); //开中断
return old_status;
}
}
/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); //关中断,cli指令将IF位置0
return old_status;
}else{
old_status = INTR_OFF;
return old_status;
}
}
/*将中断状态设置为status*/
enum intr_status intr_set_status(enum intr_status status){
return status & INTR_ON ? intr_enable() : intr_disable();
}
/*获取当前中断状态*/
enum intr_status intr_get_status(){
uint32_t eflags = 0;
GET_FLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
/*在中断处理程序数组第vector_no个元素中注册安装中断处理程序function*/
void register_handler(uint8_t vector_no,intr_handler function){
idt_table[vector_no] = function; //idt_table数组中的函数是在进入中断后根据中断向量号调用的
}中断处理函数一定得在中断描述符表中,与此中断向量对应的中断描述符里提前注册好才能用。我们的中断处理逻辑是由kernel.S 提供统一的中断入口,即中断向量0~30全是用统一的中断处理程序“模板”,在该模板中通过中断向量号调用中断处理程序数组idt_table中的C版本的处理程序,也就是文件kernel.S中代码call [idt_table + %1*4]的作用。因此,为设备注册中断处理程序的工作变得很简单,我们不用去修改中断描述符,直接把中断向量作为数组下标,去修改idttable[中断向量]数组元素即可。
某个中断源没有中断处理程序时,general_intr_handler作为默认的中断处理函数,上面的第一个修改就在general_intr_handler函数。
/* 通用的中断处理请求 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) {
// IRQ7 IRQ15 会产生伪中断, 无需处理
// 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项
return;
}
// 将光标置为屏幕左上角, 清理一块区域
set_cursor(0); // 设置光标位置
int cursor_pos = 0;
while(cursor_pos < 320) {
// 清空四行
put_char(' ');
cursor_pos++;
}
// 将光标重新置为屏幕左上角
set_cursor(0);
put_str("!!!!! exception message begin !!!!!\n");
set_cursor(88); // 从第 2 行第 8 个字符开始打印
put_str(intr_name[vec_nr]);
if(vec_nr == 14) {
// 若为 Pagefault, 将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
// cr2 存放造成 page_fault 的地址
asm("movl %%cr2, %0" : "=r"(page_fault_vaddr));
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("\n!!!!! exception message end !!!!!\n");
// 已经进入中断处理程序就表示已经处在关中断情况下
// 不会出现线程调度的情况, 故下面的死循环不会再被中断
// 将程序悬停在此, 便于观察报错信息
while(1);
}函数set_cursor,它接受一个参数,就是光标值(光标值范围是0~1999)。它的功能就是设置光标的值,其函数实现就是文件print.S中函数put_char的.set_cursor部分。
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
section .data
put_int_buffer dq 0 ;定义8字节缓冲区用于数字到字符的转换
[bits 32]
section .text
;------------------------------------------
;put_str通过put_char来打印以0字符结尾的字符串
;------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无
global put_str
put_str:
;由于函数只用到了ebx和ecx两个寄存器,所以只备份这两个
push ebx
push ecx
xor ecx,ecx ;准备用ecx存储参数,清空
mov ebx,[esp+12] ;从栈中得到待打印的字符串地址(传入的参数)
.goon:
mov cl,[ebx]
cmp cl,0 ;如果处理到了字符串尾,则跳到结束时返回
jz .str_over
push ecx ;为put_char传递参数,把ecx的值入栈
call put_char ;call时会把返回地址入栈4
add esp,4 ;回收参数的栈空间
inc ebx ;使ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
;-----------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;----------------------------------
global put_char ;将put_char导出为全局符号
put_char:
pushad ;备份32位寄存器环境,push all double,将8个32位寄存器都备份了。它们入栈的顺序为:EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI
mov ax,SELECTOR_VIDEO ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印都为gs赋值
mov gs,ax ;不能直接把立即数送入段寄存器
;;;;;获取当前光标的位置;;;;;
;先获取高8位
mov dx,0x03d4 ;索引寄存器,03d4为Address Register,用于索引寄存器。
mov al,0x0e ;用于提供光标位置的高8位
out dx,al
mov dx,0x03d5 ;03d5是Data Register;可以写数据和读数据。通过读写数据端口0x3d5来获取/设置光标的位置
in al,dx ;得到光标位置的高8位
mov ah,al ;将得到的光标高8位放入ah中
;再获取低8位
mov dx,0x03d4
mov al,0x0f ;用于提供光标位置的低8位
out dx,al
mov dx,0x03d5
in al,dx
;将光标位置存入bx,bx寄存器习惯性作为基址寻址。此时bx是下一个字符的输出位置。
mov bx,ax
;获取栈中压入字符的ASCII码
mov ecx,[esp + 36] ;pushad压入8*32b=32字节,加上主调函数4B的返回地址。故栈顶偏移36字节。
;判断字符是什么类型
cmp cl,0xd ;CR是0x0d,回车键
jz .is_carriage_return
cmp cl,0xa ;LF是0x0a,换行符
jz .is_line_feed
cmp cl,0x8 ;BS是0x08,退格键
jz .is_backspace
jmp .put_other
.is_backspace: ;理论上将光标移到该字符前即可,但怕下个字符为回车等,原字符还留着当地,所以用空格/空字符0替代原字符
dec bx ;bx值-1,光标指向前一个字符
shl bx,1 ;左移一位等于乘2,表示光标对应显存中的偏移字节
mov byte [gs:bx],0x20 ;0x20表示空格
inc bx ;bx+1
mov byte [gs:bx],0x07 ;0x07表示黑屏白字,这是显卡默认的前景色和背景色,不加也行。
shr bx,1 ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
jmp .set_cursor ;设置光标位置
.put_other: ;处理可见字符
shl bx,1 ;光标左移1位等于乘2,表示光标位置
mov [gs:bx],cl ;将ASCII字符放入光标位置中
inc bx ;bx+1
mov byte [gs:bx],0x07 ;字符属性,黑底白字
shr bx,1 ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
inc bx ;bx+1,下一个光标值
cmp bx,2000 ;看是否需要滚屏
jl .set_cursor ;"JL"是"jump if less"(如果小于则跳转):若光标值<=2000,表示未写到。显存的最后,则去设置新的光标值,若超过屏幕字符数大小(2000),则换行(滚屏)。
.is_line_feed: ;是换行符LF(\n)
.is_carriage_return: ;是回车键CR(\r),\n和\r在Linux中都是\n的意思。
xor dx,dx ;dx是被除数的高16位,清零
mov ax,bx ;ax是被被除数的低16位,bx是光标位置
mov si,80 ;si = 80为除数
div si ;对80取模,(dx + ax)/si = ax(商) + dx(余数) 即bx/80=几行(ax) + 第几列(dx)
;如果除数是16位,被除数就是32位,位于dx和ax(高16位,低16位)中;结果的商放在ax中,余数放入dx中
sub bx,dx ;bx-dx表示将bx放在行首,实现了回车的功能。
.is_carriage_return_end: ;回车符处理结束,判断是否需要滚屏
add bx,80
cmp bx,2000
.is_line_feed_end: ;若是LF,则光标移+80即可
jl .set_cursor
.roll_screen: ;若超过屏幕大小,开始滚屏:屏幕范围是0~23,滚屏原理是把1~24->0~23,再将24行用空格填充
cld
mov ecx,960 ;2000-80=1920个字符,共1920*2=3840字节,一次搬运4字节,一共要搬运3840/4=960次
mov esi,0xc00b_80a0 ;第1行行首,源索引地址寄存器
mov edi,0xc00b_8000 ;第0行行首,目的索引地址寄存器
rep movsd ;repeat move string doubleword,以32b为单位进行移动,直到ecx=0
;将最后一行填充为空白
mov ebx,3840 ;最后一行从3840开始
mov ecx,80 ;一行80字符,每次清空1字符(2B),一行要移动80次
.cls:
mov word [gs:ebx],0x0720 ;0x0720是黑底白字的空格键,一次清空一个字符(2B)
add ebx,2 ;ebx移动到下一个字符处
loop .cls ;循环.cls,直到ecx=0
mov bx,1920 ;bx存放下一个字符的光标位置,即3840/2=1920
.set_cursor: ;将光标设置为bx值
;先设置高8位
mov dx,0x03d4 ;索引寄存器,通过0x3d4写入待操作寄存器的索引
mov al,0x0e ;用于提供光标的高8位
out dx,al
mov dx,0x03d5 ;通过数据端口0x3d5来设置光标位置
mov al,bh ;将bx的光标位置的高8位放入al中,通过al输入到dx = 0x3d5端口
out dx,al ;[0x3d5端口] = bx高8位 = bh
;再设置低8位
mov dx,0x03d4
mov al,0x0f ;用于提供光标的低8位
out dx,al
mov dx,0x03d5 ;通过数据端口0x3d5来设置光标位置
mov al,bl ;将bx的光标位置的低8位放入al中,通过al输入到dx = 0x3d5端口
out dx,al ;[0x3d5端口] = bx低8位 = bl
.put_char_done:
popad ;将之前入栈的8个32b的寄存器出栈
ret
;------------将小端字节序的数字变成对应的ASCII码后,倒置--------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x
;------------------------------------------------------------------
global put_int
put_int:
pushad
mov ebp,esp
mov eax,[ebp+4*9] ;将参数写入eax中,call返回地址占4B+pushad的8个4B
mov edx,eax ;eax存储的是参数的备份,edx为每次参与位变换的参数,当转换为16进制数字后,eax将下一个参数给edx
mov edi,7 ;指定在put_int_buffer中初始的偏移量,表示指向缓冲区的最后一个字节
mov ecx,8 ;32位数字中,每4位表示一个16进制数字。所以32位可以表示8个16进制数字,位数为8。
mov ebx,put_int_buffer ;ebx为缓冲区的基址
;将32位数字按照16进制的形式从低到高逐个处理,共处理8个16进制数字
.16based_4bits:
;将32位数字按照16进制形式从低到高逐字处理
and edx,0x0000000F ;解析16进制数字的每一位,and后edx只有低4位有效(最低位的16进制数字)
cmp edx,9 ;数字0~9和a~f需要分别处理成对应的字符
jg .is_A2F ;jg:Jump if Greater 若大于9,则跳转.is_A2F
add edx,'0' ;如果是0~9,则加上'0'的ASCII码
jmp .store
.is_A2F:
sub edx,10 ;A~F减去10所得的差,10的ASCII码为1
add edx,'A' ;加上10的ASCII码得到字符的ASCII码
;将每个数字转换成对应的字符后,按照类似大端的顺序存储到缓冲区put_int_buffer中。
;高位字符放在低地址,低位字符放在高地址,这样和大端字符序类似。
.store:
;此时dl中应该是对应数字的ASCII码
mov [ebx+edi],dl
dec edi
shr eax,4 ;右移4位,去掉最低4位
mov edx,eax
loop .16based_4bits
;现在把put_int_buffer中已全是字符,打印之前把高位连续的字符去掉。
;例如:000123 -> 123
.ready_to_print:
inc edi ;此时edi为-1(0xffff_ffff),加1使其为0
.skip_prefix_0:
cmp edi,8 ;若以及比较到第9个字符,表示待打印的字符都是0
je .full0 ;Jump if Equal
;找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
mov cl,[put_int_buffer+edi]
inc edi
cmp cl,'0' ;判断下一位字符是否为0
je .skip_prefix_0
dec edi ;若当前字符不为'0',则使edi减1恢复当前字符
jmp .put_each_num ;若下一位不为0,则从这一位开始遍历
.full0:
mov cl,'0' ;当输入字符都是0时,只打印0
.put_each_num:
push ecx ;此时ecx中为可打印字符,作为参数传递入put_char中
call put_char
add esp,4 ;覆盖掉ecx,清理栈参数,相当于pop ecx
inc edi ;使edi指向下个字符
mov cl,[put_int_buffer+edi] ;将下个字符放入cl中
cmp edi,8
jl .put_each_num
popad
ret
; 对应函数 void set_cursor(uint32_t cursor_pos);
global set_cursor
set_cursor:
pushad
mov bx, [esp + 36]
; 1. 先设置高8位
mov dx, 0x03d4 ; 索引寄存器
mov al, 0x0e ; 光标高8位
out dx, al
mov dx, 0x03d5 ; 通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
; 2. 再设置低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret
print.S的变化就是添加了最下面的set_cursor函数 对应在print.h加入set_cursor函数。
#ifndef __LIB_KERNEL_PRINT_H //防止头文件被重复包含 #define __LIB_KERNEL_PRINT_H //以print.h所在路径定义了这个宏,以该宏来判断是否重复包含 #include "stdint.h" void put_char(uint8_t char_asci); void put_str(char* message); void put_int(uint32_t num); //以16进制打印 void set_cursor(uint32_t cursor_pos); #endif
回到/kernel/interrupt.c的general_intr_handler函数
/* 通用的中断处理请求 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) {
// IRQ7 IRQ15 会产生伪中断, 无需处理
// 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项
return;
}
// 将光标置为屏幕左上角, 清理一块区域
set_cursor(0); // 设置光标位置
int cursor_pos = 0;
while(cursor_pos < 320) {
// 清空四行
put_char(' ');
cursor_pos++;
}
// 将光标重新置为屏幕左上角
set_cursor(0);
put_str("!!!!! exception message begin !!!!!\n");
set_cursor(88); // 从第 2 行第 8 个字符开始打印
put_str(intr_name[vec_nr]);
if(vec_nr == 14) {
// 若为 Pagefault, 将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
// cr2 存放造成 page_fault 的地址
asm("movl %%cr2, %0" : "=r"(page_fault_vaddr));
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("\n!!!!! exception message end !!!!!\n");
// 已经进入中断处理程序就表示已经处在关中断情况下
// 不会出现线程调度的情况, 故下面的死循环不会再被中断
// 将程序悬停在此, 便于观察报错信息
while(1);
}有时候的异常是由光标错误值引发的,因此必须在异常中将光标位置纠正,否则在通过put_str输出报错信息的时候,错误的光标值将再次导致异常,可能会造成异常的死循环。程序运行时最后输出的有用信息一般都在屏幕最下方,最好不占用下方的屏幕,而是将异常信息输出在屏幕左上角,因此先调用“set_cursor(0)”将光标置为 0。
Pagefault就是通常所说的缺页异常,它表示虚拟地址对应的物理地址不存在,也就是虚拟地址尚未在页表中分配物理页,这样会导致Pagefault 异常。导致Pagefault的虚拟地址会被存放到控制寄存器CR2中,我们加入的内联汇编代码就是让Pagefault发生时,将寄存器cr2中的值转储到整型变量page_fault_vaddr中,并通过put_str函数打印出来。
因此,如果程序运行过程中出现异常 Pagefault 时,将会打印出导致 Pagefault 出现的虚拟地址。以后各设备都会注册自己的中断处理程序,不会再使用general_intr_handler。我们没有为各种异常注册相应的中断处理程序,这里是用general_intr_handler作为通用的中断处理程序来处理异常的,因此只要执行到 general_intr_handler 中就表示出了某些异常。处理器进入中断后会自动把标志寄存器eflags中的IF位置0,即中断处理程序在关中断的情况下运行。
对应/kernel/interrupt.h
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
/*定义中断的两种状态:
INTR_OFF值为0,表示关中断
INTR_ON 值为1,表示开中断*/
enum intr_status{
INTR_OFF = 0,
INTR_ON = 1
};
//枚举常量在 C 语言中被赋予默认的整数值,按照声明的顺序从 0 开始递增。因此,在这个例子中,INTR_OFF 的值为 0,INTR_ON 的值为 1。也可以显式地为枚举常量指定特定的值。
/*完成所有有关中断的初始化工作*/
void idt_init(void);
/*开中断,并返回开中断前的状态*/
enum intr_status intr_enable(void);
/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(void);
/*将中断状态设置为status*/
enum intr_status intr_set_status(enum intr_status status);
/*获取当前中断状态*/
enum intr_status intr_get_status(void);
/*在中断处理程序数组第vector_no个元素中注册安装中断处理程序function*/
void register_handler(uint8_t vector_no,intr_handler function);
#endif接下来就是注册中断处理函数的真正逻辑了 修改/device/timer.c
#ifndef __DEVICE_TIMER_H #define __DEVICE_TIMER_H #include "stdint.h" void timer_init(void); #endif
修改/device/timer.c
#include "timer.h"
#include "io.h"
#include "print.h"
#include "thread.h"
#include "debug.h"
#include "interrupt.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY/IRQ0_FREQUENCY
#define COUNTER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
uint32_t ticks; //ticks是内核自中断开启以来总共的嘀咕数
/*把操作的计数器counter_no、读写锁属性rw1、计数器模式counter_mode写入模式控制寄存器中并赋予初始值counter_value*/
static void frequency_set(uint8_t coutner_port,uint8_t counter_no,uint8_t rw1,uint8_t counter_mode,uint16_t counter_value){
/*向控制字寄存器端口0x43写入控制字*/
outb(PIT_CONTROL_PORT,(uint8_t)(counter_no << 6 | rw1 << 4 | counter_mode << 1));
/*先写入counter_value的低8位*/
outb(coutner_port,(uint8_t)counter_value);
/*再写入counter_value的高8位*/
outb(coutner_port,(uint8_t)counter_value >> 8);
}
/*时钟的中断处理函数*/
static void intr_timer_handler(void){
struct task_struct* cur_thread = running_thread();
ASSERT(cur_thread->stack_magic == 0x19870916); //检测是否溢出
cur_thread->elapsed_ticks++;
ticks++;
if(cur_thread->ticks == 0) //若时间片用完,就开始调度新进程上CPU
schedule();
else
cur_thread->ticks--; //将当前进程的时间片--
}
/*初始化PIT8253*/
void timer_init(){
put_str("timer_init start\n");
/*设置8253的定时周期,也就是发中断的周期*/
frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH,COUNTER_MODE,COUNTER0_VALUE);
register_handler(0x20,intr_timer_handler);
put_str("timer_init done\n");
}开头定义了ticks,它用来保存系统自开中断以来所运行的嘀嗒数,类似于系统运行时长的概念。
uint32_t ticks; //ticks是内核自中断开启以来总共的嘀咕数
时钟处理函数intr_timer_handler,先通过”running_thread()”获取当前正在运行的线程, 将其赋值给PCB指针cur_thread,然后通过ASSERT来判断stack_magic是否等于0x19870916,也就是检查栈是否溢出,破坏了线程信息。正常情况下不会出现栈溢出,因此只要ASSERT报警就表示哪里出了问题,不用处理了。
“cur_thread->elapsed_ticks++”,将线程总执行的时间加 1。后面的ticks++将系统运行时间加1,实际上这就是中断发生的次数。每个线程在处理器上运行期间都会有很多次时钟中断发生,每次中断处理程序都会将线程的时间片ticks 减 1。
if(cur_thread->ticks == 0) 判断当前线程的时间片ticks是否用完了,如果ticks等于0,说明当前线程cur_thread时间片耗尽,该下处理器了,此时调用schedule函数。否则将当前线程的时间片ticks减1。之后退出中断处理程序,也就是退出中断,让当前线程cur_thread继续执行。
在下面的timer_init中,我们加入了注册时钟中断处理程序的代码,即”register_handler(0x20, intr_timerhandler)”,timer_init 是由 init_all 调用的,它在内核运行开始处执行的,故,时钟中断会被提前注册好。 register_handler的实现在上面的/kernel/interrupt.h
/*在中断处理程序数组第vector_no个元素中注册安装中断处理程序function*/
void register_handler(uint8_t vector_no,intr_handler function){
idt_table[vector_no] = function; //idt_table数组中的函数是在进入中断后根据中断向量号调用的
}register_handler接受两个参数,vector_no是中断向量号,function是中断处理程序。功能是在中断处理程序数组第vector_no个元素中注册安装中断处理程序function。
3.实现调度器schedule
逻辑在/thread/thread.c,对其修改如下:
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "interrupt.h"
#include "list.h"
#include "debug.h"
#include "print.h"
#define PG_SIZE 4096
struct task_struct* main_thread; //主线程PCB
struct list thread_ready_list; //就绪队列
struct list thread_all_list; //所有任务队列
static struct list_elem* thread_tag; //用于保存队列中的线程结点
extern void switch_to(struct task_struct* cur,struct task_struct* next);
/*获取当前线程的pcb指针*/
struct task_struct* running_thread(void){
uint32_t esp;
asm("mov %%esp,%0":"=g"(esp)); //将当前的栈指针(ESP)的值存储到变量esp中。
return (struct task_struct*)(esp & 0xfffff000); //取esp整数部分,即pcb起始地址
}
/*由kernel_thread去执行function(func_arg)*/
static void kernel_thread(thread_func* funcion,void* func_arg){
intr_enable(); //开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程
funcion(func_arg);
}
/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg){
/*先预留中断使用栈的空间*/
pthread->self_kstack -= sizeof(struct intr_stack);
/*再预留出线程栈空间*/
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //线程栈指针
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0; //寄存器初始化为0
}
/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio){
memset(pthread,0,sizeof(*pthread));
strcpy(pthread->name,name);
if(pthread == main_thread)
pthread->status = TASK_RUNNING;
else
pthread->status = TASK_READY;
/*self_kstack是线程自己在内核态下使用的栈顶地址*/
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio;
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
pthread->stack_magic = 0x19870916;
}
/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg){
/*pcb都位于内核空间,包括用户进程的pcb也在内核空间*/
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,prio);
thread_create(thread,function,func_arg);
//确保之前不在就绪队列中
ASSERT(!elem_find(&thread_ready_list,&thread->general_tag));
//加入就绪队列
list_append(&thread_ready_list,&thread->general_tag);
//确保之前不在队列中
ASSERT(!elem_find(&thread_all_list,&thread->all_list_tag));
//加入全部线程队列
list_append(&thread_all_list,&thread->all_list_tag);
// asm volatile("movl %0,%%esp; \
// pop %%ebp;pop %%ebx;pop %%edi;pop %%esi; \
// ret": : "g"(thread->self_kstack) : "memory");
// //esp = thread->self_kstack为栈顶;4个出栈,将初始化的0弹出到相应寄存器中;执行ret,将栈顶数据作为返回地址送入CPU的eip中;thread->self_kstack为输入
return thread;
}
/*将kernel中的main函数完善为主线程*/
static void make_main_thread(void){
/*因为main线程早已在运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,就是为其预留pcb的。
因为pcb的地址为0xc009e000,不需要通过get_kernel_page另外分配一页*/
main_thread = running_thread();
init_thread(main_thread,"main",31);
/*main函数是当前线程,当前线程不在thread_ready_list中,所以只将其加在thread_all_list中*/
ASSERT(!elem_find(&thread_all_list,&main_thread->all_list_tag));
list_append(&thread_all_list,&main_thread->all_list_tag);
}
/*实现任务调度*/
void schedule(){
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if(cur->status == TASK_RUNNING){
//若此线程只是cpu时间片到了,将其加入就绪队列队尾
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag));
list_append(&thread_ready_list,&cur->general_tag);
cur->ticks = cur->priority;
cur->status = TASK_READY;
}else{
//若此线程需要某事件发生后才能继续上cpu运行,不需要将其放入队列中,因为当前线程不在就绪队列中
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL;
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct,general_tag,thread_tag); //将thread_tag转化为线程(链表)
next->status = TASK_RUNNING;
switch_to(cur,next);
}
/*初始化线程环境*/
void thread_init(void){
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
/*将当前main函数创建为线程*/
make_main_thread();
put_str("thread_init done\n");
}/thread/thread.h添加上新定义地函数
/*实现任务调度*/ void schedule(void); /*初始化线程环境*/ void thread_init(void);
调度器的逻辑在schedule函数,它的功能是将当前线程换下处理器,并在就绪队列中找出下个可运行的程序,将其换上处理器。
schedule主要内容就是读写就绪队列,因此它不需要参数。先通过running_thread()获取了当前运行线程的PCB,将其存入PCB指针cur中(代表cur_thread),接下来的判断都是基于 cur 的,PCB 对任务来说确实是非常重要的身份证。
如果当前线程cur的时间片到期了,就将其通过list_append函数重新加入到就绪队列thread_ready_list。由于此时它的时间片ticks已经为0,将ticks的值再次赋值为它的优先级prio,最后将cur的状态status置 为 TASK_READY。
如果当前线程cur并不是因为时间片到期而被换下处理器,肯定是由于某种原因被阻塞了,这时候不需要处理就绪队列,因为当前运行线程并不在就绪队列中。
下面来看当前运行的线程是如何从就绪队列中“出队”的。我们尚未实现idle线程,因此有可能就绪队列为空,为避免这种无线程可调度的情况,暂时用”ASSERT(!list_empty(&thread_ready_list))”来保障。下面是将thread_tag置为NULL,由于它是全局变量,避免在下面赋值失败时排查起来困难。接下来通过”thread_tag=list_pop(&thread_ready_list)”从就绪队列中弹出一个可用线程并存入thread_tag,thread_tag并不是线程,它仅仅是线程PCB中的general_tag或all_list_tag,要获得线程的信息,必须将其转换成PCB才行,因此我们用到了宏elem2entry。
elem2entry 之前我们定义在 list.h 中。
// 获取 member 在 struct_type结构体中的偏移量
#define offset(struct_type, member) (int) (&((struct_type*) 0)->member)
// 获取 pcb 首地址, 用 mem 当前地址 - mem偏移量, 然后强制类型转换
#define elem2entry(struct_type, struct_mem_name, elem_ptr) \
(struct_type*) ((int) elem_ptr - offset(struct_type, struct_mem_name))参数elem_ptr是待转换的地址,它属于某个结构体中某个成员的地址,参数struct_member_name是elem_ptr 所在结构体中对应地址的成员名字,也就是说参数struct_member_name是个字符串,参数struct_type是elem_ptr 所属的结构体的类型。
宏elem2entry的作用是将指针elem_ptr转换成struct_type类型的指针,其原理是用 elem_ptr的地址减去elem_ptr在结构体struct_type中的偏移量,此地址差便是结构体struct_type的起始地址,最后再将此地址差转换为struct type指针类型。
通过elem2entry获得了新线程的PCB地址,将其赋值给next,紧接着通过”next-> status=TASK_RUNNING”将新线程的状态置为TASK_RUNNING,这表示新线程next可以上处理器了. 调用switch_to函数,准备切换寄存器映像,意为将线程cur的上下文保护好,再将线程next的上下文装载到处理器,从而完成了任务切换。
4.实现任务切换函数
完整的程序就也因此分为两部分,一部分是做重要工作的内核级代码,另一部分就是做普通工作的用户级代码。
完整的程序 = 用户代码 + 内核代码。完整地程序就是任务,也就是线程和进程。
任务在执行的过程中会执行用户代码和内核代码,当CPU处于低特权级下执行用户代码时我们称为用户态;当CPU进入高特权级执行内核代码时,我们称为内核态。 当CPU从用户代码所在的低特权级->内核代码所在的高特权级时,这称为陷入内核。
无论是执行用户代码还是内核代码,这些代码都属于一个完整的程序,并不是说当前任务由用户态进入内核态后当前任务就切换成内核了。 在咱们的系统中,任务调度是由时钟中断发起,由中断处理程序调用switch_to函数实现的。假设当前任务在中断发生前所处的执行流属于第一层,受时钟中断的影响,处理器会进入中断处理程序,这使当前的任务执行流被第一次改变,因此在进入中断时,我们要保护好第一层的上下文,即中断前的任务状态。之后在内核中执行中断处理程序,这属于第二层执行流。当中断处理程序调用任务切换函数switch_to时,当前的中断处理程序又要被中断,因此要保护好第二层的上下文,即中断处理过程中的任务状态。
第一部分是进入中断时的保护,这保存的是任务的全部寄存器映像,也就是进入中断前任务所属第一层的状态,这些寄存器映像相当于任务中用户代码的上下文。
这些寄存器是由kernel.S中定义的中断处理入口程序intr%lentry来保护的,里面是一些push寄存器的指令,这是由汇编下的宏”%macro VECTOR 2″定义的,因此前面在介绍注册中断处理程序时曾称之为中断处理程序模板。
当把这些寄存器映像恢复到处理器中后,任务便完全退出中断,继续执行自己的代码部分。换句话说,当恢复寄存器后,如果此任务是用户进程,任务就完全恢复为用户程序继续在用户态下执行,如果此任务是内核线程,任务就完全恢复为另一段被中断执行的内核代码,依然是在内核态下运行。
第二部分是保护内核环境上下文,根据ABI,除esp外,只保护esi、 edi、ebx和ebp这4个寄存器就够了。
这4个寄存器映像相当于任务中的内核代码的上下文,也就是第二层执行流,此部分只负责恢复第二层的执行流,即恢复为在内核的中断处理程序中继续执行的状态。
当任务开始执行内核代码后,任务在内核代码中的执行路径由这 4个寄存器决定,将来恢复这 4 个寄存器,也只是让处理器继续执行任务中的内核代码,并不是让任务恢复到中断前,依然还是在内核中。
这几个寄存器的值会让处理器把程序执行到内核代码的结束处,在那里可以用第一部分中保护的全部寄存器映像来恢复任务,从而退出中断,使任务彻底恢复为进入中断前的状态。另外,其实这4个寄存器主要是用来恢复主调函数的环境,只是当前我们在讨论内核函数。中断发生时,当前运行的任务(线程或用户进程)被打断,随后会去执行中断处理程序,不管当前任务在中断前的特权级是什么,执行中断处理程序时肯定都是0特权级。
我们要完成的任务:
- 上下文保护的第一部分负责保存任务进入中断前的全部寄存器,目的是能让任务恢复到中断前。
- 上下文保护的第二部分负责保存这4个寄存器: esi,edi,ebx和ebp,目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码,保证顺利走到退出中断的出口,利用第一部分保护的寄存器环境彻底恢复任务。
任务上下文保护的第一部分已经在kernel.S 中由intr%1entry完成。
第二部分如下: /thread/switch.S
[bits 32]
section .text
global switch_to
switch_to:
;栈中此时是返回地址
push esi
push edi
push ebx
push ebp
mov eax,[esp + 20] ;得到栈中的参数cur,cur=[esp+20]
mov [eax],esp ;保存栈顶指针esp,task_struct的self_kstack字段,self_kstack在task_struct中的偏移量为0
;------------- 以上是备份当前线程的环境,下面是恢复下一个线程的环境 -----------------------
mov eax,[esp + 24] ;得到栈中参数next,next = [esp + 24]
mov esp,[eax] ;pcb的第一个成员是self_kstack成员,它用来记录0级栈,0级栈中保存了进程/线程所有的信息,包括3级指针
pop ebp
pop ebx
pop edi
pop esi
ret ;返回到上面switch_to下面的那句注释的返回地址;如果未由中断进入,第一次执行时会返回kernel_thread函数switch_to接受两个参数,第1个参数是当前线程cur,第2个参数是下一个上处理器的线程,此函数的功能是保存cur线程的寄存器映像,将下一个线程next的寄存器映像装载到处理器。
global switch_to将函数switch_to导出为全局符号,这样thread.c中的schedule便能够使用它了。 接下来遵循ABI原则,保护好esi、 edi、ebx、 ebp寄存器。栈是自高地址向低地址扩展的,因此这4个push操作步骤与线程栈 struct thread_stack 的结构是逆序的。

最下面的4个寄存器进入switch_to时压入的,我们看cur和next的位置。
在 PCB 结构 struct task_struct 中,第一个成员是 self_kstack,它用来记录每个线程自己的栈顶指针。任务在内核中的寄存器映像是保存在栈中的,这正是进入switch_to函数时立即把那4个寄存器入栈的原因。任务在下次被调度运行时,还得把寄存器映像从栈中恢复,因此,为了恢复寄存器映像,先得知道寄存器映像被保存在哪个栈中,也就是咱们得在切换前把当前的栈指针保存在某个地方,下次再被调度上处理器前,再从相同的地方恢复栈指针,将栈中的寄存器映像重新装载到处理器。这个地方就选PCB中的成员self_kstack。
self_kstack在PCB中的偏移量为0,因此在后面可以直接用PCB的地址作为保存esp。在switch_to中self_kstack已被固定引用为偏移PCB 0字节的地方,因此必须要把self_kstack放在PCB的起始处,即task_struct的开头。
“mov eax, [esp + 20]”,其中”[esp+ 20]”是为了获取栈中cur的值,也就是当前线程的PCB地址,再将它mov到寄存器eax中,因为self_kstack在PCB中偏移为0,所以此时eax可以认为是当前线程PCB中self_kstack的地址。
mov [eax], esp将当前栈顶指针esp保存到当前线程PCB中的self_kstack成员中。当前线程的上下文环境算是保存完毕。
下面要准备往处理器上装载新线程的上下文啦。
“mov eax, [esp + 24]”,其中”[esp +24]”是为了获取栈中的next的值,也就是next线程的PCB地址,之后将它mov到寄存器eax,同样此时eax可以认为是next线程PCB中self_kstack 的地址。因此,”[eax]”中保存的是next线程的栈指针。
“mov esp, [eax]”是将next线程的栈指针恢复到esp中,经过这一步后便找到了next 线程的栈,从而可以从栈中恢复之前保存过的寄存器映像。
接下来按照寄存器保存的逆顺序,依次从栈中弹出。
不要误以为此时恢复的寄存器映像是在上面刚刚保存过的那些寄存器。在同一次switch to的调用执行中,之前保存的寄存器属于当前线程cur,之后恢复的寄存器映像属于下一个上处理器运行的线程next,这些被恢复的寄存器映像是在之前某次执行switch to时,由现在的这个next线程作为那时候的当前线程cur,被换下处理器前保存的。
ret便将当前栈顶处的值作为返回地址加载到处理器的eip寄存器中,从而使next线程的代码恢复执行。如果此时的next线程之前尚未执行过,马上开始的是第一次执行,此时栈顶的值是函数kernel_thread 的地址,这是由thread_create函数设置的,执行ret指令后处理器将去执行函数kernel_thread,如果next 之前已经执行过了,这次是再次将其调度到处理器的话,此时栈顶的值是由调用函数switch_to的主调函数schedule留下的,这会继续执行schedule后面的流程。
而switch_to是schedule最后一句代码,因此执行流程马上回到schedule的调用者intr_timer_handler中。schedule同样也是intr_timer_handler中最后一句 代码,因此会完成intr_timer_handler,回到kernel.S中的jmp intr_exit,从而恢复任务的全部寄存器e像,之后通过iretd指令退出中断,任务被完全彻底地恢复。
5.启用线程调度
thread.c加入线程相关信息的初始化。代码上面已经给出,在thread_init函数
/*初始化线程环境*/
void thread_init(void){
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
/*将当前main函数创建为线程*/
make_main_thread();
put_str("thread_init done\n");
}然后把 thread_init 加入到 init.c 中
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
/*负责初始化所有模块*/
void init_all(){
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init(); //初始化PIT
}main方法调用
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void){
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a",31,k_thread_a,"argA ");
thread_start("k_thread_b",8,k_thread_b,"argB ");
intr_enable(); //打开中断,使时钟中断起作用
while(1){
put_str("Main ");
}
return 0;
}
/*在线程中运行函数*/
void k_thread_a(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
put_str(para);
}
}
/*在线程中运行函数*/
void k_thread_b(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
put_str(para);
}
}
6.执行结果
makefile文件如下
BUILD_DIR = ./build
##用来存储生成的所有目标文件
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
##-fno-builtin是告诉编译器不要采用内部函数 -Wstrict-prototypes是要求函数声明中必须有参数类型
## -Wmissing-prototypes要求函数必须有声明
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \
$(BUILD_DIR)/switch.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序
########## c代码编译 ##########
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/stdint.h \
kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o:device/timer.c device/timer.h lib/stdint.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/debug.h kernel/interrupt.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
kernel/interrupt.h lib/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
###########汇编代码编译############
$(BUILD_DIR)/kernel.o:kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o:lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/switch.o: thread/switch.S
$(AS) $(ASFLAGS) $< -o $@
##########链接所有目标文件#############
$(BUILD_DIR)/kernel.bin:$(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY: mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
###fi为终止符
hd:
dd if=$(BUILD_DIR)/kernel.bin of=/bochs/bin/dreams.img bs=512 count=200 seek=9 conv=notrunc
clean: ##将build目录下文件清空
cd $(BUILD_DIR) && rm -f ./*
build:$(BUILD_DIR)/kernel.bin ##编译kernel.bin,只要执行make build就是编译文件
all:mk_dir build hd
##依次执行伪目标mk_dir build hd,只要执行make all就是完成了编译到写入硬盘的全过程结果如下:

这里出现了问题,原因是临界区代码的资源竞争,这里后面我们通过锁解决。
7.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


