《操作系统-真象还原》10. 输入输出系统

2023-10-20 01:10

本文主要是介绍《操作系统-真象还原》10. 输入输出系统,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 同步机制 —— 锁
      • 术语介绍
      • 信号量
      • AX、AT、PS/2 键盘图
      • 线程的阻塞与唤醒
      • 锁的实现
        • Q:为什么 sema_down() 中要用 while ?
    • 用锁实现终端输出
    • 从键盘获取输入
      • 键盘输入的基本原理简介
      • 8042 介绍
    • 编写驱动程序
    • 环形输入缓冲区

同步机制 —— 锁

术语介绍

  • 公共资源: 可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。

  • 临界区: 访问公共资源的代码区域,临界区的代码只允许一个线程访问。

  • 互斥:任意时刻访问公共资源只允许一个线程访问,即任意时刻访问公共资源的临界区代码中只允许被一个线程所执行。

  • 竞争条件:竞争条件指多个线程或者进程在读写一个共享数据时,结果依赖于它们执行的相对时间的情形。

    例如:有个资源 A = 10, 此时有线程 P1 和 P2,P1 和 P2 现在都想去修改 A,因此它们并行的去竞争这个资格,而失败的那一方沦落为次执行的线程,因此后面的覆盖了前面线程所修改的值,从而最终决定了 A 最后为多少。
    由此可得:多线程并发访问和操作同一个数据,其所执行的结果和访问的特定顺序有关,称为竞争条件。

信号量

这是我们锁的实现方式之一。
信号量其中的“量”表示数值的多少,0 表示无信号,大于 0 表示可用的信号量的数量。
信号量是计数值,其对计数的操作有:

  • P:Probern,表示减少。
  • V:Verhogen,表示增加。

V 操作包含两个微操作:

  1. 将信号量加一。
  2. 唤醒在此信号量上等待的线程。

P 操作包含三个微操作:

  1. 判断信号量是否大于0。
  2. 若信号量大于 0,则将信号量减 1。
  3. 若信号量等于 0,当前线程将自己阻塞,以在信号量上等待。

信号量是个全局共享的变量,PV 操作都包含了多个微操作,因此都必须是原子操作。

信号量的初值代表的是信号资源的剩余量,若初值为 1,则它的取值只能是 0 和 1,这便是二元信号量。

在二元信号量中,P 操作就是获取锁,V 操作就是释放锁。

我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥。

大致流程为:

  • 线程 A 进入临界区前先通过 P 操作得到锁,此时信号量为 0。

  • 后续线程 B 再进入临界区也需要先通过 P 操作得到锁,由于此时信号量为 0,线程 B 便会阻塞到此信号量上,即该线程进入休眠状态。

  • 当线程 A 从临界区出来后执行 V 操作释放锁,此时信号量重新变回 1,之后线程 A 将线程 B 唤醒。

  • 线程 B 醒来后获得锁,进入临界区。

    注意:被唤醒后,线程会继续在剩余的时间片内执行,调度器并不会将它的时间片重置(即填满)。

AX、AT、PS/2 键盘图

image-20221106212553776

线程的阻塞与唤醒

thread/thread.c:

// 将当前线程阻塞
void thread_block(enum task_status stat) {// state 只能取这些值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;schedule(); // 调度下一个线程intr_set_status(old_status); // 待当前线程被解除阻塞后需要恢复中断
}// 解除某个线程的阻塞
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");}// 将被阻塞的线程 pthread 插入就绪队列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; // 为 0 时表示没有锁,为 1 时表示有锁可取struct list waiters; // 线程阻塞队列
};// 锁结构
struct lock {struct task_struct* holder; // 锁的持有者struct semaphore semaphore; // 用二元信号量实现锁uint32_t holder_repeat_nr;  // 锁的持有者重复申请锁的次数
};

thread/sync.c:

