从本篇内容开始讲解定时器。本篇内容比较简单,首先介绍定时器的概念与设置方法,然后介绍超时的中断处理,并对中断处理函数进行了优化。
1. 定时器
定时器是操作系统中十分重要的功能。它的原理很简单,只是每隔一段时间发送一个中断给CPU。如果想要知道过了多长时间,只需要在中断处理程序中记录中断的次数就可以计算得到。
管理定时器也很简单,只需要对PIT(Programmable Interval Timer的缩写)进行设置即可。通过对PIT的设置,可以设置定时器每隔多长时间发送一次中断。PIT连接着IRQ的0号中断。设置PIT需要的命令如下:
- AL = 0x34;OUT(0x43, AL);
- AL = 中断周期的低8位;OUT(0x40, AL)
- AL = 中断周期的高8位;OUT(0x40, AL)
实际设置的中断频率 = 单位时间时钟周期数(主频)/设定的数值
根据当前的主频,如果设定11932,则中断频率位100Hz,即10ms发生一次中断。把11932换算为16进制,则是0x2e9c。
设置PIT相关程序如下:
#define PIT_CTRL 0x0043
#define PIT_CNT0 0x0040
void init_pit(void)
{
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
return;
}
在主程序中调用init_pit函数:
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
char s[40], keybuf[32], mousebuf[128];
int mx, my, i;
unsigned int memtotal, count = 0;
struct MOUSE_DEC mdec;
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
struct SHTCTL *shtctl;
struct SHEET *sht_back, *sht_mouse, *sht_win;
unsigned char *buf_back, buf_mouse[256], *buf_win;
init_gdtidt();
init_pic();
……
涉及到了中断,则需要将中断处理函数注册到IDT中,这些也是前面讲过的内容了。
/* 注册IDT*/
set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
_asm_inthandler20:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler20
POP EAX
POPAD
POP DS
POP ES
IRETD
void inthandler20(int *esp)
{
io_out8(PIC0_OCW2, 0x60); /* 通知PIC, IRQ-00中断受理完毕 */
/* 暂时无内容 */
return;
}
这样定时器的中断处理就完成了。接下来就在中断处理中进行计时:
struct TIMERCTL {
unsigned int count;
};
struct TIMERCTL timerctl;
void inthandler20(int *esp)
{
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
return;
}
新建了timerctl结构体,每次进入中断处理函数时就对其中的count变量增加1。在主程序中将count实时显示出来,根据之前的设置,每秒钟显示的数值会增加100。
for (;;) {
sprintf(s, "%010d", timerctl.count);
boxfill8(buf_win, 160, COL8_C6C6C6, 40, 28, 119, 43);
putfonts8_asc(buf_win, 160, 40, 28, COL8_000000, s);
sheet_refresh(sht_win, 40, 28, 120, 44);
……
这样就可以测量一段确定的时间间隔了。
2. 超时与中断处理
2.1 超时处理
可以通过定时器记录一段时间间隔,那么操作系统就可以实现这样一种功能:
经过一段时间间隔后,进行某种特定的操作。这样的功能就被称为超时(timeout)。
为了实现这一功能,首先完善结构体TIMERCTL:
struct TIMERCTL {
unsigned int count;
unsigned int timeout;
struct FIFO8 *fifo;
unsigned char data;
};
其中timeout用来记录距离超时还有多长时间,这个时间达到0,则程序向缓冲区fifo中发送数据,这样来通知操作系统。
相关的函数修改如下:
void init_pit(void)
{
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
timerctl.count = 0;
timerctl.timeout = 0;
return;
}
void inthandler20(int *esp)
{
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.timeout > 0) { /* 设置好了超时时间 */
timerctl.timeout--;
if (timerctl.timeout == 0) {
fifo8_put(timerctl.fifo, timerctl.data);
}
}
return;
}
void settimer(unsigned int timeout, struct FIFO8 *fifo, unsigned char data)
{
int eflags;
eflags = io_load_eflags();
io_cli();
timerctl.timeout = timeout;
timerctl.fifo = fifo;
timerctl.data = data;
io_store_eflags(eflags);
return;
}
在init_pit函数中首先将count和timeout变量初始化为0,而settimer函数用于给结构体其他成员赋值,尤其是设置好timeout。超时后的实际操作放在了inthandler20函数中实现。为了防止中断混乱,在settimer函数中还是先禁用了中断。
结合以上函数,在主程序中进行如下的实现:
void HariMain(void)
{
……
struct FIFO8 timerfifo;
char s[40], keybuf[32], mousebuf[128], timerbuf[8];
……
fifo8_init(&keyfifo, 32, keybuf);
settimer(1000, &timerfifo, 1);
……
for (;;) {
……
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {
io_sti();
} else {
if (fifo8_status(&keyfifo) != 0) {
……
} else if (fifo8_status(&mousefifo) != 0) {
……
} else if (fifo8_status(&timerfifo) != 0) {
i = fifo8_get(&timerfifo); /* 首先读入,设定起始点 */
io_sti();
putfonts8_asc(buf_back, binfo->scrnx, 0, 64, COL8_FFFFFF, "10[sec]");
sheet_refresh(sht_back, 0, 64, 56, 80);
}
}
}
}
可以看出在10s后向timerfifo中写入了数据1,在主程序中,接收到数据时就在屏幕上显示10[sec]。
能够实现过10s显示一次,自然也可以调整时间的长度。并且操作系统中可能用到多个定时器,这样就可以将程序进行扩展:
#define MAX_TIMER 500
struct TIMER {
unsigned int timeout, flags;
struct FIFO8 *fifo;
unsigned char data;
};
struct TIMERCTL {
unsigned int count;
struct TIMER timer[MAX_TIMER];
};
扩充了定时器的数量,其他程序也需要进行相应的修改:
#define TIMER_FLAGS_ALLOC 1 /* 已配置状态 */
#define TIMER_FLAGS_USING 2 /* 定时器运行中 */
void init_pit(void)
{
int i;
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
timerctl.count = 0;
for (i = 0; i < MAX_TIMER; i++) {
timerctl.timer[i].flags = 0; /* ���g�p */
}
return;
}
struct TIMER *timer_alloc(void)
{
int i;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == 0) {
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
return &timerctl.timer[i];
}
}
return 0; /* 无可用定时器 */
}
void timer_free(struct TIMER *timer)
{
timer->flags = 0; /* 释放 */
return;
}
void timer_init(struct TIMER *timer, struct FIFO8 *fifo, unsigned char data)
{
timer->fifo = fifo;
timer->data = data;
return;
}
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
timer->timeout = timeout;
timer->flags = TIMER_FLAGS_USING;
return;
}
void inthandler20(int *esp)
{
int i;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {
timerctl.timer[i].timeout--;
if (timerctl.timer[i].timeout == 0) {
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);
}
}
}
return;
}
类似的程序在内存分配与图层管理中都能看到,关于初始化、分配、释放等函数就不必细说了。在中断处理函数inthandler20中,每次进入中断函数,都会把所有运行中的定时器检查一遍,看是否超时。如果超时,则向fifo中写入数据。这样就可以设置多个定时器,设置不同的超时时间了。
2.2 加快中断处理
但这里有个问题。前面中断的内容中已经讲过,中断处理函数必须在短时间内完成,否则会耽误CPU的正常工作。而我们为了实现超时,每次进入inthandler20都要对所有活动中的定时器执行timeout–操作,即CPU要先读出变量的值,执行减法运算,再写回内存中,这样浪费了很多时间。是否可用对这种实现方式进行优化呢?
我们不用timeout来表示剩余多少时间超时,而是用来表示超时的时刻。因为当前的时刻可用通过count进行计数,每次中断时只需要比较count的值是否达到timeout,就可以判断是否超时了。这样就减少了每次中断CPU所需要做的减法运算,加快了中断的处理。
void inthandler20(int *esp)
{
int i;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {
if (timerctl.timer[i].timeout <= timerctl.count) {
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);
}
}
}
return;
}
相应地也需要修改timer_settime函数,将超时时刻设置为当前时刻加上超时时间:
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
timer->timeout = timeout + timerctl.count;
timer->flags = TIMER_FLAGS_USING;
return;
}
这样依赖程序确实得到优化了。但还没完,用来计时的变量count总有溢出的时候,0xffffffff表示的时间转换成天大约是497天。为了保证超时功能的正常,没过大约一年的时间需要把count重新调整归零:
int t0 = timerctl.count;/* 所有定时器的时刻都要减去这个值*/
io_cli();/* 调整时刻时需要禁止中断 */
timerctl.count -= t0;
for(i = 0;i < MAX_TIMERS; i++)
{
if(timerctl.timer[i].flags == TIMER_FLAGS_USING)
{
timerctl.timer[i].timeout -= t0;
}
}
io_sti();
已经做了以上的改进和优化,但其实中断处理程序还有改进的空间。
inthandler20其实是定时器的中断,每隔10ms就会产生一次中断并进入中断处理函数,这样每秒钟要进入100次中断函数。而每次中断函数都要执行500次的if判断检查运行中的定时器,一秒钟就要运行50000次。但真正产生超时,更改flags的值,执行fifo8_put函数,一秒钟最多也就2次,其他的大量if判断其实都是在做无用功。
为了避免这么多次无用且浪费时间的if判断,采用这种思路。超时时间有长短的不同,每次进入中断的时候,我们只需要关注当前最近的一个即将超时的定时器就可以了。我们把当前最近的一个超时时刻记录下来,每次进入中断的时候先与当前的时刻进行比较。如果最近的超时时刻没有达到,说明没有超时,这样就直接返回;如果发现达到了最近的超时时刻,再检查一遍定时器,找出这个超时的定时器,并且更新下一个最近的超时时刻。
中断处理的过程:
struct TIMERCTL {
unsigned int count, next;
struct TIMER timers0[MAX_TIMER];
};
void inthandler20(int *esp)
{
int i;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.next > timerctl.count) {
return; /* 最近的一个超时时刻还没有达到,直接返回 */
}
timerctl.next = 0xffffffff;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {
if (timerctl.timer[i].timeout <= timerctl.count) {
/* 找到了超时的定时器,设置flags */
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);
} else {
/* 其他未超时的定时器中更新next */
if (timerctl.next > timerctl.timer[i].timeout) {
timerctl.next = timerctl.timer[i].timeout;
}
}
}
}
return;
}
初始化与设置超时时间:
void init_pit(void)
{
int i;
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
timerctl.count = 0;
timerctl.next = 0xffffffff; /* 最初没有生效的定时器 */
for (i = 0; i < MAX_TIMER; i++) {
timerctl.timer[i].flags = 0; /* 没有使用中的定时器 */
}
return;
}
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
timer->timeout = timeout + timerctl.count;
timer->flags = TIMER_FLAGS_USING;
if (timerctl.next > timer->timeout) {
/* 更新下一次的超时时刻 */
timerctl.next = timer->timeout;
}
return;
}
其实到这里已经优化的不错了。但是吹毛求疵一点,未达到超时时刻,处理的速度很快;达到超时时刻了,处理的时间就明显增加,作者认为这样可能会导致偶尔很卡的问题。(这一点说实话自己完全想不到)
为了减少达到超时时刻后进行处理所用的时间,采用的措施是把定时器按照超时时间先后排好顺序,再记录下运行中的定时器个数,这样在查找的时候就不需要把所有的定时器都检查一遍了。
struct TIMERCTL {
unsigned int count, next, using;
struct TIMER *timers[MAX_TIMER];
struct TIMER timers0[MAX_TIMER];
};
结构体中timers用于存储当前运行中的定时器,并且按照时间顺序排列好,using变量则用来存放当前运行中的定时器数量。
void inthandler20(int *esp)
{
int i, j;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.next > timerctl.count) {
return;
}
for (i = 0; i < timerctl.using; i++) {
if (timerctl.timers[i]->timeout > timerctl.count) {
break;
}
/* 达到超时时间,将flags清除 */
timerctl.timers[i]->flags = TIMER_FLAGS_ALLOC;
fifo8_put(timerctl.timers[i]->fifo, timerctl.timers[i]->data);
}
/* 有i个定时器超时,则timers中存放的其余定时器进行移位*/
timerctl.using -= i;
for (j = 0; j < timerctl.using; j++) {
timerctl.timers[j] = timerctl.timers[i + j];
}
if (timerctl.using > 0) {
timerctl.next = timerctl.timers[0]->timeout;
} else {
timerctl.next = 0xffffffff;
}
return;
}
另外还需要修改的是timer_settime函数,在设置的时候需要将timer注册到timers数组中的正确位置。同样在注册的时候也要先禁用中断。
void timer_settime(struct TIMER *timer, unsigned int timeout)
{
int e, i, j;
timer->timeout = timeout + timerctl.count;
timer->flags = TIMER_FLAGS_USING;
e = io_load_eflags();
io_cli();
/* 搜索注册的位置 */
for (i = 0; i < timerctl.using; i++) {
if (timerctl.timers[i]->timeout >= timer->timeout) {
break;
}
}
/* 从i号之后全部后移一位,腾出一个空位 */
for (j = timerctl.using; j > i; j--) {
timerctl.timers[j] = timerctl.timers[j - 1];
}
timerctl.using++;
/* 插入到空位上 */
timerctl.timers[i] = timer;
timerctl.next = timerctl.timers[0]->timeout;
io_store_eflags(e);
return;
}
到这里本篇的内容就完成了。本篇的内容还是比较简单的,也可以看出作者对与程序的运行有种执念,不只是要程序能够正常运行,还要进行种种优化。下一篇的内容仍然是定时器,可以看出作者对定时器内容的重视。敬请期待。