Songqian Li's Blog

去历史上留点故事

上一章中我们遇到的字符混乱和 GP 异常问题,根本原因是由于临界区代码的资源竞争,这需要一些互斥的方法来保证操作的原子性。

10.1 同步机制——锁

10.1.1 排查 GP 异常,理解原子操作

多线程执行刷屏时光标值越界导致 GP 异常。

10.1.2 找出代码中的临界区、互斥、竞争条件

公共资源:“显存”
临界区:put_char 的多个指令

10.1.3 信号量

信号量是个计数器,它的计数值是自然数,用来记录所积累信号的数量。
对于信号量的增减方法用 P、V 操作来表示,P 表示减少,V 表示增加。
P 操作(书中为减少操作 down)包括:

  1. 判断信号量是否大于 0;
  2. 若大于 0 则加 1;
  3. 若等于 0 则阻塞;

V 操作(书中为增加操作 up)包括:

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

若信号量初值为 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
/**
* 当前线程将自己阻塞,标志其状态为stat.
*/
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();
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);
}

对于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); //初始化信号量的等待队列
}

/* 初始化锁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();
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操作 */
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);
}

/* 获取锁plock */
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);

plock->holder = NULL; // 把锁的持有者置空放在V操作之前
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}

信号量 down 操作:

  1. 信号量>0 时,把信号量-1,恢复中断;
  2. 信号量=0 时,把当前线程加入信号量的等待队列,然后阻塞自己;

信号量 up 操作:获取阻塞线程,将其唤醒,同时信号量+1。

10.2 用锁实现终端输出

tty 表示终端,我们登录系统后,就会在后台运行一个 tty 进程,相当于一个虚拟终端。虚拟终端通过向显卡的"Start Address High Register"和"Start Address Low Register"设置不同的 16 位地址实现显存分块显示,也就是实现多个虚拟终端。
image.png
这里要提一下,通过 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();
}

/* 终端中输出16进制整数 */
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,运行测试:
image.png

10.3 从键盘获取输入

10.3.1 键盘输入原理简介

键盘是个独立的设备,其内部有个叫做键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,每当键盘发生按键操作,键盘编码器就向键盘控制器报告。
键盘控制器在主机内部的主板上,通常是 Intel 8042 或兼容芯片,用于接收来自键盘编码器的按键信息并解码保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息。
image.png
键盘上所有按键都有自己的编码,每个按键都会分配唯一的数字,这个记录“按键-数值”的编码映射表称为键盘扫描码。
同时,按键的状态有按下和弹起,所以一个键有两个编码,按键按下的编码叫通码(makecode),弹起的叫断码(breakcode)。

10.3.2 键盘扫描码

根据不同的编码方案,键盘扫描码有三套:scan code set 1、scan code set 2、scan code set 3.
第一套扫描码是 XT 键盘所用的扫描码,这是最早的键盘编码:
image.png
第二套是 AT 键盘的扫描码,也是目前常用的扫描码:
image.png
第三套是 IBM PS/2 系列计算机使用的键盘扫描码,不过这种键盘已经很少看到了:
image.png
无论使用哪套扫描码,为了兼容,在 8042 中都会转换成第一套扫描码来使用。
大多数情况下第一套扫描码中的通码和断码都是 1 字节大小,且断码= 0x80+通码
第二套扫描码一般通码是 1 字节大小,断码是在通码前再加 1 字节的 0xf0,共 2 字节。
有些键是 0xe0 作为前缀,不为 1 字节,这类是后来扩展进来的按键。
Intel 8042 的输出缓冲区寄存器只有 8 位宽度,所以每收到 1 字节扫描码就会向中断代理发送中断信号。image.png
这里放了两套扫描码方便对比和“假设”。

10.3.3 8042 简介

和键盘相关的芯片只有 8042 和 8048,它们都是独立的处理器,都有自己的寄存器和内存。
Intel 8042 芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的 IO 接口,
因此它是 8048 的代理。键盘编码器 8048 芯片通过 PS/2、USB 等接口与 8042 通信,处理器通过接口与 8042 通信。
image.png
8042 寄存器如上表所示,可以实现两个功能:

  • 当需要把数据从处理器发到 8042 时 (数据传送尚未发生时), 0x60 端口的作用是输入缓冲区,此时应该用 out 指令写入 0x60 端口。
  • 当数据己从 8048 发到 8042 时, 0x60 端口的作用是输出缓冲区,此时应该用 in 指令从 8042 的 0x60 端口(输出缓冲区寄存器〉读取 8048 的输出结果。

其中状态寄存器(8 位,只读)的作用:

  1. 位 0:置 1 时表示输出缓冲区寄存器己满,处理器通过 m 指令读取后该位自动置 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 测试键盘中断处理程序

本节完成一个键盘中断处理程序。
首先添加足够的中断向量号,在 kernel/kernel.S 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ;保留

这 16 个中断向量号正好对应两个 8259A 芯片的 16 个引脚,有关 8259A 的介绍见《操作系统真象还原》:第七章 中断 | Songqian Li’s Blog
添加完中断向量号后,需要在中断描述符表中注册中断入口程序。我们需要先更改IDT_DESC_CNT的值即目前支持的总中断数为0x30,然后开启 8259A 对键盘中断的屏蔽:

1
outb(PIC_M_DATA, 0xfc);        //OCW1: 1111 1100

同时在 init.c 中加入keyboard_init()函数,为了显示直观,可以注释掉在 main.c 中的输出代码。最终运行得到:
image.png

10.4 编写键盘驱动

本节将编写键盘中断处理程序。

10.4.1 转义字符介绍

C 语言中有三种转移字符:

  1. 一般转义字符,'+单个字母’的形式。
  2. 八进制转义字符,'\0+三位八进制数字表示的 ASCII 码形式。
  3. 十六进制转义字符,‘\x+两位十六进制数字表示的 ASCII 码’ 的形式。

10.4.2 处理扫描码

  1. 对于控制键,例如等,它通常是组合键,需要与其他键一起考虑然后做出具体的行为,在键盘驱动中完成处理。
  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' // 八进制表示字符,也可以用十六进制'\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;

然后修改键盘中断处理函数:

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);