// 初始化信号量
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 操作,P 即 获得锁
void sema_down(struct semaphore* psema) {// 关中断来保证原子操作enum intr_status old_status = intr_disable();while(psema -> value == 0) { // 若为 0,则表示目前没有锁可以获取,锁现在在别的线程那里ASSERT(!elem_find(&psema -> waiters, &running_thread() -> general_tag));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); // 阻塞当前线程,直到被唤醒}// 这里线程被唤醒的条件是:信号量 > 0, 即当前有锁可以获取// 线程被唤醒后,即拿到锁后,执行如下代码:psema -> value--; // 设置信号量状态为不可获取状态(个人感觉这样说会合适点)// 直接这样应该也行:psema = 0ASSERT(psema -> value == 0);intr_set_status(old_status); // 恢复之前的中断状态
}// 信号量的 up 操作,V 即 释放锁
void sema_up(struct semaphore* psema) {enum intr_status old_status = intr_disable(); // 关中断,保证原子操作// 只有当信号量为 0 时才可以释放锁,因为 0 表示已经有线程得到锁了// 所以此时才有可能使后来欲获取锁的线程阻塞,因此进入等待队列(waiters)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++; // 将当前的锁释放(其实这里我感觉应该说成 设置信号量为可获取状态 会合适点)// 直接这样应该也行:psema = 1ASSERT(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(); // 将 holder 指向当前线程,即当前线程的锁持有者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());if(plock -> holder_repeat_nr > 1) { // 这里并不是真正的释放锁,因为 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 操作
}
Q:为什么 sema_down() 中要用 while ?

我们在阻塞线程的时候,有这么一句话:

thread/thread.c:

// 解除某个线程的阻塞
void thread_unblock(struct task_struct* pthread) {...// 将被阻塞的线程 pthread 插入就绪队列list_push(&thread_ready_list, &pthread -> general_tag); // 放到首元素的位置,使其最快得到调度...
}

上面的 list_push 意思是插入到就绪队列的首元素的位置,若现在是 list_append 即插入到队列的队尾,那么现在的情况如下:

  1. 线程 A 得到锁,通过 P 操作得到锁,访问临界区,此时信号量为 0。
  2. 线程 B 也要访问临界区,也要通过 P 操作得到锁,此时因为信号量为 0,因此该线程在此信号量上阻塞,进入阻塞队列。
  3. 线程 A 访问完毕,通过 V 释放锁,此时信号量为 1,接着唤醒阻塞队列中的线程 B,进入就绪队列的队尾(因为我们假设调用了 list_append)。
  4. 此时又来一个线程 C 也要访问该临界区,由于线程 B 在队尾,因此线程 B 会在将来的某一时刻得到上处理器执行的时机,但绝不是现在。
  5. 所以线程 C 先于线程 B 得到锁,此时信号量又为 0 了。
  6. 时光飞逝,终于轮到线程 B 被调度上处理器了。

接下来分 ifwhile 两种情况讨论:

  • 若之前是用 if(psema -> value == 0) 来判断信号量是否为 0,那么 B 醒了后就会接着执行 psema -> value--,但是它不知道信号量已经被 C 置为 0 了,此时若执行该语句就会出问题了。
  • 若之前是用 while(psema -> value == 0) 来判断信号量是否为 0,那么 B 醒了后就会继续判断 value 是否为 0。

但是!上面说的是 list_append 情况,因为这个所以唤醒的线程可能无法第一时间得到处理器的青睐,那换成 list_push 呢?
你看,换成 list_push 后,被阻塞的线程将会第一时间被处理器所调度,因此在该前提下,是可以用 if 的,但 while 更为通用。

最后注意:锁一定是全局共享的(即 static 修饰)。

用锁实现终端输出

kernel/console.c:

// 控制台锁因为要全局使用,因此必须是静态的
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();
}

我们将原本的 put_*() 输出函数给封装成控制台,形式为 console_put_*(),增加了原子操作,保证了原子性,如果要使用控制台资源,则必须得到控制台锁 console_lock

从键盘获取输入

键盘类型:

  • PS/2 键盘
  • USB 键盘
  • 蓝牙键盘

