一, 到目前为止的程序流程图
为了让大家清楚目前的程序进度, 画了到目前为止的程序流程图, 如下.(红色部分就是我们今天要实现的)
二, 简单打通键盘中断
既然要打通键盘中断, 那必然需要你回顾一下 [自制操作系统 08] 中断 所讲述的外部中断的流程, 下面我把图贴上.
如图所示, 将上图中的某外部设备, 换成下图中的具体的键盘, 就是键盘中断流程啦. 简单说就是:
因此每当有击键发生时, 键盘中的设备 8048 会把键盘扫描码发给主板上的设备 8042.
8042 是按字节来处理的, 每处理一个字节的扫描码后, 将其存储到自己的 输出缓冲区 寄存器.
然后向中断代理 8059A 发中断信号, 这样我们的键盘 中断处理程序 通过读取 8042 的输出缓冲区寄存器, 会获得键盘扫描码.
那我们 CPU 收到的中断号是多少呢? 我们看下面两段代码
- 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, 也就是目前只接受时钟产生的中断 */
- // 测试键盘中断 0xfd
- outb (PIC_M_DATA, 0xfd);
- outb (PIC_S_DATA, 0xff);
- ...
- }
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 ; 保留
我们将 8059A 这个设备的 IR0 端口设置了起始中断号为 0x20, 这是我们自己定义的, 也就是说可以改的, 再看下硬件定死的东西.
可以看出, 键盘被固定连接在了 IR1 口上. 也就是说, 通过硬件的固定连接, 以及我们软件将 IR0 设定为了初始中断号 0x20, 所以导致了我们按下键盘后的中断向量号为 20. 这块说出来真的很简单很直观, 但我刚学的时候, 硬是没想明白这个道理.
OK, 大功告成, 接下来我们用之前已有的代码就好了, 就是将一段中断程序, 对应给 0x21 这个中断向量号.
- 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');
- 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");
- }
- init.c
- ...
- mem_init(); // 初始化内存管理
- thread_init(); // 初始化线程相关结构
- console_init(); // 控制台初始化
- keyboard_init(); // 键盘初始化
- ...
运行后可以看到, 每次我们按下键盘, 就在屏幕上输出'k', 由于我们没做其他处理, 不论按什么键, 都会只在屏幕上输出'k', 有个细节就是按下一个键会输出两个'k', 是因为键盘的按下和弹起是会传输两个键盘码, 也会发生两次中断.
虽然只是简简单单输出一个'k', 但还是很兴奋, 我觉得往往最难的地方就是打通和硬件的交互, 把控制权交给软件, 剩下的事就掌控在我们手中啦, 我们继续往下看.
三, 实现输入字符缓冲区
这块我懒了, 不想看代码了, 直接把书中的代码全部 copy 过来了. 一是因为这块是为后续的用户交互进程, 也就是 shell 做准备的; 二是因为这块十分繁琐, 又很好理解, 简单说就是, 把输入进来的键盘码转换成 ASCII 码, 并输出到一个缓冲区 (我们用队列结构实现) 里, 另外意思一下跑两个线程从缓冲区里拿数据, 直接输出到屏幕上.
那不难想象后续的用户进程, 无非就是 shell 进程读取缓冲区数据, 输出到屏幕, 遇到回车后把整个字符串理解一下, 交给指定程序去处理, 我猜的啊.
所以这块直接把代码放上来.
- #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 = 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) {
- 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); // 唤醒消费者
- }
- }
- /* 返回环形缓冲区中的数据长度 */
- uint32_t ioq_length(struct ioqueue* ioq) {
- uint32_t len = 0;
- if (ioq->head>= ioq->tail) {
- len = ioq->head - ioq->tail;
- } else {
- len = bufsize - (ioq->tail - ioq->head);
- }
- return len;
- }
- ioqueue.c
- #include "interrupt.h"
- #include "io.h"
- #include "global.h"
- #include "ioqueue.h"
- #define KBD_BUF_PORT 0x60 // 键盘 buffer 寄存器端口号为 0x60
- #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
- struct ioqueue kbd_buf; // 定义键盘缓冲区
- /* 定义以下变量记录相应键是否按下的状态,
- * ext_scancode 用于记录 makecode 是否以 0xe0 开头 */
- static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
- /* 以通码 make_code 为索引的二维数组 */
- static char keymap[][2] = {
- /* 扫描码 未与 shift 组合 与 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) {
- put_char('k');
- inb(KBD_BUF_PORT);
- return;
- /* 这次中断发生前的上一次中断, 以下任意三个键是否有按下 */
- 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);
- /* 若扫描码是 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 都是两字节,
- 所以可用下面的方法取 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]; // 在数组中找到对应的字符
- /* 如果 cur_char 不为 0, 也就是 ascii 码为除'\0'外的字符就加入键盘缓冲区中 */
- if (cur_char) {
- /***************** 快捷键 ctrl+l 和 ctrl+u 的处理 *********************
- * 下面是把 ctrl+l 和 ctrl+u 这两种组合键产生的字符置为:
- * cur_char 的 asc 码 - 字符 a 的 asc 码, 此差值比较小,
- * 属于 asc 码表中不可见的字符部分. 故不会产生可见字符.
- * 我们在 shell 中将 ascii 值为 l-a 和 u-a 的分别处理为清屏和删除输入的快捷键 */
- if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
- cur_char -= 'a';
- }
- /****************************************************************/
- /* 若 kbd_buf 中未满并且待加入的 cur_char 不为 0,
- * 则将其加入到缓冲区 kbd_buf 中 */
- if (!ioq_full(&kbd_buf)) {
- 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");
- register_handler(0x21, intr_keyboard_handler);
- put_str("keyboard init done\n");
- put_str("keyboard init start\n");
- ioqueue_init(&kbd_buf);
- register_handler(0x21, intr_keyboard_handler);
- put_str("keyboard init done\n");
- }
- keyboard.c
- main.c
- #include "print.h"
- #include "init.h"
- #include "thread.h"
- #include "interrupt.h"
- #include "ioqueue.h"
- #include "keyboard.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, "AOUT_");
- thread_start("consumer_b", 31, k_thread_b, "BOUT_");
- 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);
- console_put_str("\n");
- }
- 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);
- console_put_str("\n");
- }
- intr_set_status(old_status);
- }
- }
第一个 ioqueue.c 就是个队列的实现类, 准确说是个线程安全队列. 第二个 keyboard.c 从我们原来无论按什么键都输出'k', 变成了把键盘码转换成 ASCII, 还包括对 controll 键等处理, 反正就是一堆杂事, 转换成我们平时认知中按键应该对应的字符, 把这个字符的 ASCII 码放入队列, 等着 main.c 的两个线程取出来, 打印在屏幕上, 就这么点事, 但每个字符都要细心处理, 十分繁琐.
所以不再赘述, 不影响我们理解操作系统主流程, 运行后结果如下
写在最后: 开源项目和课程规划
如果你对自制一个操作系统感兴趣, 不妨跟随这个系列课程看下去, 甚至加入我们(下方有公众号和小助手微信), 一起来开发.
参考书籍
《操作系统真相还原》这本书真的赞! 强烈推荐
项目开源
项目开源地址: https://gitee.com/sunym1993/flashos
当你看到该文章时, 代码可能已经比文章中的又多写了一些部分了. 你可以通过提交记录历史来查看历史的代码, 我会慢慢梳理提交历史以及项目说明文档, 争取给每一课都准备一个可执行的代码. 当然文章中的代码也是全的, 采用复制粘贴的方式也是完全可以的.
如果你有兴趣加入这个自制操作系统的大军, 也可以在留言区留下您的联系方式, 或者在 gitee 私信我您的联系方式.
课程规划
本课程打算出系列课程, 我写到哪觉得可以写成一篇文章了就写出来分享给大家, 最终会完成一个功能全面的操作系统, 我觉得这是最好的学习操作系统的方式了. 所以中间遇到的各种坎也会写进去, 如果你能持续跟进, 跟着我一块写, 必然会有很好的收货. 即使没有, 交个朋友也是好的哈哈.
目前的系列包括
[自制操作系统 01] 硬核讲解计算机的启动过程
[自制操作系统 02] 环境准备与启动区实现
[自制操作系统 03] 读取硬盘中的数据
[自制操作系统 04] 从实模式到保护模式
[自制操作系统 05] 开启内存分页机制
[自制操作系统 06] 终于开始用 C 语言了, 第一行内核代码!
[自制操作系统 07] 深入浅出特权级
[自制操作系统 08] 中断
[自制操作系统 09] 中断的代码实现
[自制操作系统 10] 内存管理系统
[自制操作系统 11] 中场休息之细节是魔鬼
[自制操作系统 12] 熟悉而陌生的多线程
[自制操作系统 13] 锁
微信公众号
我要去阿里(woyaoquali)
小助手微信号
Angel(angel19980323)
来源: https://www.cnblogs.com/flashsun/p/12490765.html