《操作系统真象还原》第十章(二)—— 键盘驱动程序的编写与输入系统

ops/2024/12/28 23:32:13/

章节任务介绍

在上一节中,我们介绍了操作系统的同步机制互斥锁的内容,并手动实现了互斥锁,同时实现了线程安全的屏幕打印。 至此,我们算是基本完成了操作系统的“输出”功能,但目前为止我们的输入仍旧依赖于程序,而不是用户操控的键盘,本节我们将正式完成操作系统的“输入”

任务简介

本节的主要任务有:

  1. 键盘驱动测试

  2. 编写键盘驱动程序

  3. 基于环形缓冲区的键盘驱动程序

前置知识

键盘输入原理简介

键盘编码介绍
  • 一个键的状态要么是按下,要么是弹起,因此一个键有两个编码,这两个编码统称扫描码,一个键的扫描码由通码和断码组成

  • 按键被按下时的编码叫通码,表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为makecode

  • 按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode

  • 断码=0x80+通码

以下是各个键的扫描码

  • 无论是通码还是断码,它们基本都是一字节大小

  • 最高位也就是第7位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为1的话,表示按键弹起。

  • 有些按键的通码和断码都以0xe0开头,它们占2字节

8048芯片

无论是按下键,或是松开键,当键的状态改变后,键盘中的 8048 芯片把按键对应的扫描码(通码或断码)发送到主板上的 8042 芯片8042处理后保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理

因此,8048是键盘上的芯片负责监控键盘哪个键被按下或者松开,并将扫描码发送给8042芯片

8042芯片

8042芯片负责接收来自键盘的扫描码,将它转换为标准的字符编码(如ASCII码),并保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

如下所示,8042共有4个8位寄存器,这4个寄存器共用2个端口

8042是连接8048和处理器的桥梁,8042存在的目的是:为了处理器可以通过它控制8048的工作方式,然后让 8048的工作成果通过8042回传给处理器。此时8042就相当于数据的缓冲区、中转站,根据数据被发送的方向,8042的作用分别是输入和输出。

键盘中断处理程序测试

本节我们将简单编写一个键盘驱动程序,用以测试键盘输入过程。我们首先输入一下逻辑

  • 当键盘键入按键后,8048芯片就将扫描码发送给8042,然后8042触发中断信号,接着触发中断处理程序

  • 因此我们需要做的其实就是,编写键盘中断对应的中断处理程序,程序的逻辑就是读取8042接收到的扫描码,然后按照扫描码与键盘的对应关系,将按键显示在屏幕上或者其余操作

  • 由于最终目的是要编写键盘中断处理程序,我们需要首先在中断描述符表中添加键盘中断的中断描述符

  • 要添加中断描述符,就要知道键盘中断对应的中断向量号

故而,我们的逻辑其实很简单

  • 添加键盘中断的中断向量号和中断入口地址

  • 添加键盘中断处理程序

  • 构建中断描述符

  • 打开键盘中断

/kernel/kernel.S

首先在intr_entry_table中添加键盘中断的中断处理程序入口,键盘中断的中断号是0x21,为方便后续代码编写,以下添加了所有的中断号

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

修改中断描述符的总数量,原来只有33个中断描述符

#define IDT_DESC_CNT 0x30

然后打开键盘中断

/*初始化可编程中断控制器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

下述键盘驱动程序代码只做测试用,无论键盘的哪个按键被按下或者松开,都会只显示字符k,并未对键盘按键的情况做处理,后续我们再修改键盘驱动程序

#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");
}

添加键盘中断初始化

/kernel/init.c

/*负责初始化所有模块 */
void init_all()
{put_str("init_all\n");idt_init();      // 初始化中断mem_init();      // 初始化内存管理系统thread_init();   // 初始化线程相关结构timer_init();    // 时钟中断初始化console_init();  // 终端初始化keyboard_init(); // 键盘中断初始化
}

修改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);}
}

编译运行

mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c
# 编译keyboard文件
gcc-4.4 -o $(pwd)/bin/keyboard.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/keyboard.c#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o  $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/keyboard.o $(pwd)/bin/console.o $(pwd)/bin/timer.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9#rm -rf bin/*

结果如下所示,我们按下了空格键字母q键,由于按下和松开都会触发中断,因此每个按键会显示两次字符k,故共有四个字符k

编写键盘驱动程序

上一小节,我们测试了键盘驱动程序的流程,在这一小节,我们修改键盘驱动程序,以实现当按键被按下时,屏幕上会显示对应的字符

/device/keyboard.c

数据准备与定义

按照扫描码表格定义每个扫描码对应的按键情况

