拼一个自己的操作系统 SnailOS 0.03的实现
拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载
操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载
SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载
线程的通信机制
https://pan.baidu.com/s/19tBHKyzOSKACX-mGxlvIeA?pwd=i889
在上一章我们拿来了别人的线程实现方法用于拼凑蜗牛操作系统。运行起来以后是不是很爽啊,毕竟吗实现线程的机制是非常的严谨的,思考起来也比较烧脑,在这个地方我们几乎没有可以创新的地方,所以直接拿来了。这一章,我们要搞一些高深的问题了,那就是线程的通信机制。
相信大家都多少看过些操作系统理论的书籍吧。那些书籍中是不是大家都隐隐约约地看到过“死锁”这个词。是不是对这个词印象深刻呢?又是不是似是而非的多少知道些什么呢?那么我们的线程通信机制,就由此来展开吧!死锁是什么呢?大概就是这样的吧。倘若有甲乙丙三个任务,甲任务产生数据,乙任务也产生数据,丙任务产生数据。如果大家各个不相干的情况下,当然是相安无事,各家自扫门前雪了。可偏偏乙任务要继续产生数据必须等待甲任务的数据,而丙任务要产生数据也要等待乙任务的数据,同时甲任务产生数据要等待丙任务。想到了吧,这样一来就形成的等的我花都谢了情况了。也就是等死也不会完成任何事情。这其实只是说了死锁的死字。锁字才是我们要讲的内容了。可是现在我们还没有锁呀。那么锁是干什么用的呢。这是就要和操作系统中一个重要的概念联系上了,即是原子操作。原子这个感念最早被西方人所提出,它的本意是不可分割,所以,原子操作就是一片代码在运行中,要从头运行到尾,一口气完成,不能被打断。在前面的章节,我们讲了中断,这个中断偏偏就是把代码拆分开来执行的,也就是从中断处把任务本身分成了上下两截,也就是上下文。线程就更要命了,甲线程运行到某处时,轮到乙线程运行了,甲线程的代码只好被打断,让乙线程去运行他的代码。这回大家知道了吧。之所以搞不成原子操作,主要就是中断闹的妖。根源就是中断的时机,不能做出精确的计算。当然如果是单条指令,或者处理器设置的特殊的连续执行的指令,原子操作就自然形成了,也就不用我们操心了。可是为什么要有原子操作呢?一个原因就是操作系统代码本身的特性所决定,操作系统内核的代码本身就是要提供给应用程序调用的。也就是代码是共享给任务的。拿咱们的信息区显示字符的程序来说,那段代码就被多个线程同时使用,也被内核中断异常处理使用,但是光有这一条,还不能作为使用原子操作的理由。另一个原因是这些共享的代码还有一些公用的变量(资源),还是拿显示字符的程序说事。那个记录像素位置的变量,就被多个任务调用显示字符函数是频繁修改。所以说,在多个任务用显示字符的程序时,我们就应该对显示字符程序这段代码实现原子操作,以防止像素位置这个变量,被改的面目全非。那么现在问题来了,怎么实现原子操作呢?相信大家已经想到了吧。解铃还须系铃人,既然是中断搞的鬼,所以问题解决还是开关中断。确切的说是在单处理器的情况下,在合适的时机开关中断。开关中断的方法虽然好用,但是过于简单粗暴。如果开关中断的时间过长,对硬件的相应速度就会产生非常明显的影响。显然,这不是一种十分明智的好方法。鉴于此,早在好几十年前的上个世纪五六十年代,计算机科学家就提出了用二元信号量实现的互斥锁来这类产生竞争条件的问题。大家看到了吧,转一圈终于回到了实现锁的问题上来了。这类软件方法跟关中断的方法有着本质区别,在实现之前,有几个重要的概念我们要先了解一下。第一个就是公用资源,像上面那样像素的内存位置就是我们所称的公用资源。第二个就是临界区,显示字符串的程序就是临界区代码,注意啦,临界区是一段代码,并不是代码可能访问的公用资源。第三个就是互斥,多个任务不允许同时进入临界区,换个角度来说,就是当有任务执行临界区的代码时,其他任务需要歇菜(等待),直到该任务退出临界区后,唤醒等待的任务。第四就是竞争条件,即是多个任务在非互斥的情况下,进入了临界区,这当然是我们极力避免的情况。
这样一来,我们的工作就转化为当多个任务竞争临界区的使用权时,只要有一个在临界区上,其他的任务就不要自讨没趣了,乖乖地将自己放入等待队列就好了,直到在临界区上的任务走出临界区,然后施恩给等待队列上的任务,随便找到一个唤醒就好了。因此,我们的第一项小任务就是任务自觉地让自己进入休眠(阻塞)状态,我们也称为任务自己阻塞自己。还有就是任务唤醒那些在等待队列中痴痴地等待的任务。
下面就是我们本章加入的第一段代码,这段代码仅仅是将任务至于休眠状态和从某个等待队列中唤醒。
【thread.c 节选】
(上面省略)
/*
任务让自己进入非运行态的函数。这里我们把运行态和就绪态(准运行态)
都视为运行态,而任务的其他状态被视作非运行态。再次强调,任务之所以
能够处于非运行态,并不是系统强迫它退出运行态,而是任务自愿的。因此
由运行态转为非运行态,是任务自觉的行为,而且是在任务运行中发生的。
就此,我们把这个行为称之为阻塞。
*/
void thread_block(enum task_status stat) {
/*
阻塞自己需要原子操作,因此关闭中断。
*/
unsigned int old_status = intr_disable();
/*
首先要确保,将来的是非运行的状态。
*/
ASSERT(((stat == BLOCKED) || (stat == HANDING) || (stat == WAITING)));
/*
获得当前线程任务结构。
*/
struct task* cur = running_thread();
/*
设置非运行状态。
*/
cur->status = stat;
/*
这是第二次调用调度函数的地方,第一次在时钟中断处理程序中。线程阻塞
自己后,马上把自己换下处理器,让位给其他线程上处理器运行。注意由于
该线程已经不是处于就绪态,因此不会加入到就绪队列中。此处也没有把线程
加入到任何等待队列。
*/
schedule();
/*
此处是线程再次被调度到处理器运行的返回地址,也就是说,调用阻塞函数
的线程再次运行是会打开中断,然后返回到线程中的其他代码。
*/
set_intr_status(old_status);
}
/*
唤醒线程的函数。不管待唤醒的线程由于什么原因被阻塞,最终如果要
唤醒它,都需要得到线程的任务结构。因此,函数的参数是待唤醒线程的
任务结构。可以想象当前线程是待唤醒线程的大恩人,如果没有线程唤醒
待唤醒线程的话,所谓的待唤醒线程讲用于上不了台面。
*/
void thread_unblock(struct task* pthread) {
/*
唤醒线程也需要原子操作,因此关闭中断。
*/
unsigned int old_status = intr_disable();
/*
首先要确保,待唤醒任务的是非运行的状态。
*/
ASSERT(((pthread->status == BLOCKED) ||
(pthread->status == HANDING) ||
(pthread->status == WAITING)));
if(pthread->status != READY) {
/*
再次确认待唤醒任务的是非运行的状态。
*/
ASSERT(!double_linked_list_find(&thread_ready_list,
&pthread->general_tag));
if(double_linked_list_find(&thread_ready_list,
&pthread->general_tag)) {
panic("thread_unblock: blocked thread in ready_list\n");
}
/*
并不是马上据运行任务,而是加入到就绪队列中,并且加到队头。等待
当前唤醒线程的任务时间片用完,则调度到处理器运行。因此运行状态是
就绪态。
*/
double_linked_list_push(&thread_ready_list, &pthread->general_tag);
pthread->status = READY;
}
/*
完成原子操作后,开启中断,如果任务当前任务时间片未用完,则继续运行。
*/
set_intr_status(old_status);
}
接下来的代码便是用二元信号量实现的互斥锁,这部分代码虽然不长,但是理解的难度却不小。原因可能是多方面的,首先说互斥这个概念对初学者本身就是一个挑战,再有就是这些方法本身就是计算机数学家经过浓缩的精巧设计,每个关键部位都会十分绕脑。还有线程之间的相互关系是动态多变的,以及中断和线程切换还要在这当中掺乱。因此,对于这段程序大家还是多多思考,反复学习为妙。我在代码中加入了简单的注释,至于效果如何,还要看悟性和功力了。
【sync.h】
// sync.h 创建者:至强 创建时间:2022年8月
#ifndef __SYNC_H
#define __SYNC_H
#include "double_linked_list.h"
#include "thread.h"
/*
信号量的结构非常简单,就是一个字节值和一个等待队列。
*/
struct semaphore {
unsigned char value;
struct double_linked_list waiters;
};
/*
锁的结构也是非常的简单,第一个是锁的持有者,第二个就是
二元信号量,第三个是持有者重复申请锁的次数。
*/
struct lock {
struct task* holder;
struct semaphore semaphore;
unsigned int holder_repeat_nr;
};
void sema_init(struct semaphore* psema, unsigned char value);
void lock_init(struct lock* plock);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
#endif
【sync.c】
// sync.c 创建者:至强 创建时间:2022年8月
#include "global.h"
#include "memory.h"
#include "intr.h"
#include "debug.h"
#include "sync.h"
/*
初始化信号量,给信号量赋一个初值,同时初始化等待队列。
*/
void sema_init(struct semaphore* psema, unsigned char value) {
psema->value = value;
double_linked_list_init(&psema->waiters);
}
/*
锁的初始化函数,在最开始锁的持有者为空,持有者重复申请锁的次数。
是0,初始化锁结构中的信号量。
*/
void lock_init(struct lock* plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1);
}
/*
信号量的down操作,也就是申请进入临界区的操作,当然这是信号量
为互斥服务的情况下的涵义。
*/
void sema_down(struct semaphore* psema) {
/*
信号量结构本身就是一个公用资源,因此,操作信号量必须保证原子
操作。
*/
unsigned int old_status = intr_disable();
/*
循环判定信号量的值是否为0,如果为0,说明资源已经被其他任务持有,则
阻塞自己,等待持有者释放资源,注意临界区和公用变量都将是这里的资源。
*/
while(psema->value == 0) {
/*
无论如何当前任务不应该重复被加入到信号量的等待队列。
*/
ASSERT(!double_linked_list_find(&psema->waiters, &running_thread()->general_tag));
if(double_linked_list_find(&psema->waiters, &running_thread()->general_tag)) {
panic("seam_down: thread blocked has been in waiters_list\n");
}
double_linked_list_append(&psema->waiters, &running_thread()->general_tag);
thread_block(BLOCKED);
}
/*
信号量的值不为0,则说明资源可用,用于互斥则持有资源。
*/
psema->value--;
ASSERT(psema->value == 0);
/*
退出原子操作。
*/
set_intr_status(old_status);
}
/*
信号量的up操作会从等待队列的队头,弹出线程结构,并唤醒线程,加入到
就绪队列。
*/
void sema_up(struct semaphore* psema) {
unsigned int old_status = intr_disable();
ASSERT(psema->value == 0);
if(!double_linked_list_is_empty(&psema->waiters)) {
struct task* thread_blocked = node2entry(struct task, general_tag,
double_linked_list_pop(&psema->waiters));
thread_unblock(thread_blocked);
}
psema->value++;
ASSERT(psema->value == 1);
set_intr_status(old_status);
}
/*
请求锁的函数。
*/
void lock_acquire(struct lock* plock) {
/*
请求锁并不是线程直接请求锁,而是把可能称为临界区的函数用锁
封装。要判断当前线程是否重复申请锁,重复申请是把锁中,重复
申请的次数增1。
*/
if(plock->holder != running_thread()) {
/*
如果是第一次申请锁,则对信号量进行进行down操作,把当前线程
作为锁的持有者,并且锁的重复申请次数置为1。
*/
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());
/*
如果当前线程重复申请过锁,则锁中的计数量毕竟大于1,所以
这时只是将锁中的重复申请计数自减,即刻返回。
*/
if(plock->holder_repeat_nr > 1) {
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1);
/*
仅仅当锁中的重复申请计数为1时,才通过信号量的up操作释放锁。
*/
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore);
}
上面的代码说的是互斥锁,可是文件的名字却叫做sync,这是英文中同步的意思。大家是不是糊涂了。是啊,我开始就是这么想的。其实,大家不要纠结。因为互斥被认为是一种特殊的同步机制,而且同步依赖于互斥,所以名字才叫做同步。
现在我们有了线程的休眠(阻塞)与唤醒函数,又有了二元信号量实现的互斥锁,这样我们就该做些实验来验证实现互斥的奇迹了吧。可是大家先别急嘛!至今为止我们还没有看到产生竞争条件的任何事件呦。因此制造出让我们“眼能见、心必烦”的产生混乱的事件,才是当务之急。
怎样才能立竿见影地看见“烦心事”呢?这就要说说我们之所以没有看到的原因。我们的系统中现在有4个线程,它们都在使用显示字符串的函数put_str类函数,该函数中有个公用资源就是像素的内存位置pos,这样一来,put_str就是临界区了。我们猜测,一定会在某个特定的时刻产生竞争条件。可是屏幕并没有画乱,也没有出现任何异常,真是奇哉怪也。让笔者来仔细想想好了。大家不难看出,我们系统中有两个中断服务程序在切断4个线程的运行,那就是实时时钟中断和时钟中断。除了进入这两个中断之外,四个线程那都是在自己的时间片上嗖嗖的跑着,风驰电掣一般。所以只要不是出现严重问题,显示字符的操作就像流星一样转瞬即逝,不给我们反应的时间。因此,我们才不能在屏幕的信息区发现任何混乱的迹象。这也就是看不见产生竞争条件的原因。那么出路就是,让显示字符的出现的不那么频繁,也就显示的慢一些就好了。
这样,问题的关键就转移到显示字符函数运行的频率上来了。这个又如之奈何呢?思来想去,我们发现消息队列能够实现,慢下来的目标,因此,就让我们实现它吧。
说起来,消息队列也是一种线程间的通信机制,只不过它又分为同步的和异步的。我们这里的消息队列拟采取异步的方式。至于原因吗,我们以后再说。既然是说到异步,我们就来简单的聊聊同步和异步区别。从而接触大家心中的疑惑。
其实这也很简单了,我们就拿吃饭来说吧,要吃饭必须要先做饭,因此,这两个事件发生的先后顺序,我们要合理的安排一下,这就是所谓的同步。看电视和吃饭这两件事,我们同时进行的话,应该都不会有什么大的影响,这就是异步。放到我们线程这块,就是我们的多个线程如果存在依赖关系的话,我们必须要同步它们的运行,也就是说,如果乙线程的发生需要甲线程先完成,那么如果甲线程还没完事,我们就必须要让乙线程凉快凉快(休息一段事件)等待甲线程的完成,也就是乙发现甲没完成,则乙阻塞自己,这种就是同步。甲线程、乙线程你干你的我干我的谁也碍不着谁,这就是异步。那么又有人说了,既然是谁也碍不着谁,那还通信干什么用呢。呵呵,说到了点子上了吧。这不,让大家慢下来,平心静气的工作,就需要这种看似无用的方法。说了这么多接下来还是代码伺候吧。
【thread.h 节选】
(上面省略)
/*
循环队列结构的定义。它是实现消息队列的基础。
*/
struct ring_queue {
unsigned int* buf; // 指向整形消息数组的指针。
int head; // 队头
int tail; // 队尾
unsigned int free; // 空闲的消息个数。
};
(中间省略)
/*
这是任务结构,也就是程序控制块,将开始于某个自然页的最低端。
*/
struct task {
(中间省略)
struct ring_queue r;
unsigned int stack_magic;
};
(下面省略)
因为循环队列的实体(不是指针)被包含在任务结构中,所以把循环队列结构的定义放在threah.h中。
【ring_queue.h】
// ring_queue.h 作者:至强 时间:2022年11月
#ifndef __RING_QUEUE_H
#define __RING_QUEUE_H
#include "thread.h"
void ring_queue_init(struct ring_queue* r);
int ring_queue_is_empty(struct ring_queue* r);
int ring_queue_is_full(struct ring_queue* r);
void ring_queue_in(struct ring_queue* r, unsigned int e);
unsigned int ring_queue_out(struct ring_queue* r);
unsigned int ring_queue_pop(struct ring_queue* r);
#endif
【ring_queue.c】
// ring_queue.c 作者:至强时间:2022年11月
#include "ring_queue.h"
#include "memory.h"
/*
初始化循环队列。为消息分配空间,给头指针、尾指针和空闲消息个数
赋初值。
*/
void ring_queue_init(struct ring_queue* r) {
r->buf = (unsigned int*)get_kernel_pages(1);
r->head = 0;
r->tail = 0;
r->free = 1024;
}
/*
判断队列是否为空的函数,这里把返回值设置为整型,当然也可以为布尔
型。
*/
int ring_queue_is_empty(struct ring_queue* r) {
/*
消息队列的固定大小为1个自然页,因此整型的个数是1024,也就是空闲
个数为1024时,队是空的。
*/
if(r->free == 1024) {
return 1;
} else {
return 0;
}
}
/*
判断队列是否为满的函数,这里把返回值设置为整型,当然也可以为布尔
型。
*/
int ring_queue_is_full(struct ring_queue* r) {
/*
消息队列的固定大小为1个自然页,因此整型的个数是1024,也就是空闲
个数为0时,队是满的。
*/
if(r->free == 0) {
return 1;
} else {
return 0;
}
}
/*
循环缓冲区数据入队的函数,入队总是从队头开始,且此时,空闲元素
个数要减1。
*/
void ring_queue_in(struct ring_queue* r, unsigned int e) {
/*
入队的特殊情况是,当队满时,直接返回,可以看出,如果此时阻塞调用函数
的线程,则可生成同步消息队列。
*/
if(ring_queue_is_full(r)) {
return; // 丢弃队满是的消息。
} else {
/*
当指针越界时,即使调整为适当的值。
*/
if(r->head > 1023) {
r->head = 0;
}
r->buf[r->head++] = e;
r->free--;
}
}
/*
先进先出(fifo)的队列,从队尾向前推进,此时空闲元素要增1。
*/
unsigned int ring_queue_out(struct ring_queue* r) {
unsigned int e;
/*
出队的特殊情况是,当队空时,直接返回,可以看出,如果此时阻塞调用函数
的线程,则可生成同步消息队列。
*/
if(ring_queue_is_empty(r)) {
/*
队为空时,生成的消息内容为-1,因此,任何消息的实质内容不能为-1。
*/
return -1;
} else {
/*
当指针越界时,即使调整为适当的值。
*/
if(r->tail > 1023) {
r->tail = 0;
}
e = r->buf[r->tail++];
r->free++;
return e;
}
}
/*
后进先出(lifo)的队列,从队头往后出队,此时空闲元素要增1。
*/
unsigned int ring_queue_pop(struct ring_queue* r) {
unsigned int e;
/*
出队的特殊情况是,当队空时,直接返回,可以看出,如果此时阻塞调用函数
的线程,则可生成同步消息队列。
*/
if(ring_queue_is_empty(r)) {
/*
队为空时,生成的消息内容为-1,因此,任何消息的实质内容不能为-1。
*/
return -1;
} else {
/*
当指针越界时,即使调整为适当的值。
*/
if(r->head < 0) {
r->head = 1023;
}
e = r->buf[r->head--];
r->free++;
return e;
}
}
消息队列就是一个环形缓冲区,初始时,队头和队尾的指针都为0,随着数据的来临,头尾可以是缓冲区相对地址的任何值,这两个值一致动态的变化着。当队头和队尾相等时缓冲区为空,当队尾是队头的前一个(这是一般情况下,特殊情况仅出现在临界值时)时,缓冲区为满。
【intr.c 节选】
(上面省略)
void timer_handler(void) {
(上面省略)
/*
系统总滴答数(时间片数)自增。
*/
ticks++;
/*
这种调度算法应该叫做“极简优先级”算法吧。当任务时间片用完,执行调度器,
其他情况中断返回,继续运行当前任务。
*/
/*
每产生一次时钟中断才向当前线程发送一次数据,则当前线程中在接收到数据后
才会在信息区中打印字符串,这个动作要比之前的打印的频率低很多了。有趣的是
如果把向缓冲区发送数据的函数放入实时时钟中断中打印的频率会更低。
*/
ring_queue_in(&running_thread()->r, 0);
if(cur_thread->ticks == 0) {
schedule();
} else {
cur_thread->ticks--;
}
}
(下面省略)
【kernel.c 节选】
(上面省略)
while(1) {
asm("cli");
if(ring_queue_is_empty(&running_thread()->r)) {
asm("sti;hlt");
} else {
int data = ring_queue_pop(&running_thread()->r);
asm("sti");
printf_("____ma %d in____", data);
}
}
}
(中间省略)
void k_thread_a(void* arg) {
while(1) {
asm("cli");
if(ring_queue_is_empty(&running_thread()->r)) {
asm("sti;hlt");
} else {
int data = ring_queue_pop(&running_thread()->r);
asm("sti");
printf_("____A %d A____", data);
}
}
}
void k_thread_b(void* arg) {
while(1) {
asm("cli");
if(ring_queue_is_empty(&running_thread()->r)) {
asm("sti;hlt");
} else {
int data = ring_queue_out(&running_thread()->r);
asm("sti");
printf_("____B %d B____", data);
}
}
}
void k_thread_c(void* arg) {
while(1) {
asm("cli");
if(ring_queue_is_empty(&running_thread()->r)) {
asm("sti;hlt");
} else {
int data = ring_queue_out(&running_thread()->r);
asm("sti");
printf_("____C %d C____", data);
}
}
}
为了验证竞争条件的效果,我们在时钟中断处理程序中加入向消息队列中写入消息的ring_queue_in()函数,因为中断处理程序中关中断,所以这里保证了原则操作。
同时我们在主线程以及其他3个线程中,加入了读取消息的函数,由于要保证原子操作,所以在关键部位采取了关中断的手段。注意这里保证原子操作是保证了操作环形缓冲区的原子操作,因为队头队尾的指针是公用数据。这里并为采取互斥锁的方式,主要是中断处理程序中调用了写消息的函数。在中断中不应该阻塞任何任务的运行。
大家看下好不容易截出的效果图吧。这里发两张给大家吧。为什么好不容易呢?这个不难理解,竞争条件不是每次都产生的,只有一个线程修改了pos值时,恰巧这是切换到了另一个线程也修改pos,才会产生像素位置混乱的不良后果。
前面我们也说了解决这个问题的方法至少有两种,一个就是用关中断的方法来实现“原子操作”,但这样不好,临界区的代码有长有短,如果很长时间的关中断的话,会大幅浪费处理器时间。因此,我们用第二种方法,采取互斥锁来排除竞争条件,使各个线程互斥地进入临界区,虽然这中间并非没有关中断操作,但却精确地控制了关中断的时间,提高了系统线程间并行运行的效率。
大家可以实测一下,用关中断实现的原子操作,这也就是在put_str_white()函数或put_str()的开始关中断,返回前开中断。不过是否会排除竞争条件,大伙就要耐心的多观察一会了。我这里就不做了。原因是屏幕上动态的东西,和之前似乎没有任何的不同。
下面的代码将是锁实现互斥的实例。这里就直接展示代码了。
【screen.h】
// screen.h 创建者:至强 创建时间:2022年11月
#ifndef __SCREEN_H
#define __SCREEN_H
void screen_init_(void);
void screen_acquire(void);
void screen_release(void);
void screen_put_str_white(char* str);
#endif
【screen.c】
// screen.c 创建者:至强 创建时间:2022年11月
#include "screen.h"
#include "string.h"
#include "thread.h"
#include "sync.h"
/*
在此处定义了一个本地变量screen_lock。
*/
static struct lock screen_lock;
/*
封装一下初始化锁。
*/
void screen_init_(void) {
lock_init(&screen_lock);
}
/*
请求锁。
*/
void screen_acquire(void) {
lock_acquire(&screen_lock);
}
/*
释放锁。
*/
void screen_release(void) {
lock_release(&screen_lock);
}
/*
对信息区打印字符串的函数加锁,也就是我们认为这里可能
成为临界区,所以给它加上锁,从而实现互斥。
*/
void screen_put_str_white(char* str) {
screen_acquire();
put_str_white(str);
screen_release();
}
【string.c 节选】
(上面省略)
int printf_(const char* format_s, ...) {
char t[0x400];
// 获得可变参数中第一个参数的地址。
char* temp = (char*)&format_s;
vsprintf_(t, format_s, temp);
temp = NULL;
// put_str_white(t);
screen_put_str_white(t);
return strlen_(t);
}
(下面省略)
【kennel.c 节选】
(上面省略)
void init(void) {
(中间省略)
mem_init();
thread_init();
screen_init_();
}
(下面省略)
因为screen_init()这个函数名称在其他的地方用过了,所以这里只好改成了screen_init_(),唉,真是失算啊。还好不伤大雅了。屏幕的截图就不贴了。主要是没什么变化了。这里依然是需要大家多多观察一会儿,看看还有没有写乱屏幕的现象。
为了继续我们所谓的消息队列,笔者又在刚才循环队列的基础上,做了一个指令队列。这样我们就能以主线程为“服务器”,其他线程作为“客户端”,从而向主线程申请画图的服务了。当然对于循环队列互斥的实现,现在笔者任然倾向于“关中断”操作。对于笔者这种随心所欲、一厢情愿而且异想天开的愿望是否能够称心如意呢?我想还是要靠天吃饭了。细心的你可能早已经发现了,根本就用不到这么复杂的操作吗?在各个线程中直接调用画图函数不久好了吗。是啊,这个只不过是笔者玩的小游戏罢了。还请大家见谅了。另外,这次,我们不再用时钟中断处理程序唤醒各个线程信息区打印的循环了,而是用主线程唤醒,因此在主线程中遍历了各个线程的循环缓冲区,并且把各个线程的循环部分都移到了,kernel_thread()函数的结尾。下面把代码展示给大家,庆幸的是在笔者的电脑上,它真的运行起来了。
【queue.h】
// queue.h 作者:至强 时间:2022年11月
#ifndef __QUEUE_H
#define __QUEUE_H
#include "thread.h"
/*
指令的结构,因为只是实验,目前只能想到这些了,用它
作为消息的条目,目的是向主线程传递大量函数参数。
*/
struct buf {
unsigned int instruction;
struct task* t;
unsigned int argc;
unsigned int argv[32 - 3];
}__attribute__((packed));
struct queue {
struct buf* buffer; // 指向指令数组的指针。
int head; // 队头
int tail; // 队尾
unsigned int free; // 空闲指令的个数。
};
void queue_init(struct queue* iq);
int is_empty(struct queue* iq);
int is_full(struct queue* iq);
void queue_in(struct queue* iq, struct buf* e);
int queue_out(struct queue* iq);
int queue_pop(struct queue* iq);
#endif
【queue.c】
// queue.c 作者:至强 时间:2022年11月
#include "queue.h"
#include "global.h"
#include "memory.h"
/*
这个应该被称为指令队列吧,它的初始化工作几乎和循环消息
队列一样一样的。
*/
void queue_init(struct queue* iq) {
/*
由于每条指令的长度达128字节,因此在内核空间分配两页。
即使这样队列中也最多允许64条指令同时入队。
*/
iq->buffer = (struct buf*)get_kernel_pages(2);
iq->head = 0;
iq->tail = 0;
iq->free = 64;
}
/*
指令队列是否为空。
*/
int is_empty(struct queue* iq) {
if(iq->free == 64) {
return 1;
} else {
return 0;
}
}
/*
指令队列是否已满。
*/
int is_full(struct queue* iq) {
if(iq->free == 0) {
return 1;
} else {
return 0;
}
}
/*
入队的操作。注意:传递的参数都是指针。
*/
void queue_in(struct queue* iq, struct buf* e)
{
struct buf ee = *e;
if(is_full(iq)) {
return;
} else {
if(iq->head > 63) {
iq->head = 0;
}
iq->buffer[iq->head++] = ee;
iq->free--;
}
}
/*
先进先出队列出队的操作。注意:传递的参数都是指针。
返回值为队中元素在队中的索引值。
*/
int queue_out(struct queue* iq) {
int e;
if(is_empty(iq)) {
e = iq->head;
iq->buffer[e].instruction = 0xffffffff;
iq->buffer[e].t = NULL;
return e;
} else {
if(iq->tail > 63) {
iq->tail = 0;
}
e = iq->tail++;
iq->free++;
return e;
}
}
/*
后进先出队列出队的操作。注意:传递的参数都是指针。
返回值为队中元素在队中的索引值。
*/
int queue_pop(struct queue* iq) {
int e;
if(is_empty(iq)) {
e = iq->head;
iq->buffer[e].instruction = 0xffffffff;
iq->buffer[e].t = NULL;
return e;
} else {
if(iq->head < 0) {
iq->head = 63;
}
e = iq->head--;
iq->free++;
return e;
}
}
【thread.c 节选】
(上面省略)
void kernel_thread(thread_func* function, void* func_arg) {
/*
必须要重新打开中断,因为在时钟中断中自动关闭了处理器的中断。
*/
intr_enable();
/*
通过函数调用,真正运行起代表线程的函数。
*/
function(func_arg);
while(1) {
asm("cli");
if(ring_queue_is_empty(&running_thread()->r)) {
asm("sti;hlt");
} else {
int data = ring_queue_out(&running_thread()->r);
asm("sti");
printf_(" %s ", &running_thread()->name);
}
}
}
(下面省略)
【kernel.c 节选】
(上面省略)
struct queue ins_queue;
(中间省略)
while(1) {
asm("cli");
/*
遍历各个线程的循环缓冲区,统一向其中发送整数0。从而使
每个线程的缓冲区都有数据,让线程的循环中显示信息的条件
成立。
*/
struct list_node* n = thread_ready_list.head.next;
struct task* t;
while(n != &thread_ready_list.tail) {
struct task* t =
node2entry(struct task, general_tag, n);
ring_queue_in(&t->r, 0);
n = n->next;
}
/*
如果ins_queue指令缓冲区为空,则不做任何操作。否则赋值指令
及其参数,并执行该指令。注意指令是用整数表示的。
*/
if(is_empty(&ins_queue)) {
asm("sti");
} else {
struct buf e;
e = ins_queue.buffer[queue_pop(&ins_queue)];
asm("sti");
instructions_switch(&e);
}
}
}
/*
选择要执行的指令,这里只是随意安排的,目前仅仅是画实心矩形、
实心圆以及空心圆。
*/
void instructions_switch(struct buf* e) {
switch(e->instruction) {
case 0:
fill_rectangle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],
e->argv[3], e->argv[4], e->argv[5], e->argv[6]);
break;
case 1:
fill_circle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],
e->argv[3], e->argv[4], e->argv[5]);
break;
case 5:
draw_circle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],
e->argv[3], e->argv[4], e->argv[5]);
break;
default:
break;
}
}
void init(void) {
/*
全局描述符表的初始化。
*/
gdt_init();
/*
分页机制的初始化。
*/
page_init();
/*
中断描述符表的初始化。
*/
idt_init();
/*
可编程中断控制器的初始化。
*/
i8259_init();
/*
实时时钟的初始化。
*/
rtc_init();
/*
发生频率的初始化。
*/
i8253_init();
/*
内存管理模块的初始化。
*/
mem_init();
thread_init();
screen_init_();
/*
一定要初始化指令队列。
*/
queue_init(&ins_queue);
}
void k_thread_a(void* arg) {
/*
每个线程都通过入队指令向指令队列中插入指令。当插入指令时,
为了防止竞争条件的发生,果断的关闭中断。
*/
struct buf e;
/*
通过临时变量,构造一个符合指令格式的完整指令。
*/
e.instruction = 0;
e.argc = 7;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 400;
e.argv[3] = 400;
e.argv[4] = 30;
e.argv[5] = 50;
e.argv[6] = 0x00ffff00;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
e.instruction = 0;
e.argc = 7;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 100;
e.argv[3] = 100;
e.argv[4] = 70;
e.argv[5] = 70;
e.argv[6] = 0x0000ff00;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
}
void k_thread_b(void* arg) {
struct buf e;
e.instruction = 0;
e.argc = 7;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 600;
e.argv[3] = 200;
e.argv[4] = 80;
e.argv[5] = 80;
e.argv[6] = 0x00ff0000;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
e.instruction = 5;
e.argc = 6;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 300;
e.argv[3] = 300;
e.argv[4] = 100;
e.argv[5] = 0x00ffffff;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
/*
*/
}
void k_thread_c(void* arg) {
struct buf e;
e.instruction = 0;
e.argc = 7;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 600;
e.argv[3] = 200;
e.argv[4] = 50;
e.argv[5] = 100;
e.argv[6] = 0x00ff00ff;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
e.instruction = 1;
e.argc = 6;
e.t = running_thread();
e.argv[0] = 0xe0000000;
e.argv[1] = 1024;
e.argv[2] = 200;
e.argv[3] = 400;
e.argv[4] = 50;
e.argv[5] = 0x000f00f0;
asm("cli");
queue_in(&ins_queue, &e);
asm("sti");
/*
*/
}
大家不要忘了把intr.c文件中schedule()函数中向各个线程缓冲区发送数据的ring_queue_in()函数注释掉哦。
下面是运行效果图。
如果把e = ins_queue.buffer[queue_pop(&ins_queue)]语句改成
e = ins_queue.buffer[queue_out(&ins_queue)]屏幕截图将会是这样的。
大家一看便明白了,因为出队的顺序不一样,所以导致画图的顺序也有些不同,图案覆盖的也就有了不同的效果。
接下来,让我们看看一步唤醒缓冲区真正大显身手的时候吧。因为没有什么好说的,所以让我们直接上代码吧。
【intr.c 节选】
(上面省略)
i_table[0x28] = (unsigned int)rtc_handler;
i_table[0x20] = (unsigned int)timer_handler;
i_table[0x21] = (unsigned int)keyboard_handler;
i_table[0x2c] = (unsigned int)mouse_handler;
(中间省略)
create_gate(0x28, (unsigned int)rtc, 1 * 8, 0x8e00);
create_gate(0x20, (unsigned int)timer, 1 * 8, 0x8e00);
create_gate(0x21, (unsigned int)keyboard, 1 * 8, 0x8e00);
create_gate(0x2c, (unsigned int)mouse, 1 * 8, 0x8e00);
(中间省略)
/*
从名字上就可以知道,这是让谁等待谁准备好才有所动作的函数。
看到while语句大家就更明白了吧。当读取命令端口0x64所得到的
数据的第二位(从0开始计算)为0时才结束循环。也就是放弃等待。
*/
void wait_kbc_sendready(void) {
while((in(0x64) & 0x02));
}
/*
因为键盘控制器相较于处理器的运算速度实在是慢的可怜,所以这里
为了将就键盘控制器,处理器需要循环查看键盘控制器是否准备好这个
状态。
*/
void keyboard_init(void) {
wait_kbc_sendready();
/*
向键盘控制器发送模式设定的指令。端口0x64是命令端口,而指令0x60
是设定工作模式的指令。
*/
out(0x64, 0x60);
wait_kbc_sendready();
/*
把可以设定鼠标的命令0x47写入到键盘控制器的数据端口,即完成可以
设定鼠标的设定。
*/
out(0x60, 0x47);
}
void mouse_init(void) {
/*
继续向键盘命令端口写入0xd4,则接下下来向键盘控制器的数据端口写入
的数据是向鼠标发送命令,即0xf4是激活鼠标的命令。当然鼠标也不是吃素
的,它会立即产生一个8位数据0xfa作为回应。表示它可以了。
*/
wait_kbc_sendready();
out(0x64, 0xd4);
wait_kbc_sendready();
out(0x60, 0xf4);
}
void mouse_handler(void) {
unsigned int data = in(0x60);
/*
分别通知主从可编程中断控制器,鼠标中断受理完毕。
*/
out(0xa0, 0x64);
out(0x20, 0x62);
// kprintf_(" mouse! ");
/*
哈哈,循环队列的用武之地原来是这里,向主线程自带的缓冲区发送数据。
*/
ring_queue_in(&main_thread->r, data);
}
void keyboard_handler(void) {
unsigned int data = in(0x60);
// kprintf_(" keyboard! ");
/*
哈哈,循环队列的用武之地原来是这里,向主线程自带的缓冲区发送数据。
*/
ring_queue_in(&main_thread->r, data);
}
【system.asm 节选】
(上面省略)
; 键盘中断处理程序的汇编语言部分。
; void keyboard(void);
global _keyboard
align 8
_keyboard:
push 0x21 ; 向量号
jmp _interrupt_entry
; 鼠标中断处理程序的汇编语言部分。
; void mouse(void);
global _mouse
align 8
_mouse:
push 0x2c ; 向量号
jmp _interrupt_entry
【thread.c 节选】
(上面省略)
} else {
int data = ring_queue_out(&running_thread()->r);
asm("sti");
// printf_(" %s ", &running_thread()->name);
}
(下面省略)
【kernel.c 节选】
(上面省略)
/*
当且仅当两个缓冲区都为空时才直接恢复中断。
*/
if(is_empty(&ins_queue) && ring_queue_is_empty(&running_thread()->r)) {
asm("sti");
} else {
struct buf e;
e = ins_queue.buffer[queue_out(&ins_queue)];
/*
这是接下来要实现的事情,现在先注释掉。
*/
// int d = ring_queue_out(&main_thread->r);
asm("sti");
instructions_switch(&e);
/*
打印缓冲区元素的数目,因为一直往缓冲区中填入数据,没有人读出,所以缓冲区空间逐步减小,知道填满。
*/
if(ring_queue_is_full(&running_thread()->r)) {
printf_("...%d...", running_thread()->r.free);
} else {
printf_("___%d___", running_thread()->r.free);
}
}
(下面省略)
在头文件中的一些函数生命这里就不说了,这里主要是把除主线程外的其他线程的输出关闭了。从而更容易分辨出缓冲区操作的效果。
第一幅图可以看出最初,没有按下键盘或鼠标的情况下,缓冲区中只有一个数据,也就是缓冲区的大小为1023。等到第二幅图,多次按下鼠标和键盘,缓冲区空余空间逐渐减小。第三幅图,继续操作,缓冲区空间空间被用尽,显示缓冲区的长度为0。
接下来我们要做一个极简的键盘驱动程序,先让我把代码给大家展示在下面,然后在慢慢的解释。
【intr.c 节选】
(上面省略)
/*
鼠标和键盘都是往主线程的循环缓冲区中发送数据,原始数据的长度都是1
字节,也就是它们会发送大小相同的数据,那么怎么区分是谁发出去的数
据呢?恰好,我们的缓冲区接收的数据单位长度是双字,这样我们通过加上
能够覆盖字节长度的数据,并且让它们不在同一区间,就可以轻松区分了。
*/
ring_queue_in(&main_thread->r, data + 0x100 + 0x100);
}
void keyboard_handler(void) {
unsigned int data = in(0x60);
// kprintf_(" keyboard! ");
/*
哈哈,循环队列的用武之地原来是这里,向主线程自带的缓冲区发送数据。
*/
ring_queue_in(&main_thread->r, data + 0x100);
}
【kernel.c 节选】
(上面省略)
unsigned int keymap[0x80] = {
0,0,'1','2','3','4','5','6','7','8','9','0','-','=',0,0,
'q','w','e','r','t','y','u','i','o','p','[',']',0,0,'a','s',
'd','f','g','h','j','k','l',';','\'','`',0,'\\','z','x','c','v',
'b','n','m',',','.','/',0,'*',0,' ',0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
while(1) {
asm("cli");
/*
遍历各个线程的循环缓冲区,统一向其中发送整数0。从而使
每个线程的缓冲区都有数据,让线程的循环中显示信息的条件
成立。
*/
struct list_node* n = thread_ready_list.head.next;
struct task* t;
while(n != &thread_ready_list.tail) {
struct task* t =
node2entry(struct task, general_tag, n);
ring_queue_in(&t->r, 0);
n = n->next;
}
/*
如果ins_queue指令缓冲区为空,则不做任何操作。否则赋值指令
及其参数,并执行该指令。注意指令是用整数表示的。
*/
/*
当且仅当两个缓冲区都为空时才直接恢复中断。
*/
if(is_empty(&ins_queue) && ring_queue_is_empty(&running_thread()->r)) {
asm("sti");
} else {
struct buf e;
e = ins_queue.buffer[queue_out(&ins_queue)];
/*
这是接下来要实现的事情,现在先注释掉。
*/
int d = ring_queue_out(&main_thread->r);
asm("sti");
instructions_switch(&e);
/*
打印缓冲区元素的数目,因为一直往缓冲区中填入数据,没有人读出,所以
缓冲区空间逐步减小,知道填满。
*/
/*
if(ring_queue_is_full(&running_thread()->r)) {
printf_("...%d...", running_thread()->r.free);
} else {
printf_("___%d___", running_thread()->r.free);
}
*/
/*
这里我们键盘中断处理程序发送过来的数据,由于在原始数据的基础
上加上了0x100,所以这里要恢复原貌。
*/
if((d >= 0x100) && (d < 0x100 + 0x100)) {
if((d - 0x100) & 0x80) {
} else {
printf_("%c", keymap[(d - 0x100) & 0x7f]);
}
}
}
}
(下面省略)
【system.asm 节选】
(上面省略)
_interrupt_entry:
; 占位
push 0
pusha
push ds
push es
push fs
push gs
;很久都没有看中断处理程序的这里了,这是通知可编程中断控制器该中断
;已被处理,在完成中断处理后,可以再次响应该中断。这与我们采取的
;响应中断的模式相关。因此每次都都要通知可编程中断控制器。
mov al, 0x20
out 0x20, al
out 0xa0, al
(下面省略)
关于键盘中断处理程序用简陋来相容它是最恰当不过了。它的汇编部分几乎与时钟中断处理程序没有什么区别。在最开始通过in(0x60)函数,从键盘控制器得到一个字节的数据。这是很关键的,键盘控制器的习惯是如果你不读我的数据端口,下一次我就不会响应中断。然后我们可以看到,这时我们并没有对的到的数据进行任何处理。仅仅是吧键盘数据加上0x100后,就丢给了主线程的缓冲区。其实在中断处理程序中,就不能够处理吗?大家是不是早就在脑子里有了这个出轨的想法,为什么一定要丢给主线程去处理。我给大家的解释是,这主要是让中断处理的时间压缩到最短的原因。因为在咱们的系统中,中断处理期间是关闭其他中断的,也就是那几个强烈要求使用处理器的线程,得不到切换,所以中断处理程序是越短越好。
我们是在主线程中对键盘来的数据进行处理。那么我们就要说说键盘中断发生的时机和产生的次数。当我们按下键盘上的某个键后或者多个键后,其实是有多种情况的。比如说,我们按下一个键不抬起,那么并不是仅仅产生一次中断,而是在一个很小的时间间隔后,又产生了中断,也就是说,在你始终按着键盘上的某个键时,处理器会多次进入(运行)我们的键盘中断处理程序,产生多次数据。当你的手从键盘上抬起后,也产生一次中断。也就是说,如果你按下键然后马上抬起的话,一般情况下会产生两次中断,中断处理程序会向主线程的缓冲区发送两个数据。同时按下键盘上的多个键不抬起会发生什么呢?前面按的键都会按时间顺序陆续产生中断,最后按下的键会产生多次中断。这时抬起最后按下一个键将不产生中断,抬起其他键不影响最后按键的中断。当然按下特殊的键会产生两次的中断,目前我们不处理它,也就没有什么顾虑。现在来说第二个问题,按下键和抬起键各产生一次中断,它们都产生什么样的数据。我们可以把这两个动作产生的数据叫做通码和断码,它们都可以称之为扫描码。也就是说即使按下了可显示字符,键盘也不会直接产生ascii码,而是产生通码。通码和断码有什么神秘的关系呢?它们的区别在于数据的第7位(从0开始计算),该位为0是通码,为1则是断码。这样一来可识别的按键是不是又少了一半。可是这有什么用呢?这样我们可以通过“与”或者“或”操作来区分通断,从而不对断码进行处理。具体的来说就是通码通过和0x80(10000000b)相“或”就得到了断码,而断码与0x7f相“与”就的到了通码,并且可以保证数据始终在通码的范围内。第三个问题是我们接收到的数据(扫描码)跟我们键盘上看到的按键(ascii码)有什么样的关系呢?按照什么样的方式才能相互转换呢?说句实在话,想破脑袋也不可能猜出它们之间的规律,因为根本就没有规律。在这种情况下。我们就要做一张表格(数组),用扫描码作为数组的下标,对应的美国信息交换标准代码作为数组的元素,这样当接收的数组的下标后,我们通过查表就的到了美国信息交换标准代码。
通过上面的一连串的骚操作,我们发现标准的按键转换非常简单,只要几句c语言代码就能实现。下面让我们看一下运行效果。看到了吧。这就是从信息区得到的键盘输入。
上面我们简简单单地处理了键盘中断处理程序发给主线程的数据。当然是醉翁之意不在酒的
小动作了。其实我们是想通过处理键盘数据小试牛刀,而后顺理成章地处理来自鼠标的数据。在之前的一小节,我们已经可以接收鼠标的数据了,当时我们可以知道,鼠标数据相比键盘来说那是又如滔滔江水连绵不绝,又如黄河泛滥一发不可收拾的。在那个实验中,如果不读取缓冲区的数据,1024个单元很快就满了。
下面我们就列出鼠标处理程序的代码,赶紧请大家一睹为快吧。我们这次在主目录中添加了mouse目录作为鼠标处理程序的基地,因此在Makefile中要添加相应的编译语句哟。
【mouse.h】
// mouse.h 创建者:至强 创建时间:2022年8月
#ifndef __MOUSE_H
#define __MOUSE_H
/*
鼠标控制结构,不管是移动鼠标,还是点击鼠标上的按键,鼠标都产生
三次中断。因此鼠标中断三次为一组,就要在控制结构中,有一个3字节
的数组。phase在英文中是阶段的意思,这里是作为连续接收3个字节的
依据。x和y时鼠标移动距离的数据,单次最多不超多1字节的宽度,btn
是按键和鼠标移动方位的依据。
*/
struct mouse_dec {
unsigned char buf[3], phase;
int x, y, btn;
};
void make_mouse_pointer(unsigned int* mouse_bin, unsigned int transparent_colour);
void draw_mouse_pointer(int* buf, int x0, int y0, int x_s, int y_s,
unsigned int* mouse_bin);
void make_mouse_before(unsigned int* mouse_bin, unsigned int transparent_colour);
void draw_mouse_before(int* buf, int x0, int y0, int x_s, int y_s,
unsigned int* mouse_bin);
int mouse_decode(struct mouse_dec* mdec, unsigned char data);
#endif
【mouse.c】
// mouse.c 创建者:至强 创建时间:2022年8月
#include "mouse.h"
#include "x.h"
/*
用字节的形象图构造一个双字的鼠标图像。以此来在屏幕上画图。
*/
void make_mouse_pointer(unsigned int* mouse_bin, unsigned int transparent_colour) {
int x, y;
static char c[16][16] = {
"@...............",
"@@..............",
"@#@.............",
"@##@............",
"@###@...........",
"@####@..........",
"@#####@.........",
"@######@........",
"@#######@.......",
"@########@......",
"@#########@.....",
"@##########@....",
"@###########@...",
"@############@..",
"@#############@.",
"@@@@@@@@@@@@@@@@"
};
for(y = 0; y < 16; y++) {
for(x = 0; x < 16; x++) {
if(c[y][x] == '@') {
mouse_bin[y * 16 + x] = 0x00ffffff;
}
if(c[y][x] == '.') {
mouse_bin[y * 16 + x] = transparent_colour;
}
if(c[y][x] == '#') {
mouse_bin[y * 16 + x] = 0x00ff0000;
}
}
}
}
/*
在屏幕上画鼠标指针。
*/
void draw_mouse_pointer(int* buf, int x0, int y0, int x_s, int y_s,
unsigned int* mouse_bin) {
int x, y;
for(y = y0; y < y0 + y_s; y++) {
for(x = x0; x < x0 + x_s; x++) {
if(mouse_bin[(y - y0) * 16 + (x - x0)] != 0x003098df) {
draw_point(buf, 1024, x, y,
mouse_bin[(y - y0) * 16 + (x - x0)]);
}
}
}
}
/*
现在并没有图层这个高大上的东西,因此,还要构造一个鼠标指针的反相,
用于擦除先前画的鼠标指针。
*/
void make_mouse_before(unsigned int* mouse_bin, unsigned int transparent_colour) {
int x, y;
static char c[16][16] = {
"@...............",
"@@..............",
"@#@.............",
"@##@............",
"@###@...........",
"@####@..........",
"@#####@.........",
"@######@........",
"@#######@.......",
"@########@......",
"@#########@.....",
"@##########@....",
"@###########@...",
"@############@..",
"@#############@.",
"@@@@@@@@@@@@@@@@"
};
for(y = 0; y < 16; y++) {
for(x = 0; x < 16; x++) {
if(c[y][x] == '@') {
mouse_bin[y * 16 + x] = transparent_colour;
}
if(c[y][x] == '.') {
mouse_bin[y * 16 + x] = 0x003098dd;
}
if(c[y][x] == '#') {
mouse_bin[y * 16 + x] = transparent_colour;
}
}
}
}
/*
画鼠标指针的反相,擦除指针。
*/
void draw_mouse_before(int* buf, int x0, int y0, int x_s, int y_s,
unsigned int* mouse_bin) {
int x, y;
for(y = y0; y < y0 + y_s; y++) {
for(x = x0; x < x0 + x_s; x++) {
if(mouse_bin[(y - y0) * 16 + (x - x0)] == 0x003098df) {
draw_point(buf, 1024, x, y,
mouse_bin[(y - y0) * 16 + (x - x0)]);
}
}
}
}
/*
鼠标数据的解析函数。
*/
int mouse_decode(struct mouse_dec* mdec, unsigned char data) {
/*
当控制结构中的phase等于0时,这时接收鼠标发来的数据,应该是
鼠标被激活后反馈的数据0xfa。置phase为1,则进入下一阶段。
否则返回。
*/
if(mdec->phase == 0) {
if(data == 0xfa) {
mdec->phase = 1;
}
return 0;
}
/*
这是鼠标被激活后的第一阶段,也就是通常鼠标中断的数据1。该数据
是鼠标移动或者按下时的标志数据。
*/
if(mdec->phase == 1) {
/*
该数据是比较特殊的,其中高4位,仅有2位是有效的,标志着鼠标是否在
横向和纵向移动。低8位则是后3位有效,标志着左右中按键是否会按下
组合。
*/
if((data & 0xc8) == 0x08) {
mdec->buf[0] = data;
mdec->phase = 2;
}
return 0;
}
/*
横向移动的数据。
*/
if(mdec->phase == 2) {
mdec->buf[1] = data;
mdec->phase = 3;
return 0;
}
/*
纵向移动的数据。
*/
if(mdec->phase == 3) {
mdec->buf[2] = data;
mdec->phase = 1;
/*
通过"与"操作保留了按键的数据,以备处理。
*/
mdec->btn = mdec->buf[0] & 0x07;
mdec->x = mdec->buf[1];
mdec->y = mdec->buf[2];
/*
依据横向纵向移动与否,对横向和纵向移动数据进行有效行处理。
这里采取的是高位置1的方法,但笔者却不知道是何道理。
*/
if((mdec->buf[0] & 0x10) != 0) {
mdec->x |= 0xffffff00;
}
if((mdec->buf[0] & 0x20) != 0) {
mdec->y |= 0xffffff00;
}
/*
y的方向是相反的,所以取负值得到正确的数值。
*/
mdec->y = -mdec->y;
return 1;
}
return -1;
}
【kernel.c 节选】
(上面省略)
int mx = 0, my = 128;
struct mouse_dec mdec;
make_mouse_pointer(mouse_bin, 0x003098df);
make_mouse_before(mouse_before, 0x003098df);
while(1) {
asm("cli");
(中间省略)
/*
这里我们鼠标中断处理程序发送过来的数据。
*/
if((d >= 0x100 + 0x100) && (d < 0x100 + 0x100 + 0x100)) {
if(mouse_decode(&mdec, d - 0x100 - 0x100)) {
/*
在鼠标未动之前,把鼠标指针消隐。
*/
draw_mouse_before(video + 1024 * 128,
mx, my, 16, 16, mouse_before);
/*
鼠标数据中的横向和纵向数据其实是相对与当前的增减数据。
*/
mx += mdec.x;
my += mdec.y;
/*
对数据在画面的范围进行判断。也就是横轴和纵轴的极值。
*/
if(mx < 0) {
mx = 0;
}
if(my < 0) {
my = 0;
}
if(mx > 1024 - 1) {
mx = 1024 - 1;
}
if(my > 768 - 1 - 128) {
my = 768 - 1 - 128;
}
/*
移动后,根据新的坐标位置重新描画鼠标指针。
*/
draw_mouse_pointer(video + 1024 * 128,
mx, my, 16, 16, mouse_bin);
int btn = mdec.btn;
if(btn & 1) {
/*
这里是鼠标左键的处理,现在未作任何处理,不过大家可以发挥
想象力添加一些语句,看看运行的效果。
*/
}
if(btn & 2) {
/*
这里是鼠标右键的处理,现在未作任何处理。
*/
}
if(btn & 4) {
/*
这里是鼠标中键的处理,现在未作任何处理。
*/
}
}
}
(下面省略)
上面讲的这么热闹,其实程序的源代码几乎全部来自川合秀实先生的《30天自制操作系统》,笔者可没有这个能力独自编写上面这段代码。只不过笔者稍微润色了一下,我自己还是比较满意的。下面是效果图。
之所以在这里讲鼠标和键盘是因为像这种字符设备的驱动程序,用环形缓冲区来处理简直是在好不过了。大家已经看到了,像这种异步的处理方式,谁也不需要等待谁,你干你的,我玩我的,双方不会产生任何影响。所以响应速度很快。不过大家要是实操我们现在这个时候的系统,就会大失所望,因为键盘和鼠标都是一卡一卡的,哪有笔者说的响应速度很快的感觉。还好大家不用过于担心这个问题,容笔者以后慢慢化解。
那么接下来我们要做些什么呢?思来想去,定时器还是挺有趣的,而且这个玩意儿跟环形缓冲区也是有着千丝万缕的联系。定时器的主要作用是,当某个时间到来时,发生我们想要发生的时间。从某个方面说,定时器也可以看作一个非常不精确的同步工具。还是让我们看看代码吧。
【timer.h】
// timer.h 作者:至强, 创建时间2022-05-09。
#ifndef __TIMER_H
#define __TIMER_H
#include "global.h"
/*
我们也把定时器的结构定义成双向链表,不过这次因为系统
已经有了一定的内存管理能力,所以链表中直接就带了链表
的内容。
*/
struct timer {
/*
定时器的前驱和后继。
*/
struct timer* prev, *next;
/*
包括定时器的状态、超时时间和缓冲区区分是那个定时器的
数据。
*/
unsigned int status, timeout, rq_data;
struct ring_queue* rq;
};
/*
定时器的链表结构。
*/
struct timer_list {
struct timer head, tail;
};
/*
定时器的管理结构,即使是有了简单内存分配能力,我们仍然
需要数组来(在栈中来为每个定时器创建小的空间)。
*/
struct timer_man {
struct timer_list list;
struct timer timers[1024];
};
/*
这个外部变量大家熟悉吧,对了它就是系统启动后经历的
可编程定时器的滴答数。
*/
extern unsigned int ticks;
extern struct timer_man* t_m;
void timer_man_init(void);
struct timer* timer_alloc(struct timer_man* tm);
void timer_list_init(struct timer_list* tl);
void timer_list_insert_before(struct timer* cur, struct timer* e);
void timer_list_push(struct timer_list* tl, struct timer* e);
void timer_list_append(struct timer_list* tl, struct timer* e);
struct timer* timer_list_remove(struct timer* e);
struct timer* timer_list_pop(struct timer_list* tl);
struct timer* timer_list_delete(struct timer_list* tl) ;
bool timer_list_empty(struct timer_list* tl);
struct timer* timer_list_traversal(struct timer_list* tl, unsigned int timeout);
int timer_set(struct timer_man* tm, unsigned int timeout,
struct ring_queue* rq, unsigned int rq_data);
#endif
【timer.c】
// timer.h 作者:至强, 创建时间2022-05-09。
#include "timer.h"
#include "memory.h"
#include "ring_queue.h"
#include "intr.h"
struct timer_man* t_m;
void timer_man_init(void) {
/*
有内存管理模块在内核中动态分配空间。
*/
t_m = get_kernel_pages(up_pgs(sizeof(struct timer_man)));
/*
双向链表的初始化。
*/
timer_list_init(&t_m->list);
int i;
/*
将最多1024个定时器,都标注为未使用状态。
*/
for(i = 0; i < 1024; i++) {
t_m->timers[i].status = 0; // 0未占用,1已占用
}
}
/*
企图分配一个定时器,如果该定时状态为未用则成功获取。
*/
struct timer* timer_alloc(struct timer_man* tm) {
int i;
for(i = 0; i < 1024; i++) {
if(!(tm->timers[i].status)) {
break;
}
}
if(i == 1024) {
return NULL;
}
return &tm->timers[i];
}
/*
熟悉吧,双向链表的初始化还是老样子。
*/
void timer_list_init(struct timer_list* tl) {
tl->head.prev = NULL;
tl->head.next = &tl->tail;
tl->tail.prev = &tl->head;
tl->tail.next = NULL;
}
/*
双向链表中在某个元素前插入元素。
*/
void timer_list_insert_before(struct timer* cur, struct timer* e) {
cur->prev->next = e;
e->prev = cur->prev;
e->next = cur;
cur->prev = e;
}
/*
链表头部插入元素。
*/
void timer_list_push(struct timer_list* tl, struct timer* e) {
timer_list_insert_before(tl->head.next, e);
}
/*
链表尾部加入元素。
*/
void timer_list_append(struct timer_list* tl, struct timer* e) {
timer_list_insert_before(&tl->tail, e);
}
/*
已知一个元素的地址,在链表中删除这个元素。
*/
struct timer* timer_list_remove(struct timer* e) {
e->prev->next = e->next;
e->next->prev = e->prev;
return e;
}
/*
从头部删除元素,类似于出栈操作。
*/
struct timer* timer_list_pop(struct timer_list* tl) {
struct timer* e = tl->head.next;
timer_list_remove(e);
return e;
}
/*
从尾部删除元素,类似于出队操作。
*/
struct timer* timer_list_delete(struct timer_list* tl) {
struct timer* e = tl->tail.prev;
timer_list_remove(e);
return e;
}
/*
准确的名称应该称为timer_list_is_empty,也就是判断链表是否
为空的函数。
*/
bool timer_list_empty(struct timer_list* tl) {
return (tl->head.next == &tl->tail);
}
/*
通过关键字超时时间来遍历链表的方法,寻找刚刚大于本超时
时间的元素,并返回。
*/
struct timer* timer_list_traversal(struct timer_list* tl, unsigned int timeout) {
struct timer* t = tl->head.next;
while(t != &tl->tail) {
if(t->timeout >= timeout) {
break;
}
t = t->next;
}
return t;
}
/*
向链表中添加定时器,同时设置定时器的超时时间,向环形缓冲区
发送的数据等。关键是依据超时时间,按时间长短插入和定时器,
因此形成有序队列。
*/
int timer_set(struct timer_man* tm, unsigned int timeout,
struct ring_queue* rq, unsigned int rq_data) {
/*
为了保证操作的原子性,这里简单的关闭了中断。因为这一操作
将不是在中断处理程序中完成,所以我确信更应该使用互斥信号量
来实现。
*/
unsigned int old_status = intr_disable();
struct timer* e = timer_alloc(tm);
if(!e) {
return 0;
}
e->status = 1;
/*
值得注意的是,这里可能是很难理解的。因为我们关闭了中断,所以,
时钟中断是暂停的,也就是说,这时设定的定时器的超时时间加上
ticks后要比ticks大一些,而开启中断后,ticks继续增加,因此上
定时器才总有到期的时候。
*/
e->timeout = timeout + ticks;
e->rq = rq;
e->rq_data = rq_data;
/*
在第一个比自己超时时间数值大的元素前插入,从而形成有序队列。
意即超时时间越短的定时器越排在前面。
*/
struct timer* back = timer_list_traversal(&tm->list, timeout);
if(!back) {
return 0;
}
timer_list_insert_before(back, e);
set_intr_status(old_status);
return 1;
}
【intr.c 节选】
(上面省略)
/*
系统总滴答数(时间片数)自增。
*/
ticks++;
if(!timer_list_empty(&t_m->list)) {
if(ticks > t_m->list.head.next->timeout) {
/*
当既定时刻到来后,删除定时器,并向主线程的缓冲区发送
数据(目前暂定只有主线程接收这个数据)。
*/
struct timer* e = timer_list_pop(&t_m->list);
ring_queue_in(e->rq, e->rq_data);
e->status = 0;
}
}
/*
这种调度算法应该叫做“极简优先级”算法吧。当任务时间片用完,执行调度器,
其他情况中断返回,继续运行当前任务。
*/
if(cur_thread->ticks == 0) {
schedule();
(下面省略)
【kernel.c 节选】
(上面省略)
/*
既定定时器的设置。
*/
timer_set(t_m, 100, &main_thread->r, 0);
timer_set(t_m, 100, &main_thread->r, 1);
intr_enable();
(中间省略)
while(1) {
asm("cli");
(中间省略)
/*
用于测试定时器的部分,当既定的时刻(滴答数)到来时,经由时钟
中断处理程序删除这个定时器,并向主线程发送数据0,这里接收到数据后,
就重新添加一个定时器,拟发送的数据为1,当次定时器到期时,时钟中断
处理程序也删除它,同时发送数据0。因此,形成定时循环显示文字的效果。
*/
if(d == 0) {
put_gb2312_buf(video, 1024, 200, 300, 0x00ff0000,
"我爱你!");
timer_set(t_m, 100, &main_thread->r, 1);
}
if(d == 1) {
put_gb2312_buf(video, 1024, 200, 300, 0x000000ff,
"我爱你!");
timer_set(t_m, 100, &main_thread->r, 0);
}
(下面省略)
由于运行的效果图是动态的,因此在屏幕上中部会交替的显示红色和蓝色的“我爱你”。这里就补贴图了,实在是效果一般般了。有一个问题不知道大家注意到没有,在时钟中断处理程序中,我们对定时器到期等于和大于做了不同的处理,一个是从前面出队,一个从后面出队,这个应该之适用于两个定时器存在的时候,这里我们还没有多考虑。