30天自制操作系统
上QQ阅读APP看书,第一时间看更新

2 加快中断处理(harib04b)

程序做出来了,大家心情肯定很好,但其实这个程序里有一个问题,那就是字符显示的内容被放在了中断处理程序中。

所谓中断处理,基本上就是打断CPU本来的工作,加塞要求进行处理,所以必须完成得干净利索。而且中断处理进行期间,不再接受别的中断。所以如果处理键盘的中断速度太慢,就会出现鼠标的运动不连贯、不能从网上接收数据等情况,这都是我们不希望看到的。

另一方面,字符显示是要花大块时间来进行的处理。仅仅画一个字符,就要执行8×16=128次if语句,来判定是否要往VRAM里描画该像素。如果判定为描画该像素,还要执行内存写入指令。而且为确定具体往内存的哪个地方写,还要做很多地址计算。这些事情,在我们看来,或许只是一瞬间的事,但在计算机看来,可不是这样。

谁也不知道其他中断会在哪个瞬间到来。事实上,很可能在键盘输入的同时,就有数据正在从网上下载,而PIC正在等待键盘中断处理的结束。

■■■■■

那该如何是好呢?结论很简单,就是先将按键的编码接收下来,保存到变量里,然后由HariMain偶尔去查看这个变量。如果发现有了数据,就把它显示出来。我们就这样试试吧。

int.c节选

struct KEYBUF {
    unsigned char data, flag;
};

#define PORT_KEYDAT      0x0060

struct KEYBUF keybuf;

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.flag == 0) {
        keybuf.data = data;
        keybuf.flag = 1;
    }
    return;
}

我们先完成了上面的程序。考虑到键盘输入时需要缓冲区,我们定义了一个构造体,命名为keybuf。其中的flag变量用于表示这个缓冲区是否为空。如果flag是0,表示缓冲区为空;如果flag是1,就表示缓冲区中存有数据。那么,如果缓冲区中存有数据,而这时又来了一个中断,那该怎么办呢?这没办法,我们暂时不做任何处理,权且把这个数据扔掉。

■■■■■

下面让我们看看bootpack.c的HariMain函数吧。我们对最后的io_halt里的无限循环进行了如下修改。

bootpack.c中HariMain函数的节选

for (; ; ) {
    io_cli();
    if (keybuf.flag == 0) {
        io_stihlt();
    } else {
        i = keybuf.data;
        keybuf.flag = 0;
        io_sti();
        sprintf(s, "%02X", i);
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
    }
}

开始先用io_cli指令屏蔽中断。为什么这时要屏蔽中断呢?因为在执行其后的处理时,如果有中断进来,那可就乱套了。我们先将中断屏蔽掉,去看一看keybuf.flag的值是什么。

如果flag的值是0,就说明键还没有被按下,keybuf.data里没有值保存进来。在keybuf.data里有值被保存进来之前,我们无事可做,所以干脆就去执行io_hlt。但是,由于已经执行io_cli屏蔽了中断,如果就这样去执行HLT指令的话,即使有什么键被按下,程序也不会有任何反应。所以STI和HLT两个指令都要执行,而执行这两个指令的函数就是io_stihlt可能有人会认为,不做这个函数,而是用“io_sti(); io_hlt(); ”不也行吗?但是,实际上这样写有点问题。如果io_sti()之后产生了中断,keybuf里就会存入数据,这时候让CPU进入HLT状态,keybuf里存入的数据就不会被觉察到。根据CPU的规范,机器语言的STI指令之后,如果紧跟着HLT指令,那么就暂不受理这两条指令之间的中断,而要等到HLT指令之后才受理,所以使用io_stihlt函数就能克服这一问题。。执行HLT指令以后,如果收到了PIC的通知,CPU就会被唤醒。这样,CPU首先会去执行中断处理程序。中断处理程序执行完以后,又回到for语句的开头,再执行io_cli函数。

继续往后读程序,我们能找到else语句。它一定要跟在if语句后面,意思是说只有在if语句中的条件不满足时,才能执行else后面花括号中的语句。如果通过中断处理函数在keybuf.data里存入了按键编码,else语句就会被执行。先将这个键码(keybuf.data)值保存到变量i里,然后将flag置为0表示把键码值清为空,最后再通过io_sti语句开放中断。虽然如果在keybuf操作当中有中断进来会造成混乱,但现在keybuf.data的值已经保存完毕,再开放中断也就没关系了。最后,就可以在中断已经开放的情形下,优哉游哉地显示字符了。

回过头来看一看,可以发现,其实在屏蔽中断期间所做的处理非常少,中断处理程序本身做的事情也非常少,而这正是我们所期待的。真棒!如果我们坚持这么做,不但中断很少会被遗漏,而且最后完成的操作系统也会非常利索。

■■■■■

我们赶紧来测试一下吧。运行“make run”。哦,像以前一样,能够顺利执行……但是,发生了一点儿小问题。请按下键盘的右Ctrl键看看。不管是按下,还是松开,屏幕上显示的都是“E0”。哎?我们再试试harib04a,看看情况如何。结果按下去时显示“1D”,松开时显示“9D”。怎么回事?与harib04a结果不一样就意味着哪儿出了问题。

通过查资料http://community.osdev.info/?(AT)keyboard得知,当按下右Ctrl键时,会产生两个字节的键码值“E0 1D”,而松开这个键之后,会产生两个字节的键码值“E0 9D”。在一次产生两个字节键码值的情况下,因为键盘内部电路一次只能发送一个字节,所以一次按键就会产生两次中断,第一次中断时发送E0,第二次中断时发送1D。

在harib04a中,以上两次中断所发送的值都能收到,瞬间显示E0之后,紧接着又显示1D或是9D。而在harib04b中,HariMain函数在收到E0之前,又收到前一次按键产生的1D或者9D,而这个字节被舍弃了。

■■■■■

这么一说,可能有人会觉得还是以前的harib04a更好。但是在harib04a中,键盘控制器(设备号码0x0060)是在“想去厕所,快要忍不住了”(=屏蔽中断的状态)的情况下,等待着程序的处理,才勉强得到这样看起来还不错的结果。但这对于硬件来讲,实在有点太勉为其难了。在harib04b程序中,硬件没有负担,不会憋得肚子疼,只是笔者这里写的程序还是不够好,好不容易接收到的数据,没能很好地利用起来。

所以,我们来修改一下程序,让它再聪明点儿。