真象还原笔记:键盘驱动与环形输入缓冲区

概述

上一篇中完成了终端的输出封装,为了完成和内核的交流,输入也是必不可少的。键盘驱动的实现能够方便内核获取键盘的输入数据,而与内核的交流是通过指令实现的,这就需要一段空间暂存输入的字符,拼接成完整的指令后交给相关模块处理,这段空间即为环形输入缓冲区。

从键盘获取输入基本流程

通常,键盘上的按键按下或弹起时,键盘中的Intel 8048会将第二套键盘扫描码中该键位对应的通码或断码发送至Intel 8042。Intel 8042为了保证兼容性,会将收到的扫描码转换成第一套键盘扫描码,并存入自己的输出缓冲寄存器,同时向Intel 8259A可编程中断控制器的IR1引脚发送中断请求。Intel 8259A向CPU发出向量号为0x21的中断。此时操作系统执行键盘中断处理程序(键盘驱动),从Intel 8042的输出缓冲寄存器中读取扫描码,同时通过分析扫描码生成对应的ASCII码,并将ASCII码存入环形输入缓冲区。

环形输入缓冲区基本设计思路

线程同步

诠释线程同步的经典例子是“生产者与消费者问题”。在此问题中,有一个或多个生产者线程向缓冲区中存放数据,一个或多个消费者线程从缓冲区中取数据,一个有限容量的缓冲区。该问题主要探讨对于有限大小的公共缓冲区,如何同步生产者与消费者的运行,以达到对共享缓冲区的互斥访问,并且保证生产者不会过度生产,消费者不会过度消费,缓冲区不会被破坏。

若生产者与消费者的行为有如下规定:

  • 生产者:缓冲区中有空余位置时,向缓冲区中存放数据,缓冲区满时则休眠。

  • 消费者:缓冲区中有数据时,从缓冲区中取数据,缓冲区为空时休眠。

这样就很好地解决了生产者与消费者线程的同步问题,但仅仅如此还是不够的,因为缓冲区是多个线程共享的资源,因此对它的访问应当做到互斥,即同一时刻只能有一个生产者或消费者对缓冲区进行操作。

逻辑环

环形输入缓冲区只是形象的说法,实际的缓冲区在内存中依旧是线性的数组,只是通过取模的方式,使到达数组上边界的索引映射到了数组的下边界,在逻辑上形成了一个环形的缓冲区。

关键部分实现

环形输入缓冲区实现

  • 消费者获取数据:
//消费者从ioq中获取一个字符
char ioq_getchar(ioqueue *ioq) {
    //处于关中断状态
    ASSERT(intr_get_status() == INTR_OFF);
    
    //若缓冲区为空,将当前线程作为消费者阻塞于ioq
    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);
    
    //若ioq有睡眠的生产者,唤醒生产者
    if (ioq->producer != NULL) {
        wakeup(&ioq->producer);
    }
    
    return byte;
}

ioq_getchar()函数在关中断的情况下执行,因此能够保证对缓冲区操作的互斥性。若缓冲区为空,则当前线程获取环形输入缓冲区的锁,并进入休眠,之后来的消费者线程则会阻塞在锁的等待队列。等待生产者线程将当前线程唤醒后才释放锁。

键盘驱动实现

