《操作系统真象还原》第10章
重复一些概念:
“
- 公共资源
可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
- 临界区
临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态资源。
- 互斥
互斥也可称为排他,是指某一时刻公共资源只能被1个任务独享,即不允许多个任务同时出现在字节的临界区中。
- 竞争条件
竞争条件是指多个任务以互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行进行的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。
临界区中很少存在只有一条指令的情况,因此我们必须提供一种互斥的机制,互斥能使临界区具有原子性,避免产生竞争条件,从而避免了多任务访问公共资源时出现问题。
”
步骤:
1.实现锁
2.用锁实现终端输出
3.从键盘获得输入输出
4.编写键盘驱动
5.环形输入缓冲区
1.实现锁
先要实现线程的阻塞与解除阻塞函数,因为获得锁的目的就是要阻塞其它线程,在thread/thread.c的最后添加:
1 /* 当前线程将自己阻塞,标记状态为status */
2 void thread_block(enum task_status stat){
3 /* stat取值为TASK_BLOCKED、TASK_WATING、TASK_HANGING时不会被调度 */
4 ASSERT((stat==TASK_BLOCKED) || (stat==TASK_WAITING) || (stat==TASK_HANGING));
5 enum intr_status old_status=intr_disable();
6 struct task_struct* cur_thread=running_thread();
7 cur_thread->status=stat; // 置其状态为stat
8 schedule();
9 /* 待当前线程被解除阻塞状态后才能继续下面的intr_set_status */
10 intr_set_status(old_status);
11 }
12
13 /* 解除pthread的阻塞状态 */
14 void thread_unblock(struct task_struct* pthread){
15 enum intr_status old_status=intr_disable();
16 ASSERT((pthread->status==TASK_BLOCKED) || (pthread->status==TASK_WAITING) || (pthread->status==TASK_HANGING));
17 if (pthread->status!=TASK_READY){
18 ASSERT(!elem_find(&thread_ready_list,&pthread->general_tag));
19 if (elem_find(&thread_ready_list,&pthread->general_tag)){
20 PANIC("thread_unblock:blocked thread in ready_list\n"); // 想要解除阻塞状态的thread已经在ready_list中了,有问题
21 }
22 list_push(&thread_ready_list,&pthread->general_tag); // 放到队列最前面,使其尽快得到调度
23 pthread->status=TASK_READY;
24 }
25 intr_set_status(old_status);
26 }
并在thread/thread.h中添加函数定义。
再用信号量机制实现锁,设置semaphore->value=1为二元信号量实现互斥锁。
在thread/sync.h下:
1 #ifndef __THREAD_SYNC_H
2 #define __THREAD_SYNC_H
3 #include "list.h"
4 #include "stdint.h"
5 #include "thread.h"
6
7 struct semaphore{
8 uint8_t value; // 信号量的值
9 struct list waiters; // 等待该信号量的线程的队列
10 };
11
12 struct lock{
13 struct task_struct* holder; // 锁的持有者
14 struct semaphore semaphore; // 信号量
15 uint32_t holder_repeat_nr; // 持有者重复申请锁的次数
16 };
17
18 void sema_init(struct semaphore* psema,uint8_t value);
19 void lock_init(struct lock* lock);
20 void sema_down(struct semaphore* psema);
21 void sema_up(struct semaphore* psema);
22 void lock_acquire(struct lock* plock);
23 void lock_release(struct lock* plock);
24 #endif
在thread/sync.c下:
1 #include "sync.h"
2 #include "stdint.h"
3 #include "list.h"
4 #include "thread.h"
5 #include "debug.h"
6 #include "interrupt.h"
7 /* 初始化信号量 */
8 void sema_init(struct semaphore* psema,uint8_t value){
9 psema->value=value;
10 list_init(&psema->waiters);
11 }
12
13 /* 初始化锁 */
14 void lock_init(struct lock* plock){
15 plock->holder=NULL;
16 sema_init(&plock->semaphore,1); // 信号量初值为1
17 plock->holder_repeat_nr=0;
18 }
19
20 /* 信号量dowm操作 */
21 void sema_down(struct semaphore* psema){
22 /* 关中断来保证原子操作 */
23 enum intr_status old_status=intr_disable();
24 while (psema->value==0){ // 若value为0,表示已被别的thread持有
25 ASSERT(!elem_find(&psema->waiters,&running_thread()->general_tag));
26 /* 当前线程不应再已在信号量的waiters队列中 */
27 if (elem_find(&psema->waiters,&running_thread()->general_tag)){
28 PANIC("sema_down: thread blocked has been in waiters_list\n");
29 }
30 /* 若信号量的值等于0,则当前线程把自己加入等待队列 */
31 list_append(&psema->waiters,&running_thread()->general_tag);
32 thread_block(TASK_BLOCKED); // 阻塞线程,直到被唤醒
33 }
34 /* 若value为1或被唤醒后,会执行以下代码,也就是获得了锁 */
35 --psema->value;
36 ASSERT(psema->value==0);
37 /* 恢复之前的状态 */
38 intr_set_status(old_status);
39 }
40
41 /* 信号量的up操作 */
42 void sema_up(struct semaphore* psema){
43 /* 关中断,保证原子操作 */
44 enum intr_status old_status=intr_disable();
45 ASSERT(psema->value==0);
46 if (!list_empty(&psema->waiters)){
47 struct task_struct* thread_blocked=elem2entry(struct task_struct,general_tag,list_pop(&psema->waiters));
48 thread_unblock(thread_blocked);
49 }
50 ++psema->value;
51 ASSERT(psema->value==1);
52 /* 恢复之前的状态 */
53 intr_set_status(old_status);
54 }
55
56 /* 获取锁 */
57 void lock_acquire(struct lock* plock){
58 if (plock->holder!=running_thread()){
59 sema_down(&plock->semaphore); // 对信号P操作,原子操作
60 plock->holder=running_thread();
61 ASSERT(plock->holder_repeat_nr==0);
62 plock->holder_repeat_nr=1;
63 }else{
64 ++plock->holder_repeat_nr;
65 }
66 }
67
68 /* 释放锁 */
69 void lock_release(struct lock* plock){
70 ASSERT(plock->holder==running_thread());
71 if (plock->holder_repeat_nr>1){
72 --plock->holder_repeat_nr;
73 return;
74 }
75 ASSERT(plock->holder_repeat_nr==1);
76
77 plock->holder=NULL; // 把锁的持有者置空放在V操作之前
78 plock->holder_repeat_nr=0;
79 sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
80 }
2.用锁实现终端输出
在device/下创建console.h:
1 #ifndef __DEVICE_CONSOLE_H
2 #define __DEVICE_CONSOLE_H
3
4 #include "print.h"
5 #include "../thread/sync.h"
6
7 void console_init(void);
8 void console_acquire(void);
9 void console_release(void);
10 void console_put_str(char* str);
11 void console_put_char(uint8_t char_asci);
12 void console_put_int(uint32_t num);
13
14 #endif
及其console.c:
1 #include "console.h"
2 #include "print.h"
3 #include "stdint.h"
4 #include "sync.h"
5 #include "thread.h"
6 static struct lock console_lock; // 控制台锁
7
8 /* 初始化终端 */
9 void console_init(){
10 lock_init(&console_lock);
11 }
12
13 /* 获得终端 */
14 void console_acquire(){
15 lock_acquire(&console_lock);
16 }
17
18 /* 释放终端 */
19 void console_release(){
20 lock_release(&console_lock);
21 }
22
23 /* 终端中输出字符串 */
24 void console_put_str(char* str){
25 console_acquire();
26 put_str(str);
27 console_release();
28 }
29
30 /* 终端中输出字符 */
31 void console_put_char(uint8_t char_asci){
32 console_acquire();
33 put_char(char_asci);
34 console_release();
35 }
36
37 /* 终端中输出十六进制整数 */
38 void console_put_int(uint32_t num){
39 console_acquire();
40 put_int(num);
41 console_release();
42 }
好吧,我们就顺着作者的意思修改一下kernel/init.c(部分):
1 #include "console.h"
2
3 /*负责初始化所有模块*/
4 void init_all(){
5 put_str("init_all\n");
6 idt_init(); // 初始化中断
7 mem_init(); // 初始化内存
8 thread_init(); // 初始化线程
9 timer_init(); // 初始化PIT
10 console_init(); // 初始化终端(控制台)
11 }
现在就可以将kernel/main.c下的put_str()函数修改为console_put_str()了,记得将while(1)中的开关中断语句注释掉并添加头文件。
至于makefile,相信大家可以自己完成的!( •̀ ω •́ )y
看看运行结果:
3.从键盘获得输入输出
在kernel/kernel.S添加中断入口:
1 VECTOR 0x20 ,ZERO
2 VECTOR 0x21 ,ZERO ; 时钟中断
3 VECTOR 0x22 ,ZERO
4 VECTOR 0x23 ,ZERO
5 VECTOR 0x24 ,ZERO
6 VECTOR 0x25 ,ZERO
7 VECTOR 0x26 ,ZERO
8 VECTOR 0x27 ,ZERO
9 VECTOR 0x28 ,ZERO
10 VECTOR 0x29 ,ZERO
11 VECTOR 0x2A ,ZERO
12 VECTOR 0x2B ,ZERO
13 VECTOR 0x2C ,ZERO
14 VECTOR 0x2D ,ZERO
15 VECTOR 0x2E ,ZERO
16 VECTOR 0x2F ,ZERO
修改kernel/interrupt.c的这两部分:
#define IDT_DESC_CNT 0x30 // 目前总共支持的中断数
outb(PIC_M_DATA,0xfd); // 只支持键盘中断
outb(PIC_S_DATA,0xff);
将kernel/main.c中的main()修改为:
1 int main(void) {
2 put_str("Welcome,\nI am kernel!\n");
3 init_all();
4
5 // thread_start("k_thread_a",31,k_thread_a,"argA");
6 // thread_start("k_thread_b",8,k_thread_b,"argB ");
7
8 intr_enable();
9
10 // while(1){
11 // //intr_disable();
12 // console_put_str("Main ");
13 // //intr_enable();
14 // }
15 while (1);
16 return 0;
17 }
顺便说一下vim中多行注释的方法(今天刚学的),wsl2+ubuntu18.04下:
- 多行注释:
键入Esc进入命令行模式下,按下Ctrl+Alt+v进入列(也叫区块)模式;
在行首使用上下键选择需要注释的行;
按下‘I’键(必须大写),进入插入模式;
然后输入注释符(如‘#’、‘//’等);
最后两次按下Esc键
- 删除多行注释:
键入Esc、Ctrl+Alt+v;
光标覆盖要注释的符号:上下(左右)选定要取消注释的行,如果为‘#’只需要上下,如果为‘//’则还需要左右将‘//’整个覆盖;
最后按下‘d’
以上只是准备工作,然后开始写键盘中断处理程序。
创建device/keyboard.c:
1 #include "keyboard.h"
2 #include "print.h"
3 #include "interrupt.h"
4 #include "io.h"
5 #include "global.h"
6
7 #define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60
8
9 /* 键盘中断处理程序 */
10 static void intr_keyboard_handler(void){
11 put_char('k');
12 /* 必须读取输出缓冲区寄存器,否则8042不再继续响应键盘中断 */
13 inb(KBD_BUF_PORT);
14 return;
15 }
16
17 /* 键盘初始化 */
18 void keyboard_init(){
19 put_str("keyboard_init start\n");
20 register_handler(0x21,intr_keyboard_handler);
21 put_str("keyboard_init done\n");
22 }
及其头文件:
1 #ifndef __DEVICE_KEYBOARD_H
2 #define __DEVICE_KEYBOARD_H
3
4 static void intr_keyboard_handler(void);
5 void keyboard_init(void);
6
7 #endif
在kernel/init.c中添加头文件和初始化语句:
#include "keyboard.h"
keyboard_init(); // 初始化键盘
自己再修改一下makefile吧,测试一下:
每当按下键盘中的一个键时,成功显示两个2个'k'。
你可能会对扫描码很感兴趣,让我们再改进一下device/keyboard.c:
运行后如下(我随便输入的):
4.编写键盘驱动
修改device/keyboard.c:

1 #include "keyboard.h"
2 #include "print.h"
3 #include "interrupt.h"
4 #include "io.h"
5 #include "global.h"
6
7 #define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60
8
9 /* 用转义字符定义部分控制字符 */
10 #define esc '\033' // 八进制表示字符,也可以用十六进制'\x1b'
11 #define backspace '\b'
12 #define tab '\t'
13 #define enter '\r'
14 #define delete '\177' // 八进制表示字符,十六进制为'\x7f'
15
16 /* 以下不可见字符一律定义为0 */
17 #define char_invisible 0
18 #define ctrl_l_char char_invisible
19 #define ctrl_r_char char_invisible
20 #define shift_l_char char_invisible
21 #define shift_r_char char_invisible
22 #define alt_l_char char_invisible
23 #define alt_r_char char_invisible
24 #define caps_lock_char char_invisible
25
26 /* 定义控制字符的通码和断码 */
27 #define shift_l_make 0x2a
28 #define shift_r_make 0x36
29 #define alt_l_make 0x38
30 #define alt_r_make 0xe038
31 #define alt_r_break 0xe0b8
32 #define ctrl_l_make 0x1d
33 #define ctrl_r_make 0xe01d
34 #define ctrl_r_break 0xe09d
35 #define caps_lock_make 0x3a
36
37 /* 定义以下变量记录相应键是否按下
38 * ext_scancode用于记录makecode是否以0xe0开头 */
39 static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
40
41 /* 以扫描码make_code为索引的二维数组 */
42 static char keymap[][2]={
43 /* 扫描码未与shift组合 */
44 /************************************/
45 /* 0x00 */ {0, 0},
46 /* 0x01 */ {esc, esc},
47 /* 0x02 */ {'1', '!'},
48 /* 0x03 */ {'2', '@'},
49 /* 0x04 */ {'3', '#'},
50 /* 0x05 */ {'4', '$'},
51 /* 0x06 */ {'5', '%'},
52 /* 0x07 */ {'6', '^'},
53 /* 0x08 */ {'7', '&'},
54 /* 0x09 */ {'8', '*'},
55 /* 0x0A */ {'9', '('},
56 /* 0x0B */ {'0', ')'},
57 /* 0x0C */ {'-', '_'},
58 /* 0x0D */ {'=', '+'},
59 /* 0x0E */ {backspace, backspace},
60 /* 0x0F */ {tab, tab},
61 /* 0x10 */ {'q', 'Q'},
62 /* 0x11 */ {'w', 'W'},
63 /* 0x12 */ {'e', 'E'},
64 /* 0x13 */ {'r', 'R'},
65 /* 0x14 */ {'t', 'T'},
66 /* 0x15 */ {'y', 'Y'},
67 /* 0x16 */ {'u', 'U'},
68 /* 0x17 */ {'i', 'I'},
69 /* 0x18 */ {'o', 'O'},
70 /* 0x19 */ {'p', 'P'},
71 /* 0x1A */ {'[', '{'},
72 /* 0x1B */ {']', '}'},
73 /* 0x1C */ {enter, enter},
74 /* 0x1D */ {ctrl_l_char, ctrl_l_char},
75 /* 0x1E */ {'a', 'A'},
76 /* 0x1F */ {'s', 'S'},
77 /* 0x20 */ {'d', 'D'},
78 /* 0x21 */ {'f', 'F'},
79 /* 0x22 */ {'g', 'G'},
80 /* 0x23 */ {'h', 'H'},
81 /* 0x24 */ {'j', 'J'},
82 /* 0x25 */ {'k', 'K'},
83 /* 0x26 */ {'l', 'L'},
84 /* 0x27 */ {';', ':'},
85 /* 0x28 */ {'\'', '"'},
86 /* 0x29 */ {'`', '~'},
87 /* 0x2A */ {shift_l_char, shift_l_char},
88 /* 0x2B */ {'\\', '|'},
89 /* 0x2C */ {'z', 'Z'},
90 /* 0x2D */ {'x', 'X'},
91 /* 0x2E */ {'c', 'C'},
92 /* 0x2F */ {'v', 'V'},
93 /* 0x30 */ {'b', 'B'},
94 /* 0x31 */ {'n', 'N'},
95 /* 0x32 */ {'m', 'M'},
96 /* 0x33 */ {',', '<'},
97 /* 0x34 */ {'.', '>'},
98 /* 0x35 */ {'/', '?'},
99 /* 0x36 */ {shift_r_char, shift_r_char},
100 /* 0x37 */ {'*', '*'},
101 /* 0x38 */ {alt_l_char, alt_l_char},
102 /* 0x39 */ {' ', ' '},
103 /* 0x3A */ {caps_lock_char, caps_lock_char}
104 /* 其它按键暂不处理 */
105 };
106
107 /* 键盘中断处理程序 */
108 static void intr_keyboard_handler(void){
109
110 /* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
111 bool ctrl_down_last=ctrl_status;
112 bool shift_down_last=shift_status;
113 bool caps_lock_last=caps_lock_status;
114
115 bool break_code;
116 uint16_t scancode=inb(KBD_BUF_PORT);
117
118 /* 若扫描码scancode是e0开头的,表示此键的按下将产生多个扫描码,
119 * 所以马上结束此次中断处理函数,等待下一个扫描码进来 */
120 if (scancode==0xe0){
121 ext_scancode=true; // 打开e0标记
122 return;
123 }
124
125 /* 如果上次是以0xe0开头的,将扫描码合并 */
126 if (ext_scancode){
127 scancode=((0xe000) | scancode);
128 ext_scancode=false; // 关闭e0标记
129 }
130
131 break_code=((scancode & 0x0080)!=0); // 获取break_code
132
133 if(break_code){ // 若是断码(按键弹起时产生的扫描码)
134
135 uint16_t make_code=(scancode &= 0xff7f); //多字节扫描码暂不处理
136
137 /* 若是以下三个键弹起了,将状态设为false */
138 if (make_code==ctrl_l_make || make_code==ctrl_r_make){
139 ctrl_status=false;
140 }
141 else if (make_code==shift_l_make || make_code==shift_r_make){
142 shift_status=false;
143 }
144 else if (make_code==alt_l_make || make_code==alt_r_make){
145 alt_status=false;
146 }
147
148 return; // 直接返回结束此次中断处理程序
149
150 }
151 /* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */
152 else if ((scancode>0x00 && scancode<0x3b) || (scancode==alt_r_make) || (scancode==ctrl_r_make)){
153 bool shift=false; // 判断是否与shift结合,用来在一维数组中索引对应的字符,先默认设置成false
154 if ((scancode<0x0e) || (scancode==0x29) || (scancode==0x1a) || \
155 (scancode==0x1b) || (scancode==0x2b) || (scancode==0x27) || \
156 (scancode==0x28) || (scancode==0x33) || (scancode==0x34) || \
157 (scancode==0x35)){
158 if (shift_down_last){ // 如果同时按下了shift键
159 shift=true;
160 }
161 }
162 else{ // 默认为字母键
163 if (shift_down_last && caps_lock_last){ // 同时按下shift和capslock
164 shift=false;
165 }
166 else if (shift_down_last || caps_lock_last){ // 按下shift或capslock
167 shift=true;
168 }
169 else{
170 shift=false;
171 }
172 }
173
174 uint8_t index=(scancode & 0x00ff);
175 char cur_char=keymap[index][shift]; // 在数组中找到对应的字符
176
177 /* 只处理ASCII码不为0的键 */
178 if (cur_char){
179 //if (!ioq_full(&ioqueue)){
180 //ioq_putchar(&ioqueue,cur_char);
181 put_char(cur_char);
182 //}
183 return;
184 }
185
186 /* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */
187 if (scancode==ctrl_l_make || scancode==ctrl_r_make){
188 ctrl_status=true;
189 }
190 else if (scancode==shift_l_make || scancode==shift_r_make){
191 shift_status=true;
192 }
193 else if (scancode==alt_l_make || scancode==alt_r_make){
194 alt_status=true;
195 }
196 else if (scancode==caps_lock_make){
197 caps_lock_status=!caps_lock_status;
198 }
199 else{
200 put_str("unknown key\n");
201 }
202 }
203 return;
204 }
205
206 /* 键盘初始化 */
207 void keyboard_init(){
208 put_str("keyboard_init start\n");
209 register_handler(0x21,intr_keyboard_handler);
210 put_str("keyboard_init done\n");
211 }
运行效果如图:
终于能和计算机交流了,amazing!
5.环形输入缓冲区
虽然bochs中已经能够显示我们所输入的内容了,但真正的交互是要它能够解析我们输入的shell命令并响应,不是吗?让我们来构建一个缓冲区,存下已键入的命令,然后将完整的命令交由其它模块一并处理,我已经迫不及待了:)
依次进行以下编写/修改:
①device/ioqueue.h:
1 #ifndef __DEVICE_IOQUEUE_H
2 #define __DEVICE_IOQUEUE_H
3 #include "stdint.h"
4 #include "thread.h"
5 #include "sync.h"
6
7 #define bufsize 64
8
9 /* 环形队列 */
10 struct ioqueue{
11 // 生产者消费者问题
12 struct lock lock;
13 /* 生产者,缓冲区不满就可以继续往里面写数据;
14 * 否则就睡眠,等待消费者消费后唤醒 */
15 struct task_struct* producer;
16
17 /* 消费者,缓冲区不空就可以继续从里面读数据;
18 * 否则就睡眠,等待生产者生产后唤醒 */
19 struct task_struct* consumer;
20
21 char buf[bufsize]; // 缓冲区
22 int32_t head; // 队头,从此处写
23 int32_t tail; // 队尾,从此处读
24 };
25
26 void ioqueue_init(struct ioqueue* ioq);
27 bool ioq_full(struct ioqueue* ioq);
28 bool ioq_empty(struct ioqueue* ioq);
29 char ioq_getchar(struct ioqueue* ioq);
30 void ioq_putchar(struct ioqueue* ioq,char byte);
31
32 #endif
②device/ioqueue.c:
1 #include "ioqueue.h"
2 #include "interrupt.h"
3 #include "global.h"
4 #include "debug.h"
5
6 /* 初始化io队列ioq */
7 void ioqueue_init(struct ioqueue* ioq){
8 lock_init(&ioq->lock); // 初始化锁
9 ioq->producer=ioq->consumer=NULL; // 生产者和消费者置空
10 ioq->head=ioq->tail=0; // 头尾指针指向缓冲区的第0个位置
11 }
12
13 /* 返回pos在缓冲区中的位置 */
14 static int32_t next_pos(int32_t pos){
15 return (pos+1)%bufsize;
16 }
17
18 /* 判断队列是否为空 */
19 bool ioq_empty(struct ioqueue* ioq){
20 return ioq->head==ioq->tail;
21 }
22
23 /* 判断队列是否已满 */
24 bool ioq_full(struct ioqueue* ioq){
25 return next_pos(ioq->head)==ioq->tail;
26 }
27
28 /* 使当前生产者或消费者在此缓冲区上等待 */
29 static void ioq_wait(struct task_struct** waiter){
30 ASSERT(*waiter==NULL && waiter!=NULL);
31 *waiter=running_thread();
32 thread_block(TASK_BLOCKED);
33 }
34
35 /* 唤醒waiter */
36 static void wakeup(struct task_struct** waiter){
37 ASSERT(*waiter!=NULL);
38 thread_unblock(*waiter);
39 *waiter=NULL;
40 }
41
42 /* 消费者从ioq队列中获取一个字符 */
43 char ioq_getchar(struct ioqueue* ioq){
44 ASSERT(intr_get_status()==INTR_OFF);
45
46 /* 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,
47 * 目的就是将来生产者往缓冲区里装商品后,生产者知道唤醒哪个消费者,
48 * 也就是唤醒当前线程 */
49 while (ioq_empty(ioq)){
50 lock_acquire(&ioq->lock);
51 ioq_wait(&ioq->consumer);
52 lock_release(&ioq->lock);
53 }
54
55 char byte=ioq->buf[ioq->tail]; // 从缓冲区中取出
56 ioq->tail=next_pos(ioq->tail); // 队尾指向下一个位置
57
58 if (ioq->producer!=NULL){
59 wakeup(&ioq->producer); // 消费了,唤醒睡眠的生产者
60 }
61
62 return byte;
63 }
64
65 void ioq_putchar(struct ioqueue* ioq,char byte){
66 ASSERT(intr_get_status()==INTR_OFF);
67
68 /* 若缓冲区(队列)已满,把生产者ioq—>producer记为当前线程自己,
69 * 目的就是将来消费者从缓冲区里取商品后,消费者知道唤醒哪个生产者,
70 * 也就是当前线程 */
71 while (ioq_full(ioq)){
72 lock_acquire(&ioq->lock);
73 ioq_wait(&ioq->producer);
74 lock_release(&ioq->lock);
75 }
76 ioq->buf[ioq->head]=byte; // 向缓冲区写入
77 ioq->head=next_pos(ioq->head); // 队头指向下一个位置
78
79 if (ioq->consumer!=NULL){
80 wakeup(&ioq->consumer); // 生产了,唤醒睡眠的消费者
81 }
82 }
③device/keyboard.h,添加一句:
extern struct ioqueue kbd_buf;
④device/keyboard.c,第180行左右修改为:
⑤修改一下kernel/interruput.h的pic_init():