/*
二维数组,用于记录从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}};

其中不可见字符以及控制字符的显式定义宏为

#define esc '\033' // esc 和 delete都没有\转义字符这种形式,用8进制代替
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'// 功能性 不可见字符均设置为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

由于控制字符常常和其余键作为组合键使用,因此需要记录控制字符的状态

int ctrl_status = 0;      // 用于记录是否按下ctrl键
int shift_status = 0;     // 用于记录是否按下shift
int alt_status = 0;       // 用于记录是否按下alt键
int caps_lock_status = 0; // 用于记录是否按下大写锁定
int ext_scancode = 0;     // 用于记录是否是扩展码

键盘驱动程序逻辑

首先从8042芯片中读取扫描码,需要注意的是,8042每次只接受一个字节的扫描码,但是有些按键的扫描码是两个字节,因此会触发两次中断,并向8042依次发送这两个字节的数据

因此,需要根据第一次接受到的扫描码是否是0xe0,如果是,说明该按键的扫描码是由两个字节组成的,需要再次接受一个字节的扫描码,然后才能拼接出完整的两个字节的扫描码

    // 从0x60端口读入一个字uint16_t scancode = inb(KBD_BUF_PORT);// 如果传入是0xe0,说明是处理两字节按键的扫描码,那么就应该立即退出去取出下一个字节if (scancode == 0xe0){ext_scancode = 1;return;}// 如果能进入这个if,那么ext_scancode==1,说明上次传入的是两字节按键扫描码的第一个字节if (ext_scancode){scancode |= (0xe000); // 合并扫描码,这样两字节的按键的扫描码就得到了完整取出ext_scancode = 0;}

接受到扫描码后需要判断是断码还是通码,然后分别进行处理,而由以上我们知道

断码=0x80+通码

因此有

    // 断码=通码+0x80,如果是断码,那么&出来结果!=0,那么break_code值为1int break_code = ((scancode & 0x0080) != 0);

如果是断码,说明松开了按键

  • 如果松开的按键是字母键,则不进行处理

  • 如果松开的是控制按键,则清除对应控制按键的状态(因为控制按键在按下的时候我们会置状态位,因此松开的时候需要清除)

    // 如果是断码,就要判断是否是控制按键的断码// 如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序if (break_code){// 将扫描码(现在是断码)还原成通码uint16_t make_code = (scancode &= 0xff7f);if (make_code == ctrl_l_make || make_code == ctrl_r_make)ctrl_status = 0;if (make_code == shift_l_make || make_code == shift_r_make)shift_status = 0;if (make_code == alt_l_make || make_code == alt_r_make)alt_status = 0;return;}

以下是通码对应的处理逻辑

  • 判断按键是否是控制键(ctrl、alt、shift、大写锁定键):如果是,说明用户可能在使用组合键,因此首先记录该控制按键的状态是被按下了,然后返回接受下一个中断的按键(这里我们并没有实现具体的组合键处理情况)

  • 判断按键是否是特殊两个字母的键(和shift可以组合使用的键):如果是,则判断shift按键的状态是否被按下,如果被按下就打印转换的字符,如果shift状态没有被按下,就直接打印对应字符即可

  • 判断正常字母按键:正常字母按键可能会和shift或者大写锁定键组合使用,但只有一个会起作用,但无论是哪个起作用,都将shift状态位置为1,表示接下来该字母输出的是大写

    // 如果是通码,首先保证我们只处理这些数组中定义了的键,以及右alt和ctrl。else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){// 是否开启shift转换标志int shift = 0;// 将扫描码留下低字节,这就是在数组中对应的索引uint8_t index = (scancode & 0x00ff);if (scancode == ctrl_l_make || scancode == ctrl_r_make){ctrl_status = 1;return;}if (scancode == shift_l_make || scancode == shift_r_make){shift_status = 1;return;}if (scancode == alt_l_make || scancode == alt_r_make){alt_status = 1;return;}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;}

编译运行

如下是运行的结果,我在键盘输入的是“nihao hello world

可以看到程序正常显示了我的按键情况

环形输入缓冲区

到现在,我们的键盘驱动仅能够输出咱们所键入的按键,这还没有什么实际用途。

在键盘上操作是为了与系统进行交互,交互的过程一般是键入各种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队列*/
void ioqueue_init(struct ioqueue *ioq)
{lock_init(&ioq->lock);ioq->producer = ioq->consumer = NULL;ioq->head = ioq->tail = 0;
}
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;
}
/*
使当前生产者或消费者在此缓冲区上等待
传入参数是ioq->producer或者ioq->consumer
*/
static void ioq_wait(struct task_struct **waiter)
{ASSERT(*waiter == NULL && waiter != NULL);// *waiter = running_thread();thread_block(TASK_BLOCKED);
}
/* 唤醒waiter */
static void ioq_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);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)ioq_wakeup(&ioq->producer);return byte;
}
/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue *ioq, char byte)
{ASSERT(intr_get_status() == INTR_OFF);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)ioq_wakeup(&ioq->consumer);
}

接下来我们需要进行测试

  • 生产者自然是键盘驱动程序

  • 为了模拟消费者,我们在main函数中添加两个子线程,两个线程都用于从缓冲区中取字符

  • 由于有两个线程取字符,因此每次按下键盘后,字符可能由不同的线程接收并显示在屏幕,我们在代码中显示每次显示的字符是由哪个线程打印的

