第十章 输入输出系统

第十章 输入输出系统

本文是对《操作系统真象还原》第十章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 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; // 状态改为statschedule();                // 将当前线程换下处理器/* 待当前线程被解除阻塞后才继续运行下面的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;}elseplock->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); // 主片除了最低位其他全部置为1outb(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值为1if (break_code)                          // 如果是断码,就要判断是否是控制按键的断码,如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序{uint16_t make_code = (scancode &= 0xff7f); // 将扫描码(现在是断码)还原成通码if (make_code == ctrl_l_make || make_code == ctrl_r_make)ctrl_status = 0; // 判断是否松开了ctrlelse if (make_code == shift_l_make || make_code == shift_r_make)shift_status = 0; // 判断是否松开了shiftelse if (make_code == alt_l_make || make_code == alt_r_make)alt_status = 0; // 判断是否松开了altreturn;}// 来到这里,说明不是断码,而是通码,这里的判断是保证我们只处理这些数组中定义了的键,以及右alt和ctrl。else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){int shift = 0;                       // 确定是否开启shift的标志,先默认设置成0uint8_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;}elseput_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;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/875639.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【每日一题】20250125

不等和等下去,同样苦涩。【每日一题】已知变量 \(x\) 和变量 \(y\) 的一组成对样本数据为 \((x_i,y_i)(i=1,2,3,\cdotp\cdotp\cdotp,8)\),其中 \(\overline{x}=\frac98\),其回归直线方程为 \(\hat{y}=2x-\frac14\),当增加两个样本数据 \((-1,5)\) 和 \((2,9)\) 后,重新得到…

云手机还是会被检测!还能用来多开吗?

云手机还是会被检测!还能用来多开吗? 云手机确实可以用于多开,但是否会被检测到以及是否安全,取决于多种因素,包括云手机服务提供商的技术、用户的操作方式以及目标应用(如微信、游戏等)的检测机制。以下是关于云手机多开的安全性和可行性的详细分析:云手机多开的原理 …

P1038神经网络

神经网络 题目背景 人工神经网络(Artificial Neural Network)是一种新兴的具有自我学习能力的计算系统,在模式识别、函数逼近及贷款风险评估等诸多领域有广泛的应用。对神经网络的研究一直是当今的热门方向,兰兰同学在自学了一本神经网络的入门书籍后,提出了一个简化模型,…

Phi小模型开发教程:C#使用本地模型Phi视觉模型分析图像,实现图片分类、搜索等功能

大家好,我是编程乐趣。 我们都知道,要实现对结构化的数据(文本)搜索是比较容易的,但是对于非结构化的数据,比如图片,视频就没那么简单了。 但是现在有了AI模型,实现图片分类、搜索等功能,就变得容易很多。 在前面的文章里,我们有提到:Phi-vision 是一个拥有 42 亿参…

E95 01分数规划+树上背包 P1642 规划

视频链接: P1642 规划 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)// 01分数规划+树上背包 复杂度:n*m*log(1e9) #include <bits/stdc++.h> using namespace std;int read(){int x=0,f=1;char c=getchar();while(!isdigit(c)){if(c==-)f=-1;c=getchar();}while(is…

年级第一暗杀计划

[SDOI2008] 仪仗队 题目描述 作为体育委员,C 君负责这次运动会仪仗队的训练。仪仗队是由学生组成的 \(N \times N\) 的方阵,为了保证队伍在行进中整齐划一,C 君会跟在仪仗队的左后方,根据其视线所及的学生人数来判断队伍是否整齐(如下图)。现在,C 君希望你告诉他队伍整齐…

Burp Suite Professional 2025.1 发布下载,新增功能简介

Burp Suite Professional 2025.1 发布下载,新增功能简介Burp Suite Professional 2025.1 (macOS, Linux, Windows) - Web 应用安全、测试和扫描 Burp Suite Professional, Test, find, and exploit vulnerabilities. 请访问原文链接:https://sysin.org/blog/burp-suite-pro/ …

ubuntu配置核心转储文件路径并调试(nju ics PA)

调整 core pattern编辑 /etc/sysctl.conf sudo nano /etc/sysctl.conf修改kernel.core_pattern kernel.core_pattern=./core.%d.%f.%p.%t# %d 可执行文件目录名 # %f 可执行文件名 # %p 进程 ID # %t 时间的十进制值 (2) 可以自行修改格式,参考变量名和含义 使其生效 sudo sy…

solon-flow 你好世界!

solon-flow 是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有 “开放式” 驱动定制支持,像 jdbc 有 mysql 或 pgsql 等驱动,可为不同的应用场景定制不同的驱动处理。solon-flow 是一个基础级的流处理引擎(可用于业务规则、决策处…

1/25 遇到的问题

1.数据库连接报错 错误代码: nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/JAXBExceptionorg.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘entityManagerFactory’ defined inclass path resource [org/s…

【模拟电子技术】14-基本共射放大电路的动态分析

【模拟电子技术】14-基本共射放大电路的动态分析给出问题,求三个参数。反推:需要Aus就需要求解交流通路(动态参数H等效模型),交流通路需要知道Rbe,Rbe需要知道Rbb,Rbb需要知道静态工作点,静态工作点需要分析直流通路,思路有了。得到Au现在再次探讨输入电阻对放大电路的…