/* 若扫描码是e0开头的,表示此键的按下将产生多个扫描码,
* 所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if (scancode == 0xe0) {
ext_scancode = true; // 打开e0标记
return;
}

/* 如果上次是以0xe0开头,将扫描码合并 */
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 关闭e0标记
}

break_code = ((scancode & 0x0080) != 0); // 获取break_code

if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码)
/* 由于ctrl_r 和alt_r的make_code和break_code都是两字节,
所以可用下面的方法取make_code,多字节的扫描码暂不处理 */
uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按键按下时产生的扫描码)
/* 若是任意以下三个键弹起了,将状态置为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_r_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) { // 如果同时按下了shift键
shift = true;
}
} else { // 默认为字母键
if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下
shift = false;
} else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
shift = true;
} else {
shift = false;
}
}

uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
char 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键,当再次按下时则状态取反,
* 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
caps_lock_status = !caps_lock_status;
}
} else {
put_str("unknown key\n");
}
}

10.5 环形输入缓冲区

用来存储键盘输入结果,组成命令字符串传入其他模块使用。

10.5.1 生产者与消费者问题简述

10.5.2 环形缓冲区实现

内存中的缓冲区就是用来暂存数据的一片内存区域,内存是按地址来访问的,因此内存缓冲区实际上是线性存储。但是我们可以设计出逻辑上非线性的内存缓冲区,通过合理的操作方式可以构造出任何我们想要的数据结构,故这里引入环形缓冲区的概念。
环形缓冲区本质上依然是线性缓冲区,但其使用方式像环一样,没有固定的起始地址和终止地址,环内任何地址都可以作为起始和结束。对于缓冲区访问这里提供两个指针,一个头指针,用于往缓冲区写数据,也可称为写指针;一个尾指针,用户往缓冲区读数据,也可称为读指针。
image.png
我们在 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"

/* 初始化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); // 唤醒消费者
}
}

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) {
// 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用
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 就会交替运行输出。

相关文章
评论
分享
  • 《操作系统真象还原》:第九章 线程

    线程和进程将分两部分实现,本章先讲解线程。 9.1 实现内核线程 9.1.1 执行流 在处理器数量不变的情况下,多任务操作系统采用多道程序设计的方式,使处理器在所有任务之间来回切换,这称为“伪并行”,由操作系统中的任务调度器决定当...

    《操作系统真象还原》:第九章 线程
  • GPU虚拟化

    用户层虚拟化 本地 API 拦截和 API formwarding 在用户态实现一个函数库,假设叫 libwrapper, 它要实现底层库的所有 API; 让 APP 调用这个 libwrapper。如何做? libwrap...

    GPU虚拟化
  • 硬件虚拟化

    硬件虚拟化介绍 硬件虚拟化要做的事情 体系结构支持 体系结构 实现功能 作用 模式切换 Host CPU <-> Guest CPU 切换 CPU 资源隔离 二阶段地址转换 GVA-> GPA...

    硬件虚拟化
  • 《操作系统真象还原》:第八章 内存管理系统

    8.1 makefile 简介 这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html 8.1.1 makefile 是什么 makefile 是 Linu...

    《操作系统真象还原》:第八章 内存管理系统
  • 《操作系统真象还原》:第七章 中断

    7.1 中断是什么,为什么要有中断 运用中断能够显著提升并发,从而大幅提升效率。 7.2 操作系统是中断驱动的 略 7.3 中断分类 把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部...

    《操作系统真象还原》:第七章 中断
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。 stdcall是被调用者清理参数所占的栈空间。 举例来说: 12int subtract(int ...

    《操作系统真象还原》:第六章 完善内核
  • 《操作系统真象还原》:第五章 保护模式进阶——加载内核

    5.3 加载内核 5.3.1 用 C 语言写内核 第一个 C 语言代码: 1234int main(void) { while(1); return 0;} 这个内核文件什么都没做,通过while(1)这个死循...

    《操作系统真象还原》:第五章 保护模式进阶——加载内核
  • 《操作系统真象还原》:第五章 保护模式进阶——内存分页机制

    从这一刻起,我们才算开始了真正的操作系统学习之旅 5.1 获取物理内存容量 5.1.1 Linux 获取内存的方法 在 Linux 2.6 内核总是用detect_memory函数来获取内存容量的。其函数本质上是通过调用 BI...

    《操作系统真象还原》:第五章 保护模式进阶——内存分页机制
  • 《操作系统真象还原》:第四章 保护模式入门

    4.1 保护模式概述 在本章大家会见到全局描述符表、中断描述符表、各种门结构,这是 CPU 提供给应用的,咱们用好就行。 保护模式强调的是“保护”,它是在 Intel 80286 CPU 中首次出现,这是继 8086 之后,Inte...

    《操作系统真象还原》:第四章 保护模式入门
  • 《操作系统真象还原》:第三章 完善MBR——I/O接口

    3.3 让我们对显示器说点什么吧 3.3.1 CPU 如何与外设通信——IO 接口 IO 接口功能: 设置数据缓冲,解决 CPU 与外设的速度不匹配 设置信号电平转换电路 设置数据格式转换 设置时序控制电路来同步 CPU 和外部...

    《操作系统真象还原》:第三章 完善MBR——I/O接口