之前为了测试键盘中断,我们关闭了时钟中断,仅打开了键盘中断,而此时由于要使用子线程,因此我们需要开启时钟中断

/kernel/interrupt.c

/*初始化可编程中断控制器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);// 同时打开时钟中断与键盘中断outb(PIC_M_DATA, 0xfc);outb(PIC_S_DATA, 0xff);put_str("pic_init done\n");
}

/kernel/main.c

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"/* 临时为测试添加 */
#include "ioqueue.h"
#include "keyboard.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("consumer_a", 31, thread_work_a, "consumer_A:");thread_start("consumer_b", 8, thread_work_b, "consumer_B:");/*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/intr_enable();while (1);return 0;
}/* 线程执行函数 */
void thread_work_a(void *arg)
{char *para = (char *)arg;while (1){enum intr_status old_status = intr_disable();if (!ioq_empty(&kbd_buf)){console_put_str(para);char byte = ioq_getchar(&kbd_buf);console_put_char(byte);console_put_str("   ");}intr_set_status(old_status);}
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{char *para = (char *)arg;while (1){enum intr_status old_status = intr_disable();if (!ioq_empty(&kbd_buf)){console_put_str(para);char byte = ioq_getchar(&kbd_buf);console_put_char(byte);console_put_str("   ");}intr_set_status(old_status);}
}

编译运行

mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c
# 编译keyboard文件
gcc-4.4 -o $(pwd)/bin/keyboard.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/keyboard.c
# 编译ioqueue文件
gcc-4.4 -o $(pwd)/bin/ioqueue.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/ioqueue.c#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o  $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/console.o $(pwd)/bin/keyboard.o $(pwd)/bin/timer.o $(pwd)/bin/ioqueue.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9#rm -rf bin/*

以下是运行结果


http://www.ppmy.cn/ops/145765.html

相关文章

蓝桥杯——神奇的数组

题目描述 欢迎来到异或王国&#xff0c;这是一个特殊的王国&#xff0c;对于一个数组它的价值并非所有数相加&#xff0c;而是所有数异或得到的值。 当然对于某些神奇的数组来说值可能是一样的&#xff0c;给定一个长度为 n 的数组 a &#xff0c;请问有多少个子数组是神奇数…

CCF-GESP 等级考试 2023年9月认证C++五级真题解析

2023年9月真题 一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 正确答案&#xff1a;A 解析&#xff1a;考察知识点&#xff1a;计算机基础 本题属于考察计算机基础知识知识。手写板是输入信息的设备&#xff0c;选A。 正确答案&#xff1a;D 解析&#xff1a;…

Unity中如何修改Sprite的渲染网格

首先打开SpriteEditor 选择Custom OutLine,点击Genrate 则在图片边缘会出现边缘线&#xff0c;调整白色小方块可以调整边缘 调整后&#xff0c;Sprite就会按照调整后的网格渲染了。 如何在UI中使用&#xff1f; 只要在UI的Image组件中选择Use Sprite Mesh 即可 结果&#xff1…

Java全栈项目-校园公告管理系统开发实践

项目简介 校园公告管理系统是一个基于Spring Boot Vue.js的全栈Web应用&#xff0c;旨在为学校提供一个现代化的公告发布和管理平台。系统支持公告的发布、编辑、查询和分类管理&#xff0c;并提供用户权限控制等功能。 技术栈 后端技术 Spring Boot 2.7.0Spring Security…

预约参观华为基地,见证行业巅峰

✨ 大家好呀&#xff01;今天要跟大家分享一个超酷的体验&#xff0c;关于华为的参观学习之旅&#xff01;&#x1f680; 华为成立于1987年&#xff0c;位于深圳&#xff0c;是全球领先的信息与通信技术&#xff08;ICT&#xff09;解决方案供应商哦&#xff01;他们专注于科技…

Scala迭代更新

在Scala中&#xff0c;迭代器&#xff08;Iterator&#xff09;是一种用于遍历集合&#xff08;如数组、列表、集合等&#xff09;的元素而不暴露其底层表示的对象。迭代器提供了一种统一的方法来访问集合中的元素&#xff0c;而无需关心集合的具体实现。 在Scala中&#xff0c…

华为OD E卷(100分)38-数组拼接

前言 工作了十几年&#xff0c;从普通的研发工程师一路成长为研发经理、研发总监。临近40岁&#xff0c;本想辞职后换一个相对稳定的工作环境一直干到老, 没想到离职后三个多月了还没找到工作&#xff0c;愁肠百结。为了让自己有点事情做&#xff0c;也算提高一下自己的编程能力…

【国产NI替代】基于全国产FPGA的16振动+2转速+8路IO口输入输出(24bits)256k采样率,高精度终端采集板卡

16振动2转速8路IO口输入输出&#xff08;24bits&#xff09; 高精度终端采集板卡 采用AG16KF256国产FPGA的硬件架构&#xff0c;外扩 16MB的SDRAM做为千兆网的发送缓存&#xff0c;比起同 款带ARM的硬件架构&#xff0c;纯FPGA的架构虽然边缘 计算能力不如ARM&#xff0c;但…