《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 }
程序很簡單,我們在其中設定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 }
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,上層不必知道下層的實現原理,直接使用就行了。
很快就要進入"多任務"的設計實現了!
