第十章 输入输出系统
本文是对《操作系统真象还原》第十章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 fdx-xdf/MyTinyOS 。
上一章遗留的问题
在上一节中,我们实现了多线程轮转调度,但是当我们运行一段时间后,就会发生GP异常
待解决的几个问题:
- 输出中,有些字符串看似少了字符;
- put_str()操作的原子性没有得到保证,当下可通过在put_str()操作前后增加开关中断函数暂时保证其执行的原子性。
- 输出中,有大片连续的空缺;
- GP异常;
出现的字符丢失、大片空缺、GP异常问题,是由于字符串写入操作没有使用原子操作所导致的。
字符串写入分为3个步骤:
- 获取光标值
- 将光标值转为字节地址,在地址中写入字符
- 更新光标值
线程调度工作的核心是线程的上下文保护与还原。
这里访问的公共资源是显存,任务调度的时候,如果线程A执行到了获取光标值被中断,当线程A还原执行的时候,此时光标值已经被改变了,而线程A会从第二个步骤开始执行,所以导致字符丢失、字符出现的位置不对的问题。
GP异常则是在写入光标值的时候发生中断所导致的,导致光标被赋予了错误的值,甚至超出了边界,导致了GP异常。
根本原因就是访问公共资源需要多个操作,而这多个操作执行不具有原子性,导致被任务调度器断开了,从而让其他线程有机会破坏显存和光标寄存器这两类公共资源现场。
用锁实现终端输出
线程的阻塞与唤醒
以下函数定义在thread.c中,由于是对线程状态进行操作,所以我们需要原子操作,函数的前面需要关闭中断,最后需要恢复中断状态。
线程阻塞函数先将自己状态改为stat,然后进行调度,让其他线程上处理机。
/* 当前线程将自己阻塞,标志其状态为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; // 状态改为statschedule(); // 将当前线程换下处理器/* 待当前线程被解除阻塞后才继续运行下面的intr_set_status */intr_set_status(old_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);
}
信号量的实现
/thread/sync.h
/*信号量结构体*/
struct semaphore
{// 信号量值uint8_t value;// 阻塞在当前信号量上的线程的阻塞队列struct list waiters;
};
void sema_init(struct semaphore *psema, uint8_t value);
void sema_down(struct semaphore *psema);
void sema_up(struct semaphore *psema);
信号量结构体主要有两个成员,分别是信号量的值和该信号量的阻塞队列。
/thread/sync.c
#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"
#include "thread.h"/* 初始化信号量 */
void sema_init(struct semaphore *psema, uint8_t value)
{psema->value = value;list_init(&psema->waiters);
}/* 信号量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);
}
三个操作,down,up,init,其中down和up也都是原子操作。
down操作首先判断当前中断的value,如果已经是0,则将当前线程加入到该信号量的阻塞队列,然后阻塞该线程,当value为1或被唤醒后,就会执行下面获得锁的操作,即value--
up操作就是先判断信号量所在阻塞队列是否有等待者,如果有的话就去唤醒进程,然后value++。
锁的实现
锁是为了保证信号量的互斥访问存在的。
/thread/sync.h
/* 锁结构 */
struct lock {struct task_struct* holder; // 锁的持有者struct semaphore semaphore; // 用二元信号量实现锁uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
};void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
锁的结构体有三个成员,分别是锁的持有者,信号量,锁的持有者重复申请锁的次数
/thread/sync.c
/* 初始化锁plock */
void lock_init(struct lock *plock)
{plock->holder = NULL;plock->holder_repeat_nr = 0;sema_init(&plock->semaphore, 1);
}/* 获取锁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;}elseplock->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操作,也是原子操作
}
三个操作,也都是init、获取、释放。
获取锁时候首先排除重复申请锁的情况,若重复申请,则plock->holder_repeat_nr++,再释放时也要重复释放,正常情况下就是sema_down,信号量减1,然后锁的所有者给到当前线程,然后holder_repeat_nr=1
释放锁的时候则先要判断有没有重复申请的情况,有的话就plock->holder_repeat_nr--,然后正式释放的时候就持有者为空,plock->holder_repeat_nr = 0;,然后sema_up,执行V操作,注意sema_up必须放到最后,否则执行完sema_up可能被中断,该信号量被别的进程申请了,但是轮转到原进程的时候又把锁的持有者和计数归零了。
输出终端的实现
/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);
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();
}/* 终端中输出16进制整数 */
void console_put_int(uint32_t num) {console_acquire(); put_int(num); console_release();
}
很简单,不再解释,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);}
}
再次编译运行,一切正常,不会再出现上一章遗留的问题
键盘驱动程序的编写与输入系统
键盘输入原理简介
键盘编码介绍
-
一个键的状态要么是按下,要么是弹起,因此一个键有两个编码,这两个编码统称扫描码,一个键的扫描码由通码和断码组成。
-
按键被按下时的编码叫通码,表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为makecode。
-
按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。
-
断码=0x80+通码 即第八位的1
-
无论是通码还是断码,它们基本都是一字节大小
-
最高位也就是第7位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为1的话,表示按键弹起。
-
但有些按键的通码和断码都以0xe0开头,它们占2字节
8048芯片
无论是按下键,或是松开键,当键的状态改变后,键盘中的 8048 芯片把按键对应的扫描码(通码或断码)发送到主板上的 8042 芯片,8042处理后保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
8042芯片
我们在编写键盘中断程序时主要用到的就是8042
8042芯片负责接收来自键盘的扫描码,将它转换为标准的字符编码(如ASCII码),并保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
如下所示,8042共有4个8位寄存器,这4个寄存器共用2个端口
8042是连接8048和处理器的桥梁,8042存在的目的是:为了处理器可以通过它控制8048的工作方式,然后让 8048的工作成果通过 8042回传给处理器。此时8042就相当于数据的缓冲区、中转站,根据数据被发送的方向,8042的作用分别是输入和输出。
代码逻辑
- 添加键盘中断的中断向量号
- 添加键盘中断处理程序
- 构建中断描述符
- 打开键盘中断
添加中断向量号
/kernel/kernel.S
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/interrupt.c
#define IDT_DESC_CNT 0x30 //支持的中断描述符个数48/* 初始化可编程中断控制器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); // 主片除了最低位其他全部置为1outb(PIC_S_DATA, 0xff); // 从片全部置1, 全屏蔽put_str(" pic_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); // 键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了outb(PIC_S_DATA, 0xff);put_str("pic_init done\n");
}
接下来编写键盘驱动程序
/device/keyboard.h
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void);
#endif
/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读走键盘8048传递过来的数据,否则8042不会接收后续8048传递过来的数据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");
}
目前键盘驱中断处理程序做测试用,无论键盘的哪个按键被按下或者松开,都会只显示字符k
,并未对键盘按键的情况做处理,后续我们再修改键盘驱动程序
main.c如下,编译即可
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
void thread_work_a(void *arg);
void thread_work_b(void *arg);int main(void)
{put_str("I am kernel\n");init_all();// thread_start("thread_work_a", 31, thread_work_a, "pthread_A ");// thread_start("thread_work_b", 8, thread_work_b, "pthread_B ");/*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/intr_enable();while (1);// {// console_put_str("Main ");// }return 0;
}/* 线程执行函数 */
void thread_work_a(void *arg)
{char *para = (char *)arg;while (1){console_put_str(para);}
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{char *para = (char *)arg;while (1){console_put_str(para);}
}
进一步完善键盘中断处理程序
#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' // esc 和 delete都没有\转义字符这种形式,用8进制代替
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'#define char_invisible 0 // 功能性 不可见字符均设置为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// 二维数组,用于记录从0x00到0x3a通码对应的按键的两种情况(如0x02,不加shift表示1,加了shift表示!)的ascii码值
// 如果没有,则用ascii0替代
char keymap[][2] = {/* 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}};int ctrl_status = 0; // 用于记录是否按下ctrl键
int shift_status = 0; // 用于记录是否按下shift
int alt_status = 0; // 用于记录是否按下alt键
int caps_lock_status = 0; // 用于记录是否按下大写锁定
int ext_scancode = 0; // 用于记录是否是扩展码
先定义一些宏,一些控制字符的通码断码及其状态,以及一个二维数组,用于表示shift+字符
static void intr_keyboard_handler(void)
{int break_code; // 用于判断传入值是否是断码uint16_t scancode = inb(KBD_BUF_PORT); // 从8042的0x60取出码值if (scancode == 0xe0) // 如果传入是0xe0,说明是处理两字节按键的扫描码,那么就应该立即退出去取出下一个字节{ext_scancode = 1; // 打开标记,记录传入的是两字节扫描码return; // 退出}if (ext_scancode) // 如果能进入这个if,那么ext_scancode==1,说明上次传入的是两字节按键扫描码的第一个字节{scancode = ((0xe000) | (scancode)); // 合并扫描码,这样两字节的按键的扫描码就得到了完整取出ext_scancode = 0; // 关闭记录两字节扫描码的标志}break_code = ((scancode & 0x0080) != 0); // 断码=通码+0x80,如果是断码,那么&出来结果!=0,那么break_code值为1if (break_code) // 如果是断码,就要判断是否是控制按键的断码,如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序{uint16_t make_code = (scancode &= 0xff7f); // 将扫描码(现在是断码)还原成通码if (make_code == ctrl_l_make || make_code == ctrl_r_make)ctrl_status = 0; // 判断是否松开了ctrlelse if (make_code == shift_l_make || make_code == shift_r_make)shift_status = 0; // 判断是否松开了shiftelse if (make_code == alt_l_make || make_code == alt_r_make)alt_status = 0; // 判断是否松开了altreturn;}// 来到这里,说明不是断码,而是通码,这里的判断是保证我们只处理这些数组中定义了的键,以及右alt和ctrl。else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){int shift = 0; // 确定是否开启shift的标志,先默认设置成0uint8_t index = (scancode & 0x00ff); // 将扫描码留下低字节,这就是在数组中对应的索引if (scancode == ctrl_l_make || scancode == ctrl_r_make) // 如果扫描码是ctrl_l_make,或者ctrl_r_make,说明按下了ctrl{ctrl_status = 1;return;}else if (scancode == shift_l_make || scancode == shift_r_make){shift_status = 1;return;}else if (scancode == alt_l_make || scancode == alt_r_make){alt_status = 1;return;}else if (scancode == caps_lock_make) // 大写锁定键是按一次,然后取反{caps_lock_status = !caps_lock_status;return;}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_status) // 如果同时按下了shift键shift = true;}else{ // 默认为字母键if (shift_status + caps_lock_status == 1)shift = 1; // shift和大写锁定,那么判断是否按下了一个,而且不能是同时按下,那么就能确定是要开启shift}put_char(keymap[index][shift]); // 打印字符return;}elseput_str("unknown key\n");return;
}/* 键盘初始化 */
void keyboard_init() {put_str("keyboard init start\n");register_handler(0x21, intr_keyboard_handler);put_str("keyboard init done\n");
}
然后是中断处理程序,先取出来码值,看是否是0xe0,是的话就先保存状态然后退出,然后看是通码还是断码,断码的话就去设置对应控制字符的状态,然后退出。通码的话就先看是不是控制字符,如果是的话就修改对应状态信息,不是的话就证明是正常字符,然后判断shift和CapsLk状态,从二维数组中取出来值打印
然后在键盘初始化的时候注册即可。
环形缓冲区
到现在,我们的键盘驱动仅能够输出咱们所键入的按键,这还没有什么实际用途。
在键盘上操作是为了与系统进行交互,交互的过程一般是键入各种shell 命令,然后shel 解析并执行。
shell 命令是由多个字符组成的,并且要以回车键结束,因此咱们在键入命令的过程中,必须要找个缓冲区把已键入的信息存起来,当凑成完整的命令名时再一并由其他模块处理。
本节咱们要构建这个缓冲区
- 环形缓冲区本质上是用数组进行表示,并使用模运算实现区域的回绕
- 当缓冲区满时,要阻塞生产者继续向缓冲区写入字符
- 当缓冲区空时,要阻塞消费者取字符
以下是具体代码
/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 ioqueue_init(struct ioqueue* ioq);
bool ioq_full(struct ioqueue* ioq);
bool ioq_empty(struct ioqueue* ioq);
char ioq_getchar(struct ioqueue* ioq);
void ioq_putchar(struct ioqueue* ioq, char byte);
#endif
/device/ioqueue.c
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue *ioq)
{ioq->consumer = ioq->producer = NULL;lock_init(&ioq->lock);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)
{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;
}
较为简单,看注释即可然后编译运行