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

同步机制 —— 锁

术语介绍

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

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

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

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

    例如:有个资源 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 = 0
    ASSERT(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 = 1
    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(); // 将 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,需要 ICW4
    outb(PIC_M_DATA, 0x20); // ICW2:起始中断向量号为 0x20(即十进制32)
                            // 也就是 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_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_code
        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;
        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 + capsLock
            else if(shift_down_last || caps_lock_last) target = 1;  // shift 或 capsLock
            else 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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值