1 使用定时器(harib09a)

定时器1(Timer)对于操作系统非常重要。它在原理上却很简单,只是每隔一段时间(比如0.01秒)就发送一个中断信号给CPU。幸亏有了定时器,CPU才不用辛苦地去计量时间。……如果没有定时器会怎么样呢?让我们想象一下吧。

1 英文的Timer在汉语中有“定时器”或“时钟”等多种译法。另外,Clock这个词,也经常译作“时钟”。这两个词,意义上是不同的,如果都译作“时钟”,会引起混乱。本文原文为日文,用到了“Timer”和“Clock”这两个词的音译词。为了区别这两个词,本文中我们将Timer称作“定时器”,Clock称作“时钟周期”或“周期”。——译者注

假如CPU看不到定时器而仍想计量时间的话,就只能牢记每一条指令的执行时间了。比如,往寄存器写入常数的MOV指令是1个时钟周期(Clock);加法计算的ADD指令原则上是1个时钟周期,但根据条件不同可能是2个时钟周期……等等。CPU不仅要牢记这些内容,然后还要据此调查一下调用这些函数所需的时间,比如,调用这个函数需要150个时钟周期,调用那个函数因参数不同需要106到587个时钟周期等。

而这里的“时钟周期”又不是一个固定值。比如CPU主频是100MHz的话,一个时钟周期是10 纳秒;但主频如果是200MHz,1个时钟周期就是5纳秒。既然CPU有各种主频,那么1个时钟周期的时间也就各不相同。

这样做可以勉强通过程序对时间进行管理,实现每隔一定时间进行一次某种处理,比如让钟表(程序)的秒针动起来。如果程序中时间计算出错了,那么做出的钟表不是快就是慢,没法使用。

如果没有定时器,还会出现别的麻烦,即不能使用HLT指令。完成这个指令所需的时钟周期,不是个固定值。这样,一旦执行HLT指令,程序就不知道时间了。不能执行HLT指令,就意味着要浪费很多电能。所以只能二选一,要么放弃时间的计量,要么选择浪费电能。左右为难,实在糟糕透顶。

打个比方说,如果大家没有手表还想知道时间,那该怎么办呢?当然,不准看太阳,也不准看星星。那就只能根据肚子的饥饿程度,或者烧一壶开水所用的时间等方法来判断了。总之只能是一边干点儿什么,一边计算时间,而且决不能睡觉!一睡觉就没法计时了……嗯,就类似这种情况。

然而实际上,由于有定时器中断,所以不用担心会发生这样的悲剧。程序只需要以自己的步调处理自己的问题就行了。至于到底经过了多长时间,只要在中断处理程序中数一数定时器中断发生的次数就可以了。就算CPU处于HLT状态,也可以通过中断来唤醒。根本就没必要让程序自己去记忆时间。CPU也就可以安心地去睡觉了(HLT)。这样,大家还可以省点电费(笑)。

所以说定时器非常重要。管理定时器是操作系统的重大任务之一,所以在“纸娃娃操作系统”中我们也想使用定时器。

■■■■■

要在电脑中管理定时器,只需对PIT进行设定就可以了。PIT是“ Programmable Interval Timer”的缩写,翻译过来就是“可编程的间隔型定时器”。我们可以通过设定PIT,让定时器每隔多少秒就产生一次中断。因为在电脑中PIT连接着IRQ(interrupt request,参考第6章)的0号,所以只要设定了PIT就可以设定IRQ0的中断间隔。……在旧机种上PIT是作为一个独立的芯片安装在主板上的,而现在已经和PIC(programmable interrupt controller,参考第6章)一样被集成到别的芯片里了。

前几天我们学习PIC时曾经非常辛苦,从现在开始,我们又要重温那种感觉了。大家可不要想:“怎么又学这个?”刚开始学习PIC时,陌生的东西比较多,学起来很费力。这次就不会那么辛苦了。

