章节任务介绍
在上一节中,我们介绍了操作系统的同步机制互斥锁的内容,并手动实现了互斥锁,同时实现了线程安全的屏幕打印。 至此,我们算是基本完成了操作系统的“输出”功能,但目前为止我们的输入仍旧依赖于程序,而不是用户操控的键盘,本节我们将正式完成操作系统的“输入”
任务简介
本节的主要任务有:
键盘驱动测试
编写键盘驱动程序
基于环形缓冲区的键盘驱动程序
前置知识
键盘输入原理简介
键盘编码介绍
一个键的状态要么是按下,要么是弹起,因此一个键有两个编码,这两个编码统称扫描码,一个键的扫描码由通码和断码组成。
按键被按下时的编码叫通码,表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为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/*
以下是运行结果