字符打印执行过程中不能被切换成其他任务。字符打印过程中的三个步骤像原子一样不可拆分,因此字符打印必须具有原子性。
您肯定想到了,每个任务都有时间片限制,迟早会执行任务调度,所以,任务调度保不准就是在某个
线程执行字符打印时发生的,也就是说,字符打印中的三个步骤被拆开了。线程在时间片内执行时并不会受调度器的影响,因此,此过程中字符打印“算是”具有原子性,在任务调度前的执行期间,打印的是连续完整的字符串。在任务调度前的一刻,如果此线程执行了 put_char 中的步骤 1 ,并且步骤 3 尚未执行(不管第 2 步是否执行完),那么就一定会出问题。
既然是任务调度破坏了字符打印的原子性,而任务调度又是由时钟中断调用的,可以在宇符串打印前后通过关中断的方式来保证原子性
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, "ar_gB ");
intr_enable(); // 打开中断,使时钟中断起作用
while(1) {
intr_disable(); // 关中断
put_str("Main ");
intr_enable(); // 开中断
};
return 0;
}
有关以上的两个问题,根本原因是访问公共资源需要多个操作,而这多个操作的执行过程不具备原子性,它被任务调度器断开了,从而让其他线程有机会破坏显存和光标寄存器这两类公共资源的现场 。
临界区
程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。怕有同学看得不仔细,强调一下,临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源。
虽然关中断可以实现互斥,但关中断的操作应尽量靠近临界区,这样才更高效。关中断操作离临界区越远,多任务调度越低效,不夸张地说,若将关中断操作加在了任务执行之初,多任务并行(伪并行)系统将退化成单任务串行执行。
二元信号量中, down 操作就是获得锁, up 操作就是释放锁 。我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥 。大致流程为:
- 线程 A 进入临界区前先通过 down 操作获得锁(我们有强制通过锁进入临界区的手段),此时信号量的值便为 0 。
- 后续线程 B 再进入临界区时也通过 down 操作获得锁,由于信号量为 0,线程 B 便在此信号量上等待,也就是相当于线程 B 进入了睡眠态 。
- 当线程 A 从临界区出来后执行 up 操作释放锁,此时信号量的值重新变成 1 ,之后线程 A 将线程 B唤醒 。
- 线程 B 醒来后获得了锁,进入临界区 。
阻塞是线程自己发出的动作,也就是线程自己阻塞自己,并不是被别人阻塞的,阻塞是线程主动的行
为 。 已阻塞的线程是由别人来唤醒的,唤醒是被动的。
/* 当前线程将自己阻塞,标志其状态为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();//当前线程状态必为TASK_RUNNING
cur_thread->status = stat; // 置其状态为stat(非TASK_RUNNING)
schedule(); // 将当前线程换下处理器(换下thread_ready_list)
/* 待当前线程被解除阻塞后才继续运行下面的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) { //保证被阻塞线程状态不是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);
}
/* 信号量结构 */
struct semaphore {
uint8_t value;//信号量值
struct list waiters;//记录在此信号量上等待(阻塞)的所有线程
};
/* 锁结构 */
struct lock {
struct task_struct* holder; // 锁的持有者
struct semaphore semaphore; // 用二元信号量实现锁
uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
//一般情况下我们应该在进入临界区之前加锁,但有时候可能持有了某临界区的锁后,在未释放锁之前,有可能会再次调用重复申请此锁的函数
};
/* 初始化信号量 */
void sema_init(struct semaphore* psema, uint8_t value) {
psema->value = value; // 为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}
/* 初始化锁plock */
void lock_init(struct lock* plock) {
plock->holder = NULL;//锁的持有者置空
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1); // 信号量初值为1
}
/* 信号量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); // 阻塞线程,直到被唤醒
//被唤醒后,也不一定就能获得资源,只是再次获得了去竞争锁的机会而己,所以判断信号量的值最好用 while,而不是用 if
}
/* 若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)) {
//信号量等待队列 psema->waiters 中通过 list_pop 弹出队首的第一个线程,并通过宏 elem2entry将其转换成 PCB,存储到thread_blocked 中
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);
}
/* 获取锁plock */
void lock_acquire(struct lock* plock) {
/* 排除曾经自己已经持有锁但还未将其释放的情况,避免死锁*/
if (plock->holder != running_thread()) {
sema_down(&plock->semaphore); // 对信号量P操作,原子操作,信号量-1,可能阻塞
plock->holder = running_thread();//将当前线程标为锁的持有者
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
} else {
plock->holder_repeat_nr++;//当前线程申请锁次数+1
}
}
/* 释放锁plock */
void lock_release(struct lock* plock) {
ASSERT(plock->holder == running_thread());
if (plock->holder_repeat_nr > 1) {
plock->holder_repeat_nr--;//多次申请该锁,此时不能真正释放,等到多次进此函数释放完(减为1)才真正释放
return;
}
ASSERT(plock->holder_repeat_nr == 1);
plock->holder = NULL; // 把锁的持有者置空放在V操作之前
//原因是释放锁的操作并不在关中断下进行,有可能会被调度器换下处理器,换下后若新线程请求锁,此时已执行sema_up,value=1,锁拥有者改为新线程,之后又将锁持有者置空,错误
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}
伪控制台实现
put_str + 锁
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();
}
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化PIT
console_init(); //控制台初始化最好放在开中断之前
}
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);
}
}
我们平时所熟悉的键盘操作,是由独立的模块分层实现的,但是并不是简单地由键盘把数据塞到主机里,这涉及两个功能独立的芯片的配合。
键盘是个独立的设备,在它内部有个叫作键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,按键是否弹起。
这个键盘控制器可并不在键盘内部,它在主机内部的主板上,通常是 Intel 8042 或兼容芯片,它的作用是接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息。
一个键的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键被按下时的编码叫通码,也就是表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为 breakcode。一个键的扫描码是由通码和断码组成的。
无论是按下键,或是松开键,当键的状态改变后,键盘中的 8048 芯片把按键对应的扫描码(通码或断码)发送到主板上的 8042 芯片,由 8042 处理后保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042 处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
这个键盘中断处理程序是咱们程序员负责编写的,值得注意的是我们只能得到键的扫描码,并不会得到键的 ASCII 码,扫描码是硬件提供的编码集, ASCII 是软件中约定的编码集,这两个是不同的编码方案。我们的键盘中断处理程序是同硬件打交道的,因此只能得到硬件提供的扫描码,但我们可以将得到的“硬件”扫描码转换成对应的“软件” ASCII 码。
键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,根据不同的编码方案,键盘扫描码有三套,分别称为 scan code set 1(XT键盘) 、 scan code set 2(AT键盘) 、 scan code set 3
大多数’情况下第一套扫描码中的通码和断码都是 1 字节大小。它们的关系是:断码 = 0x80 +通码 。
对于通码和断码可以这样理解, 它们都是一字节大小,最高位也就是第 7 位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为 1 的话,表示按键弹起。 比如按键,不管键盘向 8042 发送的是第几套扫描码, 当我们按下它的时候,最终被 8042 转换成 0x1 ,当我们松开它的时候,最终会被 8042 转换成 0x80+0x1=0x81 。
有些按键的通码和断码都以 0xe0 开头,它们占 2 字节,甚至 Pause 键以 0xe1 开头,占 6 字节 。原因是这样的,最初的XT 键盘上的键很少,比如右边回车键附近就没有 alt 和 ctrI 键,这是在后来的键盘中才加进去的,因此表示扩展 extend,所以在扫描码前面加了 0xe0 作为前缀。 比如在 XT 键盘上, 左边有 alt 键,其通码为 0x38,断码为 0xb8 。 右边的 alt 键是后来在新的键盘上加进去的,因此, 一方面为了表示都是同样功能的 alt 键,另一方面表示不是左边那个 alt,而是右边的 alt,于是这个扩展的础键的扫描码便为“0xe0 和原来左边础的扫描码”。 因此,右边 alt 键的通码便为“0xe0,0x38 ”,断码为“ 0xe0,0xb8”
总结一下
- 扫描码有 3 套,现在一般键盘中的 8048 芯片支持的是第二套扫描码 。 因此每当有击键发生时, 8048发给 8042 的都是第二套键盘扫描码。
- 8042 为了兼容性,将接收到的第二套键盘扫描码转换成第一套扫描码。 8042 是按字节来处理的,每处理一个字节的扫描码后,将其存储到自己的输出缓冲区寄存器 。
- 然后向中断代理 8059A 发中断信号,这样我们的键盘中断处理程序通过读取 8042 的输出缓冲区寄存器,会获得第一套键盘扫描码。
和键盘相关的芯片只有 8042 和 8048,它们都是独立的处理器,都有自己的寄存器和内存。
Intel 8048 芯片或兼容芯片位于键盘中,它是键盘编码器,相当于键盘的“代言” 人,是键盘对外表
现击键信息、帮助键盘“说话”的部件。它除了负责监控按键扫描码外,还用来对键盘设置,比如设置键盘上的各种 LED 显示灯的开启和关闭,默认情况下 NumLock 的 LED 灯是亮的,这就是 8048 的功劳。
Intel 8042 芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的 IO 接口,因此它是 8048 的代理,也是前面所得到的处理器和键盘的“中间层”。 8048 通过 PS/2 、 USB 等接口与 8042 通信,处理器通过端口与 8042 通信( IO 接口就是外部硬件的代理,它和处理器都位于主机内部,因此处理器与 IO 接口可以通过端口直接通信)。
既然 8042 是 8048 的 IO 接口,对 8048 的编程也是通过 8042 完成的,所以只要学习 8042 足矣, 8048不再介绍。
- 当需要把数据从处理器发到 8042 时(数据传送尚未发生时),0x60 端口的作用是输入缓冲区,此时应该用 out 指令写入 0x60 端口。
- 当数据己从 8048 发到 8042 时, 0x60 端口的作用是输出缓冲区,此时应该用 in 指令从 8042 的 0x60 端口(输出缓冲区寄存器)读取 8048 的输出结果。
注册8259A的全部中断
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 ;保留
进行键盘测试
#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
/* 测试键盘,只打开键盘中断,其它全部关闭 */
outb (PIC_M_DATA, 0xfd);//1111_1101
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
#define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
put_char('k');
/* 必须要读取输出缓冲区寄存器,否则8042不再继续响应键盘中断 */
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");
}
处理转义字符
#define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60
/* 用转义字符定义部分控制字符 */
#define esc '\033' // 八进制表示字符,也可以用十六进制'\x1b',为了兼容
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\x7f'
/* 以下不可见字符一律定义为0 */
//只有字符控制键才有ASCII码,操作控制键没有
#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
/* 定义以下变量记录相应键是否按下的状态,
* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码 未与shift组合 与shift组合*/
/* ---------------------------------- */
/* 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}
/*其它按键暂不处理*/
};
/* 键盘中断处理程序 */
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;// 是否是断码
uint16_t scancode = inb(KBD_BUF_PORT);// 从KBD_BUF_PORT端口获取扫描码
/* 若扫描码是e0开头的,说明是扩展扫描码,e0是扫描码前缀,此键的按下将产生多个扫描码,
* 所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if (scancode == 0xe0) {
ext_scancode = true; // 打开e0标记
return;
}
/* 如果上次是以0xe0开头,将扫描码合并 */
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 关闭e0标记
}
break_code = ((scancode & 0x0080) != 0); //断码第8位为1,是否是break_code
if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码)
/* 由于ctrl_r 和alt_r的make_code和break_code都是两字节,
所以可用下面的方法取make_code,多字节的扫描码暂不处理 */
//第一套键盘扫描码,断码第8位为1,通码第8位为0
uint16_t make_code = (scancode &= 0xff7f); // 得到其通码make_code(按键按下时产生的扫描码)
/* 若是任意以下三个键弹起了,将状态置为false */
//只是判断哪个键弹起,所以虽然是用通码在判断但不影响
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;
} /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */
return; // 直接返回结束此次中断处理程序
}
/* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */
else if ((scancode > 0x00 && scancode < 0x3b) || \
(scancode == alt_r_make) || \
(scancode == ctrl_r_make)) {
bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符
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_down_last) { // 如果同时按下了shift键
shift = true;
}
} else { // 默认为字母键
if (shift_down_last && caps_lock_last){ // 如果shift和capslock同时按下
shift = false;
}else if(shift_down_last || caps_lock_last){//如果shift和capslock任意被按下
shift = true;
} else {
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
char cur_char = keymap[index][shift]; // 在数组中找到对应的字符
/* 只处理ascii码不为0的键 */
if (cur_char) {
put_char(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键,当再次按下时则状态取反,
* 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
caps_lock_status = !caps_lock_status;
}
} else {
put_str("unknown key\n");
}
}
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
shell 命令是由多个字符组成的,并且要以回车键结束,因此咱们在键入命令的过程中,必须要找个缓冲区把己键入的信息存起来,当凑成完整的命令名时再一并由其他模块处理。
/* 环形队列 */
struct ioqueue {
// 生产者消费者问题
struct lock lock;//本缓冲区的锁,每次对缓冲区操作时都要先申请这个锁,从而保证缓冲区操作互斥。
/* 生产者,缓冲区不满时就继续往里面放数据,
* 否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠。*/
struct task_struct* producer;
/* 消费者,缓冲区不空时就继续从往里面拿数据,
* 否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠。*/
struct task_struct* consumer;
char buf[bufsize]; // 缓冲区大小
int32_t head; // 队首,数据往队首处写入
int32_t tail; // 队尾,数据从队尾处读出
};
/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化io队列的锁
ioq->producer = ioq->consumer = NULL; // 生产者和消费者置空
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;
}
/* 判断队列是否已空 */
static 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;
}
/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
ASSERT(intr_get_status() == INTR_OFF);
/* 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,
* 为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
* 也就是唤醒当前线程自己*/
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) {
wakeup(&ioq->consumer); // 唤醒消费者
}
}
生产者是键盘驱动,消费者是将来的 shell
struct ioqueue kbd_buf; // 定义键盘缓冲区
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
...
if (cur_char) {
/* 若kbd_buf中未满并且待加入的cur_char不为0,
* 则将其加入到缓冲区kbd_buf中 */
if (!ioq_full(&kbd_buf)) {
put_char(cur_char); // 临时的
ioq_putchar(&kbd_buf, cur_char);
}
return;
}
...
}
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}