配合视频学习体验更佳!
第一节:https://www.bilibili.com/video/BV18P411q7hg/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV19W4y1Q7yE/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1nX4y1t7Xz/?vd_source=701807c4f8684b13e922d0a8b116af31
第四节:https://www.bilibili.com/video/BV1ZX4y1W75c/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31
第五节:https://www.bilibili.com/video/BV1Ch411P71C/?vd_source=701807c4f8684b13e922d0a8b116af31#reply171635664400
代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system
之前,我们可以通过先关后开中断来保障打印代码不会因为多线程的调度而出现问题。多线程调度很自然的引出了公共资源的有序访问问题。
以下是一些基本概念:
公共资源:可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
互斥:互斥也可称为排他,是指某一时刻公共资源只能被1个任务独享,其他任务想访问公共资源时,必须等待当前公共资源的访问者使用完资源后再开始访问。
临界区:程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。强调一下,临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源。
竞争条件:竞争条件是指多个任务以非互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行进行的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。
所以同步机制的核心,就是让公共资源在某一时刻只能被一个线程的临界区所访问,并且要让它执行完。
围绕上面的目的,我们提出信号量的概念,它实质就是个计数器,有真实含义(如某个资源的数量),常见取值是0与1。当某个进程的临界区需要访问公共资源,就要去查询这个公共资源的信号量,只有信号量>0时(一般就是1),也就是资源可用,才能访问公共资源,此时该公共资源的信号量被减为0。另个进程此时要来访问这个公共资源,发现信号量为0,也就意味着自己需要的资源不可用,但是进程没有所需要的资源,就无法继续推进,所以最好的方法不是让进程在此处死等,而是将自己换下处理器让出处理器,这就是把自己阻塞起来,等待信号量为1后才能被唤醒运行。我们把只有0和1两种情况的信号量叫做二元信号量。
所以,同步机制的核心,现在就变成了,获得信号量的进程才可以运行,没有获得信号量的进程把自己阻塞起来,获得信号量的进程运行完毕释放信号量的时候,要把阻塞起来的进程唤醒,唤醒就是把阻塞进程的pcb从信号量的阻塞队列移入就绪队列。
为了实现同步机制,我们先实现两个函数thread_block与thread_unlock,前者用于将进程阻塞起来,实现原理就是将线程的pcb中的状态字段修改,然后使用调度schedule函数(后面需要修改其对于BLOCKED线程的调度策略);后者用于将进程解除阻塞,原理就是修改pcb的状态字段,然后将线程的pcb放入就绪队列队首(为了尽快调度)。这两个函数均不涉及对某信号量的阻塞队列的操作,我们在别处实现
**myos/thread/thread.c **
//将当前正在运行的线程pcb中的状态字段设定为传入的status,一般用于线程主动设定阻塞
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(); //得到当前正在运行的进程的pcb地址
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);
}
更新 myos/thread/thread.h
void thread_block(enum task_status stat);
void thread_unblock(struct task_struct* pthread);
接下来我们要实现锁机制,来实现对二元信号量的有序分配。首先为信号量与锁建立数据结构。信号量与锁的关系:信号量是对某项资源的管理,实际就是表示资源有多少,与哪些线程在等待这个资源。锁是在信号量机制上实现的,相比信号量多了记录谁造成了锁(也就是二元信号量,或者叫资源分配给了谁)。
myos/thread/sync.h sync取synchronization(同步)的首字母
#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; //有时候线程拿到了信号量,但是线程内部不止一次使用该信号量对应公共资源,就会不止一次申请锁
//内外层函数在释放锁时就会对一个锁释放多次,所以必须要记录重复申请的次数
};
#endif
接下来实现一堆有关于信号量与锁的函数,包含初始化信号量,初始化锁,对信号量的pv操作(代码中是down与up),获取锁与释放锁。
myos/thread/sync.c
#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"
//用于初始化信号量,传入参数就是指向信号量的指针与初值
void sema_init(struct semaphore* psema, uint8_t value) {
psema->value = value; // 为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}
//用于初始化锁,传入参数是指向该锁的指针
void lock_init(struct lock* plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1); //将信号量初始化为1,因为此函数一般处理二元信号量
}
//信号量的down操作,也就是减1操作,传入参数是指向要操作的信号量指针。线程想要申请信号量的时候用此函数
void sema_down(struct semaphore* psema) {
enum intr_status old_status = intr_disable(); //对于信号量的操作是必须关中断的
//一个自旋锁,来不断判断是否信号量已经被分配出去了。为什么不用if,见书p450。
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操作,也就是+1操作,传入参数是指向要操作的信号量的指针。且释放信号量时,应唤醒阻塞在该信号量阻塞队列上的一个进程
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); //对信号量进行down操作
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1; //申请了一次锁
} else {
plock->holder_repeat_nr++;
}
}
//释放锁的函数,参数是指向锁的指针
void lock_release(struct lock* plock) {
ASSERT(plock->holder == running_thread());
//如果>1,说明自己多次申请了该锁,现在还不能立即释放锁
if (plock->holder_repeat_nr > 1) {
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1); //判断现在lock的重复持有数是不是1只有为1,才能释放
plock->holder = NULL; //这句必须放在up操作前,因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,
//就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}
更新myos/thread/sync.h
void sema_init(struct semaphore* psema, uint8_t value);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
现在,我们利用锁机制,建立锁console_lock(意为终端锁)用于协调打印,将原有的put_int,put_char,put_str进行封装。
myos/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();
}
并为其建立头文件 myos/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
将console_init函数封装进入init_all中
myos/kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init();
console_init();
}
现在我们来测试同步机制能否正常运行,写出测试代码 myos/kernel/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);
}
}
接下来我们做一个小实验,作为编写键盘驱动,完成输入输出系统的准备。实验内容就是当我们按下键盘任意键时,屏幕显示k
在键盘内部,会有一个8048芯片,主板内部有8042芯片,当我们按下某个键/松开某个键,8048就会报告这个按键的按下与松开情况给8042,然后8042向中断控制器发送信号。
键盘按下键位后的流程:
1、当键位被按下(不弹起)
2、8048 监控哪个键位被按下,8048 把键位对应的通码(用于描述一个键按下的码,断码就是描述一个键松开的码)发送给 8042,如果不松开,那么就持续发送
3、8042 接收到通码后,便知道具体哪个键位被按下了,对其进行处理,接着保存通码到自己的寄存器
4、8042 接着向中断代理 8259A 发送中断,如果不松开,那么就持续发送中断
5、发生中断后,处理器执行对应的中断处理程序
键位弹起的过程和按下的过程一致。
扫描码(通码与断码合称扫描码)由键盘编码器决定,不同的键盘编码器会产生不同的编码方案,如今有三套:
scan code set 1, 应用:XT 键盘
scan code set 2, 应用:AT 键盘
scan code set 3, 应用:IBM PS/2 系列高端计算机所用键盘
现在大多数用的都是第二套,因此大多数键盘向 8042 发生的都是第二套的扫描码,为了兼容,不管我们用的是第几套编码方案,当键盘发送扫描码到 8042 后,由 8042 进行处理,转为第一套扫描码,这也是 8042 存在的理由之一。因此我们只需要在键盘的中断处理程序中只处理第一套扫描码就可以了。
p465、p466、p467剖析代码kernel.S、interrupt.c、main.c、keyboard.c:
1、代码功能
按下键盘任意键后,屏幕上打印字符k,如果一直按住不松开,那么就连续打印
2、实现原理
键盘信号最后都是由中断处理器来处理,最后都是由对应的中断处理函数来处理
3、代码逻辑
A、为键盘中断建立中断处理函数
B、设定中断控制器,打开键盘中断信号
4、怎么写代码?
A、kernel.S:增加用汇编模板定义的汇编中断函数处理入口
B、kerboard.c:写出键盘中断处理函数intr_keyboard_handler;写出键盘初始化函数keyboard_init(调用中断注册函数注册键盘中断处理函数),并将其封装进入init_all
C、interrupt.c:修改支持中断数量;修改中断控制器初始化代码pic_inic,只打开键盘中断(键盘中断信号接在主片的IR1引脚上)
D、写出测试代码main.c,就是一个死循环
5、代码实现如下:
修改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 ;保留
myos/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");
}
支持代码myos/device/keyboard.h
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void);
#endif
修改kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init();
console_init();
keyboard_init(); // 键盘初始化
}
修改myos/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, 0xfe);
//outb (PIC_S_DATA, 0xff);
/* 测试键盘,只打开键盘中断,其它全部关闭 */
outb (PIC_M_DATA, 0xfd); //键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
测试代码myos/kernel/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);
}
}
6、其他代码详解查看书p465
现在,我们来编写键盘驱动。实质就是完善键盘中断处理函数
p469剖析keyboard.c代码:
1、代码功能
支持最基本的输入功能,并将结果显示在屏幕上
2、实现原理
键盘信号最终都会调用键盘中断处理函数处理,我们可以从8042中取出传入的通码、断码信息来还原按键的按下松开信息,以此针对性处理
3、代码逻辑
从8042的0x60寄存器取出码值,然后判断字符针对性处理
4、怎么写代码?
5、代码实现如下:(myos/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
#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; //用于记录是否是扩展码
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");
}
6、其他代码详解查看书p470
接下来,我们为键盘输入实现环形输入缓冲区,就是在一个逻辑上成环的缓冲区上实现消费者消费,生产者生产。他们对缓冲区的访问是互斥的。这是为了将来实现shell,我们将来会在缓冲区中输入完整命令,然后从缓冲区中取出命令去执行。由于我们是用于键盘输入的缓冲区,所以生产者和消费者均只有1个。
先在myos/device/ioqueue.h实现缓冲区的数据结构struct ioqueue
#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; // 队尾,数据从队尾处读出
};
#endif
实现一大堆与环形缓冲区有关的小函数 myos/device/ioqueue.c
#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;
}
/* 判断队列是否已空 */
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); // 唤醒消费者
}
}
添加函数声明myos/device/ioqueue.h
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);
修改myos/device/keyboard.c 增加键盘缓冲区的定义、将键盘的输入放入缓冲区、在键盘初始化函数中增加对缓冲区的初始化
#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");
}
然后我们修改myos/keyboard.h来将键盘的缓冲区全局化,能让外部函数访问到
extern struct ioqueue kbd_buf;
测试函数myos/main.c 函数想要实现的效果是:我按下键盘,然后向缓冲区中写入数据,两个消费者进程来争抢这个数据,谁抢到了,谁打印出来这个数据,并告知自己是谁
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
/* 临时为测试添加 */
#include "ioqueue.h"
#include "keyboard.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("consumer_a", 31, k_thread_a, " A_");
thread_start("consumer_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);
}
}
然后开启时钟中断
myos/kernel/interrupt.c
/* 初始化可编程中断控制器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);
//同时打开时钟中断与键盘中断
outb (PIC_M_DATA, 0xfc);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}