首先来看资料,还是到我们每次必去的那个网站。电脑里的定时器用的是8254芯片(或其替代品),那就查一下这个芯片吧。

http://community.osdev.info/?(PIT)82548254)

以上内容,如果全部讲解的话太长了。所以这里就不一一详述了,大家来看下面“给怕麻烦的读者”这一部分吧。

  • IRQ0的中断周期变更:

    • AL=0x34:OUT(0x43,AL);

    • AL=中断周期的低8位; OUT(0x40,AL);

    • AL=中断周期的高8位; OUT(0x40,AL);

    • 到这里告一段落。

    • 如果指定中断周期为0,会被看作是指定为65536。实际的中断产生的频率是单位时间时钟周期数(即主频)/设定的数值。比如设定值如果是1000,那么中断产生的频率就是1.19318KHz。设定值是10000的话,中断产生频率就是119.318Hz。再比如设定值是11932的话,中断产生的频率大约就是100Hz了,即每10ms发生一次中断。

我们不清楚其中的详细原理,只知道只要执行3次OUT指令设定就完成了。将中断周期设定为11932的话,中断频率好像就是100Hz,也就是说1秒钟会发生100次中断。那么我们就设定成这个值吧。把11932换算成十六进制数就是0x2e9c,下面是我们编写的函数init_pit。

本次的timer.c节选

  1. #define PIT_CTRL 0x0043
  2. #define PIT_CNT0 0x0040
  3. void init_pit(void)
  4. {
  5. io_out8(PIT_CTRL, 0x34);
  6. io_out8(PIT_CNT0, 0x9c);
  7. io_out8(PIT_CNT0, 0x2e);
  8. return;
  9. }

本次的bootback.c节选

  1. void HariMain(void)
  2. {
  3. (中略)
  4. init_gdtidt();
  5. init_pic();
  6. io_sti(); /* IDT/PIC的初始化已经结束,所以解除CPU的中断禁止*/
  7. fifo8_init(&keyfifo, 32, keybuf);
  8. fifo8_init(&mousefifo, 128, mousebuf);
  9. init_pit(); /* 这里! */
  10. io_out8(PIC0_IMR, 0xf8); /* PIT和PIC1和键盘设置为许可(11111000) */ /* 这里! */
  11. io_out8(PIC1_IMR, 0xef); /* 鼠标设置为许可(11101111) */
  12. (中略)
  13. }

这样的话IRQ0就会在1秒钟内发生100次中断了。

■■■■■

下面我们来编写IRQ0发生时所调用的中断处理程序。它几乎和键盘中断处理程序一样,所以就不用再讲了吧。

本次的timer.c节选

  1. void inthandler20(int *esp)
  2. {
  3. io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知给PIC */
  4. /* 暂时什么也不做 */
  5. return;
  6. }

本次的naskfunc.nas节选

  1. _asm_inthandler20:
  2. PUSH ES
  3. PUSH DS
  4. PUSHAD
  5. MOV EAX,ESP
  6. PUSH EAX
  7. MOV AX,SS
  8. MOV DS,AX
  9. MOV ES,AX
  10. CALL _inthandler20
  11. POP EAX
  12. POPAD
  13. POP DS
  14. POP ES
  15. IRETD

为了把这个中断处理程序注册到IDT,inlt_gdtidt函数中也要加上几行。这也和键盘处理的时候差不多。

本次的dsctbl.c节选

  1. void init_gdtidt(void)
  2. {
  3. (中略)
  4. /* IDT的设定 */
  5. set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32); /* 这里! */
  6. set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
  7. set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
  8. set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
  9. return;
  10. }

■■■■■

到这里准备工作就完成了。也不知能不能正常运行。正常的话,嗯,应该什么都不发生(笑)。下面我们执行“make run”。哦,什么也没发生。太好了!但这样有点不过瘾,还是在中断处理程序中做点什么吧!