上网查了一下,找到了这张表:
image-20221106213234430

键盘输入的基本原理简介

image-20221106184951469

键盘按下键位后的流程:

  1. 当键位被按下(不弹起)
  2. 8048 监控哪个键位被按下,8048 把键位对应的扫描码发送给 8042(每个键位对应的扫描码所映射的表称为键盘扫描码
  3. 8042 接收到扫描码后,便知道具体哪个键位被按下了,对其进行处理,接着保存扫描码到自己的寄存器
  4. 8042 接着向中断代码 8259A 发送中断
  5. 发生中断后,处理器执行对应的中断处理程序,读入由 8042 所处理后的结果

键位弹起的过程和按下的过程一致。

一个键位有两个状态:

  • 通码(makecode):按下状态。
  • 断码(breakcode):弹起状态。

注意:我们只能得到扫描码,扫描码是硬件提供的编码集,ASCII 是软件中约定的编码集,这是两个不同的编码方案。

扫描码由键盘编码器决定,不同的键盘编码器会产生不同的编码方案,如今有三套:

  • scan code set 1, 应用:XT 键盘
  • scan code set 2, 应用:AT 键盘
  • scan code set 3, 应用:IBM PS/2 系列高端计算机所用键盘

现在大多数用的都是第二套,因此大多数键盘向 8042 发生的都是第二套的扫描码,但是如何兼容这三套呢?
此时 8042 就充当了一个中间层,它是 8048 和 CPU 之间的中间层,不管我们用的是第几套编码方案,当键盘发送扫描码到 8042 后,由 8042 进行处理,转为第一套扫描码,这也是 8042 存在的理由之一。
因此我们只需要在键盘的中断处理程序中只处理第一套扫描码就可以了。

图:键盘扫描码表的图看书上的就行。

  • 断码 = 0x80 + 通码
  • 大多数情况下,第一套扫描码不论通码还是断码都是一个字节,其最高位(第7位)表示按键的状态,0 表示按下,1 表示弹起。
  • 可以到有些断码通码前缀为 0xe0,折表示扩展(extend,表示该键位是后来才有的),所以在扫描码前面加了 0xe0 作为前缀。

8042 收到 1 字节扫描码后,便会向中断代码发中断信号。因此 8042 所发的中断次数取决于该键位扫描码中包含的字节数。
并不是所有的扫描码都是 1 字节,但它们都是以字节为单位发送的。

8042 介绍

我累了,再见,没时间洗澡了,后面单独开一篇吧,这本书的键盘操作太简陋了,书上讲的四个寄存器也只用了一个。

编写驱动程序

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/interrup 开启键盘中断:

#define IDT_DESC_CNT 0x30 // 目前总共支持的中断数// 初始化可编程中断控制器 8259A
static void pic_init(void) {// 初始化主片outb(PIC_M_CTRL, 0x11); // ICW1:边沿触发,级联8259,需要 ICW4outb(PIC_M_DATA, 0x20); // ICW2:起始中断向量号为 0x20(即十进制32)// 也就是 IR[0~7] 为 0x20~0x27outb(PIC_M_DATA, 0x04); // ICW3:IR2 接从片outb(PIC_M_DATA, 0x01); // ICW4:8086模式,正常 EOI// 初始化从片outb(PIC_S_CTRL, 0x11); // ICW1:边沿触发,级联8259,需要 ICW4outb(PIC_S_DATA, 0x28); // ICW2:起始中断向量号为 0x28// 也就是 IR[8~15] 为 0x28~0x2Foutb(PIC_S_DATA, 0x02); // ICW3:设置从片连接到主片的 IR2 引脚outb(PIC_S_DATA, 0x01); // ICW4:8086 模式,正常 EOI// 打开主片上的 IR0, 也就是说目前只接受时钟产生的中断// outb(PIC_M_DATA, 0xfe); // 开启时钟中断outb(PIC_M_DATA, 0xfd); // 开启键盘中断outb(PIC_S_DATA, 0xff); // 屏蔽从片的中断put_str("   pic_init done\n");
}

device/keyboard.c 编写键盘的中断处理程序 intr_keyboard_handler:

#define KBD_BUF_PORT 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_lock_last = caps_lock_status;uint16_t scancode = inb(KBD_BUF_PORT);if(scancode == 0xe0) { // 判断按下的键是否为扩展键,若是,则表示需要读入两个扫描码ext_scancode = true;return;}if(ext_scancode) { // 若按下的键位是扩展键位scancode = ((0xe000) | scancode); // 则对描述符进行合并ext_scancode = false; // 重置扩展状态}bool break_code = ((scancode & 0x0080) != 0); // 是否为断码if(break_code) {uint16_t make_code = (scancode &= 0xFF7F); // 将断码转为通码,并赋值给 make_codeif(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;return; // 按键的弹起不进行处理} else if ((scancode > 0x00 && scancode < 0x3b) || \(scancode == alt_r_make) || \(scancode == ctrl_r_make)) {uint8_t target = 0; // 用于判断最后得到的结果是 keymap 表中一维数组的第 0 个还是第 1 个// 单个键表示双字符的键位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) target = 1;} else {if(shift_down_last && caps_lock_last) target = 0;       // shift + capsLockelse if(shift_down_last || caps_lock_last) target = 1;  // shift 或 capsLockelse target = 0; // 其它}uint8_t index = (scancode &= 0x00FF);  // 00是避免0xe0扩展,最后得到的是低8位扫描码char cur_char = keymap[index][target]; // 找到对应的字符if(cur_char) { // 只处理 ASCII 码不为 0 的键位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");}
}// 键盘初始化
void keyboard_init() {put_str("keyboard init start.\n");register_handler(0x21, intr_keyboard_handler);put_str("keyboard init done.\n");
}

