《操作系统真象还原》第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 }
View Code
复制代码

运行效果如图:

终于能和计算机交流了,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 }
View Code
复制代码

⑥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 }
View Code
复制代码

⑦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
View Code
复制代码

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)。它的作用是协调进程对共享资源的访问,让临界区同一时间只有一个进程在访问它。


参考博客:

posted @   Hell0er  阅读(185)  评论(0编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示