操作系统真象还原:输入输出系统

第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语言有三种转义字符:

  1. 一般转义字符,'\+单个字母’的形式。
  2. 八进制转义字符,'\+三位八进制数字'表示的 ASCII的形式 。
  3. 十六进制转义字符,'\+两位十六进制数字'表示的 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);}
}

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

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

相关文章

【教学类-64-04】20240619彩色鱼骨图(一)6.5*1CM 6根棒子720种

背景需求&#xff1a; 幼儿益智早教玩具❗️鱼骨拼图 - 小红书在家也能自制的木棒鱼骨拼图&#xff0c;你也收藏起来试一试吧。 #母婴育儿 #新手爸妈 #玩具 #宝宝玩具怎么选 #早教 #早教玩具 #幼儿早教 #益智早教 #玩具 #宝宝早教 #益智拼图 #宝宝拼图 #玩不腻的益智玩具 #儿童…

vscode插件开发之 - Treeview视图

一些测试类插件&#xff0c;往往需要加载测试文件&#xff0c;并执行这些测试文件。以playwright vscode为例&#xff0c;该插件可以显示目录下所有的测试文件。如下图所示&#xff0c;显示了tests目录下的所有xxx.spec.js文件&#xff0c;那么如何在vscode插件开发中显示TreeV…

[Python学习篇] Python公共操作

公共运算符 运算符描述支持的容器类型合并字符串、列表、元组*复制字符串、列表、元组in元素是否存在字符串、列表、元组、字典not in元素是否不存在字符串、列表、元组、字典 示例&#xff1a; 字符串 str1 ab str2 cd print(str1 str2) # abcd print(str1 * 3) # ab…

Go语言day1

下载go语言的安装程序&#xff1a; All releases - The Go Programming Language 配置go语言的环境变量&#xff1a; 写第一个go语言 在E:\go_workspace当前窗口使用cmd命令: 输入 go run test.go

炭熄卡顿、延迟高、联机报错的解决方法一览

炭熄在制作中巧妙地结合了程序随机生成的元素&#xff0c;为玩家呈现出了一个充满未知与惊险的开放世界&#xff0c;是一款独具匠心的中式民俗恐怖题材游戏。在这款游戏中&#xff0c;玩家将化身为一位意外闯入村子的青年&#xff0c;面对种种鬼怪、努力活下来。游戏将于6月24日…

分页插件结合collection标签后分页数量不准确的问题

问题1:不使用collection 聚合分页正确 简单列子 T_ATOM_DICT表有 idname1原子12原子23原子34原子45原子56原子6 T_ATOM_DICT_AUDIT_ROUTE表审核记录表有 idaudit1拒绝1通过4拒绝 我要显示那些原子审核了,我把两个表inner join 就是那些原子审核过了 idnameaudit1原子1拒绝…

iOS原生APP开发的技术难点

iOS原生APP开发的技术难点主要体现在以下几个方面&#xff0c;总而言之&#xff0c;iOS原生APP开发是一项技术难度较高的工作&#xff0c;需要开发者具备扎实的编程基础、丰富的开发经验和良好的学习能力。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xf…

org.springframework.boot:spring-boot-starter-parent:pom:2.3.4.RELEAS

前言 git上拉了一个项目构建过程中无论是clean还是install都报错 注&#xff1a;很看不惯某博主一点简单的经验分享都要开VIP才能查看的作风 org.springframework.boot:spring-boot-starter-parent:pom:2.3.4.RELEASE failed to transfer from https://maven.aliyun.com/rep…

鸿蒙开发通信与连接:【@ohos.bluetooth (蓝牙)】

蓝牙 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 蓝牙模块提供了基础的传统蓝牙能力以及BLE的扫描、广播等功能。 导入模块 import bluetooth from ohos.bluetooth;bluetooth.enableBluet…

【YOLOv10:在简约中发现卓越,VanillaNet定义目标检测新标准】