环形输入缓冲区

device/ioqueue.h:

#define bufsize 64// 环形队列
struct ioqueue {struct lock lock;/** 生产者,当缓冲区不满时,就往 buf 中插入数据* 否则就休眠(阻塞线程),此项记录哪个生产者在此缓冲区上休眠**/struct task_struct* producer;/** 消费者,当缓冲区不为空时,就读取 buf 中的数据* 否则就休眠,此项记录哪个消费者在此缓冲区上休眠**/struct task_struct* consumer;char buf[bufsize]; // 顺序存储结构的循环队列 缓冲区int32_t head; // 对头int32_t tail; // 队尾
};

device/ioqueue.c:

// 初始化 IO 队列
void ioqueue_init(struct ioqueue* ioq) {lock_init(&ioq -> lock);ioq -> producer = ioq -> consumer = NULL;ioq -> head = ioq -> tail = 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 队列中生产数据
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);
}

device/keyboard.c 修改:

...if(cur_char) { // 只处理 ASCII 码不为 0 的键位/*****************  快捷键ctrl+l和ctrl+u的处理 ********************** 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为:* cur_char的asc码-字符a的asc码, 此差值比较小,* 属于asc码表中不可见的字符部分.故不会产生可见字符.* 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {cur_char -= 'a';}// 若 kbd_buf 未满,便插入数据if(!ioq_full(&kbd_buf)) {put_char(cur_char);ioq_putchar(&kbd_buf, cur_char);}return;}
...

生产者和消费者都可以有多个,但我们这里就用一个,即生产者为键盘驱动,消费者为将来的 shell。

这篇关于《操作系统-真象还原》10. 输入输出系统的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/243602

相关文章

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统? 从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kerel),因为它相对较小,而且位于环境的核心。  从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特生。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等

【操作系统】信号Signal超详解|捕捉函数

🔥博客主页: 我要成为C++领域大神🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞👍收藏⭐评论✍️ 本博客致力于知识分享,与更多的人进行学习交流 ​ 如何触发信号 信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程 kill -l 查看系统支持的信号

通信系统网络架构_2.广域网网络架构