1 /* 初始化可编程中断控制器8259A */
2 static void pic_init(void){
3
4 /*初始化主片*/
5 outb(PIC_M_CTRL,0x11);
6 outb(PIC_M_DATA,0x20);
7 outb(PIC_M_DATA,0x04);
8 outb(PIC_M_DATA,0x01);
9
10 /*初始化从片*/
11 outb(PIC_S_CTRL,0x11);
12 outb(PIC_S_DATA,0x28);
13 outb(PIC_S_DATA,0x02);
14 outb(PIC_S_DATA,0x01);
15
16 outb(PIC_M_DATA,0xfc); // 只支持键盘中断
17 outb(PIC_S_DATA,0xff);
18
19 put_str(" pic_init done\n");
20 }
⑥kernel/main.c:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "keyboard.h"
8
9 void k_thread_a(void*);
10 void k_thread_b(void*);
11
12 int main(void) {
13 put_str("Welcome,\nI am kernel!\n");
14 init_all();
15
16 thread_start("consumer_a",31,k_thread_a," A_");
17 thread_start("consumer_b",31,k_thread_b," B_");
18
19 intr_enable();
20
21 while (1);
22 return 0;
23 }
24
25 /* 在线程中运行的函数 */
26 void k_thread_a(void* arg){
27 while (1){
28 enum intr_status old_status=intr_disable();
29 if (!ioq_empty(&kbd_buf)){
30 console_put_str(arg);
31 char byte=ioq_getchar(&kbd_buf);
32 console_put_char(byte);
33 }
34 intr_set_status(old_status);
35 }
36 }
37
38 void k_thread_b(void* arg){
39 while (1){
40 enum intr_status old_status=intr_disable();
41 if (!ioq_empty(&kbd_buf)){
42 console_put_str(arg);
43 char byte=ioq_getchar(&kbd_buf);
44 console_put_char(byte);
45 }
46 intr_set_status(old_status);
47 }
48 }
⑦makefile(我就大发慈悲地放出来吧,bushi):

