上一篇和上上一篇解决了绘制窗口和窗口刷新的问题。关于窗口的东西就此告一段落。本篇介绍一个相对独立且十分重要的操作系统部件——定时器的使用方法。可编程的间隔型定时器(Programmable Interval Timer)简称定时器(PIT),是集成到电脑上的一个硬件部件。之前讲过的用于实现中断机制的PIC也是个硬件部件。有了PIT,我们才能在计算机中计时。
《30天自制操作系统》笔记(10)——定时器
进度回顾
上一篇和上上一篇解决了绘制窗口和窗口刷新的问题。关于窗口的东西就此告一段落。本篇介绍一个相对独立且十分重要的操作系统部件——定时器的使用方法。
定时器是一个硬件
可编程的间隔型定时器(Programmable Interval Timer)简称定时器(PIT),是集成到电脑上的一个硬件部件。之前讲过的用于实现中断机制的PIC也是个硬件部件。有了PIT,我们才能在计算机中计时。
初始化定时器
前面,CPU、PIC都需要设置好才能用,PIT也需要设置。PIT类似C#Winform里的Timer控件,能设置的只有激发Tick事件的时间间隔(Interval)这个属性。PIT里的Tick事件,对应的是PIC里的0号中断。也就是说,PIT会根据你设定的Interval,每隔Interval时间就发送一个0号中断。这里又印证了"事件小名中断"的说法。
1 #define PIT_CTRL 0x0043
2 #define PIT_CNT0 0x0040
3 void init_pit(void)
4 {
5 io_out8(PIT_CTRL, 0x34);/*中断周期(Interval)即将变更*/
6 io_out8(PIT_CNT0, 0x9c);/*中断周期的低8位*/
7 io_out8(PIT_CNT0, 0x2e);/*中断周期的高8位*/
8 return;
9 }
10 void HariMain(void)
11 {
12 /* 略 */
13 init_gdtidt();
14 init_pic();
15 io_sti(); /* IDT/PIC的初始化已经结束,所以解除CPU的中断禁止 */
16 fifo8_init(&keyfifo, 32, keybuf);
17 fifo8_init(&mousefifo, 128, mousebuf);
18 init_pit();/* 这里! */
19 io_out8(PIC0_IMR, 0xf8); /* PIT和PIC1和键盘设置为许可(11111000) *//* 这里! */
20 io_out8(PIC1_IMR, 0xef); /* 鼠标设置为许可(11101111) */
21 /* 略 */
22 }
设置中断函数
设置Tick时,如果指定中断周期为0,会被看做指定为65536。如果设定为1000,中断频率就是1.19318Hz。如果设定为11932,中断频率就是100Hz,即每10ms发生一次中断。11932写成十六进制就是0x2e9c。
PIT会发送0号中断,那就得写一个响应此中断的函数。
1 void inthandler20(int *esp)
2 {
3 io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知PIC */
4 /* TODO: 暂时什么也不做 */
5 return;
6 }
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 ; 这里会调用void inthandler20(int *esp);函数
11 POP EAX
12 POPAD
13 POP DS
14 POP ES
15 IRETD
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
10 return;
11 }
这样就好了。
用PIT做点什么呢?
Hello PIT!
保守起见,先做个PIT的hello world比较好。
1 struct TIMERCTL {
2 unsigned int count;
3 };
4 struct TIMERCTL timerctl;
5 void init_pit(void)
6 {
7 io_out8(PIT_CTRL, 0x34);
8 io_out8(PIT_CNT0, 0x9c);
9 io_out8(PIT_CNT0, 0x2e);
10 timerctl.count = 0;/* 这里! */
11 return;
12 }
13 void inthandler20(int *esp)
14 {
15 io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知PIC */
16 timerctl.count++;/* 这里! */
17 return;
18 }
19 void HariMain(void)
20 {
21 /* 略 */
22 for (;;) {
23 sprintf(s, "%010d", timerctl.count);/* 这里! */
24 boxfill8(buf_win, 160, COL8_C6C6C6, 40, 28, 119, 43);
25 putfonts8_asc(buf_win, 160, 40, 28, COL8_000000, s);
26 sheet_refresh(sht_win, 40, 28, 120, 44);
27
28 /* 略 */
29 }
30 }
效果如下图所示。
超时功能
定时器经常被用于这样一种情形:"hi操作系统老兄!麻烦你10秒钟后通知我一下,我要执行某函数M"。这样的功能就叫做超时(timeout)。
1 struct TIMERCTL {
2 unsigned int count;
3 unsigned int timeout;
4 struct FIFO8 *fifo;
5 unsigned char data;
6 };
7 void init_pit(void)
8 {
9 io_out8(PIT_CTRL, 0x34);
10 io_out8(PIT_CNT0, 0x9c);
11 io_out8(PIT_CNT0, 0x2e);
12 timerctl.count = 0;
13 timerctl.timeout = 0;
14 return;
15 }
16 void inthandler20(int *esp)
17 {
18 io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知PIC */
19 timerctl.count++;
20 if (timerctl.timeout > 0) { /* 如果已经设定了超时 */
21 timerctl.timeout--;
22 if (timerctl.timeout == 0) {
23 fifo8_put(timerctl.fifo, timerctl.data);
24 }
25 }
26 return;
27 }
28 void settimer(unsigned int timeout, struct FIFO8 *fifo, unsigned char data)
29 {
30 int eflags;
31 eflags = io_load_eflags();
32 io_cli();
33 timerctl.timeout = timeout;
34 timerctl.fifo = fifo;
35 timerctl.data = data;
36 io_store_eflags(eflags);
37 return;
38 }
39 void HariMain(void)
40 {
41 /* 略 */
42 struct FIFO8 timerfifo;
43 char s[40], keybuf[32], mousebuf[128], timerbuf[8];
44 /* 略 */
45 fifo8_init(&timerfifo, 8, timerbuf);
46 settimer(1000, &timerfifo, 1);
47 /* 略 */
48 for (;;) {
49 /* 略 */
50 io_cli();
51 if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {
52 io_sti();
53 } else {
54 if (fifo8_status(&keyfifo) != 0) {
55 /* 略 */
56 } else if (fifo8_status(&mousefifo) != 0) {
57 /* 略 */
58 } else if (fifo8_status(&timerfifo) != 0) {
59 i = fifo8_get(&timerfifo); /* 首先读入(为了设定起始点) */
60 io_sti();
61 putfonts8_asc(buf_back, binfo->scrnx, 0, 64, COL8_FFFFFF, "10[sec]");
62 sheet_refresh(sht_back, 0, 64, 56, 80);
63 }
64 }
65 }
66 }
超时功能的hello world
程序很简单,我们在其中设定10秒钟后向timerinfo写入"1"(暂时没什么特别的含义,写"2"也没问题),而timerinfo接收到数据时,就会在屏幕上显示"10[sec]"。
图就不贴了,没什么新东西。
设定多个定时器
很多应用程序都会使用定时器,所以PIT要能够变幻出多个定时器。
1 #define MAX_TIMER 500
2 struct TIMER {
3 unsigned int timeout, flags;
4 struct FIFO8 *fifo;
5 unsigned char data;
6 };
7 struct TIMERCTL {
8 unsigned int count;
9 struct TIMER timer[MAX_TIMER];
10 };
11
12 #define TIMER_FLAGS_ALLOC 1 /* 已配置状态 */
13 #define TIMER_FLAGS_USING 2 /* 定时器运行中 */
14 void init_pit(void)
15 {
16 int i;
17 io_out8(PIT_CTRL, 0x34);
18 io_out8(PIT_CNT0, 0x9c);
19 io_out8(PIT_CNT0, 0x2e);
20 timerctl.count = 0;
21 for (i = 0; i < MAX_TIMER; i++) {
22 timerctl.timer[i].flags = 0; /* 未使用 */
23 }
24 return;
25 }
26 struct TIMER *timer_alloc(void)
27 {
28 int i;
29 for (i = 0; i < MAX_TIMER; i++) {
30 if (timerctl.timer[i].flags == 0) {
31 timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
32 return &timerctl.timer[i];
33 }
34 }
35 return 0; /* 没找到 */
36 }
37 void timer_free(struct TIMER *timer)
38 {
39 timer->flags = 0; /* 未使用 */
40 return;
41 }
42 void timer_init(struct TIMER *timer, struct FIFO8 *fifo, unsigned char data)
43 {
44 timer->fifo = fifo;
45 timer->data = data;
46 return;
47 }
48 void timer_settime(struct TIMER *timer, unsigned int timeout)
49 {
50 timer->timeout = timeout;
51 timer->flags = TIMER_FLAGS_USING;
52 return;
53 }
54 void inthandler20(int *esp)
55 {
56 int i;
57 io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知PIC */
58 timerctl.count++;
59 for (i = 0; i < MAX_TIMER; i++) {
60 if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {
61 timerctl.timer[i].timeout--;
62 if (timerctl.timer[i].timeout == 0) {
63 timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
64 fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);
65 }
66 }
67 }
68 return;
69 }
设定MAX_TIMER个定时器
1 void HariMain(void)
2 {
3 /* 略 */
4 struct FIFO8 timerfifo, timerfifo2, timerfifo3;
5 char s[40], keybuf[32], mousebuf[128], timerbuf[8], timerbuf2[8], timerbuf3[8];
6 struct TIMER *timer, *timer2, *timer3;
7 /* 略 */
8 fifo8_init(&timerfifo, 8, timerbuf);
9 timer = timer_alloc();
10 timer_init(timer, &timerfifo, 1);
11 timer_settime(timer, 1000);
12 fifo8_init(&timerfifo2, 8, timerbuf2);
13 timer2 = timer_alloc();
14 timer_init(timer2, &timerfifo2, 1);
15 timer_settime(timer2, 300);
16 fifo8_init(&timerfifo3, 8, timerbuf3);
17 timer3 = timer_alloc();
18 timer_init(timer3, &timerfifo3, 1);
19 timer_settime(timer3, 50);
20 /* 略 */
21 for (;;) {
22 /* 略 */
23 io_cli();
24 if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo)
25 + fifo8_status(&timerfifo2) + fifo8_status(&timerfifo3) == 0) {
26 io_sti();
27 } else {
28 if (fifo8_status(&keyfifo) != 0) {
29 /* 略 */
30 } else if (fifo8_status(&mousefifo) != 0) {
31 /* 略 */
32 } else if (fifo8_status(&timerfifo) != 0) {
33 i = fifo8_get(&timerfifo); /* 首先读入(为了设定起始点) */
34 io_sti();
35 putfonts8_asc(buf_back, binfo->scrnx, 0, 64, COL8_FFFFFF, "10[sec]");
36 sheet_refresh(sht_back, 0, 64, 56, 80);
37 } else if (fifo8_status(&timerfifo2) != 0) {
38 i = fifo8_get(&timerfifo2); /* 首先读入(为了设定起始点) */
39 io_sti();
40 putfonts8_asc(buf_back, binfo->scrnx, 0, 80, COL8_FFFFFF, "3[sec]");
41 sheet_refresh(sht_back, 0, 80, 48, 96);
42 } else if (fifo8_status(&timerfifo3) != 0) {/* 模拟光标闪烁 */
43 i = fifo8_get(&timerfifo3);
44 io_sti();
45 if (i != 0) {
46 timer_init(timer3, &timerfifo3, 0); /* 然后设置0 */
47 boxfill8(buf_back, binfo->scrnx, COL8_FFFFFF, 8, 96, 15, 111);
48 } else {
49 timer_init(timer3, &timerfifo3, 1); /* 然后设置1 */
50 boxfill8(buf_back, binfo->scrnx, COL8_008484, 8, 96, 15, 111);
51 }
52 timer_settime(timer3, 50);
53 sheet_refresh(sht_back, 8, 96, 16, 112);
54 }
55 }
56 }
57 }
使用多个定时器
定时器优化
前面都算是使用定时器的实验,以此为基础进行优化,使其更实用。
原作者的优化进行了好几步,在此仅罗列一下,并给出最后的程序。
- 将timeout的含义从"所剩时间"改变为"予定时间",这样就可以去掉inthandler20(int*)函数里的"timerctl.timer[i].timeout--"。
- 现在的定时器,每隔42949673秒(497天)后count就是0xFFFFFFFF了,在这之前必须重启计算机,否则程序就会出错。因此让OS每隔一年自动调整一次。
- timer数组按timeout升序排序,在inthandler20(int*)中每次只检查第一个timer元素即可。
- 上一步中,发现超时时,inthandler20(int*)会准备下一个要检查的timer,这延长了处理时间。为解决这个问题,增加变量using,用于记录有几个定时器处于活动中(需要检查)。(类似于窗口图层部分的sheet中的top)不过这样只能缓解问题,不能彻底解决问题。
- 将静态数组timers改为链表,从而省掉了上一步中可能发生的移位操作。
- 使用数据结构中的"哨兵"概念简化上一步的链表处理函数。"哨兵"是为了简化循环的边界条件而引入的。在timers链表最后加上一个timeout为0xFFFFFFFF的定时器(作为哨兵)。由于OS会在1年后将定时器count重置,所以这个哨兵定时器永远不会到达触发的时候。这其实就是永恒吊车尾啊。不管你信不信,添上这样一个吊车尾就可以减少链表相关的代码。
经过若干次优化后的代码如下。
1 void init_pit(void)
2 {
3 int i;
4 struct TIMER *t;
5 io_out8(PIT_CTRL, 0x34);
6 io_out8(PIT_CNT0, 0x9c);
7 io_out8(PIT_CNT0, 0x2e);
8 timerctl.count = 0;
9 for (i = 0; i < MAX_TIMER; i++) {
10 timerctl.timers0[i].flags = 0; /* 没有使用 */
11 }
12 t = timer_alloc(); /* 取得一个 */
13 t->timeout = 0xffffffff;
14 t->flags = TIMER_FLAGS_USING;
15 t->next = 0; /* 末尾 */
16 timerctl.t0 = t; /* 因为现在只有哨兵,所以他就在最前面 */
17 timerctl.next = 0xffffffff; /* 因为只有哨兵,所以下一个超时时刻就是哨兵的时刻 */
18 return;
19 }
1 void timer_settime(struct TIMER *timer, unsigned int timeout)
2 {
3 int e;
4 struct TIMER *t, *s;
5 timer->timeout = timeout + timerctl.count;
6 timer->flags = TIMER_FLAGS_USING;
7 e = io_load_eflags();
8 io_cli();
9 t = timerctl.t0;
10 if (timer->timeout <= t->timeout) {
11 /* 插入最前面的情况 */
12 timerctl.t0 = timer;
13 timer->next = t; /* 下面是设定t */
14 timerctl.next = timer->timeout;
15 io_store_eflags(e);
16 return;
17 }
18 /* 搜寻插入位置 */
19 for (;;) {
20 s = t;
21 t = t->next;
22 if (timer->timeout <= t->timeout) {
23 /* 插入s和t之间的情况 */
24 s->next = timer; /* s下一个是timer */
25 timer->next = t; /* timer的下一个是t */
26 io_store_eflags(e);
27 return;
28 }
29 }
30 }
1 void inthandler20(int *esp)
2 {
3 struct TIMER *timer;
4 io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00接收信号结束的信息通知给PIC */
5 timerctl.count++;
6 if (timerctl.next > timerctl.count) {
7 return;
8 }
9 timer = timerctl.t0; /* 首先把最前面的地址赋给timer */
10 for (;;) {
11 /* 因为timers的定时器都处于运行状态,所以不确认flags */
12 if (timer->timeout > timerctl.count) {
13 break;
14 }
15 /* 超时 */
16 timer->flags = TIMER_FLAGS_ALLOC;
17 fifo32_put(timer->fifo, timer->data);
18 timer = timer->next; /* 将下一个定时器的地址代入timer */
19 }
20 /* 新移位 */
21 timerctl.t0 = timer;
22 /* timerctl.next的设定 *//* 这里 */
23 timerctl.next = timer->timeout;
24 return;
25 }
曾经引入的using变量现在又被去掉了。
总结
定时器是如此重要,以至于我一时想不出它有多重要。定时器使用起来并不复杂,只不过为了尽可能优化提高效率,原作者讲了很多链表之类的数据结构和算法的东西。
到现在,终于看完了《30天自制操作系统》的三分之一。收获么,可以说是坚定了我之前对软件工程的理念,也可以说是加强了自我封闭和顽固的理由。数字电路构成了硬件,但从软件工程师的角度看,硬件也是一种软件,它为上层软件(操作系统)提供了API。操作系统则为应用程序提供了API。如果应用程序做成插件式的,那这个应用程序也可以被称为一个"操作系统",或者叫做"平台"(例如chrome OS、Visual Studio、Eclipse)。这就像计算机网络体系结构一样,分为多个层,每个下层都为上层提供API,上层不必知道下层的实现原理,直接使用就行了。
很快就要进入"多任务"的设计实现了!