一、同步机制——锁
1. 判断GP中断问题
- 原因:因为put_str分了几步走,没有完全执行完进程就结束了,所以出现GP中断问题。
- “/home/lily/OS/boot/kernel/main.c”
#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){
intr_disable(); //关中断
put_str("Main ");
intr_enable(); //开中断
}
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);
}
}
2. 临界区、互斥、竞争条件与信号量
名称 | 含义 |
---|---|
公共资源 | 被所有任务共享的一套资源,可以是公共内存、公共文件、公共硬件等 |
临界区 | 程序中访问公共资源的指令代码 |
互斥 | 某一时刻公共资源只能被一个任务独享,不允许多个任务同时出现在自己的临界区中 |
竞争条件 | 多个任务以非互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行进行的,公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序 |
- 信号量
- 定义:信号量是个计算器,用来记录所积累信号的数量。
- P操作:
- 判断信号量是否>0
- 若信号量>0,则信号量-1
- 若信号线<0,则当前线程将自己阻塞,以在此信号量上等待
- V操作:
- 将信号量+1
- 唤醒在此信号量上等待的线程
3. 线程的阻塞与唤醒
- 阻塞:阻塞是线程自己发出的动作,是线程自己阻塞自己。
- 唤醒:
- 已阻塞的线程是由别人来唤醒的,唤醒是被动的。
- 线程阻塞是线程执行时的“动作”,因此线程的时间片还没用完。在唤醒后,线程会继续在剩余的时间片内运行。
-----------------------代码----------------------------
- “/home/lily/OS/boot/thread/thread.c”
// 新增代码
/*当前线程将自己阻塞,标志其状态为stat*/
void thread_block(enum task_status stat){
/*stat取值为TASK_BLOCKED,TASK_WAITING,TASK_HANGING,只有这三种状态才不会被调度*/
ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
enum intr_status old_status = intr_disable();
struct task_struct* cur_thread = running_thread();
cur_thread->status = stat;
schedule(); //将当前线程换下CPU
intr_set_status(old_status); //待当前线程被解除阻塞后才能继续运行intr_set_status
}
/*将线程pthread接触阻塞*/
void thread_unblock(struct task_struct* pthread){
enum intr_status old_status = intr_disable();
ASSERT(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
if(pthread->status != TASK_READY){
ASSERT(!elem_find(&thread_ready_list,&pthread->general_tag));
if(elem_find(&thread_ready_list,&pthread->general_tag)){
PANIC("thread_unblock:blocked thread in ready_list\n");
}
list_push(&thread_ready_list,&pthread->general_tag);
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
- “/home/lily/OS/boot/thread/thread.h”
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"
/*自定义通用函数类型,它将在很多线程函数中作为形参类型*/
typedef void thread_func(void*);
/*进程或线程的状态*/
enum task_status{
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED
};
/*中断栈intr_stack
用于中断发生时保护程序上下文环境:
进程/线程被外部中断/软中断打断时,会按照此结构压入上下文
寄存器,intr_exit中的出栈操作是此结构的逆操作
此栈在线程自己的内核栈中位置固定,所在页的最顶端
*/
struct intr_stack{
uint32_t vec_no; //kernel.S宏VECTOR中push %1压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy; //虽然pushad把esp也压入了,但esp是不断变化的,所以会被popad忽略
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
/*下面由CPU从低特权级进入高特权级时压入*/
uint32_t err_code; //err_code会被压入在eip之后
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/*线程栈thread_stack
线程自己的栈,用于存储线程中待执行的函数
此结构在线程自己的内核栈中位置不固定
仅用在switch_to时保存线程环境
实际位置取决于实际情况。
*/
struct thread_stack{
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
/*线程第一次执行时,eip指向待调用函数kernel_thread,
其他时候,eip指向switch_to的返回地址*/
void (*eip) (thread_func* func,void* func_arg);
/*以下仅供第一次被调度上CPU时使用*/
/*参数unused_ret只为占位置充数为返回地址*/
void (*unused_retaddr); //占位符,基线,表示返回地址
thread_func* function; //由kernel_thread所调用的函数名
void* func_arg; //由kernel_thread所调用的函数所需的参数
};
/*进程或线程的pcb,程序控制块*/
struct task_struct{
uint32_t* self_kstack; //各内核线程都用自己的内核栈
enum task_status status;
uint8_t priority; //线程优先级
char name[16];
uint8_t ticks; //每次在处理器上的嘀咕数
uint32_t elapsed_ticks; //此任务自上CPU运行至今用了多少CPU嘀咕数,也就是任务运行了多久
struct list_elem general_tag; //用于线程在一般队列中的节点
struct list_elem all_list_tag; //用于线程队列thread_all_list中的结点
uint32_t* pgdir; //进程自己页表的虚拟地址,如果是线程则为NULL
uint32_t stack_magic; //栈的边界标记,用于检测栈的溢出
};
/*获取当前线程的pcb指针*/
struct task_struct* running_thread(void);
/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);
/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio);
/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
/*将kernel中的main函数完善为主线程*/
// static void make_main_thread(void);
/*实现任务调度*/
void schedule(void);
/*初始化线程环境*/
void thread_init(void);
/*当前线程将自己阻塞,标志其状态为stat*/
void thread_block(enum task_status stat);
/*将线程pthread接触阻塞*/
void thread_unblock(struct task_struct* pthread);
#endif
4. 锁的实现
-----------------------代码----------------------------
- “/home/lily/OS/boot/thread/sync.h”
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"
/*信号量结构*/
struct semaphore{
uint8_t value;
struct list waiters;
};
/*锁结构*/
struct lock{
struct task_struct* holder; //锁的持有者
struct semaphore semaphore; //用二元信号量实现锁
uint32_t holder_repeat_nr; //锁的持有者重复申请锁的次数
};
/*函数声明*/
/*初始化信号量*/
void sema_init(struct semaphore* psema,uint8_t value);
/*初始化锁plock*/
void lock_init(struct lock* plock);
/*P操作:信号量down操作*/
void sema_down(struct semaphore* psema);
/*V操作:信号量的up操作*/
void sema_up(struct semaphore* psema);
/*获取锁plock*/
void lock_acquire(struct lock* plock);
/*释放锁*/
void lock_release(struct lock* plock);
#endif
- “/home/lily/OS/boot/thread/sync.c”
#include "sync.h"
#include "string.h"
#include "interrupt.h"
/*初始化信号量*/
void sema_init(struct semaphore* psema,uint8_t value){
psema->value = value; //为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}
/*初始化锁plock*/
void lock_init(struct lock* plock){
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore,1);
}
/*P操作:信号量down操作*/
void sema_down(struct semaphore* psema){
/*关中断来保证原子操作*/
enum intr_status old_status = intr_get_status(); //关中断保证原子操作
//当semaphore == 1时,down操作才成功返回,否则就阻塞
while (psema->value == 0){ //若value==0,则表示semaphore已被别人持有
ASSERT(!elem_find(&psema->waiters,&running_thread()->general_tag));
if(elem_find(&psema->waiters,&running_thread()->general_tag)) //当前线程不应该已在信号量的waiters队列中
PANIC("sema_down:thread blocked has been in waiters_list\n");
//若信号量值 == 0,则当前线程把自己加入该锁的等待队列,然后阻塞自己
list_append(&psema->waiters,&running_thread()->general_tag);
thread_block(TASK_BLOCKED);
}
//若semaphore == 1或线程被唤醒,则执行下面代码
psema->value--;
ASSERT(psema->value == 0);
intr_set_status(old_status);
}
/*V操作:信号量的up操作*/
void sema_up(struct semaphore* psema){
enum intr_status old_status = intr_get_status();
ASSERT(psema->value == 0);
if(!list_empty(&psema->waiters)){
struct task_struct* thread_blocked = elem2entry(struct task_struct,general_tag,list_pop(&psema->waiters));
thread_unblock(thread_blocked);
}
psema->value++;
ASSERT(psema->value == 1);
intr_set_status(old_status);
}
/*获取锁plock*/
void lock_acquire(struct lock* plock){
//排除曾经自己已经持有锁但未释放的情况
if(plock->holder != running_thread()){
sema_down(&plock->semaphore);
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}else{
plock->holder_repeat_nr++;
}
}
/*释放锁*/
void lock_release(struct lock* plock){
ASSERT(plock->holder == running_thread());
if(plock->holder_repeat_nr > 1){
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1);
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore);
}
二、用锁实现终端输出
- 终端的作用:把用户键入的命令传送给主机,待主机运算完成后再将结果送回给用户,终端不提供任何额外功能,仅是个显示窗口。
-----------------------代码----------------------------
- “/home/lily/OS/boot/device/console.c”
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock; //控制台锁,必须是static的
/*初始化终端*/
void console_init(){
lock_init(&console_lock);
}
/*获取终端*/
void console_acquire(){
lock_acquire(&console_lock);
}
/*释放终端*/
void console_release(){
lock_release(&console_lock);
}
/*在终端输出字符串*/
void console_put_str(char* str){
console_acquire();
put_str(str);
console_release();
}
/*在终端中输出字符*/
void console_put_char(uint8_t char_asci){
console_acquire();
put_char(char_asci);
console_release();
}
/*在终端中输出16进制整数*/
void console_put_int(uint32_t num){
console_acquire();
put_int(num);
console_release();
}
- “/home/lily/OS/boot/device/console.h”
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
/*初始化终端*/
void console_init(void);
/*获取终端*/
void console_acquire(void);
/*释放终端*/
void console_release(void);
/*在终端输出字符串*/
void console_put_str(char* str);
/*在终端中输出字符*/
void console_put_char(uint8_t char_asci);
/*在终端中输出16进制整数*/
void console_put_int(uint32_t num);
#endif
- “/home/lily/OS/boot/kernel/init.c”
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
#include "console.h"
/*负责初始化所有模块*/
void init_all(){
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init(); //初始化PIT
console_init(); //控制台初始化最好放在开中断之前
}
- “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.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){
console_put_str("Main ");
}
return 0;
}
/*在线程中运行函数*/
void k_thread_a(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
console_put_str(para);
}
}
/*在线程中运行函数*/
void k_thread_b(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
console_put_str(para);
}
}
- “/home/lily/OS/boot/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)/switch.o $(BUILD_DIR)/list.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序
########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/kernel/stdint.h kernel/init.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/kernel/stdint.h \
kernel/interrupt.h device/timer.h kernel/memory.h thread/thread.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/kernel/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/kernel/stdint.h lib/kernel/io.h lib/kernel/print.h thread/thread.c \
kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/kernel/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/interrupt.h lib/kernel/list.h kernel/debug.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o:lib/string.c lib/string.h kernel/debug.h kernel/global.h lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o:kernel/memory.c kernel/memory.h lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h \
lib/kernel/print.h kernel/global.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 lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h kernel/interrupt.h lib/kernel/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h lib/kernel/list.h lib/kernel/stdint.h thread/thread.h lib/string.h \
kernel/interrupt.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h lib/kernel/print.h lib/kernel/stdint.h thread/sync.h thread/thread.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=/home/lily/bochs/hd60M.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就是完成了编译到写入硬盘的全过程
三、从键盘获取输入
1. 键盘输入原理
- 键盘编码器(8048):键盘编码器是一个芯片,每当键盘上发生按键操作时,它就向键盘控制器报告哪个键被按下,按键是否弹起。
- 键盘控制器(8042):是键盘的IO接口,在主机内部的主板上,它负责接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后CPU执行相应的中断处理程序读入8042处理保存过的案件信息。
- 键盘扫描码:所有按键对应的数组组成的一张“按键-数值”的编码映射表。
- 键的状态:按键被按下时的编码叫通码;按键被松开弹起时的编码叫断码。
- 键盘的中断过程:按键/松开时,键盘中的8048芯片把按键对应的扫描码(通码/断码)发送到主板上的8042芯片,由8042处理后保存在自己寄存器中,然后向8059A发送中断信号,这样CPU便去执行键盘中断处理程序。将8042处理过的扫描码从它寄存器中读取出来,继续下一步处理。
- 键盘中断处理程序是由程序员编写的,但我们只能得到按键的扫描码,而非ASCII码。扫描码是硬件提供的编码集,ASCII码是软件中约定的编码集。
2. 8042
- 8042是8048的IO接口,对8048的编程也是通过8042完成的。
- 8042是连接8048和CPU的桥梁,8042存在的目的是:为了CPU可以通过它控制8048的工作方式,然后让8048的工作成果通过8042回传给CPU。此时8042就相当于数据的缓冲区、中转站,根据数据被发送的方向,8042的作用分别的输入和输出。
- 当需要把data从CPU发送到8042时(数据尚未发送时),0x60端口的作用是输入缓冲区,此时应该用out指令写入0x60端口。
- 当data已从8048发到8042时,0x60端口的作用是输出缓冲区,此时应该用in指令从8042的0x60(输出缓冲区寄存器)读取8048的输出结果。
- 寄存器:
-
输出缓冲区寄存器
- 8位宽,只读。
- 键盘驱动程序从此寄存器中通过in指令读取来自8048的扫描码、来自8048的命令应答以及对8042本身设置时,8042自身的应答也从该寄存器中获取。
- 输出缓存寄存器中的扫描码是给CPU准备的,在CPU未读取之前,8042不会再向此寄存器中存入新的扫描码。
-
输入缓冲区寄存器:8位宽寄存器,键盘驱动程序通过out指令向此寄存器写入对8048的控制命令、参数等,对于8042本身的控制命令也是写入此寄存器。
-
状态寄存器:
-
控制寄存器:
-
3. 测试键盘中断处理程序
-----------------------代码----------------------------
- 演示键盘操作——每当按键时屏幕便显示一个’K’
- “/home/lily/OS/boot/kernel/kernel.S”
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了错误码,为保持栈中格式同一,这里不做操作
%define ZERO push 0 ;若在相关异常中CPU没有压入错误码,为了格式统一,就手工压入一个0
extern put_str ;声明外部函数
extern idt_table
section .data ;代码段开始
global intr_entry_table ;intr_entry_table位于data段,链接时会和宏中的data段放在一起
intr_entry_table: ;构造intr_entry_table数组,只是构造数组,并不调用中断,中断由硬件触发,目前由时钟触发
;宏开始
%macro VECTOR 2 ;定义了一个叫VECTOR的宏,接收两个参数。一个参数是中断向量号,第二个参数也是个宏
section .text
intr%1entry: ;%1表示第一个参数,不是英文L。中断处理程序的起始地址;每个中队处理程序都要压入中断向量号,所以一个中断类型一个向量号。自己知道自己的向量号是多少
%2 ;压入错误码,中断若有错误码会压入到eip后
;保存上下文环境,保存ds,es,fs,gs和8个32位通用寄存器
push ds
push es
push fs
push gs
pushad
;发送EOI信号的目的是告知硬件中断控制器8259A,当前中断已经处理完成,可以继续处理其他中断请求。
;如果是从片中进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ;中断结束命令EOI。OCW2的第5位是EOI位,其余都是0,所以是0x20。我们要向8259A发送结束标志,不然手动结束的8259A不知道中断结束了。
out 0xa0,al ;向主片发送
out 0x20,al ;向从片发送
;处理中断,即调用中断处理函数
push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量号
call [idt_table + %1*4]
;中断结束,返回原进程
jmp intr_exit
section .data ;表示上一个代码段结束
dd intr%1entry ;dd用来定义数组元素的宽度,元素值为intr%lentry;存储各个中断入口程序的地址,形成intr_entry_table数组。这样每个宏调用都会在数组中产生新的地址元素
%endmacro
;宏结束
section .text
global intr_exit
intr_exit:
;跳过中断号
add esp,4
;恢复上下文环境
popad
pop gs
pop fs
pop es
pop ds
;跳过error_code
add esp,4
;返回主程序,iretd用于从中断服务例程(ISR)返回到被中断的程序。
iretd
; [bits 32]
; %define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码, 为保持栈中格式统一, 这里不做操作
; %define ZERO push 0 ; 为了栈中格式统一, 如果 CPU 在异常中没有自动压入错误码, 这里填充 0
; extern idt_table ; 声明 c 注册的中断处理函数数组
; section .data
; ; intr_entry_table位于data段, 之后会和宏中的data段组合在一起(注意: 宏中的text段与intr_entry_table不是同一个段)
; global intr_entry_table
; intr_entry_table:
; ;--------------- 宏 VECTOR 开始, 参数数目为2, 第一个参数为中断号, 第二个参数为该中断对 ERROR_CODE 的操作 ---------------
; %macro VECTOR 2
; section .text
; intr%1entry: ; 每个中断处理程序都要压入中断向量号, 所以1个中断类型1个处理程序, 自己知道自己的中断号是多少, %1: 调用宏时的第一个参数
; %2 ; %2: 调用宏时的第二个参数, 有错误码时什么都不做, 没有时压入 0 使格式统一
; ; 保存上下文环境
; push ds
; push es
; push fs
; push gs
; pushad
; ; 如果从片上进入中断, 除了往从片上发送 EOI 外,还要往主片上发送 EOI, 因为后面要在 8259A 芯片上设置手动结束中断, 所以这里手动发送 EOI
; mov al, 0x20 ; 中断结束命令 EOI, 0x20 = 0010 0000, 第五位为EOI位
; out 0xa0, al ; 往从片发送
; out 0x20, al ; 往主片发送
; push %1 ; 不管中断处理程序是否需要, 一律压入中断向量号
; call [idt_table + %1*4] ; 调用中断处理程序
; jmp intr_exit
; section .data ; 这个 section .data 的作用就是让数组里全都是地址, 编译器会将属性相同的 Section 合成一个大的 Segmengt, 所以这里就是紧凑排列的数组了
; dd intr%1entry ; 存储各个中断入口程序的地址, 形成 intr_entry_table 数组
; %endmacro
; ;---------------宏 VECTOR 结束---------------
; section .text
; global intr_exit
; intr_exit:
; ; 恢复上下文环境
; add esp, 4 ; 跳过参数中断号
; popad
; pop gs
; pop fs
; pop es
; pop ds
; add esp, 4 ; 手动跳过错误码
; iretd
VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ERROR_CODE
VECTOR 0x09, ZERO
VECTOR 0x0a, ERROR_CODE
VECTOR 0x0b, ERROR_CODE
VECTOR 0x0c, ZERO
VECTOR 0x0d, ERROR_CODE
VECTOR 0x0e, ERROR_CODE
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ERROR_CODE
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ERROR_CODE
VECTOR 0x19, ZERO
VECTOR 0x1a, ERROR_CODE
VECTOR 0x1b, ERROR_CODE
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODE
VECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO ;时钟中断对应的入口
VECTOR 0x21, ZERO ;键盘中断对应的入口
VECTOR 0x22, ZERO ;级联用的
VECTOR 0x23, ZERO ;串口2对应的入口
VECTOR 0x24, ZERO ;串口1对应的入口
VECTOR 0x25, ZERO ;并口2对应的入口
VECTOR 0x26, ZERO ;软盘对应的入口
VECTOR 0x27, ZERO ;并口1对应的入口
VECTOR 0x28, ZERO ;实时时钟对应的入口
VECTOR 0x29, ZERO ;重定向
VECTOR 0x2a, ZERO ;保留
VECTOR 0x2b, ZERO ;保留
VECTOR 0x2c, ZERO ;ps/2鼠标
VECTOR 0x2d, ZERO ;fpu浮点单元异常
VECTOR 0x2e, ZERO ;硬盘
VECTOR 0x2f, ZERO ;保留
- “/home/lily/OS/boot/kernel/interrupt.c”
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x30 //目前共支持的中断数,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]; //用于保存异常的名字
/*定义中断处理函数:在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序*/
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);
/*测试键盘,只打开键盘中断,其他全部关闭*/
outb(PIC_M_DATA,0xfd);
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和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
//将光标置零,从屏幕左上角清出一片打印异常信息的区域,方便阅读
set_cursor(0);
int cursor_pos = 0;
while (cursor_pos < 320)
{
put_char(' ');
cursor_pos++;
}
set_cursor(0); //重置光标为屏幕左上角
put_str("!!!!!!! excetion message begin !!!!!!!\n");
set_cursor(88); //从第2行第8个字符开始打印
put_str(intr_name[vec_nr]);
if(vec_nr == 14){ //若为PageFault,将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
asm("movl %%cr2,%0" : "=r"(page_fault_vaddr)); //将cr2的值转存到page_fault中。cr2是存放造成page_fault的地址
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("!!!!!!! excetion 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数组中的函数是在进入中断后根据中断向量号调用的
}
- “/home/lily/OS/boot/device/keyboard.c”
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
put_char('K');
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
inb(KBD_BUF_PORT); //读取8042的输出缓冲区寄存器,返回值存放在eax中;这里是为了演示每敲一下键盘便出现一个'K'
return;
}
/*键盘初始化*/
void keyboard_init(){
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}
- “/home/lily/OS/boot/device/keyboard.h”
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
#include "stdint.h"
//函数声明
/*键盘初始化*/
void keyboard_init(void);
#endif
- “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.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);//{
// console_put_str("Main ");
// };
return 0;
}
/*在线程中运行函数*/
void k_thread_a(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
console_put_str(para);
}
}
/*在线程中运行函数*/
void k_thread_b(void* arg){
/*用void来通知表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用*/
char* para = arg;
while (1){
console_put_str(para);
}
}
- “/home/lily/OS/boot/kernel/init.c”
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
#include "console.h"
#include "keyboard.h"
/*负责初始化所有模块*/
void init_all(){
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init(); //初始化PIT
console_init(); //控制台初始化最好放在开中断之前
keyboard_init();
}
- “/home/lily/OS/boot/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)/switch.o $(BUILD_DIR)/list.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \
$(BUILD_DIR)/keyboard.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序
########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/kernel/stdint.h kernel/init.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/kernel/stdint.h \
kernel/interrupt.h device/timer.h kernel/memory.h thread/thread.h device/console.h device/keyboard.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/kernel/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/kernel/stdint.h lib/kernel/io.h lib/kernel/print.h thread/thread.c \
kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/kernel/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/interrupt.h lib/kernel/list.h kernel/debug.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o:lib/string.c lib/string.h kernel/debug.h kernel/global.h lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o:kernel/memory.c kernel/memory.h lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h \
lib/kernel/print.h kernel/global.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 lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h kernel/interrupt.h lib/kernel/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h lib/kernel/list.h lib/kernel/stdint.h thread/thread.h lib/string.h \
kernel/interrupt.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h lib/kernel/print.h lib/kernel/stdint.h thread/sync.h thread/thread.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h lib/kernel/io.h \
kernel/global.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=/home/lily/bochs/hd60M.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就是完成了编译到写入硬盘的全过程
- 输出扫描码
- “/home/lily/OS/boot/device/keyboard.c”
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
uint8_t scancode = inb(KBD_BUF_PORT);
put_int(scancode);
return;
}
/*键盘初始化*/
void keyboard_init(){
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}
四、编写键盘驱动
- 键盘驱动程序的作用:将扫描码->ASCII码
- 转义字符:
- 一般转义字符,'+单个字母’的形式
- 八进制转义字符,'\0+三位八进制数字表示的ASCII码’的形式
- 十六进制转义字符,'\x+两位十六进制数字表示的ASCII码’的形式
-----------------------代码----------------------------
- “/home/lily/OS/boot/device/keyboard.c”
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*用转义字符定义部分控制字符*/
#define esc '\033' //8进制表示字符,也可以用16进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' //8进制表示字符,也可以用16进制'\x7f'
/*以上不可见字符一律定义为0*/
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
/*定义控制字符的通码和断码*/
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
/*定义一下变量记录相应键是否按下的状态,ext_scancode用于记录makecode是否以0xe0开头*/
static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
/*以通码make_code为索引的二维数组*/
static char keymap[][2] = {
/*扫描码未与shift组合*/
/*0x00*/ {0,0},
/*0x01*/ {esc,esc},
/*0x02*/ {'1','!'},
/*0x03*/ {'2','@'},
/*0x04*/ {'3','#'},
/*0x05*/ {'4','$'},
/*0x06*/ {'5','%'},
/*0x07*/ {'6','^'},
/*0x08*/ {'7','&'},
/*0x09*/ {'8','*'},
/*0x0a*/ {'9','('},
/*0x0b*/ {'0',')'},
/*0x0c*/ {'-','_'},
/*0x0d*/ {'=','+'},
/*0x0e*/ {backspace,backspace},
/*0x0f*/ {tab,tab},
/*0x10*/ {'q','Q'},
/*0x11*/ {'w','W'},
/*0x12*/ {'e','E'},
/*0x13*/ {'r','R'},
/*0x14*/ {'t','T'},
/*0x15*/ {'y','Y'},
/*0x16*/ {'u','U'},
/*0x17*/ {'i','I'},
/*0x18*/ {'o','O'},
/*0x19*/ {'p','P'},
/*0x1a*/ {'[','{'},
/*0x1b*/ {']','}'},
/*0x1c*/ {enter,enter},
/*0x1d*/ {ctrl_l_char,ctrl_l_char},
/*0x1e*/ {'a','A'},
/*0x1f*/ {'s','S'},
/*0x20*/ {'d','D'},
/*0x21*/ {'f','F'},
/*0x22*/ {'g','G'},
/*0x23*/ {'h','H'},
/*0x24*/ {'j','J'},
/*0x25*/ {'k','K'},
/*0x26*/ {'l','L'},
/*0x27*/ {';',':'},
/*0x28*/ {'\'','"'},
/*0x29*/ {'`','~'},
/*0x2a*/ {shift_l_char,shift_l_char},
/*0x2b*/ {'\\','|'},
/*0x2c*/ {'z','Z'},
/*0x2d*/ {'x','X'},
/*0x2e*/ {'c','C'},
/*0x2f*/ {'v','V'},
/*0x30*/ {'b','B'},
/*0x31*/ {'n','N'},
/*0x32*/ {'m','M'},
/*0x33*/ {',','<'},
/*0x34*/ {'.','>'},
/*0x35*/ {'/','?'},
/*0x36*/ {shift_r_char,shift_r_char},
/*0x37*/ {'*','*'},
/*0x38*/ {alt_l_char,alt_l_char},
/*0x39*/ {' ',' '},
/*0x3a*/ {caps_lock_char,caps_lock_char}
/*其他按键暂不处理*/
};
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
/*这次中断发送前的上一次中断,一下任意三个键是否有按下*/
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
/*若扫描码scancode是e0开头的,表示此键的按下将产生多个扫描码,
所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if(scancode == 0xe0){
ext_scancode = true; //打开e0标志
return; //表示后面还有扫描码,返回
}
/*如果上次是以0xe0开头的,将扫描码合并*/
if(ext_scancode){
scancode = ((0xe000) | scancode);
ext_scancode = false;
}
/*判断扫描码是通码还是断码*/
break_code = ((scancode & 0x0080) != 0); //获取break_code
//若是断码break_code(按键弹起时产生的扫描码)
if(break_code){
/*由于ctrl_r和alt_r的make_code和break_code都是2字节,所以可以用下面的方法取make_code,多字节的扫描码暂不处理*/
uint16_t make_code = (scancode &= 0xff7f); //通码与断码的区别在于第8位是0还是1
//取得其make_code(按键按下时产生的扫描码)
/*若是任意下面三个键弹起,将其状态置为false*/
if(make_code == ctrl_l_make || make_code == ctrl_r_make){
ctrl_status = false;
}else if(make_code == shift_l_make || make_code == shift_r_make){
shift_status = false;
}else if(make_code == alt_l_make || make_code == alt_r_make){
alt_status = false;
}//由于caps_lock不是弹起后关闭,所以需要单独处理
return;
}
/*若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code*/
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){
bool shift = false; //判断是否与shift组合,用来在一维数组中索引对应的字符
if((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || (scancode == 0x1b) \
|| (scancode == 0x2b) || (scancode == 0x27) || (scancode == 0x28) || (scancode == 0x33) \
|| (scancode == 0x34) || (scancode == 0x35)){
if(shift_down_last) //如果同时按下shift键
shift = true;
}else{ //默认为字母键
if(shift_down_last && caps_lock_last){ //如果shift和capslock同时按下
shift = false;
}else if(shift_down_last || caps_lock_last){
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); //将扫描码的高字节置零,主要针对高字节是e0的扫描码
char cur_char = keymap[index][shift]; //在字节中找到对应的字符
/*只处理ASCII码不为0的键*/
if(cur_char){
put_char(cur_char);
return;
}
/*记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键*/
if(scancode == ctrl_l_make || scancode == ctrl_r_make){
ctrl_status = true;
}else if(scancode == shift_l_make || scancode == shift_r_make){
shift_status = true;
}else if(scancode == alt_l_make || scancode == alt_r_make){
alt_status = true;
}else if(scancode == caps_lock_make){ //不管之前是否按下过caps_lock键,再次按下时状态相反
caps_lock_status = !caps_lock_status;
}
}else{
put_str("unkown key\n");
}
return;
}
/*键盘初始化*/
void keyboard_init(){
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}
- “/home/lily/OS/boot/device/keyboard.h”
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
#include "stdint.h"
//函数声明
/*键盘初始化*/
void keyboard_init();
#endif
五、环形输入缓冲区
1. 环形缓冲区的实现
- 两个指针:一个是头指针,用于向缓冲区中写数据;一个是尾指针,用于从缓冲区中读数据。
-----------------------代码----------------------------
- "ioqueue.c"中有个错误,解释如下:
在C语言中,static关键字用于限制函数或变量的作用域。当static用于函数时,它会使函数的作用域限定在当前源文件中,也就是说,该函数只能在定义它的源文件中使用,无法被其他源文件调用。
这种行为是由于编译器的链接过程所决定的。当编译器处理一个源文件时,它会将其中定义的函数和变量与其他源文件的函数和变量进行链接。在默认情况下,不带static关键字的全局函数具有外部链接(external linkage),可以被其他源文件访问和调用。而带有static关键字的函数具有内部链接(internal linkage),只能在定义它的源文件中使用。
- “/home/lily/OS/boot/device/ioqueue.c”
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
#define NULL 0
/*初始化io队列ioq*/
void ioqueue_init(struct ioqueue* ioq){
lock_init(&ioq->lock); //初始化io队列的锁
ioq->producer = ioq->consumer = NULL; //生产者和消费者置空
ioq->head = ioq->tail = 0; //队列的首尾指针指向缓冲区数组第0个位置
}
/*返回pos缓冲区中的下一个位置值*/
static int32_t next_pos(int32_t pos){
return (pos+1)%bufsize;
}
/*判断队列是否已满*/
bool ioq_full(struct ioqueue* ioq){ //书中是static,但是不能被keyboard.c引用,所以改为非static并声明
ASSERT(intr_get_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}
/*判断队列是否已空*/
static bool ioq_empty(struct ioqueue* ioq){
ASSERT(intr_get_status() == INTR_OFF);
return ioq->head == ioq->tail;
}
/*使当前生产者/消费者在此缓冲区上等待*/
static void ioq_wait(struct task_struct** waiter){
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
/*唤醒waiter*/
static void wakeup(struct task_struct** waiter){
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
/*消费者ioq队列中获取一个字符*/
char ioq_getchar(struct ioqueue* ioq){
ASSERT(intr_get_status() == INTR_OFF);
/*若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,
目的是将来生产者往缓冲区里包装商品后,生产者知道唤醒哪个消费者,
也就是唤醒当前线程自己*/
while(ioq_empty(ioq)){
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char byte = ioq->buf[ioq->tail];
ioq->tail = next_pos(ioq->tail);
if(ioq->producer != NULL)
wakeup(&ioq->producer);
return byte;
}
/*生产者往ioq队列中写入一个字节byte*/
void ioq_putchar(struct ioqueue* ioq,char byte){
ASSERT(intr_get_status() == INTR_OFF);
/*若缓冲区(队列)满了,把生产者ioq->producer记为自己,
为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
也就是唤醒当前线程自己*/
while (ioq_full(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte;
ioq->head = next_pos(ioq->head);
if(ioq->consumer != NULL){
wakeup(&ioq->consumer);
}
}
- “/home/lily/OS/boot/device/ioqueue.h”
#ifndef __DEVICE_IOQUEUE_H
#define __DEVICE_IOQUEUE_H
#include "stdint.h"
#include "thread.h"
#include "sync.h"
#define bufsize 64
/*环形队列*/
struct ioqueue{
//生产者消费者问题
struct lock lock;
/*生产者,缓冲区不满时就继续往里面放数据,
否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠*/
struct task_struct* producer;
/*消费者,缓冲区不空时就继续从里面拿数据,
否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠*/
struct task_struct* consumer;
char buf[bufsize]; //缓冲区大小
int32_t head; //队首,数据往队首处写入
int32_t tail; //队尾,数据从队尾处读出
};
//函数声明
/*判断队列是否已满*/
bool ioq_full(struct ioqueue* ioq);
/*初始化io队列ioq*/
void ioqueue_init(struct ioqueue* ioq);
/*消费者ioq队列中获取一个字符*/
char ioq_getchar(struct ioqueue* ioq);
/*生产者往ioq队列中写入一个字节byte*/
void ioq_putchar(struct ioqueue* ioq,char byte);
#endif
- “/home/lily/OS/boot/device/keyboard.c”
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#include "ioqueue.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*用转义字符定义部分控制字符*/
#define esc '\033' //8进制表示字符,也可以用16进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' //8进制表示字符,也可以用16进制'\x7f'
/*以上不可见字符一律定义为0*/
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
/*定义控制字符的通码和断码*/
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
/*定义一下变量记录相应键是否按下的状态,ext_scancode用于记录makecode是否以0xe0开头*/
static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
struct ioqueue kbd_buf; //定义键盘缓冲区
/*以通码make_code为索引的二维数组*/
static char keymap[][2] = {
/*扫描码未与shift组合*/
/*0x00*/ {0,0},
/*0x01*/ {esc,esc},
/*0x02*/ {'1','!'},
/*0x03*/ {'2','@'},
/*0x04*/ {'3','#'},
/*0x05*/ {'4','$'},
/*0x06*/ {'5','%'},
/*0x07*/ {'6','^'},
/*0x08*/ {'7','&'},
/*0x09*/ {'8','*'},
/*0x0a*/ {'9','('},
/*0x0b*/ {'0',')'},
/*0x0c*/ {'-','_'},
/*0x0d*/ {'=','+'},
/*0x0e*/ {backspace,backspace},
/*0x0f*/ {tab,tab},
/*0x10*/ {'q','Q'},
/*0x11*/ {'w','W'},
/*0x12*/ {'e','E'},
/*0x13*/ {'r','R'},
/*0x14*/ {'t','T'},
/*0x15*/ {'y','Y'},
/*0x16*/ {'u','U'},
/*0x17*/ {'i','I'},
/*0x18*/ {'o','O'},
/*0x19*/ {'p','P'},
/*0x1a*/ {'[','{'},
/*0x1b*/ {']','}'},
/*0x1c*/ {enter,enter},
/*0x1d*/ {ctrl_l_char,ctrl_l_char},
/*0x1e*/ {'a','A'},
/*0x1f*/ {'s','S'},
/*0x20*/ {'d','D'},
/*0x21*/ {'f','F'},
/*0x22*/ {'g','G'},
/*0x23*/ {'h','H'},
/*0x24*/ {'j','J'},
/*0x25*/ {'k','K'},
/*0x26*/ {'l','L'},
/*0x27*/ {';',':'},
/*0x28*/ {'\'','"'},
/*0x29*/ {'`','~'},
/*0x2a*/ {shift_l_char,shift_l_char},
/*0x2b*/ {'\\','|'},
/*0x2c*/ {'z','Z'},
/*0x2d*/ {'x','X'},
/*0x2e*/ {'c','C'},
/*0x2f*/ {'v','V'},
/*0x30*/ {'b','B'},
/*0x31*/ {'n','N'},
/*0x32*/ {'m','M'},
/*0x33*/ {',','<'},
/*0x34*/ {'.','>'},
/*0x35*/ {'/','?'},
/*0x36*/ {shift_r_char,shift_r_char},
/*0x37*/ {'*','*'},
/*0x38*/ {alt_l_char,alt_l_char},
/*0x39*/ {' ',' '},
/*0x3a*/ {caps_lock_char,caps_lock_char}
/*其他按键暂不处理*/
};
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
/*这次中断发送前的上一次中断,一下任意三个键是否有按下*/
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
/*若扫描码scancode是e0开头的,表示此键的按下将产生多个扫描码,
所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if(scancode == 0xe0){
ext_scancode = true; //打开e0标志
return; //表示后面还有扫描码,返回
}
/*如果上次是以0xe0开头的,将扫描码合并*/
if(ext_scancode){
scancode = ((0xe000) | scancode);
ext_scancode = false;
}
/*判断扫描码是通码还是断码*/
break_code = ((scancode & 0x0080) != 0); //获取break_code
//若是断码break_code(按键弹起时产生的扫描码)
if(break_code){
/*由于ctrl_r和alt_r的make_code和break_code都是2字节,所以可以用下面的方法取make_code,多字节的扫描码暂不处理*/
uint16_t make_code = (scancode &= 0xff7f); //通码与断码的区别在于第8位是0还是1
//取得其make_code(按键按下时产生的扫描码)
/*若是任意下面三个键弹起,将其状态置为false*/
if(make_code == ctrl_l_make || make_code == ctrl_r_make){
ctrl_status = false;
}else if(make_code == shift_l_make || make_code == shift_r_make){
shift_status = false;
}else if(make_code == alt_l_make || make_code == alt_r_make){
alt_status = false;
}//由于caps_lock不是弹起后关闭,所以需要单独处理
return;
}
/*若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code*/
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){
bool shift = false; //判断是否与shift组合,用来在一维数组中索引对应的字符
if((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || (scancode == 0x1b) \
|| (scancode == 0x2b) || (scancode == 0x27) || (scancode == 0x28) || (scancode == 0x33) \
|| (scancode == 0x34) || (scancode == 0x35)){
if(shift_down_last) //如果同时按下shift键
shift = true;
}else{ //默认为字母键
if(shift_down_last && caps_lock_last){ //如果shift和capslock同时按下
shift = false;
}else if(shift_down_last || caps_lock_last){
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); //将扫描码的高字节置零,主要针对高字节是e0的扫描码
char cur_char = keymap[index][shift]; //在字节中找到对应的字符
/*只处理ASCII码不为0的键*/
if(cur_char){
/*若kbd_buf未满并且待加入的cur_char不为0,则将其加入到缓冲区区kbd_buf中*/
if(!ioq_full(&kbd_buf)){
put_char(cur_char); //临时的
ioq_putchar(&kbd_buf,cur_char);
}
return;
}
/*记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键*/
if(scancode == ctrl_l_make || scancode == ctrl_r_make){
ctrl_status = true;
}else if(scancode == shift_l_make || scancode == shift_r_make){
shift_status = true;
}else if(scancode == alt_l_make || scancode == alt_r_make){
alt_status = true;
}else if(scancode == caps_lock_make){ //不管之前是否按下过caps_lock键,再次按下时状态相反
caps_lock_status = !caps_lock_status;
}
}else{
put_str("unkown key\n");
}
return;
}
/*键盘初始化*/
void keyboard_init(){
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}
- “/home/lily/OS/boot/device/keyboard.h”
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
#include "stdint.h"
//函数声明
/*键盘初始化*/
void keyboard_init(void);
#endif
2. 生产者和消费者实例测试
-----------------------代码----------------------------
- “/home/lily/OS/boot/kernel/interrupt.c”
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x30 //目前共支持的中断数,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]; //用于保存异常的名字
/*定义中断处理函数:在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序*/
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);
// /*测试键盘,打开键盘中断*/
// outb(PIC_M_DATA,0xfd);
// outb(PIC_S_DATA,0xff);
/*打开时钟中断和键盘中断*/
outb(PIC_M_DATA,0xfc);
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和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
//将光标置零,从屏幕左上角清出一片打印异常信息的区域,方便阅读
set_cursor(0);
int cursor_pos = 0;
while (cursor_pos < 320)
{
put_char(' ');
cursor_pos++;
}
set_cursor(0); //重置光标为屏幕左上角
put_str("!!!!!!! excetion message begin !!!!!!!\n");
set_cursor(88); //从第2行第8个字符开始打印
put_str(intr_name[vec_nr]);
if(vec_nr == 14){ //若为PageFault,将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
asm("movl %%cr2,%0" : "=r"(page_fault_vaddr)); //将cr2的值转存到page_fault中。cr2是存放造成page_fault的地址
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("!!!!!!! excetion 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数组中的函数是在进入中断后根据中断向量号调用的
}
- “/home/lily/OS/boot/device/keyboard.h”
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
#include "stdint.h"
extern struct ioqueue kbd_buf;
//函数声明
/*键盘初始化*/
void keyboard_init(void);
#endif
- “/home/lily/OS/boot/device/ioqueue.c”
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
#define NULL 0
/*初始化io队列ioq*/
void ioqueue_init(struct ioqueue* ioq){
lock_init(&ioq->lock); //初始化io队列的锁
ioq->producer = ioq->consumer = NULL; //生产者和消费者置空
ioq->head = ioq->tail = 0; //队列的首尾指针指向缓冲区数组第0个位置
}
/*返回pos缓冲区中的下一个位置值*/
static int32_t next_pos(int32_t pos){
return (pos+1)%bufsize;
}
/*判断队列是否已满*/
bool ioq_full(struct ioqueue* ioq){ //书中是static,但是不能被keyboard.c引用,所以改为非static并声明
ASSERT(intr_get_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}
/*判断队列是否已空*/
bool ioq_empty(struct ioqueue* ioq){
ASSERT(intr_get_status() == INTR_OFF);
return ioq->head == ioq->tail;
}
/*使当前生产者/消费者在此缓冲区上等待*/
static void ioq_wait(struct task_struct** waiter){
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
/*唤醒waiter*/
static void wakeup(struct task_struct** waiter){
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
/*消费者ioq队列中获取一个字符*/
char ioq_getchar(struct ioqueue* ioq){
ASSERT(intr_get_status() == INTR_OFF);
/*若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,
目的是将来生产者往缓冲区里包装商品后,生产者知道唤醒哪个消费者,
也就是唤醒当前线程自己*/
while(ioq_empty(ioq)){
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char byte = ioq->buf[ioq->tail];
ioq->tail = next_pos(ioq->tail);
if(ioq->producer != NULL)
wakeup(&ioq->producer);
return byte;
}
/*生产者往ioq队列中写入一个字节byte*/
void ioq_putchar(struct ioqueue* ioq,char byte){
ASSERT(intr_get_status() == INTR_OFF);
/*若缓冲区(队列)满了,把生产者ioq->producer记为自己,
为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
也就是唤醒当前线程自己*/
while (ioq_full(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte;
ioq->head = next_pos(ioq->head);
if(ioq->consumer != NULL){
wakeup(&ioq->consumer);
}
}
- ‘/home/lily/OS/boot/device/ioqueue.h’
#ifndef __DEVICE_IOQUEUE_H
#define __DEVICE_IOQUEUE_H
#include "stdint.h"
#include "thread.h"
#include "sync.h"
#define bufsize 64
/*环形队列*/
struct ioqueue{
//生产者消费者问题
struct lock lock;
/*生产者,缓冲区不满时就继续往里面放数据,
否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠*/
struct task_struct* producer;
/*消费者,缓冲区不空时就继续从里面拿数据,
否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠*/
struct task_struct* consumer;
char buf[bufsize]; //缓冲区大小
int32_t head; //队首,数据往队首处写入
int32_t tail; //队尾,数据从队尾处读出
};
//函数声明
/*判断队列是否已满*/
bool ioq_full(struct ioqueue* ioq);
/*判断队列是否已空*/
bool ioq_empty(struct ioqueue* ioq);
/*初始化io队列ioq*/
void ioqueue_init(struct ioqueue* ioq);
/*消费者ioq队列中获取一个字符*/
char ioq_getchar(struct ioqueue* ioq);
/*生产者往ioq队列中写入一个字节byte*/
void ioq_putchar(struct ioqueue* ioq,char byte);
#endif
- “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "keyboard.h"
#include "ioqueue.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void){
put_str("I am kernel\n");
init_all();
thread_start("consumer_a",31,k_thread_a,"A ");
thread_start("consumer_b",8,k_thread_b,"B ");
intr_enable(); //打开中断,使时钟中断起作用
while(1);//{
// console_put_str("Main ");
// };
return 0;
}
/*在线程中运行函数*/
void k_thread_a(void* arg){
while (1)
{
enum intr_status old_status = intr_disable();
if(!ioq_empty(&kbd_buf)){
console_put_str(arg);
char byte = ioq_getchar(&kbd_buf);
console_put_char(byte);
}
intr_set_status(old_status);
}
}
/*在线程中运行函数*/
void k_thread_b(void* arg){
while (1)
{
enum intr_status old_status = intr_disable();
if(!ioq_empty(&kbd_buf)){
console_put_str(arg);
char byte = ioq_getchar(&kbd_buf);
console_put_char(byte);
}
intr_set_status(old_status);
}
}
- “/home/lily/OS/boot/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)/switch.o $(BUILD_DIR)/list.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \
$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序
########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/kernel/stdint.h kernel/init.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h device/keyboard.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/kernel/stdint.h \
kernel/interrupt.h device/timer.h kernel/memory.h thread/thread.h device/console.h device/keyboard.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/kernel/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/kernel/stdint.h lib/kernel/io.h lib/kernel/print.h thread/thread.c \
kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/kernel/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/interrupt.h lib/kernel/list.h kernel/debug.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o:lib/string.c lib/string.h kernel/debug.h kernel/global.h lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o:kernel/memory.c kernel/memory.h lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h \
lib/kernel/print.h kernel/global.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 lib/kernel/stdint.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h kernel/interrupt.h lib/kernel/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h lib/kernel/list.h lib/kernel/stdint.h thread/thread.h lib/string.h \
kernel/interrupt.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h lib/kernel/print.h lib/kernel/stdint.h thread/sync.h thread/thread.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h lib/kernel/io.h \
kernel/global.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h thread/thread.h \
kernel/global.h thread/sync.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=/home/lily/bochs/hd60M.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就是完成了编译到写入硬盘的全过程