//键盘中断处理程序
static void intr_keyboard_handler(void) {
    //检测控制键是否处于按下状态
    bool ctrl_down_last = ctrl_status;
    bool shift_down_last = shift_status;
    bool caps_lock_last = caps_lock_status;
    //检测此次扫描码是否为断码
    bool break_code;
    //读取扫描码,可能为扩展扫描码,即0xe0xx,因此变量声明为uint16_t
    uint16_t scancode = inb(KBD_BUF_PORT);
    
    //若此次扫描码为0xe0,则为扩展扫描码
    if (0xe0 == scancode) {
        ext_scancode = true;    //扩展扫描码标志位置1并返回
        return;                 //等待下一次中断读取扩展扫描码的剩余字节
    }
    
    //若扩展扫描码标志位为1,则此次扫描码为扩展扫描码的后续字节
    if (ext_scancode) {
        scancode = (0xe000 | scancode);     //合并扩展扫描码
        ext_scancode = false;               //扩展扫描码标志位复位
    }
    
    //检测该扫描码是否为断码
    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 = 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;
        }
        
        return;
    }
    //若为通码,只处理数组中存在的键位和ctrl_right与alt_right
    else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == ctrl_r_make) || (scancode == alt_r_make)) {
        //判断是否有shift组合
        bool shift = false;
        
        if ((scancode < 0x0e) || (scancode == 0x29) || \
        (scancode == 0x1a) || (scancode == 0x1b) || \
        (scancode == 0x2b) || (scancode == 0x27) || \
        (scancode == 0x28) || (scancode == 0x33) || \
        (scancode == 0x34) || (scancode == 0x35)) {
            //已登录的非字母扫描码
            if (shift_down_last) {
                shift = true;
            }
        }
        else {
            //默认为字母扫描码
            if (shift_down_last && caps_lock_last) {
                return;     //shift与capslock同时按住再按字母键,直接返回
            }
            else if (shift_down_last || caps_lock_last) {
                shift = true;
            }
            else {
                shift = false;
            }
        }
        
        //获取通码,过滤掉扩展扫描码的0xe0
        uint8_t index = (scancode &= 0x00ff);
        //获取对应字符ASCII
        char cur_char = keymap[index][shift];
        //若为可显示字符,即不为0
        if (cur_char) {
            //键盘缓冲区不满时响应
            if (!ioq_full(&kbd_buf)) {
                //字符输出至终端
                //put_char(cur_char);
                //字符写入键盘缓冲区
                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_status = !caps_lock_status;
        }
    }
    else {
        put_str("unknown key\n");
    }
}

对于控制键位来说,持续的按压并不会使Intel 8042持续地发出中断请求,控制键只会在按下与弹起时发出中断,因此控制键通码即为按下的标志,断码即为弹起标志。

实验检测

main.c文件代码:

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "keyboard.h"
#include "ioqueue.h"

//测试内核线程函数
void k_thread_a(void *);
void k_thread_b(void *);

int main(void) {
    put_str("kernel\n\n");
    init_all();
    
    //创建内核线程执行void k_thread_*(void*)
    thread_start("consumer_a", 32, k_thread_a, " A_");
    thread_start("consumer_b", 32, k_thread_b, " B_");
    
    //开中断
    intr_enable();
    
    while (1);
    return 0;
}

void k_thread_a(void *arg_) {
    char *arg = (char*)arg_;
    while (1) {
        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);
        }
        intr_set_status(old_status);
    }
}

void k_thread_b(void *arg_) {
    char *arg = (char*)arg_;
    while (1) {
        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);
        }
        intr_set_status(old_status);
    }
}

对于此段代码来说,其运行结果是十分正常的,但我总觉得哪里还是不太清楚,就是这个if (!ioq_empty(&kbd_buf))。不知道有没有哪位和我有一样的问题——“按理来说,没有这句代码也是能够正常运行的,为什么要做这步判断呢?”。

本着“不懂就删”的原则(我的一点个人见解,不理解功能的东西被删去时,暴露出的问题往往就是理解其功能的关键),就有了这样的结果:

deleted.png

暴露出的问题总共有两个:

  • 各线程的参数 " X_" 的输出会与 byte 的输出脱节,就比如图中的 " B_k" 中的 k 实际是前面的 " A_"在被键盘驱动唤醒后获取的 byte。

  • 后面的输出中没有再出现过 " B_"。

