10.1 锁
没锁之前,输出的字符串被中途打断就是一个例子。
破坏了完整性
所以一个简单的方法是加中断
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
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)
{
char *para = arg;
while(1){
intr_disable();
put_str(para);
intr_enable();
}
while(1);
}
void k_thread_b(void *arg)
{
char *para = arg;
while(1){
intr_disable();
put_str(para);
intr_enable();
}
while(1);
}
运行截图
10.1.2 找出代码中的临界区、互斥、竞争条件
- 公共资源
- 临界区
各任务中访问公共资源的指令代码组成的区域就称为临界区 - 互斥
也可称为排他 - 竞争条件
多个线程并行混杂访问了公共的资源,也就是说,这种访问方式不满足互斥,从而产生了竞争条件
即使多个线程是真并行执行,对于访问共享资源也会有个前后顺序,因此显存和光标寄存器这两个公共资源的状态取决于所有线程的访问时序。
多线程访问公共资源时出问题的原因是产生了竞争条件,也就是多个任务同时出现在自己的临界区。为避免产生竞争条件,必须保证任意时刻只能有一个任务处于临界区。因此,只要保证各线程自己临界区中的所有代码都是原子操作,即临界区中的指令要么一条不做,要么一气呵成全部执行完,执行期间绝对不能被换下处理器。
所以我们必须提供一种互斥的机制,互斥能使临界区具有原子性,避免
产生竞争条件,从而避免了多任务访问公共资源时出问题。
10.1.3 信号量
当多个线程访问同一公共资源时(当然这也属于线程合作),为了保证结果正确,必然要用一套额外的机制来控制它们的工作步调,也就是使线程们同步工作。
信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。
所谓信号,是个泛指,取决于环境。
信号量是计数值,必然要有对计数增减的方法。由于 Dijkstra 是荷兰人,他用 P、V 操作来表示信号量的减、增,这两个都是荷兰语中的单词的缩写。
P 是指 Proberen,表示减少,
V 是指单词 Verhogen,表示增加。
增加操作 up 包括两个微操作。
(1)将信号量的值加 1。
(2)唤醒在此信号量上等待的线程。
减少操作 down 包括三个子操作。
(1)判断信号量是否大于 0。
(2)若信号量大于 0,则将信号量减 1。
(3)若信号量等于 0,当前线程将自己阻塞,以在此信号量上等待。
信号量的初值代表是信号资源的累积量,也就是剩余量,若初值为 1 的话,它的取值就只能为 0 和 1,这便称为二元信号量,我们可以利用二元信号量来实现锁
在二元信号量中,
down 操作就是获得锁,
up 操作就是释放锁。
流程:
- A down 获取锁 信号量-1,进入临界区,信号量变为0
- B down 发现信号量为0,B睡眠
- A 出,up 信号量+1,信号量=1, B被A唤醒。
- B 醒,获得锁,进入临近区
10.1.4 线程的阻塞和唤醒
thread_block 实现阻塞
thread_unblock 实现了线程唤醒
线程是主动阻塞自己,被动被别人唤醒
唤醒已阻塞的线程是由别的线程,通常是锁的持有者来做的。
thread / thread.c
添加了
thread_block
thread_unblock 在最后
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "debug.h"
#include "interrupt.h"
#include "list.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(){
uint32_t esp;
asm ("mov %%esp, %0" : "=g" (esp));
/* 取 esp 整数部分,即 pcb 起始地址 */
return (struct task_struct*)(esp & 0xfffff000);
}
/* 由 kernel_thread 去执行 function(func_arg) */
static void kernel_thread(thread_func *function, void *func_arg){
/* 执行 function 前要开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程 */
intr_enable();
function(func_arg);
}
/* 初始化线程栈 thread_stack,
将待执行的函数和参数放到 thread_stack 中相应的位置 */
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg){
/*先预留中断使用栈的空间,可见 thread.h 中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack);
/*在留出线程栈空间,可见 thread.h 中定义 */
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->esi = kthread_stack->edi = 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){
/* 由于把 main 函数也封装成一个线程,
并且它一直是运行的,故将其直接设为 TASK_RUNNING */
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);
ASSERT(thread != 0);
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);
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;
// 重新将当前线程的 ticks 再重置为其 priority
cur->status = TASK_READY;
}else{
/* 若此线程需要某事件发生后才能继续上 cpu 运行,
不需要将其加入队列,因为当前线程不在就绪队列中 */
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag 清空
/* 将 thread_ready_list 队列中的第一个就绪线程弹出,
准备将其调度上 cpu */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, \
general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}
/* 当前线程将自己阻塞,标志其状态为 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; // 置其状态为 stat
schedule(); // 将当前线程换下处理器
intr_set_status(old_status);
}
/* 将线程 pthread 解除阻塞
按常理说就绪队列中不会出现已阻塞的线程
为防止已经在就绪队列中的线程再次被添加使用了ASSERT来判断,
但 ASSERT 只是调试期间用的,最后会把它去掉*/
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);
}
/*初始化线程环境*/
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");
}
10.1.5 锁的实现
thread / sync.h
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
/* 信号量结构 */
struct semaphore {
uint8_t value;
struct list waiters; //记录在此信号量上等待(阻塞)的所有线程
};
/* 锁结构 */
struct lock{
struct task_struct* holder; // 锁的持有者
struct semaphore semaphore; // 用二元信号量实现锁
/*在未释放锁之前,有可能会再次调用重复申请此锁的函数,这样一来,内外层函数在释
放锁时会对同一个锁释放两次,为了避免这种情况的发生,
用此变量来累积重复申请的次数,释放锁时会
根据变量 holder_repeat_nr 的值来执行具体动作*/
uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
};
#endif
thread \ sync.c
#include "sync.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); // 信号量初值为 1
}
/* 信号量 down 操作 */
void sema_down(struct semaphore* psema) {
/* 关中断来保证原子操作 */
enum intr_status old_status = intr_disable();
while(psema->value == 0) {
// 若 value 为 0,表示已经被别人持有
ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
/* 当前线程不应该已在信号量的 waiters 队列中 */
if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
PANIC("sema_down: thread blocked has been in waiters_list\n");
}
/*若信号量的值等于 0,则当前线程把自己加入该锁的等待队列,
然后阻塞自己*/
list_append(&psema->waiters, &running_thread()->general_tag);
thread_block(TASK_BLOCKED); // 阻塞线程,直到被唤醒
}
/* 若 value 为 1 或被唤醒后,会执行下面的代码,也就是获得了锁*/
psema->value--;
ASSERT(psema->value == 0);
/* 恢复之前的中断状态 */
intr_set_status(old_status);
}
/* 信号量的 up 操作 */
void sema_up(struct semaphore* psema) {
/* 关中断,保证原子操作 */
enum intr_status old_status = intr_disable();
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); // 对信号量 P 操作,原子操作
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}else{
plock->holder_repeat_nr++;
}
}
/* 释放锁 plock */
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; // 把锁的持有者置空放在 V 操作之前
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的 V 操作,也是原子操作
}
10.2 用锁实现终端输出
device / console.h
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
/* 初始化终端 */
void console_init();
/* 获取终端 */
void console_acquire() ;
/* 释放终端 */
void console_release() ;
/* 终端中输出字符串 */
void console_put_str(char *str);
/* 终端中输出字符 */
void console_put_char(uint8_t char_asci);
/* 终端中输出十六进制整数 */
void console_put_int(uint32_t num);
#endif
device/ console.c
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock; // 控制台锁
/* 初始化终端 */
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();
}
/* 终端中输出十六进制整数 */
void console_put_int(uint32_t num) {
console_acquire();
put_int(num);
console_release();
}
kernel / init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
/*负责初始化所有模块*/
void init_all()
{
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init();
console_init();
}
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
#include "sync.h"
#include "console.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
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();// 打开中断,使时钟中断起作用
int n = 100;
while(n--){
console_put_str("Main ");
// console_put_int(0x2000);
}
while(1);
return 0;
}
/*在线程中运行的函数*/
void k_thread_a(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
void k_thread_b(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
Mafile
CC = clang
LIB = -I lib/kernel/ -I lib/ -I kernel/ -I thread/ -I device/
loader.bin:loader.asm inc/boot.inc
nasm -I inc/ -o loader.bin loader.asm
mbr.bin:mbr.asm inc/boot.inc
nasm -I inc/ -o mbr.bin mbr.asm
kernel.bin:kernel/main.c lib/kernel/print.asm kernel/kernel.asm kernel/interrupt.c kernel/init.c device/timer.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/main.o kernel/main.c
nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm
nasm -f elf -o build/switch.o thread/switch.asm
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/init.o kernel/init.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/timer.o device/timer.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/debug.o kernel/debug.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/memory.o kernel/memory.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/string.o lib/string.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/bitmap.o lib/kernel/bitmap.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/thread.o thread/thread.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/list.o lib/kernel/list.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/sync.o thread/sync.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/console.o device/console.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o \
build/init.o build/interrupt.o build/print.o \
build/debug.o build/kernel.o build/timer.o \
build/string.o build/memory.o build/bitmap.o \
build/thread.o build/list.o build/switch.o \
build/sync.o build/console.o
dd: dd_mbr dd_loader dd_kernel
dd_mbr:
dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc
dd_loader:
dd if=loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc
dd_kernel:
dd if=build/kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc
device / timer.c
//...略
/* 初始化 PIT8253 */
void timer_init()
{
put_str("timer_init start\n");
/* 设置 8253 的定时周期,也就是发中断的周期 */
// frequency_set( CONTRER0_PORT,\
// COUNTER0_NO,\
// READ_WRITE_LATCH,\
// COUNTER_MODE,\
// COUNTER0_VALUE);
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}
10.3 从键盘获取输入
80 后这一代人用过的键盘有三种类型:PS/2 键盘、USB 键盘和蓝牙键盘。虽然 PS/2键盘已经很少有人用了,但后来的新型键盘都是基于它发展起来的,基础原理是不变的。
以经典的 PS/2 键盘为例展开介绍。
10.3.1 键盘输入原理简介
键盘是个独立的设备,在它内部有个叫作键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,按键是否弹起。
键盘控制器可并不在键盘内部,它在主机内部的主板上,通常是 Intel 8042 或兼容芯片,它的作用是接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息。
一个键的状态要么是按下,要么是弹起,因此一个键便有两个编码。按键被按下或持续按下时的编码叫通码(通码也称为 makecode),按键被松开弹起时产生的编码叫断码(断码也称为 breakcode)。
我们只能得到键的扫描码,并不会得到键的 ASCII 码,扫描码是硬件提供的编码集,ASCII 是软件中约定的编码集,这两个是不同的编码方案。
10.3.2 键盘扫描码
键盘扫描码有三套,分别称为 scan code set 1、scan code set 2、scan code set 3。
我们在键盘的中断处理程序中只处理第一套键盘扫描码就可以了.
大多数情况下第一套扫描码中的通码和断码都是 1 字节大小。您看,表 10-1 中的通码和断码,它们的关系是:断码 = 0x80 + 通码。顺便说一句,在第二套键盘扫描码中,一般的通码是 1 字节大小,断码是在通码前再加 1 字节的 0xF0,共 2 字节,我们的 8042 工作之一就是根据第二套扫描码中通码和断码的关系将它们解码,然后按照第一套扫描码中通码和断码的关系转换成第一套扫描码。
通码和断码可以这样理解,它们都是一字节大小,最高位也就是第 7 位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为 1 的话,表示按键弹起。比如按键,不管键盘向 8042发送的是第几套扫描码,当我们按下它的时候,最终被 8042 转换成 0x1,当我们松开它的时候,最终会被 8042 转换成 0x80+0x1=0x81。
有些按键的通码和断码都以 0xe0 开头,它们占 2 字节。
这些0xe0开头的,松开是按下的逆过程,为了体现这个“逆”字,咱们看下截屏键 PrintScreen 的通码和断码。PrintScreen 的通码是 4 字节,其值为:e0,2a,e0,37,给人的感觉是由两个扩展通码组成的,即“e0,2a”为一对,“e0,37”为一对。断码依然是 0x80+通码,但由于松开是按下的逆过程,故断码值和通码值的顺序相反,即 e0,b7,e0,aa。断码中的“e0,b7”对应通码中的“e0,37”,断码中的“e0,aa”对应通码中的“e0,2a”。也就是说,当我们按下截屏键 PrintScreen 时(假设键盘使用的是第一套键盘扫描码),键盘发送的通码是以 e0->2a->e0->37 的顺序,松开 PrintScreen 时,键盘发送的断码是以 e0->b7->e0->aa 的顺序。
举例:
(1)ctrl 键先被按下。
按 L-ctrl,得到8048发送给8042一个通码 0x14,我们翻译为 0x1d,记录按下ctrl
(2)保持 ctrl 键按住不松手。
持续的 0x1d
(3)按下 a 键。
得到通码0x1c,翻译为 0x1e,组合 ctrl+a,清空记录 ctrl 和 a。
(4)松开 ctrl 键。
得到扫描码 0x1f0和0x1f(断码),翻译为 0x9d,忽略断码。
(5)松开 a 键。
得到扫描码 0xf0 和 0x1c(断码),翻译为 0x9e ,忽略断码。
强调的是按
此 8042 的输出缓冲区寄存器
也是 8 位宽度,即每次只能存储一个扫描码,要么是通码,要么是断码。
键盘只负责输入,具体的表现形式取决于字符处理软件
8048 芯片支持的是第二套扫描码,8042 为了兼容性,将接收到的第二套键盘扫描码转换成第一套扫描码。8042 是按字节来处理的,每处理一个字节的扫描码后,将其存储到自己的输出缓冲区寄存器。
Intel 8048 芯片或兼容芯片位于键盘中,它是键盘编码器
Intel 8042 芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器
结论:
- 当需要把数据从处理器发到 8042 时(数据传送尚未发生时),
0x60 端口的作用是输入缓冲区,此时应该用 out 指令写入 0x60 端口。 - 当数据已从 8048 发到 8042 时,0x60 端口的作用是输出缓冲
区,此时应该用 in 指令从 8042 的 0x60 端口(输出缓冲区寄存器)读
取 8048 的输出结果。
10.3.4 测试键盘中断处理程序
键盘中断处理程序中必须要用 in 指令读取“输出缓冲寄存器”,否则 8042 无法继续响
应键盘操作。 - 输入缓冲区寄存器
8 位宽度的寄存器,只写,键盘驱动程序通过 out 指令向此寄存器写入对 8048 的控制命令、参数等,
对于 8042 本身的控制命令也是写入此寄存器。 - 状态寄存器
8 位宽度的寄存器,只读,反映 8048 和 8042 的内部工作状态。各位意义如下。
(1)位 0:置 1 时表示输出缓冲区寄存器已满,处理器通过 in 指令读取后该位自动置 0。
(2)位 1:置 1 时表示输入缓冲区寄存器已满,8042 将值读取后该位自动置 0。
(3)位 2:系统标志位,最初加电时为 0,自检通过后置为 1。
(4)位 3:置 1 时,表示输入缓冲区中的内容是命令,置 0 时,输入缓冲区中的内容是普通数据。
(5)位 4:置 1 时表示键盘启用,置 0 时表示键盘禁用。
(6)位 5:置 1 时表示发送超时。
(7)位 6:置 1 时表示接收超时。
(8)位 7:来自 8048 的数据在奇偶校验时出错。 - 控制寄存器
8 位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。
(1)位 0:置 1 时启用键盘中断。
(2)位 1:置 1 时启用鼠标中断。
(3)位 2:设置状态寄存器的位 2。
(4)位 3:置 1 时,状态寄存器的位 4 无效。
(5)位 4:置 1 时禁止键盘。
(6)位 5:置 1 时禁止鼠标。
(7)位 6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位 7:保留位,默认为 0。
10.3.4 测试键盘中断处理程序
kernel / kernel.asm
补全 VECTOR 0x21 - 0x30
[bits 32]
%define ERROR_CODE nop
%define ZERO push 0
extern put_str
extern idt_table
section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry:
%2
push ds
push es
push fs
push gs
pushad
;如果是从从片上进入的中断,除了往从片发送EOI,主片也发送EOI
mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向从片发送
out 0x20,al ;向主片发送
push %1
call [idt_table + %1 * 4];调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry
%endmacro
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,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
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 ;保留
kernel / interrup.c
只是在之前的代码中 把键盘的中断开开,关闭时钟的中断。
outb(PIC_M_DATA, 0xfd)
#include "io.h"
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "print.h"
#include "list.h"
#include "thread.h"
#include "debug.h"
#define IDT_DESC_CNT 0x30 //支持的中断数目
#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_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0":"=g"(EFLAG_VAR));
// 中断门描述符结构体
struct gate_desc{
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount;
uint8_t attribute;
uint16_t func_offset_high_word;
};
//静态函数声明 非必须
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 是中断描述符表
// 声明引用定义在 kernel.S 中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT];
char *intr_name[IDT_DESC_CNT]; //保存异常的名字
/*定义中断处理程序数组,在kernel.asm中定义的intrXXentry
只是中断处理程序的入口,最终调用的是 ide_table 中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];
/*初始化可编程中断处理器 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, 0xfd); //1111 1110 fe 只开时钟 // 1111 1101 fd 只开键盘
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (int32_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");
}
/*通用的中断处理函数*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){
//IRQ7和IRQ5会产生伪中断,无需处理
//0x2f是从片 8259A 上的最后一个 IRQ 引脚,保留项
return;
}
/* 将光标置为 0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */
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 的地址
put_str("\npage fault addr is ");put_int(page_fault_vaddr);
}
put_str("\n!!!!!!! excetion message end !!!!!!!!\n");
// 能进入中断处理程序就表示已经处在关中断情况下
// 不会出现调度进程的情况。故下面的死循环不会再被中断
while(1);
}
/*完成一般中断处理函数注册及异常名称注册*/
static void exception_init(void){
int i;
for(i = 0; i < IDT_DESC_CNT; ++i){
/*idt_table中的函数是在进入中断后根据中断向量好调用的
见kernel/kernel.asm 的 call[idt_table + %1*4]*/
idt_table[i] = general_intr_handler;
//默认为general_intr_handler
//以后会有 register_handler注册具体的处理函数
intr_name[i] = "unknown"; //先统一为 "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] 第 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";
intr_name[0x20] = "#CLOCK";
}
/*获取当前中断状态*/
enum intr_status intr_get_status(void){
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
/*设置中断状态*/
enum intr_status intr_set_status(enum intr_status status){
return (status & INTR_ON) ? intr_enable():intr_disable();
}
/*开中断,返回之前的状态*/
enum intr_status intr_enable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
}else{
old_status = INTR_OFF;
asm volatile("sti");//关中断 cli 将IF置1
}
return old_status;
}
/*关闭中断*/
enum intr_status intr_disable(void){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
asm volatile("cli" : : : "memory");
}else{
old_status = INTR_OFF;
}
return old_status;
}
/* 在中断处理程序数组第 vector_no 个元素中
注册安装中断处理程序 function */
void register_handler(uint8_t vector_no, intr_handler function) {
/* idt_table 数组中的函数是在进入中断后根据中断向量号调用的
* 见 kernel/kernel.S 的 call [idt_table + %1*4] */
idt_table[vector_no] = function;
}
/*完成有关中断的所有初始化工作*/
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)));
asm volatile("lidt %0"::"m"(idt_operand));
put_str("idt_init done\n");
}
kernel / main.c
暂时关掉 thread_start
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
#include "sync.h"
#include "console.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
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();// 打开中断,使时钟中断起作用
int n = 100;
while(n--){
console_put_str("Main ");
// console_put_int(0x2000);
}
while(1);
return 0;
}
/*在线程中运行的函数*/
void k_thread_a(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
void k_thread_b(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
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);
return ;
}
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
kerne / init.c
加入 init_all 中 。
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "../device/keyboard.h"
#include "memory.h"
/*负责初始化所有模块*/
void init_all()
{
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init();
console_init();
keyboard_init();
}
实验证明,按一下键盘出两个 k。
device/ keyboard.c
//略
static void intr_keyboard_handler(void) {
/* 必须要读取输出缓冲区寄存器,否则 8042 不再继续响应键盘中断 */
uint8_t scancode = inb(KBD_BUF_PORT);
put_int(scancode);
return ;
}
//略
10.4 编写键盘驱动
10.4.1 转义字符介绍
10.4.2 处理扫描码
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' // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\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 不再继续响应键盘中断 */
// uint8_t scancode = inb(KBD_BUF_PORT);
// put_int(scancode);
// return ;
// }
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
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; // 关闭 e0 标记
}
break_code = ((scancode & 0x0080) != 0); // 获取 break_code
if (break_code) { // 若是断码 break_code(按键弹起时产生的扫描码)
/* 由于 ctrl_r 和 alt_r 的 make_code 和 break_code 都是两字节,
136 所以可用下面的方法取 make_code,多字节的扫描码暂不处理 */
uint16_t make_code = (scancode &= 0xff7f);
// 得到其 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)) {
/****** 代表两个字母的键 ********
0x0e 数字'0'~'9',字符'-',字符'='
0x29 字符'`'
0x1a 字符'['
0x1b 字符']'
0x2b 字符'\\'
0x27 字符';'
0x28 字符'\''
0x33 字符','
0x34 字符'.'
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 和 capslock 任意被按下
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff);
// 将扫描码的高字节置 0,主要针对高字节是 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("unknown key\n");
}
}
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
10.5 环形输入缓冲区
10.5.1 生产者与消费者问题简述
“同步”是指多个线程相互协作,共同完成一个任务,属于线程间工作步调的相互制约。
“互斥”是指多个线程“分时”访问共享资源。
生产者产生数据,消费者取出数据。
10.5.2 环形缓冲区的实现
环形缓冲区本质上依然是线性缓冲区,但其使用方式像环一样,没有固定的起始地址和终止地址,环内任何地址都可以作为起始和结束,如图 10-24 所示。
对于缓冲区的访问,我们提供两个指针,一个是头指针,用于往缓冲区中写数据,另一个是尾指针,用于从缓冲区中读数据。
头指针往缓冲区中写入一个数据后,使头指针加 1 指向缓冲区中下一个可写入数据的地址,每次通过尾指针从缓冲区中读取一个数据后,使尾指针加 1 指向缓冲区中下一个可读入数据的地址,也就是说,缓冲区相当于一个队列,数据在队列头被写入,在队尾处被读出。
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; // 队尾,数据从队尾处读出
};
/* 键盘初始化 */
void keyboard_init() ;
/* 判断队列是否满 */
bool ioq_full(struct ioqueue* ioq);
/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq);
#endif
device / ioqueue.c
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
/* 初始化 io 队列 ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化 io 队列的锁
ioq->producer = NULL;// 生产者和消费者置空
ioq->consumer = NULL;
ioq->head = 0;//队列的首尾指针指向缓冲区数组第 0 个位置
ioq->tail = 0;
}
/* 返回 pos 在缓冲区中的下一个位置值 */
static int32_t next_pos(int32_t pos) {
return (pos + 1) % bufsize;
}
/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
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); // 唤醒消费者
}
}
10.5.3 添加键盘输入缓冲区
device / keyboard.c
在 202行,时候把处理好的字符不打印出来,选择放入队列中。
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#include "keyboard.h"
#include "ioqueue.h"
#define KBD_BUF_PORT 0x60 // 键盘 buffer 寄存器端口号为 0x60
/* 用转义字符定义部分控制字符 */
#define esc '\033' // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\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 不再继续响应键盘中断 */
// uint8_t scancode = inb(KBD_BUF_PORT);
// put_int(scancode);
// return ;
// }
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
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; // 关闭 e0 标记
}
break_code = ((scancode & 0x0080) != 0); // 获取 break_code
if (break_code) { // 若是断码 break_code(按键弹起时产生的扫描码)
/* 由于 ctrl_r 和 alt_r 的 make_code 和 break_code 都是两字节,
136 所以可用下面的方法取 make_code,多字节的扫描码暂不处理 */
uint16_t make_code = (scancode &= 0xff7f);
// 得到其 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)) {
/****** 代表两个字母的键 ********
0x0e 数字'0'~'9',字符'-',字符'='
0x29 字符'`'
0x1a 字符'['
0x1b 字符']'
0x2b 字符'\\'
0x27 字符';'
0x28 字符'\''
0x33 字符','
0x34 字符'.'
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 和 capslock 任意被按下
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff);
// 将扫描码的高字节置 0,主要针对高字节是 e0 的扫描码
char cur_char = keymap[index][shift]; // 在数组中找到对应的字符
/* 只处理 ASCII 码不为 0 的键 */
if(cur_char){
// put_char(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("unknown key\n");
}
}
/* 键盘初始化 */
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");
}
/kernel/interrupt.c 打开时钟和键盘中断
static void pic_init(void) {
//…略
/* 测试键盘,只打开键盘中断,其他全部关闭 */
outb (PIC_M_DATA, 0xfc);
outb (PIC_S_DATA, 0xff);
//…略
59 }
device / keyboard.h
1 #ifndef __DEVICE_KEYBOARD_H
2 #define __DEVICE_KEYBOARD_H
3 void keyboard_init(void);
4 extern struct ioqueue kbd_buf; //添加队列
5 #endif
kernel / main.c
在 main.c 中添加消费者线程
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
#include "sync.h"
#include "console.h"
#include "keyboard.h"
#include "ioqueue.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
int main(void){
put_str("I am kernel\n");
init_all();
thread_start("consumer_a", 31, k_thread_a, "A_");
thread_start("consumer_b", 31, k_thread_b, "B_");
intr_enable();// 打开中断,使时钟中断起作用
while(1);
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);
}
while(1);
}
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);
}
while(1);
}