《操作系统真象还原》第十章 输入输出系统

第十章 输入输出系统

本文是对《操作系统真象还原》第十章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 fdx-xdf/MyTinyOS

上一章遗留的问题

在上一节中,我们实现了多线程轮转调度,但是当我们运行一段时间后,就会发生GP异常

image

待解决的几个问题:

  1. 输出中,有些字符串看似少了字符;
  2. put_str()操作的原子性没有得到保证,当下可通过在put_str()操作前后增加开关中断函数暂时保证其执行的原子性。
  3. 输出中,有大片连续的空缺;
  4. GP异常;

出现的字符丢失、大片空缺、GP异常问题,是由于字符串写入操作没有使用原子操作所导致的。

字符串写入分为3个步骤:

  1. 获取光标值
  2. 将光标值转为字节地址,在地址中写入字符
  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; // 状态改为stat
    schedule();                // 将当前线程换下处理器
    /* 待当前线程被解除阻塞后才继续运行下面的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;
    }
    else
        plock->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处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

image

8042芯片

我们在编写键盘中断程序时主要用到的就是8042

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

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

image

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

image

代码逻辑

  • 添加键盘中断的中断向量号
  • 添加键盘中断处理程序
  • 构建中断描述符
  • 打开键盘中断

添加中断向量号

/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); // 主片除了最低位其他全部置为1
    outb(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值为1
   if (break_code)                          // 如果是断码,就要判断是否是控制按键的断码,如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序
   {
      uint16_t make_code = (scancode &= 0xff7f); // 将扫描码(现在是断码)还原成通码
      if (make_code == ctrl_l_make || make_code == ctrl_r_make)
         ctrl_status = 0; // 判断是否松开了ctrl
      else if (make_code == shift_l_make || make_code == shift_r_make)
         shift_status = 0; // 判断是否松开了shift
      else if (make_code == alt_l_make || make_code == alt_r_make)
         alt_status = 0; // 判断是否松开了alt
      return;
   }
   // 来到这里,说明不是断码,而是通码,这里的判断是保证我们只处理这些数组中定义了的键,以及右alt和ctrl。
   else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
   {
      int shift = 0;                       // 确定是否开启shift的标志,先默认设置成0
      uint8_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;
   }
   else
      put_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;
}

较为简单,看注释即可然后编译运行

posted @   fdx_xdf  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示