上一章中我们遇到的字符混乱和 GP 异常问题,根本原因是由于临界区代码的资源竞争,这需要一些互斥的方法来保证操作的原子性。
10.1 同步机制——锁
10.1.1 排查 GP 异常,理解原子操作
多线程执行刷屏时光标值越界导致 GP 异常。
10.1.2 找出代码中的临界区、互斥、竞争条件
公共资源:“显存”
临界区:put_char 的多个指令
10.1.3 信号量
信号量是个计数器,它的计数值是自然数,用来记录所积累信号的数量。
对于信号量的增减方法用 P、V 操作来表示,P 表示减少,V 表示增加。
P 操作(书中为减少操作 down)包括:
判断信号量是否大于 0;
若大于 0 则加 1;
若等于 0 则阻塞;
V 操作(书中为增加操作 up)包括:
将信号量的值加 1;
唤醒在此信号量上等待的线程;
若信号量初值为 1,它的取值就只能为 0 和 1,这成为二元信号量,可以利用二元信号量来实现锁。
10.1.4 线程的阻塞和唤醒
本节实现线程阻塞函数thread_block
和线程唤醒函数thread_unblock
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void thread_block (enum task_status stat) { 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) { 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); }
对于thread_block
来说,阻塞线程只需要将当前线程换下 CPU 并不放入就绪队列。
而对于thread_unblock
来说,被阻塞的线程无法唤醒自己,需要被其他线程唤醒,所以参数 pthread 指向的是目前已经被阻塞,又希望被唤醒的线程。
10.1.5 锁的实现
本节会实现信号量和锁。
我们创建 thread/sync.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #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 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) ;#endif
和 thread/sync.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #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 ); } void sema_down (struct semaphore* psema) { enum intr_status old_status = intr_disable(); while (psema->value == 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" ); } list_append(&psema->waiters, &running_thread()->general_tag); thread_block(TASK_BLOCKED); } psema->value--; ASSERT(psema->value == 0 ); intr_set_status(old_status); } 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); 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()); if (plock->holder_repeat_nr > 1 ) { plock->holder_repeat_nr--; return ; } ASSERT(plock->holder_repeat_nr == 1 ); plock->holder = NULL ; plock->holder_repeat_nr = 0 ; sema_up(&plock->semaphore); }
信号量 down 操作:
信号量>0 时,把信号量-1,恢复中断;
信号量=0 时,把当前线程加入信号量的等待队列,然后阻塞自己;
信号量 up 操作:获取阻塞线程,将其唤醒,同时信号量+1。
10.2 用锁实现终端输出
tty 表示终端,我们登录系统后,就会在后台运行一个 tty 进程,相当于一个虚拟终端。虚拟终端通过向显卡的"Start Address High Register"和"Start Address Low Register"设置不同的 16 位地址实现显存分块显示,也就是实现多个虚拟终端。
这里要提一下,通过 ssh 登录的终端称为 pts,可以通过 who 命令输出 tty 还是 pts 来判断用户是否为远程登录。
本节没有实现终端,只是互斥访问一个终端。
device/console.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 #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
device/console.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #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(); }
然后在init.c 中添加上console_init();
,将main.c 的 while 循环里的put_str
函数改为console_put_str
,运行测试:
10.3 从键盘获取输入
10.3.1 键盘输入原理简介
键盘是个独立的设备,其内部有个叫做键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,每当键盘发生按键操作,键盘编码器就向键盘控制器报告。
键盘控制器在主机内部的主板上,通常是 Intel 8042 或兼容芯片,用于接收来自键盘编码器的按键信息并解码保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息。
键盘上所有按键都有自己的编码,每个按键都会分配唯一的数字,这个记录“按键-数值”的编码映射表称为键盘扫描码。
同时,按键的状态有按下和弹起,所以一个键有两个编码,按键按下的编码叫通码(makecode),弹起的叫断码(breakcode)。
10.3.2 键盘扫描码
根据不同的编码方案,键盘扫描码有三套:scan code set 1、scan code set 2、scan code set 3.
第一套扫描码是 XT 键盘所用的扫描码,这是最早的键盘编码:
第二套是 AT 键盘的扫描码,也是目前常用的扫描码:
第三套是 IBM PS/2 系列计算机使用的键盘扫描码,不过这种键盘已经很少看到了:
无论使用哪套扫描码,为了兼容,在 8042 中都会转换成第一套扫描码来使用。
大多数情况下第一套扫描码中的通码和断码都是 1 字节大小,且断码= 0x80+通码 。
第二套扫描码一般通码是 1 字节大小,断码是在通码前再加 1 字节的 0xf0,共 2 字节。
有些键是 0xe0 作为前缀,不为 1 字节,这类是后来扩展进来的按键。
Intel 8042 的输出缓冲区寄存器只有 8 位宽度,所以每收到 1 字节扫描码就会向中断代理发送中断信号。
这里放了两套扫描码方便对比和“假设”。
10.3.3 8042 简介
和键盘相关的芯片只有 8042 和 8048,它们都是独立的处理器,都有自己的寄存器和内存。
Intel 8042 芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的 IO 接口,
因此它是 8048 的代理。键盘编码器 8048 芯片通过 PS/2、USB 等接口与 8042 通信,处理器通过接口与 8042 通信。
8042 寄存器如上表所示,可以实现两个功能:
当需要把数据从处理器发到 8042 时 (数据传送尚未发生时), 0x60 端口的作用是输入缓冲区,此时应该用 out 指令写入 0x60 端口。
当数据己从 8048 发到 8042 时, 0x60 端口的作用是输出缓冲区,此时应该用 in 指令从 8042 的 0x60 端口(输出缓冲区寄存器〉读取 8048 的输出结果。
其中状态寄存器(8 位,只读)的作用:
位 0:置 1 时表示输出缓冲区寄存器己满,处理器通过 m 指令读取后该位自动置 0 。
位 1:置 1 时表示输入缓冲区寄存器己满, 8042 将值读取后该位自动置 0。
位 2:系统标志位,最初加电时为 0,自检通过后置为 1 。
位 3:置 1 时,表示输入缓冲区中的内容是命令,置 0 时,输入缓冲区中的内容是普通数据。
位 4:置 1 时表示键盘启用,置 0 时表示键盘禁用。
位 5:置 1 时表示发送超时。
位 6:置 1 时表示接收超时。
位 7:来自 8048 的数据在奇偶校验时出错。
控制寄存器(8 位,只写)的作用:
位 0:置 1 时启用键盘中断。
位 1 :置 1 时启用鼠标中断。
位 2:设置状态寄存器的位 2 。
位 3:置 1 时,状态寄存器的位 4 无效。
位 4:置 1 时禁止键盘。
位 5:置 1 时禁止鼠标。
位 6:将第二套键盘扫描码转换为第一套键盘扫描码。
位 7:保留位,默认为 0 。
10.3.4 测试键盘中断处理程序
本节完成一个键盘中断处理程序。
首先添加足够的中断向量号,在 kernel/kernel.S 中添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 VECTOR 0 x20, ZERO ;时钟中断对应的入口 VECTOR 0 x21, ZERO ;键盘中断对应的入口 VECTOR 0 x22, ZERO ;级联用的 VECTOR 0 x23, ZERO ;串口2 对应的入口 VECTOR 0 x24, ZERO ;串口1 对应的入口 VECTOR 0 x25, ZERO ;并口2 对应的入口 VECTOR 0 x26, ZERO ;软盘对应的入口 VECTOR 0 x27, ZERO ;并口1 对应的入口 VECTOR 0 x28, ZERO ;实时时钟对应的入口 VECTOR 0 x29, ZERO ;重定向 VECTOR 0 x2a, ZERO ;保留 VECTOR 0 x2b, ZERO ;保留 VECTOR 0 x2c, ZERO ;ps/2 鼠标 VECTOR 0 x2d, ZERO ;fpu浮点单元异常 VECTOR 0 x2e, ZERO ;硬盘 VECTOR 0 x2f, ZERO ;保留
这 16 个中断向量号正好对应两个 8259A 芯片的 16 个引脚,有关 8259A 的介绍见《操作系统真象还原》:第七章 中断 | Songqian Li’s Blog 。
添加完中断向量号后,需要在中断描述符表中注册中断入口程序。我们需要先更改IDT_DESC_CNT
的值即目前支持的总中断数为0x30
,然后开启 8259A 对键盘中断的屏蔽:
同时在 init.c 中加入keyboard_init()
函数,为了显示直观,可以注释掉在 main.c 中的输出代码。最终运行得到:
10.4 编写键盘驱动
本节将编写键盘中断处理程序。
10.4.1 转义字符介绍
C 语言中有三种转移字符:
一般转义字符,'+单个字母’的形式。
八进制转义字符,'\0+三位八进制数字表示的 ASCII 码形式。
十六进制转义字符,‘\x+两位十六进制数字表示的 ASCII 码’ 的形式。
10.4.2 处理扫描码
对于控制键,例如、等,它通常是组合键,需要与其他键一起考虑然后做出具体的行为,在键盘驱动中完成处理。
对于字符方面的键,统统交给字符处理程序完成。
首先在 keyboard.c 中添加按键的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define esc '\033' #define backspace '\b' #define tab '\t' #define enter '\r' #define delete '\177' #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 static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
然后修改键盘中断处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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; bool break_code; uint16_t scancode = inb(KBD_BUF_PORT); if (scancode == 0xe0 ) { ext_scancode = true ; return ; } if (ext_scancode) { scancode = ((0xe000 ) | scancode); ext_scancode = false ; } break_code = ((scancode & 0x0080 ) != 0 ); if (break_code) { uint16_t make_code = (scancode &= 0xff7f ); 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)) { bool shift = false ; if ((scancode < 0x0e ) || (scancode == 0x29 ) || \ (scancode == 0x1a ) || (scancode == 0x1b ) || \ (scancode == 0x2b ) || (scancode == 0x27 ) || \ (scancode == 0x28 ) || (scancode == 0x33 ) || \ (scancode == 0x34 ) || (scancode == 0x35 )) { if (shift_down_last) { shift = true ; } } else { if (shift_down_last && caps_lock_last) { shift = false ; } else if (shift_down_last || caps_lock_last) { shift = true ; } else { shift = false ; } } uint8_t index = (scancode &= 0x00ff ); char cur_char = keymap[index][shift]; 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" ); } }
10.5 环形输入缓冲区
用来存储键盘输入结果,组成命令字符串传入其他模块使用。
10.5.1 生产者与消费者问题简述
略
10.5.2 环形缓冲区实现
内存中的缓冲区就是用来暂存数据的一片内存区域,内存是按地址来访问的,因此内存缓冲区实际上是线性存储。但是我们可以设计出逻辑上非线性的内存缓冲区,通过合理的操作方式可以构造出任何我们想要的数据结构,故这里引入环形缓冲区的概念。
环形缓冲区本质上依然是线性缓冲区,但其使用方式像环一样,没有固定的起始地址和终止地址,环内任何地址都可以作为起始和结束。对于缓冲区访问这里提供两个指针,一个头指针,用于往缓冲区写数据,也可称为写指针;一个尾指针,用户往缓冲区读数据,也可称为读指针。
我们在 device 目录下实现 ioqueue
ioqueue.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #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; }; 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) ;#endif
这里定义环形缓冲区有六个成员变量
ioqueue.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include "ioqueue.h" #include "interrupt.h" #include "global.h" #include "debug.h" void ioqueue_init (struct ioqueue* ioq) { lock_init(&ioq->lock); ioq->producer = ioq->consumer = NULL ; ioq->head = ioq->tail = 0 ; } 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); } static void wakeup (struct task_struct** waiter) { ASSERT(*waiter != NULL ); thread_unblock(*waiter); *waiter = NULL ; } char ioq_getchar (struct ioqueue* ioq) { ASSERT(intr_get_status() == INTR_OFF); 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; } void ioq_putchar (struct ioqueue* ioq, char byte) { ASSERT(intr_get_status() == INTR_OFF); 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 添加键盘输入缓冲区
修改 keyboard.c 中intr_keyboard_handler()
函数的 if(cur_char)语句内容:
并且在keyboard_init()
的 register_handler 前对 ioqueue 初始化:
1 2 3 4 5 #include "ioqueue.h" ... struct ioqueue kbd_buf ; ... ioqueue_init(&kbd_buf);
10.5.4 生产者与消费者测试
在 keyboard.h 中添加 kbd_buf 的外部声明:
1 extern struct ioqueue kbd_buf ;
在 main.c 中添加消费者线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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_a, "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); } }
编译运行,发现键盘每输入一个字符,线程 A 和 B 就会交替运行输出。