第13天
在这之前的程序中每次新设置一个定时器都要创造一个队列与之对应,这样效率低而且导致操作系统主程序中逻辑复杂。我们先想办法把定时器的消息队列合并。怎么合并呢?首先想一下我们为什么之前要把定时器的消息队列分开设置,为每个定时器分配一个队列呢。主要是因为每个定时器超时所对应的操作不一样,因此为了区分不同定时器超时操作,所以才把队列分开。那如果我们给每个定时器往队列中放入数据的值都不同,再每个定时器超时的时候先判断队列中的数据属于哪个计时器,我们就可以分别进行不同的操作了,也达到了之前的目前。而且代码肯定更简单了,起码不用作那么多判断了。
接下来还要做很多计时器的优化工作,但是我们现在还没有考虑怎么样测试优化的结果。我们可以这样,在计时器3秒中断的时候把count设置为0,一直全力计数,然后在10秒中断的时候停止计数,并把结果显示在窗口中。如果数字越大说明系统执行速度越快,性能也就越好。
我们之前区分定时器中断时候,是用向消息队列中发送数据的不同来区分的。那能不能我们把鼠标和键盘使用的消息队列也跟定时器合在一起,也用向消息队列中传送的数据来区分呢?下面我们定义一下中断类型。
- 0~1 光标闪烁定时器
- 3 3秒定时器
- 10 10秒定时器
- 256~511 键盘输入(从键盘控制器读入的值再加上256)
- 512~767 鼠标输入(从键盘控制器读入的值再加上 512)
我们之前的队列定的使用的数据类型为char只有8位,现在我们将其改为int类型。其他基本没有什么变化。只是在队列中读入键盘和鼠标灵数据的时候要分别减去256和512。
在处理定时器中断的时候主要的时间开销是找下一个超时定时器之后的移位操作,我们就是要想办法取消移位操作。
我们在TIMER结构体中新定义一个TIMER指针,指向下一个即将超时的定时器。
struct TIMER {
struct TIMER *next;
unsigned int timeout, flags;
struct FIFO32 *fifo;
int data;
};
其实就是把线性表改造成链表操作,中断处理程序这么改写
void inthandler20(int *esp)
{
int i;
struct TIMER *timer;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.next > timerctl.count) {
return;
}
timer = timerctl.t0; //首先把最前面的地址赋给timer
for (i = 0; i < timerctl.using; i++) {
//因为timers的定时器都处于运行状态,所以不确认flags
if (timer->timeout > timerctl.count) {
break;
}
//超时
timer->flags = TIMER_FLAGS_ALLOC;
fifo32_put(timer->fifo, timer->data);
timer = timer->next; //下一个定时器的地址赋给timer
}
timerctl.using -= i;
/*新移位 */
timerctl.t0 = timer;
/* timerctl.next的设定*/
if (timerctl.using > 0) {
timerctl.next = timerctl.t0->timeout;
} else {
timerctl.next = 0xffffffff;
}
return;
}
定时器设置函数
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
int e;
struct TIMER *t, *s;
timer->timeout = timeout + timerctl.count;
timer->flags = TIMER_FLAGS_USING;
e = io_load_eflags();
io_cli();
timerctl.using++;
if (timerctl.using == 1) {
timerctl.t0 = timer;
timer->next = 0;
timerctl.next = timer->timeout;
io_store_eflags(e);
return;
}
t = timerctl.t0;
if (timer->timeout <= t->timeout) {
timerctl.t0 = timer;
timer->next = t;
timerctl.next = timer->timeout;
io_store_eflags(e);
return;
}
for (;;) {
s = t;
t = t->next;
if (t == 0) {
break;
}
if (timer->timeout <= t->timeout) {
s->next = timer;
timer->next = t;
io_store_eflags(e);
return;
}
}
s->next = timer;
timer->next = 0;
io_store_eflags(e);
return;
}
虽然程序变长了,但是由于引入了链表的概念,不再需要做移位操作,在定时器多的情况下,效率绝对比线性表慢慢移位要快很多。
分析一下上面程序变长的原因。主要是插入链表的时候产生了4种情况:1、运行中的定时器只有一个;2、插入到最前面的情况;3、插入到中间的情况;4、插入到最后的情况。
为了减少插边链表时候所考虑的情况,我们引入了哨兵的概念。也就是设置一个永无存在且总是在最后到期的定时器。有了这个定时器之后,插入链表的时候就只有2种情况了,插入到最前面的情况和插入到中间的情况。
修改之后的定时器设置函数
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
int e;
struct TIMER *t, *s;
timer->timeout = timeout + timerctl.count;
timer->flags = TIMER_FLAGS_USING;
e = io_load_eflags();
io_cli();
t = timerctl.t0;
if (timer->timeout <= t->timeout) {
timerctl.t0 = timer;
timer->next = t;
timerctl.next = timer->timeout;
io_store_eflags(e);
return;
}
for (;;) {
s = t;
t = t->next;
if (timer->timeout <= t->timeout) {
s->next = timer;
timer->next = t;
io_store_eflags(e);
return;
}
}
}
可以看出简化了不少。简化后的中断处理函数
void inthandler20(int *esp)
{
struct TIMER *timer;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.next > timerctl.count) {
return;
}
timer = timerctl.t0;
for (;;) {
if (timer->timeout > timerctl.count) {
break;
}
timer->flags = TIMER_FLAGS_ALLOC;
fifo32_put(timer->fifo, timer->data);
timer = timer->next;
}
timerctl.t0 = timer;
timerctl.next = timer->timeout;
return;
}
第14天
目前我们的操作系统使用的分辨率为320*200,我们可以想办法把分辨率提高上去。以前设置分辨率的时候是用ah = 0; al = 画面模式;设置的。更大一点的画面模试叫作VBE。在很早时候电脑规格是由IBM公司制定的 ,当然也规定了显卡画面模式,各家显卡公司就以IBM的标准制作显卡。但是后来显卡公司的技术力量超过了IBM,原来的显卡标准已经不适用了,各家显卡公司就制定了自己的标准。为了让操作系统和应用程序能使用各家公司的显卡,各家显卡公司联合起来成立了VESA(Video Electronics Standards Association),也就是视频电子标准协会。这个协会制定了显示通用的设定方法,也制作了专门的BIOS。这个BIOS被称作VESA BIOS extension,简称为VBE。切换到VBE使用ax = 0x4f02; bx = 画面模式。
- 0x101 6404808位彩色
- 0x103 8006008位彩色
- 0x105 10247688位彩色
- 0x107 128010248位彩色
想要提高分辨率要先查询一下机器支持不支持VBE的显示模式。
MOV AX,0x9000
MOV ES,AX
MOV DI,0
MOV AX,0x4f00
INT 0x10
CMP AX,0x004f
JNE scrn320
先把es赋值为0x9000,di赋值为0,ax赋值为0x4f00,然后int 0x10。如果ax变为0x004f的话就说明有VBE不是这个值的话就只能使用320*200的分辨率了。
接下来检查VBE的版本是不是2.0以上,如果不是2.0以上那也不能使用高分辨率。
MOV AX,[ES:DI+4]
CMP AX,0x0200
JB scrn320
然后再检查0x105的画面模式能不能使用
MOV CX,VBEMODE
MOV AX,0x4f01
INT 0x10
CMP AX,0x004f
JNE scrn320
好,如果证明0x105可以使用了,我们再确认0x105的画面信息,重要的信息有6个。
- word[es:di+0x00] 模式属性,bit7不是1就不好办(能加上0x4000)
- word[es:di+0x12] X的分辨 率
- word[es:di+0x14] Y的分辨率
- byte[es:di+0x19] 颜色数,必须为8
- byte[es:di+0x1b] 颜色的指定方法,必须为4,调色板模式
- word[es:di+0x28] VRAM的地址
上面的6个重要属性我们只要确定3个,都是正确的之后,就可以把这些信息定入指定的内存了,然后跳过scrn320程序段直接进入高级画面模式了。
接下来做键盘输入的处理。我们已经可以从键盘中断处理程序中取到键盘控制寄存器中的值了。这个值我具体对应哪个按键被按下或者松开有一个对应的表格。其中按键松开时的值为按键按下时值加上0x80。可以创建一个数组,从0开始按照这个表格把键盘的扫描码换成字符的ASCII码。之前我们已经用ASCII码创建过字体文件,然后根据获取的ASCII码在屏幕上显示出来。
如果我们把光标按照键盘输入情况左右移动也很简单。首先定义一个cursor_x变量,用于存储光标的位置。一开始鼠标靠近窗口的最左边。然后判断键盘输入的按键是否是需要显示的按键,如果需要显示,那么在cursor_x位置开写入对应的字符,然后cursor_x+8,然后在新的cursor_x位置重新画出光标。
这本书还实现了按住鼠标拖动实现鼠标跟着鼠标指针动。但是跟我们平时windows下移动鼠标的不同,这里只是简单得实现。处理鼠标循环的部分中,先判断鼠标左键是不是已经按下,如果按下,那把窗口马上移动到鼠标所在的位置。
第15天
这一天想办法实现多任务。所谓的多任务就是CPU在快速得切换各个任务,使电脑使用者感觉CPU在同时处理不同的任务。切换任务的速度不能太快也不能太慢。因为切换任务也是有成本的,如果太快,切换任伤的开销就太大,如果太慢,还不如不要多作务,因为把应太慢。
当CPU处理任务切换时,会先把寄存器中的值全部写入内存,然后把运行另一个任务所需要的CPU寄存器的值从内存中读取出来,这样就完成了一次切换。这写入内存和读取内存所需要的时间就一任务切换所需要的开销。
寄存器写入内存的数据结构叫做“任务状态段”(task status segment)。
struct TSS32 {
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
int es, cs, ss, ds, fs, gs;
int ldtr, iomap;
};
TSS32结构体中共有26个int变量,合计104字节第一行保存的不是寄存器数据,而是与任务育设置 相关的其它数据。第二行是32位寄存器。第二行是16位寄存器,但是我们还是用32位内存空间存储它。第4行也是与任务有关的其它设置。我们暂时将ldtr设置为0,将iomap设置为0x40000000。
要进行任务切换要用jmp指令。jmp指令分两种,第一种只改写EIP也就是所谓的near模式;第二种改写cs和eip,就是所谓的far模式。如果一条jmp指令的目标地址段不是可执行的代码,而是tss的话,cpu就不会执行通常的改写cs和eip,而是将这条指令理解为任务切换。CPU中还有一个TR寄存器,task register,它的作用是让CPU记住当前正在运行哪一个任务,我们给TR赋值的时候,必须把GDT编号乘以8。给TR赋值不能用MOV指令,有一个专门的指令:LTR。下面我们看一下tss结构体如何赋值:
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
tss_b.eip = (int) &task_b_main;
tss_b.eflags = 0x00000202; /* IF = 1; */
tss_b.eax = 0;
tss_b.ecx = 0;
tss_b.edx = 0;
tss_b.ebx = 0;
tss_b.esp = task_b_esp;
tss_b.ebp = 0;
tss_b.esi = 0;
tss_b.edi = 0;
tss_b.es = 1 * 8;
tss_b.cs = 2 * 8;
tss_b.ss = 1 * 8;
tss_b.ds = 1 * 8;
tss_b.fs = 1 * 8;
tss_b.gs = 1 * 8;
我们先从后6个段寄存器赋值开始看,我们给cs赋值为GDT2号,其他是GDT1号,和bootpack.c使用了相同的地址段。然后是eip,我们把task_b_main的函数地址赋给它。然后是esp,也就是栈地址,我们新申请了64K内存空间,给taskB。如果和原来的主函数使用同样的栈那些切换任务的时候肯定会出问题。
我们先这样尝试多任务:在主函数定时器10秒超时的时候切换到任务b,然后在任务b的定时候5秒超时的时候切换回任务a。
void task_b_main(void)
{
struct FIFO32 fifo;
struct TIMER *timer;
int i, fifobuf[128];
fifo32_init(&fifo, 128, fifobuf);
timer = timer_alloc();
timer_init(timer, &fifo, 1);
timer_settime(timer, 500);
for (;;) {
io_cli();
if (fifo32_status(&fifo) == 0) {
io_sti();
io_hlt();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 1) {
taskswitch3();
}
}
}
}
已经实现了两个任务之间的跳转。接下来我们实现A,B两个任务每过0.02秒就转换一次。先设置一个定时器time_ts变量,超时的时间为0.02秒,如果超时向队列中发送0x2。任务A可以接受鼠标和键盘输入的,我们很容易确认任务A是否在运行,问题就出在任务B我们如何确定任务B也能正常运行呢?在任务B中设置一个计数器,每0.01秒在屏幕上写出计数器的数值就可以了。
但是碰到一个问题,如何才能让任务B知道sht_back,只有让任务B知道这个地址才能在桌面上显示数字。我们使用栈来传递数据。首先要知道C语言的函数传递参数就是用栈,比如C语言的函数void setA(int a);这个函数被调用后,函数会从esp+4的内存中取出a这个数值。我们这里就使用C语言的这个特性。在跳到任务B的时候传递一个参数,任务B的函数声明为 void task_b_main(struct SHEET *sht_back);这样运行任务B的时候可以直接使用sht_back这个变量。我们先将任务B的esp减去8,然后把esp+4内存地址的值赋为sht_back,这样就可以了,任务B函数在使用sht_back变量值的时候就是esp+4。
目前我们都是在任务中直接写切换任务的程序段,如果要真正实现多任务最好是写一段程序调动任务之间的切换,而不是证任务自己切换。
struct TIMER *mt_timer;
int mt_tr;
void mt_init(void)
{
mt_timer = timer_alloc();
timer_settime(mt_timer, 2);
mt_tr = 3 * 8;
return;
}
void mt_taskswitch(void)
{
if (mt_tr == 3 * 8) {
mt_tr = 4 * 8;
} else {
mt_tr = 3 * 8;
}
timer_settime(mt_timer, 2);
farjmp(0, mt_tr);
return;
}
以上是实现任务切换的函数,这里设置了一个0.02秒的计时器,然后在计时器中断里如果出现这个中断时调用mt_taskwitch这个函数就可以了,两个任务的程序中就不需要自己写切换任务的程序了。