1 BUILD_DIR = ./build 2 ENTRY_POINT = 0xc0001500 3 AS = nasm 4 CC = gcc 5 LD = ld 6 LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ 7 ASFLAGS = -f elf 8 CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes 9 LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map 10 OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \ 11 $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \ 12 $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \ 13 $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \ 14 $(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \ 15 $(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o 16 ############## c代码编译 ############### 17 $(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \ 18 lib/stdint.h kernel/init.h lib/string.h kernel/memory.h \ 19 thread/thread.h kernel/interrupt.h device/console.h 20 $(CC) $(CFLAGS) $< -o $@ 21 22 $(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \ 23 lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h \ 24 thread/thread.h device/console.h device/keyboard.h 25 $(CC) $(CFLAGS) $< -o $@ 26 27 $(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \ 28 lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h 29 $(CC) $(CFLAGS) $< -o $@ 30 31 $(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h\ 32 lib/kernel/io.h lib/kernel/print.h thread/thread.h kernel/interrupt.h \ 33 kernel/debug.h 34 $(CC) $(CFLAGS) $< -o $@ 35 36 $(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \ 37 lib/kernel/print.h lib/stdint.h kernel/interrupt.h 38 $(CC) $(CFLAGS) $< -o $@ 39 40 $(BUILD_DIR)/string.o: lib/string.c lib/string.h \ 41 kernel/debug.h kernel/global.h 42 $(CC) $(CFLAGS) $< -o $@ 43 44 $(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \ 45 lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h 46 $(CC) $(CFLAGS) $< -o $@ 47 48 $(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \ 49 lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h 50 $(CC) $(CFLAGS) $< -o $@ 51 52 $(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \ 53 lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \ 54 kernel/interrupt.h kernel/debug.h lib/kernel/print.h 55 $(CC) $(CFLAGS) $< -o $@ 56 57 $(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \ 58 kernel/interrupt.h lib/stdint.h kernel/debug.h 59 $(CC) $(CFLAGS) $< -o $@ 60 61 $(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \ 62 lib/stdint.h thread/thread.h kernel/debug.h kernel/interrupt.h 63 $(CC) $(CFLAGS) $< -o $@ 64 65 $(BUILD_DIR)/console.o: device/console.c device/console.h \ 66 lib/kernel/print.h lib/stdint.h thread/sync.h thread/thread.h 67 $(CC) $(CFLAGS) $< -o $@ 68 69 $(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \ 70 lib/kernel/print.h kernel/interrupt.h lib/kernel/io.h kernel/global.h 71 $(CC) $(CFLAGS) $< -o $@ 72 73 $(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \ 74 lib/stdint.h thread/thread.h thread/sync.h \ 75 kernel/interrupt.h kernel/global.h kernel/debug.h 76 $(CC) $(CFLAGS) $< -o $@ 77 78 ############## 汇编代码编译 ############### 79 $(BUILD_DIR)/kernel.o: kernel/kernel.S 80 $(AS) $(ASFLAGS) $< -o $@ 81 82 $(BUILD_DIR)/print.o: lib/kernel/print.S 83 $(AS) $(ASFLAGS) $< -o $@ 84 85 $(BUILD_DIR)/switch.o: thread/switch.S 86 $(AS) $(ASFLAGS) $< -o $@ 87 88 ############## 链接所有目标文件 ############# 89 $(BUILD_DIR)/kernel.bin: $(OBJS) 90 $(LD) $(LDFLAGS) $^ -o $@ 91 92 .PHONY : mk_dir hd clean all 93 94 mk_dir: 95 if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi 96 97 hd: 98 dd if=$(BUILD_DIR)/kernel.bin \ 99 of=hd60M.img \ 100 bs=512 count=200 seek=9 conv=notrunc 101 102 clean: 103 cd $(BUILD_DIR) && rm -f ./* 104 105 build: $(BUILD_DIR)/kernel.bin 106 107 all: mk_dir build hd
ok,现在我们,准确的说键盘驱动就是生产者,线程k_thread_a和k_thread_b就是消费者。让我们看看执行效果:
当我们按下‘k’键时,屏幕上交替输出“A_k”、“B_k”,当然按下其它键也会相应地显示。嘿嘿,又向前迈进了一大步呢。
本章的重点在于(互斥)锁的实现:
可以将其分为四步:操作中断状态;线程的阻塞与解除阻塞;信号量的PV操作;锁的获取与释放。书中所实现的锁就是对信号量的封装。
1.操作中断状态(实现于第7章):
(1)获取中断状态——intr_get_status()——汇编代码取eflags寄存器的if位,1为开中断,0为关中断
(2)开中断并返回开中断前的状态——intr_enable()——若当前就是开中断状态则直接返回当前状态即可;若当前为关中断状态则"sti"开中断并返回old_status;
(3)关中断并返回关中断前的状态——intr_disable()——类似(2)
(4)设置中断状态——intr_set_status(status)——判断status是开还是关,以此调用(2)或(3)的函数即可。
2.线程的阻塞与解除阻塞:
(1)线程只能阻塞自己——thread_block(status):
①关中断,以免阻塞过程中发生线程调度。
②获得当前线程PCB,并置状态为status(一般都是置为阻塞态)。
③线程调度schedule(),当前线程被阻塞了,当然要让CPU去执行其它线程。
④待线程解除阻塞后,再恢复①之前的中断状态。
(2)让running线程去解除blocked线程的阻塞状态——thread_unblock(pthread):
①关中断。
②将blocked线程放到就绪队列的最前面,尽快调度,并更改其状态为ready。
③恢复中断。
注意:在取出就绪队列的队首线程(刚解除阻塞状态的线程)后,线程会从(1)的④开始执行。
3.信号量的PV操作:
要说明的是,信号量的初始值为1,在同一时刻,只有一个线程可以持有它。
(1)P——sema_down():
①首先还是要关中断保证原子操作。
②当信号量的value==0时,说明此时已被别的线程持有,那就加入该信号量的等待队列吧,既然线程是在等待,自然不能继续执行,于是thread_blocked();;当信号量的value==1时,说明此时没有被持有,那就--value,使之为0。
③恢复中断。
(2)V——sema_up():
①关中断。
②当信号量的等待队列不为空,则取出队首线程,直接让信号量被该线程所持有,于是thread_unblock();,转去执行该线程;当等待队列为空后,就++value,使之为1。
4.锁的获取与释放:
(1)获取锁——lock_acquire()——如果当前线程已经持有锁,则重复获取该锁,++repeat_nr;否则当前线程对锁的信号量执行P操作,尝试持有该锁(本质上是持有锁中的信号量),若已经有线程持有了,就阻塞吧。
(2)释放锁——lock_release()——如果当前线程已经重复获取该锁,则--repeat_nr;否则repeat_nr置0,对锁的信号量执行V操作,然后就释放了该锁(本质上是释放了锁中的信号量)。
可以看出,互斥锁(量)是信号量的一种特殊情况(value=1时),两者是有区别的:
1.根本区别在于,互斥量用于线程的互斥,信号量用于线程的同步;
所谓多线程互斥,是指,我们要保护一个共享资源,使得其它线程无法访问,直到该线程离开,才可以访问。
所谓多线程同步,是指,一个线程完成了某个动作时,通过信号告诉其它线程,让其它线程去执行相应的动作。
2.互斥量的值只能为0/1,信号量的值可以为任意非负整数;
也就是说,一个互斥量只能实现一个资源的互斥访问,不能实现多个资源的多线程互斥访问。
3.互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
此外需要注意的是,信号与信号量也是不同的:
信号(signal):用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
信号量(semaphore):一种特殊的变量,本质上是计数器,信号量里记录了临界资源的数目,有多少数目,信号量的值就有多少,进程对其访问都是原子操作(P、V)。它的作用是协调进程对共享资源的访问,让临界区同一时间只有一个进程在访问它。
参考博客:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库