第10章-输入输出系统
这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件
10.1 同步机制–锁
10.1.1 排查GP异常,理解原子操作
线程调度工作的核心内容就是线程的上下文保护+上下文恢复 。
根本原因是访问公共资源需要多个操作,而这多个操作的执行过程不具备原子性,它被任务调度器断开了,从而让其他线程有机会破坏显存和光标寄存器这两类公共资源的现场。
10.1.2 找出代码中的临界区、互斥、竞争条件
- 公共资源
可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
- 临界区
程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。
- 互斥
互斥也可称为排他,是指某一时刻公共资源只能被 1 个任务独享,即不允许多个任务同时出现在自己的临界区中。公共资源在任意时刻只能被一个任务访问,即只能有一个任务在自己的临界区中执行,其他任务想访问公共资源时,必须等待当前公共资源的访问者完全执行完他自己的临界区代码后(使用完资源后)再开始访问。
- 竞争条件
竞争条件是指多个任务以非互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行进行的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。
多线程访问公共资源时出问题的原因是产生了竞争条件,也就是多个任务同时出现在自己的临界区 。 为避免产生竞争条件,必须保证任意时刻只能有一个任务处于临界区 。 因此,只要保证各线程自己临界区中的所有代码都是原子操作,即临界区中的指令要么一条不做,要么一气呵成全部执行完,执行期间绝对不能被换下处理器。
10.1.3 信号量
在计算机中,信号量就是个 0 以上的整数值,当为 0 时表示己无可用信号 ,或者说条件不再允许,因此它表示某种信号的累积“量“故称为信号量。 信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。
同步一般是指合作单位之间为协作完成某项工作而共同遵守的工作步调,强调的是配合时序。同步简单来说就是不能随时随意工作,工作必须在某种条件具备的情况下才能开始,工作条件具备的时间顺序就是时序。
用P、V操作来表示信号量的减、增,这两个都是荷兰语中的单词的缩写 。这里我们实现用方便记忆的名字用up,down。
增加操作 up 包括两个微操作:
- 将信号量的值加 1.
- 唤醒在此信号量上等待的线程
减少操作 down 包括三个子操作:
- 判断信号量是否大于 0 。
- 若信号量大于 0 ,则将信号量减 1。
- 若信号量等于 0 ,当前线程将自己阻塞,以在此信号量上等待。
信号量是个全局共享变量, up 和 down 又都是读写这个全局变量的操作,而且它们都包含一系列的子操作,因此它们必须都是原子操作。
10.1.4 线程的阻塞与唤醒
信号量 down 操作中的第 3 个微操作提到了阻塞当前线程的功能, 信号量 up 操作中的第 2 个微操作提到了唤醒线程的功能,因此在实现锁之前,我们必须提前实现这两个功能。
我们用函数 thread_block 实现了线程阻塞,用函数 thread_unblock 实现了线程唤醒。
实现阻塞功能的方法了,就是不让线程在就绪队列中出现就行了。阻塞发生的时间是在线程自己的运行过程中,是线程自己阻塞自己,并不是被谁阻塞。唤醒己阻塞的线程是由别的线程,通常是锁的持有者来做的。
值得注意的是线程阻塞是线程执行时的“动作”,因此线程的时间片还没用完,在唤醒之后,线程会继续在剩余的时间片内运行,调度器并不会将该线程的时间片“充满”,也就是不会再用线程的优先级priority 为时间片 ticks 赋值。因为阻塞是线程主动的意愿,它也是“迫于无奈”才“慷慨”地让出处理器资源给其他线程,所以调度器没必要为其“大方”而“赏赐”它完整的时间片。
thread.c函数中添加阻塞和解除阻塞的函数:
/*当前线程将自己阻塞,标志其状态为 stat.*/
void thread_block(enum task_status stat){/*stat取值为 TASK_RUNNING、TASK_WAITING、TASK_HANGING也就是只有这三种状态才不会被调度*/ASSERT(((stat == TASK_RUNNING)||(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_RUNNING)||(pthread->status == TASK_WAITING)||(pthread->status == TASK_HANGING)));if(pthread->status!=TASK_READY){ASSERT(!elem_find(&thread_read_list,&pthread->general_tag));if(elem_find(&thread_read_list,&pthread->general_tag)){PANIC("thread_unblock: block thread in ready_list\n");}list_push(&thread_read_list,&pthread->general_tag);//放到队列的最前面,使其尽快得到调度pthread->status=TASK_READY;}intr_set_status(old_status);
}
实现锁机制,首先为信号量与锁建立数据结构。信号量与锁的关系:信号量是对某项资源的管理,实际就是表示资源有多少,与哪些线程在等待这个资源。锁是在信号量机制上实现的,相比信号量多了记录谁造成了锁。
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-15 10:27:16* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-15 11:06:10* @FilePath: /OS/chapter10/10.1/thread/sync.h* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"/*信号量结构*/
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);
void lock_init(struct lock* plock);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);#endif // !__THREAD_SYNC_H
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-15 10:43:15* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-15 11:11:47* @FilePath: /OS/chapter10/10.1/thread/sync.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#include "sync.h"
#include "interrupt.h"
#include "debug.h"
//初始化信号
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();//别用if他只做一次判断,当阻塞后,被唤醒就开始后面的操作了,不会再次判断,如果线程数多会出错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 ,贝lj 当前线程把自己加入该锁的等待队列,然后阻塞自己*/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);
}/*获取锁*/
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);//把锁的持有者置空放在v操作之前因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了plock->holder = NULL; plock->holder_repeat_nr = 0;sema_up(&plock->semaphore); //信号量的 V 操作,也是原子操作
}
10.2 用锁实现终端输出
我们利用锁机制,建立锁console_lock
(意为终端锁)用于协调打印,将原有的put_int,put_char,put_str
进行封装
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-15 11:26:12* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-15 11:30:48* @FilePath: /OS/chapter10/10.1/device/console.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#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();
}/*终端中输出十六进制整数*/
void console_put_int(uint32_t num){console_acquire();put_int(num);console_release();
}
测试:
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-03-26 10:04:44* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-15 12:28:13* @FilePath: /OS/chapter6/6.3.4/kernel/main.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#include "print.h"
#include "init.h"void k_thread_a(void* arg);
void k_thread_b(void* arg);
int main(void){put_str("I am kernel\n");init_all(); //初始化int i = 999999;thread_start("k_thread_a", 31, k_thread_a, "argA ");thread_start("k_thread_b", 8, k_thread_b, "argB ");intr_enable();//asm volatile("sti"); //为了演示中断处理,在此临时开中断while(1){while(i--);i=999999;console_put_str("main ");}return 0;
}void k_thread_a(void* arg){int i = 999999;char* para = arg;while(1){while(i--);i=999999;console_put_str(para);}
}
void k_thread_b(void* arg){int i = 999999;char* para = arg;while(1){while(i--);i=999999;console_put_str(para);}
}
10.3 从键盘获取输入
10.3.1 键盘输入原理简介
键盘是个独立的设备,在它内部有个叫作键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,按键是否弹起。
这个键盘控制器可并不在键盘内部,它在主机内部的主板上,通常是 Intel 8042 或兼容芯片,它的作用是接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息 。
一个键的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键被按下时的编码叫通码,也就是表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为 makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为 breakcode。一个键的扫描码是由通码和断码组成的。
这个键盘中断处理程序是咱们程序员负责编写的,值得注意的是我们只能得到键的扫描码,并不会得到键的 ASCII 码,扫描码是硬件提供的编码集, ASCII 是软件中约定的编码集,这两个是不同的编码方案。
10.3.2 键盘扫描码
第一套扫描码中的通码和断码都是一字节大小,他们关系是:断码=0x80+通码。对于通码和断码可以这样理解, 它们都是一字节大小,最高位也就是第7 位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态 1 否则为 1 的话,表示按键弹起。
击键产生的扫描码是由键盘中的 8048 传给主板上的 8042 的, 8042 将扫描码转码处理后存入自己的输出缓冲区寄存器中 。 虽然并不是所有的扫描码都是 1 字节,但它们是以字节为单位发送的 因此 8042 的输出缓冲区寄存器也是 8 位宽度,即每次只能存储一个扫描码,要么是通码,要么是断码。
- 扫描码有 3 套,现在一般键盘中的 8048 芯片支持的是第二套扫描码 。 因此每当有击键发生时, 8048发给 8042 的都是第二套键盘扫描码。
- 8042 为了兼容性,将接收到的第二套键盘扫描码转换成第一套扫描码。 8042 是按字节来处理的,每处理一个字节的扫描码后,将其存储到自己的输出缓冲区寄存器 。
- 然后向中断代理 8059A 发中断信号,这样我们的键盘中断处理程序通过读取 8042 的输出缓冲区寄存器,会获得第一套键盘扫描码。
10.3.3 8042简介
和键盘相关的芯片只有 8042 和 8048,它们都是独立的处理器,都有自己的寄存器和内存。
Intel 8048 芯片或兼容芯片位于键盘中,它是键盘编码器,相当于键盘的“代言” 人,是键盘对外表现击键信息、帮助键盘“说话”的部件。 Intel 8042 芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的 IO 接口,因此它是 8048 的代理,也是前面所得到的处理器和键盘的“中间层”。
8042 就相当于数据的缓冲区、中转站,根据数据被发送的方向, 8042 的作用分别是输入和输出。
- 处理器把对 8048 的控制命令临时放在 8042 的寄存器中,让 8042 把控制命令发送给 8048,此时8042 充当了 8048 的参数输入缓冲区。
- 8048 把工作成果临时提交到 8042 的寄存器中,好让处理器能从 8042 的寄存器中获取它(8048)的工作成果,此时 8042 充当了 8048 的结果输出缓冲区。
结论:
- 当需要把数据从处理器发到 8042 时 (数据传送尚未发生时),0x60 端口的作用是输入缓冲区,此时应该用 out 指令写入 0x60 端口。
- 当数据己从 8048 发到 8042 时, 0x60 端口的作用是输出缓冲区,此时应该用 in 指令从 8042 的 0x60 端口(输出缓冲区寄存器)读取 8048 的输出结果。
各寄存器的作用:
- 输出缓冲区寄存器
8 位宽度的寄存器,只读,键盘驱动程序从此寄存器中通过 in 指令读取来自 8048 的扫描码、来自 8048 的命令应答以及对 8042 本身设置时, 8042 自身的应答也从该寄存器中获取。处理器未读取之前, 8042 不会再往此寄存器中存入新的扫描码。
8042 也有个智能芯片,它为处理器提供服务,当处理器通过端口跟它要数据的时候它当然知道了,因此,每当有in指令来读取此寄存器时, 8042 就将状态寄存器中的第 0 位置成 0,这就表示寄存器中的扫描码数据己经被取走,可以继续处理下一个扫描码了。当再次往输出缓冲寄存器存入新的扫描码时, 8042 就将状态寄存器中的第 0 位置为 1 ,这表示输出缓冲寄存器己满,可以读取了。
- 输入缓冲区寄存器
8 位宽度的寄存器,只写,键盘驱动程序通过 out 指令向此寄存器写入对 8048 的控制命令、 参数等,对于 8042 本身的控制命令也是写入此寄存器。
- 状态寄存器
8 位宽度的寄存器,只读,反映 8048 和 8042 的内部工作状态。
(1 )位 0:置1 时表示输出缓冲区寄存器己满,处理器通过in指令读取后该位自动置0.
(2 )位 1:置 1时表示输入缓冲区寄存器己满, 8042 将值读取后该位自动置0
(3 )位 2:系统标志位,最初加电时为 0,自检通过后置为1
(4 )位 3:置1 时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5 )位 4:置1时表示键盘启用,置0时表示键盘禁用。
(6 )位 5:置1表示发送超时
(7 )位 6:置 1 时表示接收超时。
(8 )位 7 :来自 8048 的数据在奇偶校验时出错。
- 控制寄存器
8 位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式:
(1 )位 0:置 1 时启用键盘中断。 .
(2 )位 1:置1 时启用鼠标中断。
(3 )位 2:设置状态寄存器的位2.
(4 )位 3:置1 时,状态寄存器的位4无效。
(5 )位 4:置1时禁止键盘。
(6 )位 5:置1表示禁止鼠标
(7 )位 6:将第二套键盘扫描码转换为第一套键盘扫描码。 。
(8 )位 7 :保留位,默认为0。
10.3.4 测试键盘中断处理程序
修改myos/kernel/kernel.S 一步到位,将8259A的中断全部注册完(一共16个IR引脚,所以中断也注册了16个)
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 ;保留
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-16 09:45:38* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-16 09:57:20* @FilePath: /OS/chapter10/10.3/device/keyboard.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#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不再继续响应键盘中断*/uint8_t scancode = inb(KBD_BUF_PORT);put_int(scancode);return;
}/*键盘初始化*/
void keyboard_init(){put_str("keyboard init start\n");register_handler(0x21,intr_keyboard_handler);put_str("keyboard init done\n");
}
10.4 编写键盘驱动
10.4.1 转义字符介绍
c语言有三种转义字符:
- 一般转义字符,
'\+单个字母’
的形式。 - 八进制转义字符,
'\+三位八进制数字'
表示的 ASCII的形式 。 - 十六进制转义字符,
'\+两位十六进制数字'
表示的 ASCII 码’的形式 。
10.4.2 处理扫描码
一维数组中的第 0 个元素是某个按键未与 shift 键组合时对应的字符 ASCII 码值,第 1 个元素是某个按键与 shift 键组合时对应的字符 ASCII 码值。
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-16 09:45:38* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-16 13:03:19* @FilePath: /OS/chapter10/10.3/device/keyboard.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#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' // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\x7f'/* 以上不可见字符一律定义为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/* 定义控制字符的通码和断码 */
#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_down_last = caps_lock_status;bool break_code;uint16_t scancode = inb(KBD_BUF_PORT);/*若扫描码 scancode 是e0的,表示此键的按下将产生多个扫描码,所以马上结束此次中断处理函数,等待下一个扫描码进来*/if(scancode == 0xe0){ext_scancode = true; //打开e0标签return;}/*如果上次是以 OxeO 开头的,将扫描码合并*/if(ext_scancode){scancode=((0xe000)|scancode);ext_scancode = false; //关闭e0标签}break_code = ((scancode & 0x0080)!=0); //获取break_codeif(break_code){ //若是断码break_code/*由于ctrl_r和alt_r的make_code和break_code都是两个字节,所以可以用下面的方法获取make_code,多字节的扫描码暂不处理*/uint16_t make_code = (scancode &= 0xff7f); //得到其make_code 就是把那个为0x80的那个位置位0嘛/*若是任意以下三个键弹起了,将状态置为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_l_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){ //如果同时按下了shiftshift = true;}}else {//默认字母按键if(shift_down_last && caps_down_last){//如果shift和capslock同时按下shift = false;}else if(shift_down_last || caps_down_last){//如果shift和capslock任意被按下shift = true;}else{shift = false;} }uint8_t index = (scancode &= 0x00ff);//将高字节置0,主要针对高字节的e0char 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_status =! caps_lock_status; }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");
}
10.5 环形输入缓冲区
shell 命令是由多个字符组成的,并且要以回车键结束,因此咱们在键入命令的过程中,必须要找个缓冲区把己键入的信息存起来,当凑成完整的命令名时再一井由其他模块处理。本节咱们要构建这个缓冲区。
10.5.1 生产者与消费者问题简述
生产者与消费者问题描述的是:
对于有限大小的公共缓冲区,如何同步生产者与消费者的运行,以达到对共享缓冲区的互斥访问,并且保证生产者不会过度生产,消费者不会过度消费,缓冲区不会被破坏 。
10.5.2 环形缓冲区的实现
内存中的缓冲区就是用来暂存数据的一片内存区域,内存是按地址来访问的,因此内存缓冲区实际上是线性存储。
环形缓冲区本质上依然是线性缓冲区,但其使用方式像环一样,没有固定的起始地址和终丘地址,环内任何地址都可以作为起始和结束。
对于缓冲区的访问,我们提供两个指针,一个是头指针,用于往缓冲区中写数据,另一个是尾指针,用于从缓冲区中读数据。每次通过头指针往缓冲区中写入一个数据后,使头指针加 1 指向缓冲区中下一个可写入数据的地址,每次通过尾指针从缓冲区中读取一个数据后,使尾指针加 1 指向缓冲区中下一个可读入数据的地址,也就是说,缓冲区相当于一个队列,数据在队列头被写入,在队尾处被读出。
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-17 10:12:49* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-17 10:35:41* @FilePath: /OS/chapter10/10.5/device/ioqueue.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"/*初始化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); //唤醒消费者}
}
10.5.3 添加键盘输入缓冲区
虽然我们的环形缓冲区支持多个生产者和消费者,但目前我们应用的场合非常简单,只是用在单一生产者和单一消费者的环境中,即生产者是键盘驱动,消费者是将来的 shell,那现在您知道了,本节要将在键盘驱动中处理的字符存入环形缓冲区当中 。
#include "ioqueue.h"struct ioqueue kbd_buf; // 定义键盘缓冲区char cur_char = keymap[index][shift];if (cur_char) {/***************** 快捷键ctrl+l和ctrl+u的处理 ********************** 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为:* cur_char的asc码-字符a的asc码, 此差值比较小,* 属于asc码表中不可见的字符部分.故不会产生可见字符.* 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/if ((ctrl_status && cur_char == 'l') || (ctrl_status && cur_char == 'u')) {cur_char -= 'a';}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");
}
10.5.4 生产者和消费者实例测试
实现的效果是:我按下键盘,然后向缓冲区中写入数据,两个消费者进程来争抢这个数据,谁抢到了,谁打印出来这个数据,并告知自己是谁
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-03-26 10:04:44* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-17 14:30:53* @FilePath: /OS/chapter6/6.3.4/kernel/main.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#include "print.h"
#include "init.h"
#include "ioqueue.h"
#include "keyboard.h"void k_thread_a(void* arg);
void k_thread_b(void* arg);
int main(void){put_str("I am kernel\n");init_all(); //初始化thread_start("k_thread_a", 31, k_thread_a, " A_");thread_start("k_thread_b", 31, k_thread_b, " B_");intr_enable();while(1){}return 0;
}void k_thread_a(void* arg){while(1){enum 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){while(1){enum 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);}
}