第一个问题的原因是很容易理解的,那么第二个问题是如何出现的呢?其实这也不难理解,仔细想想就可以得出大概的运行过程:

  1. 最开始时由 Main 线程创建 AB 两个线程;

  2. Main 线程时间片到期时调度 A 线程上CPU执行,由于此时kbd_buf为空,所以 A 线程被阻塞于kbd_buf.consumer,并调度 B 线程上CPU执行;

  3. 由于当前 A 线程拥有kbd_buf.lock,所以 B 线程被阻塞于kbd_buf.lock.semophore.waiters,同时调度 Main 线程上CPU执行;

  4. 由于此时 AB 线程都被阻塞,因此只有 Main 线程在CPU与就绪队列之间反复切换;

  5. 当按下按键时,键盘驱动向kbd_buf中存入数据,同时唤醒kbd_buf.consumer上的 A 线程,将其添加至就绪队列队首(此时就绪队列中只有 A 线程),待 Main 线程时间片到期时调度 A 线程上CPU执行;

  6. A 线程释放锁,此时 B 线程被唤醒,插入到就绪队列队首(此时就绪队列中有 BMain 线程),A 线程从kbd_buf中获取byte并输出,紧接着进入下一次循环,又被阻塞在kbd_buf.consumer,同时调度 B 线程上CPU执行;

  7. B 线程此时心态是炸裂的,它又被阻塞于kbd_buf.lock.semophore.waiters,只能再次调度 Main 线程上CPU执行,之后就是第4步到第7步的循环。

因此可以看出,B 线程其实每次按键都会被调度上CPU,只是一直没有机会执行输出语句罢了,为了验证想法,我将函数thread_block()修改为:

//阻塞当前运行的线程,将其状态置为stat
void thread_block(task_status stat) {
    //检测将置为的状态是否符合阻塞要求
    ASSERT(stat == TASK_BLOCKED || stat == TASK_WAITING || stat == TASK_HANGING);
    //关闭中断,保证阻塞操作的原子性
    intr_status old_status = intr_disable();
    
    //将当前线程的状态设为stat
    task_struct *cur_thread = running_thread();
    
    //添加部分,输出当前将被阻塞线程信息
    put_str(cur_thread->name);
    put_char('\n');
    
    cur_thread->status = stat;
    //调用任务调度器
    schedule();
    
    //恢复中断状态(在下次该线程被调上CPU时执行)
    intr_set_status(old_status);
}

能够得到结果:

add_put.png

和之前得到的结论相符。

但如果细想的话,其实 B 线程是有机会获取锁的,只要在 A 线程执行关中断前和开中断后的某一条指令时,时间片到期,B 线程就能够调度上CPU并获取锁。但是删去了if (!ioq_empty(&kbd_buf))后,使得时钟中断非常玄学,因为只有 A 线程处于开中断时,收到时钟中断才会使ticks减小,但 A 线程又基本属于关中断的状态。还是上图能清晰一些:

intr.png

如上图,时钟中断是由Intel 8253按之前设定好的频率发出的,假设按住键盘上的某一个键位不放,Intel 8042发出的键盘中断也是具有周期的(这里只是大概定性说明,实际Intel 8042的中断频率应当远远低于Intel 8253),A 线程只有在键盘中断发生后很短时间内处于开中断的状态,也就是说只有图中时钟中断与键盘中断的线大概重合时,才会使 A 线程的ticks减少。

因此综上所述,if (!ioq_empty(&kbd_buf))的作用就是当kbd_buf为空时,使各线程空转,至少不会全阻塞在缓冲区的消费者和锁中,能够使各线程稳定消耗时间片,使各个线程都有从缓冲区中获取 byte 的机会。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容

  • 环形缓冲区经常被使用到,尤其在生产者和消费者的模型中,假设生产者专门用于产生数据,而消费者专门用于处理数据,由于各...
    为瞬间停留阅读 3,931评论 2 1
  • 技术原理 何为符号链接?符号链接是一个别名,可以指向任意一个有名字的对象. ZwCreateFile 不但可以打开...
    f675b1a02698阅读 606评论 0 0
  • 键盘驱动 按键按下时的编码是通码,makecode 按键弹起时的编码是断码,breakcode一个键的扫描码由通码...
    HAPPYers阅读 478评论 0 0
  • 一、生产者消费者模式 生产者消费者模式是一种用于解决多个模块之间数据通信问题的高效机制。通过在数据生产者和数据消费...
    得州安打阅读 713评论 0 0
  • 本系列博客习题来自《算法(第四版)》,算是本人的读书笔记,如果有人在读这本书的,欢迎大家多多交流。为了方便讨论,本...
    kyson老师阅读 3,229评论 2 50