1.概述          通俗来讲,广域网是将分布于相比局域网络更广区域的计算机设备联接起来的网络。广域网由通信子网于资源子网组成。通信子网可以利用公用分组交换网、卫星通信网和无线分组交换网构建,将分布在不同地区的局域网或计算机系统互连起来,实现资源子网的共享。 2.网络组成          广域网属于多级网络,通常由骨干网、分布网、接入网组成。在网络规模较小时,可仅由骨干网和接入网组成

操作系统实训复习笔记(1)

目录 Linux vi/vim编辑器(简单) (1)vi/vim基本用法。 (2)vi/vim基础操作。 进程基础操作(简单) (1)fork()函数。 写文件系统函数(中等) ​编辑 (1)C语言读取文件。 (2)C语言写入文件。 1、write()函数。  读文件系统函数(简单) (1)read()函数。 作者本人的操作系统实训复习笔记 Linux

Linux系统稳定性的奥秘:探究其背后的机制与哲学

在计算机操作系统的世界里,Linux以其卓越的稳定性和可靠性著称,成为服务器、嵌入式系统乃至个人电脑用户的首选。那么,是什么造就了Linux如此之高的稳定性呢?本文将深入解析Linux系统稳定性的几个关键因素,揭示其背后的技术哲学与实践。 1. 开源协作的力量Linux是一个开源项目,意味着任何人都可以查看、修改和贡献其源代码。这种开放性吸引了全球成千上万的开发者参与到内核的维护与优化中,形成了

SQL Server中,用Restore DataBase把数据库还原到指定的路径

restore database 数据库名 from disk='备份文件路径' with move '数据库文件名' to '数据库文件放置路径', move '日志文件名' to '日志文件存放置路径' Go 如: restore database EaseWe from disk='H:\EaseWe.bak' with move 'Ease

PS系统教程25

介绍软件 BR(bridge) PS 配套软件,方便素材整理、管理素材 作用:起到桥梁作用 注意:PS和BR尽量保持版本一致 下载和安装可通过CSDN社区搜索,有免费安装指导。 安装之后,我们打开照片只需双击照片,就自动在Ps软件中打开。 前提:电脑上有PS软件 三种预览格式 全屏预览 评星级 直接按数字键就可以 方向键可以更换图片 esc退出 幻灯片放

风水研究会官网源码系统-可展示自己的领域内容-商品售卖等

一款用于展示风水行业,周易测算行业,玄学行业的系统,并支持售卖自己的商品。 整洁大气,非常漂亮,前端内容均可通过后台修改。 大致功能: 支持前端内容通过后端自定义支持开启关闭会员功能,会员等级设置支持对接官方支付支持添加商品类支持添加虚拟下载类支持自定义其他类型字段支持生成虚拟激活卡支持采集其他站点文章支持对接收益广告支持文章评论支持积分功能支持推广功能更多功能,搭建完成自行体验吧! 原文

Django 路由系统详解

Django 路由系统详解 引言 Django 是一个高级 Python Web 框架,它鼓励快速开发和干净、实用的设计。在 Django 中,路由系统是其核心组件之一,负责将用户的请求映射到相应的视图函数或类。本文将深入探讨 Django 的路由系统,包括其工作原理、配置方式以及高级功能。 目录 路由基础URL 映射路由参数命名空间URL 反向解析路由分发include 路由路由修饰符自

HarmonyOS NEXT:华为开启全新操作系统时代

在全球科技浪潮的汹涌澎湃中,华为再次以创新者的姿态,引领了一场关于操作系统的革命。HarmonyOS NEXT,这一由华为倾力打造的分布式操作系统,不仅是对现有技术的一次大胆突破,更是对未来智能生活的一次深邃展望。 HarmonyOS NEXT并非简单的迭代升级,而是在华为多年技术积淀的基础上,对操作系统的一次彻底重构。它采用微内核架构,摒弃了传统的宏内核模式,实现了模块化和组件化的设计理念