耒阳市建设局网站,企业标准网站模板,中国风html5网站模板免费下载,做ppt医学专业图片网站前言#xff1a;在上一期的线程章节中#xff0c;我们的线程输出貌似有大问题#xff0c;今天我们便要来学习同步锁来解决这个问题#xff0c;同时再次基础上拿下键盘输入#xff0c;实现操作系统的输入和输出。从今天开始我们的操作系统不在是一块“看板”了#xff01;… 前言在上一期的线程章节中我们的线程输出貌似有大问题今天我们便要来学习同步锁来解决这个问题同时再次基础上拿下键盘输入实现操作系统的输入和输出。从今天开始我们的操作系统不在是一块“看板”了 一上期线程输出的不足
其实13 Day的线程输出并不完美有很大的问题。但是这个问题经过我多天的观察其实有两种。
1我们会发现有的线程输出的内容并没有输出完成而是被很大的一片空白取代
2触发global exception 异常
至于为什么会导致这样子相信聪明的小伙伴已经知道了如果有学过并发编程相关的知识肯定会对该情况十分熟悉。 首先我们知道屏幕输出的操作由多个指令组成且需要操作显存那么就会出现以下问题 显存作为公共空间也就是临界区(不过多介绍)所有的线程进行输出都需要操作这块空间就会导致线程安全问题屏幕输出操作有多个指令组成这就说明屏幕输出并不是原子性的也就是说线程的切换可能导致另一个线程屏幕输出只执行了一半就被别人踹下台去了就会出现原本要输出Hello可是只输出了Hell那我们要怎么解决这种问题呢没错就是关闭中断这样操作就变成原子性的线程无阻碍的往下执行。我们可以在原本main函数的两个线程方法while的上下加上一个 关中断与开中断当大家再次进行输出就会发现输出正常了。
但这大家仔细想想整个while模块里面有些操作并不需要都是原子性的我们只有某些关键操作是原子性把整个线程的函数都进行关中断那不就变成以前的串行操作了吗这就涉及到一个关键性问题关中断与开中断之间的粒度也就是JUC中经常讨论到的一点锁的粒度他锁定的范围越小那么线程切换工作的效率就越高。在这里我们显然只要对输出函数进行加锁处理那么如何让一个方法加锁请接着往下看
① 信号量
有学过操作系统大学课程的同学肯定知道信号量其中P,V代表信号量的操作这是一个荷兰语。P代表减少V代表上升。那么对应锁的话就是如下几个操作
up
(1) 信号量的值1
(2) 唤醒在此信号量上等待的线程
down
(1)判断信号量是否1
(2)信号量大于0信号量-1
(3)信号量等于0线程阻塞在此信号量上等待
② 同步锁的实现
知道以上操作之后我们便可以来实现一个锁了在此之前我们还需要完善线程的两个操作阻塞与唤醒
thread/thread.c
void thread_block(enum task_status stat) {ASSERT(((stat TASK_BLOCKED) || (stat TASK_WAITING) || (stat TASK_HANGINH)));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_HANGINH));if (pthread-status ! TASK_READY) {ASSERT(!find(thread_ready_list, pthread-general_tag));if (find(thread_ready_list, pthread-general_tag)) {PANIC(thread_unblock);}push(thread_ready_list, pthread-general_tag);pthread-status TASK_READY;}intr_set_status(old_status);
}
接下来我们来实现同步锁
/thread/sync.h
#ifndef _THREAD_SYNC_H
#define _THREAD_SYNC_H
#include list.h
#include stdint.h
#include thread.hstruct semaphore {uint8_t value; //信号量值struct list waiters; //等待队列
};struct lock {struct task_struct* holder; //持有锁的线程struct semaphore semaphore;uint32_t holder_repeat_nr; //锁重入次数
};
void lock_init(struct lock* plock);
void try_lock(struct lock* plock);
void try_release(struct lock* plock);
#endif // ! _THREAD_SYNC_H/thread/sync.c
#include sync.h
#include stdint.h
#include list.h
#include thread.h
#include interrupt.h
#include debug.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) {//检测当前线程是否在等待队列中在的话则报错if (find(psema-waiters, running_thread()-general_tag)) {PANIC(sema_down: thread blocked has been in waiter);}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();//从waiter队列中唤醒第一个线程if (!empty(psema-waiters)) {struct task_struct* thread_blocked elem2entry(struct task_struct, general_tag, pop(psema-waiters));thread_unblock(thread_blocked);}psema-value;ASSERT(psema-value 1);intr_set_status(old_status);
}void try_lock(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 try_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);//注意这里不允许调换sema_up的位置如果调换位置就会导致中断开启但是plock-hodler还没设为空就进行线程切换了plock-holder NULL;plock-holder_repeat_nr 0;sema_up(plock-semaphore);
}
② 实现终端输出
这样子信号量我们就完成了接下来我们用锁来实现一个终端输出在此之前我们先来了解了解
什么是虚拟终端。
虚拟终端(tty)为了能让更多的人同时使用计算机必须在一个显示器下实现多个用户为每个用户虚拟出一个显示器
原理不同的用户使用不同显存区域让显存分快显示就达到了虚拟终端的效果 device/console.h
#include stdint.hvoid console_init();
void console_acquire();
void console_release();
void console_put_str(char* str);
void console_put_char(uint8_t ch);
void console_put_int(uint32_t num); device/console.c
#include console.h
#include stdint.h
#include sync.h
#include print.h
static struct lock console_lock;void console_init() {lock_init(console_lock);put_str(\nconsole init done!\n);
}void console_acquire() {try_lock(console_lock);
}void console_release() {try_release(console_lock);
}void console_put_str(char* str) {console_acquire();put_str(str);console_release();
}void console_put_char(uint8_t ch) {console_acquire();put_char(ch);console_release();
}void console_put_int(uint32_t num) {console_acquire();put_int(num);console_release();
}将main函数中的普通put_str改为console_put_str接下来修改一下makefile文件运行结果是密密麻麻的输出字符即代表我们锁编写成功时代太过久远忘记截图了 三键盘键入的原理
1两个芯片 键盘嘛相信大家都见过的哈没见过的同学我相信你也肯定看不到我的文章捏。在计算机这个系统中键盘作为一个外部设备他并不是完完全全独立于操作系统存在的他的功能实现涉及到了两个功能独立的芯片 Intel 8048(或兼容芯片)位于键盘内部称作键盘编码器负责监听键盘按键事件并向键盘控制器报告哪个按键按下哪个按键弹起Intel 8042(或兼容芯片)位于计算机主板称作键盘控制器负责接受键盘编码起的按键消息并将其解码保存然后向中断代码发送中断之后处理器读取8042处理过并保存的数据当然为了知道键盘到底安乐哪个键8042与8048之间必须保持一个协议那就是所有按键与对应竖直组成一个类似于hash映射的键盘扫描码 2键的编码 键的编码 一个键通常对应两个码按下的通码与松开的断码当你一直按着键不松开会产生练习相同的码当你松开的时候才会终止这个就叫做断码 键盘扫描码
键盘的扫描码有三套scan code set 1scan code set 2scan code set 3下图分别是三套扫描码的附图 在此我们使用第一套键盘扫描码 当今键盘内部芯片默认使用第二套编码但是我们使用第一套编码所以说8048将编码信息发送给8042的时候会进行一次转换8042每次接受到一个字节的扫描码后就会向中断代理发送信号一个按键操作至少触发两次中断(通码和断码各一次一般中断处理程序不会处理断码信息当按下a键时8048向8042发出a键的第二套扫描码0x1c8042将其转换为第一套编码0x1e并保存到自己的缓冲区当保存完毕后向中断代理发送中断中断处理程序开始执行并从8042缓冲区读取0x1e当松开后重复上述操作。38042简介 8048是键盘的控制者(键盘监控键盘设置灯光)8042是键盘的IO接口因此8042是8048的代理8048通过PS/2USB接口 与8042通信处理器通过端口与8042通信。 8042有4个8位寄存器 8042作为8048的中转站8048通过out 0x60将自己的数据写入8042中让自己的数据通过8042被操作系统读入8042作为8048的输入缓冲区将8048的信息暂存在8042缓冲区操作系统通过 in 0x60读入数据要注意一点的是当8042缓冲区已经有数据时就不会再次读入数据要等到数据被中断程序读取后才会再次读入8048的数据他是根据一个状态寄存器的第0位来判断的。
8042有3个寄存器状态寄存器输入缓冲区寄存器控制寄存器由于我们只需要用到部分功能我也就贴出图大家稍微参考一下即可 三编写键盘驱动
#include interrupt.h
#include stdint.h
#include keyboard.h
#include io.h
#include print.h#define KB_BUF_PORT 0x60#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 0x3astatic bool ctrl_status, alt_status, caps_status, shift_status, ext_scancode;char keymap[][2] {/* 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 casps_lock_status;bool break_code;uint16_t scancode inb(KB_BUF_PORT);//判断是不是为多余字符是的话马上退出处理下一个if (scancode 0xe0) {ext_scancode true;return;}//如果上次以0xee0开头则合并通码并去除ext_scancode标志if (ext_scancode) {scancode | 0xe000;ext_scancode false;}//获取break codebreak_code ((scancode 0x0080) ! 0);//如果是断码对ctrl和shift和alt进行状态判断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_char || 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_char || 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_intr(0x21, intr_keyboard_handler, keyboard);put_str(keyboard_init done\n);
} 四环形输入缓冲区 我们知道操作键盘通常是为了和系统交互而和系统交互一般都是写入某些shell指令而这些shell指令需要用一个缓冲区存入起来当形成一个完整的命令时再一并由其他模块处理。 ① 生产者消费者模型
对于缓冲区我们要有以下几个认识
缓存数据公共区域多个线程同时使用该空间需要对存取操作进行上锁数据可能存在存满和取空两种状态
线程之间要相互合作存在资源共享问题。对此我们便使用由Dijkstra提出的生产者消费者模型 生产者消费者模型是什么我不太想用学术的语言来介绍我在此来举个例子 以前去吃过KFC的同学应该都了解吃KFC的时候是需要排队的那么此时我们把服务员当作生产者(就当作他又做饭又服务吧)备餐口就是缓冲区而顾客则是消费者 ① 当客流量比较少时供大于求服务员做好一大堆汉堡放在备餐区当餐做满时便停下来休息并告诉顾客”新鲜的汉堡做好了来取餐嘞“唤醒消费者)休息的顾客听到了便来到备餐区取餐 ② 当客流量中等时供等于求只要服务员看到备餐口有空位马上开始忙活起来。 ③ 当客流量很大时供不应求餐取完的时候顾客便会在等待队列中等待并催促服务员“搞快点咯肚子饿了“(唤醒生产者)摸鱼的服务员便会快马加鞭的做汉堡。 总结对于有限大小的公共缓冲区同步生产者和消费者的运行对共享缓冲区互斥访问并不会过度消费和过度生产这便是生产者消费者模型以及疯狂星期四KFCV50 ② 环形缓冲区实现
其实线形的缓冲区也是OK的怎么设计因人而异我这里用队列来实现一个环形缓冲区环形缓冲区的本质就是没有起始地址没有终止地址很简单我们直接开始吧。 device/ioqueue.h
#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);
int32_t next_pos(int32_t pos);
bool ioq_full(struct ioqueue* ioq);
bool ioq_empty(struct ioqueue* ioq);
void ioq_wait(struct task_struct** waiter);
void ioq_wakeup(struct task_struct** waiter);
char ioq_getchar(struct ioqueue* ioq);
void ioq_setchar(struct ioqueue* ioq, char byte);
#endif // ! _DEVICE_IOQUEUE_Hdevice/ioqueue.c
#include debug.h
#include ioqueue.h
#include interrupt.hvoid ioqueue_init(struct ioqueue* ioq) {lock_init(ioq-lock);ioq-producer ioq-consumer NULL;ioq-head ioq-tail 0;
}int32_t next_pos(int32_t pos) {return (pos 1) % bufsize;
}bool ioq_full(struct ioqueue* ioq) {ASSERT(intr_get_status() INTR_OFF);return (ioq-tail - ioq-head) 1;
}bool ioq_empty(struct ioqueue* ioq) {ASSERT(intr_get_status() INTR_OFF);return ioq-head ioq-tail;
}//使当前的生产者或消费之在缓冲区上等待
void ioq_wait(struct task_struct** waiter) {ASSERT(*waiter NULL waiter ! NULL);*waiter running_thread();thread_block(TASK_BLOCKED);
}//唤醒生产者或消费者
void ioq_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)) {try_lock(ioq-lock);ioq_wait(ioq-consumer);try_release(ioq-lock);}//获取buf中的数据char ch ioq-buf[ioq-tail];ioq-tail next_pos(ioq-tail);if (ioq-producer ! NULL) {ioq_wakeup(ioq-producer);}return ch;
}void ioq_setchar(struct ioqueue* ioq, char byte) {//先判断是否关闭中断ASSERT(intr_get_status() INTR_OFF);//判断是否满缓冲区如果满了生产需要休眠等待while (ioq_full(ioq)) {try_lock(ioq-lock);ioq_wait(ioq-producer);try_release(ioq-lock);}//获取buf中的数据ioq-buf[ioq-head]byte;ioq-head next_pos(ioq-head);if (ioq-consumer ! NULL) {ioq_wakeup(ioq-consumer);}return byte;
}
然后我们完善一下键盘操作
device/keyboard.c
struct ioqueue ioqueue;void keyboard_init()
{put_str(keyboard init start\n);register_handler(0x21, intr_keyboard_handler);init_ioqueue(ioqueue);put_str(keyboard init done\n);
}
void intr_keyboard_handler(void)
{
//......if (cur_char){if (!ioq_full(ioqueue))ioq_putchar(ioqueue, cur_char);return;}
//......
}
接下来我们让两个内核线程变成消费者
kernel/main.c
#include print.h
#include init.h
#include debug.h
#include thread.h
#include console.h
#include keyboard.h
#include ioqueue.h
#include interrupt.hvoid g_thread(void* arg);
void g_thread2(void* arg);
void main(void) {put_str(Hello GeniusOS\n);put_int(2023);put_str(\n);init_all();thread_start(genius, 5, g_thread, A_);thread_start(genius2, 31, g_thread2, B_);intr_enable();while (1) {//console_put_str(Main );}
}void g_thread(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);console_put_char( );}intr_set_status(old_status);}
}void g_thread2(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);console_put_char( );}intr_set_status(old_status);}
}
改一下makefile运行一下当你敲下键盘且字符在屏幕上显示这就代表你成功了恭喜你完成本章任务!!!