本文改进:神经网络模型VanillaNet 1.YOLOv10介绍 论文:[https://arxiv.org/pdf/2405.14458] 代码: https://gitcode.com/THU-MIG/yolov10?utm_source=csdn_github_accelerator&isLogin=1 摘要:在过去的几年里,由于其在计算成本和检测性能之间的有效平衡,YOLOS已经成…

【论文精读】RayMVSNet

今天读的是发表在CVPR2022上的无监督MVS文章&#xff0c;作者来自于国防科大。 文章链接&#xff1a;RayMVSNet 项目地址&#xff1a;Github Abstract 作者希望直接优化每个camera ray上的深度值&#xff0c;所以提出这个RayMVSNet来学习1D implicit field的序列预测。使用了…

软件测试之Linux常用基础

目录 1 总体介绍2 Linux操作系统3 应用领域和主流操作系统4 远程连接4.1 常用命令4.2 系统目录4.3 目录和文件管理4.3.1 目录管理命令4.3.2 文件管理命令 4.5 压缩和解压缩 5 用户权限5.1 用户和权限5.2 权限修改5.3 超级用户 6 进程管理7 端口面试题 1 总体介绍 操作系统作用…

第四篇:精通Docker构建:Dockerfile的艺术与策略

精通Docker构建&#xff1a;Dockerfile的艺术与策略 1. 开篇&#xff1a;探索Docker的革命 在探讨我们的主题之前&#xff0c;让我们先回顾一下Docker的概念。Docker是一个开源平台&#xff0c;用于自动化应用程序的部署、扩展和管理&#xff0c;这一切都是在轻量级的容器中进…

【通过新能源汽车的智慧数字底盘技术看计算机的相关技术堆栈?以后是软硬结合的全能程序员的天下,取代全栈(前后端都会的全栈程序员)】

汽车的“智慧数字底盘”是一个综合性的技术平台&#xff0c;旨在提升车辆的性能、安全性和驾驶体验。它集成了多种先进的技术和系统&#xff0c;是全能程序员的必杀技&#xff01; 1. 传感器技术 a. 激光雷达&#xff08;LiDAR&#xff09; 用于生成高分辨率的3D地图&#…

[吃瓜教程]概览西瓜书+南瓜书第1、2章

第一章 绪论 1.1机器学习的定义,什么是机器学习&#xff1f; 1&#xff09;机器学习是这样一门学科&#xff0c;它致力于研究如何通过计算的手段&#xff0c;利用经验来改善系统自身的性能。 2&#xff09;机器学习所研究的主要内容是关于在计算机上从数据中产生模型的算法&a…

给日期加上15天

// 给当前日期加上15天 function toAndTimeFifteen(params) {let startDate new Date(params); // 创建一个Date对象表示2024年5月31日startDate.setDate(startDate.getDate() 15); // 给当前日期加上15天let dateString formatDate(startDate)// 转换时间格式return dateSt…

之所以选择天津工业大学,因为它是双一流、报考难度适宜,性价比高!天津工业大学计算机考研考情分析!

天津工业大学&#xff08;Tiangong University&#xff09;&#xff0c;简称“天工大”&#xff0c;位于天津市&#xff0c;是教育部与天津市共建高校、国家国防科技工业局和天津市共建的天津市重点建设高校、国家“双一流”建设高校、天津市高水平特色大学建设高校、中国研究生…

6.Hugging Face Transformers 快速入门

Hugging Face Transformers 库独特价值 丰富的预训练模型&#xff1a;提供广泛的预训练模型&#xff0c;如BERT、GPT、T5等&#xff0c;适用于各种NLP任务。易于使用&#xff1a;设计注重易用性&#xff0c;使得即使没有深厚机器学习背景的开发者也能快速上手。最新研究成果的…

chrome 录制器及性能分析工具的使用

需求背景&#xff1a; 对比不同VPN方案网络延迟的差异。 验证工具&#xff1a; chrome浏览器自带的录制器、性能插件可以完美的解决这个问题。 注意&#xff1a;录制的操作都在当前页面&#xff0c;不存在新开标签页的场景 解决方案&#xff1a; 使用chrome录制器&#xf…

18张Python数据科学速查表.png

数据科学已经发展成为一个庞大的系统&#xff0c;包含数学、统计学、概率论、计算机、数据库、编程等各种理论技术。 目前在主流的数据科学领域一般有三大生态&#xff0c;一是以sas、matlab、spss等为代表的商业软件生态&#xff0c;二是围绕R语言建立起来的开源生态&#xf…