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

章节任务介绍

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

任务简介

本节的主要任务有:

  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,为方便后续代码编写,以下添加了所有的中断号

bash 复制代码
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个中断描述符

cpp 复制代码
#define IDT_DESC_CNT 0x30

然后打开键盘中断

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

cpp 复制代码
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void); 
#endif

/device/keyboard.c

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

cpp 复制代码
#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

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

修改main.c测试键盘中断

cpp 复制代码
#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);
    }
}

编译运行

bash 复制代码
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

数据准备与定义

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

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

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

cpp 复制代码
#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

接下来定义控制字符的通码和断码

cpp 复制代码
/// 定义控制字符的通码和断码
#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

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

cpp 复制代码
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,如果是,说明该按键的扫描码是由两个字节组成的,需要再次接受一个字节的扫描码,然后才能拼接出完整的两个字节的扫描码

cpp 复制代码
    // 从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+通码

因此有

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

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

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

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

cpp 复制代码
    // 如果是断码,就要判断是否是控制按键的断码
    // 如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序
    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,表示接下来该字母输出的是大写

cpp 复制代码
    // 如果是通码,首先保证我们只处理这些数组中定义了的键,以及右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

cpp 复制代码
#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

cpp 复制代码
#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

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

cpp 复制代码
#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);
    }
}

编译运行

bash 复制代码
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/*

以下是运行结果

相关推荐
袁庭新8 小时前
CentOS7通过yum无法安装软件问题解决方案
centos·操作系统
别说我什么都不会1 天前
鸿蒙轻内核M核源码分析系列十二 事件Event
操作系统·harmonyos
qq_437896432 天前
动态内存分配算法对比:最先适应、最优适应、最坏适应与邻近适应
操作系统
别说我什么都不会2 天前
鸿蒙轻内核M核源码分析系列十一 (2)信号量Semaphore
操作系统·harmonyos
别说我什么都不会2 天前
鸿蒙轻内核M核源码分析系列十 软件定时器Swtmr
操作系统·harmonyos
别说我什么都不会3 天前
鸿蒙轻内核M核源码分析系列九 互斥锁Mutex
操作系统·harmonyos
别说我什么都不会3 天前
鸿蒙轻内核M核源码分析系列七 动态内存Dynamic Memory
操作系统·harmonyos
别说我什么都不会4 天前
鸿蒙轻内核M核源码分析系列六 任务及任务调度(3)任务调度模块
操作系统·harmonyos
徐徐同学4 天前
【操作系统】操作系统概述
操作系统·计算机系统
守望时空335 天前
Linux内核升级指南
linux·操作系统