Day15
一些感想
什么是多任务
这里的多任务,应该是多进程的意思吧。至于CPU如何分时间片给各个进程,应该很清楚了。但是值得注意的一点是,CPU要每隔多长时间切换一次进程:太长会让人感到延迟,太短会大大降低运行效率。
如何切换多任务
首先,我们要定义任务状态段TSS(task status segment):
struct TSS32
{
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
int es, cs, ss, ds, fs, gs;
int ldtr, iomap;
};
第1行的各个成员,保存的是任务设置的相关信息,执行任务切换的时候一般不会被写入。
第2行是32位的寄存器。其中,EIP是extended instruction pointer,即扩展指令指针寄存器,用于记录下一条需要执行的指令位于内存中的哪个地址,当CPU从别的任务切换回来之后,需要用到这个寄存器。
啊。。另外说一句,看到IP就不由得想到了PC,于是百度了一下发现 PC = CS * 16 + IP
第3行为16位的寄存器。
第4行也是任务设置有关的。不过ldtr要设置为0,iomap要设置为0x40000000。像这样:
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
虽说EIP是JMP时候用的,但是如果CS(code segment)不变的话,就是JMP的near模式,用于一个任务内部的跳转。所以要用到JMP的far模式,同时把EIP和CS改写一下。例如:
JMP DWORD 2 * 8:0x0000001b
意思就是CS设置为2 * 8, EIP设置为0x1b。
当使用JMP的far模式之后,如果该地址是TSS(已经在GDT中设置过了),则会理解为是任务切换,否则是JMP的far模式。
再说回设置GDT,像这样:
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);
这样就设置好了对应的TSS。
当要进行任务切换的时候,我们需要往一个特定的寄存器TR(task register)里赋值,赋的值为对应GDT编号*8
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
在TR更改之后,还需要进行farJMP
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
这个函数的RET是因为从切换到别的程序之后,还会切回来。
这样,就是初步的“多任务”了。
改进为自动切换的多任务
对于手动切换多任务,对于程序员来说很不方便。因为既要自己设定时间片,还不能忘了写定时器。所以一般来说多任务都是操作系统自己管的。
今天是初步自动切换的多任务,只会在两个状态之间来回切换,估计明天会写进程管理程序之类的吧,这里猜一下。
至于如何进行自动切换的多任务,要设定一个定时器,对于来了一个超时中断的信号,我们判断一下是不是这个特定的定时器。如果是的话,在处理完各种其它的中断信号之后,切换任务。
void inthandler20(int *esp)
{
struct TIMER *timer;
char ts = 0;
io_out8(PIC0_OCW2, 0x60); // 向PIC通知IRQ00处理完毕
timerctl.count++;
if(timerctl.next_time > timerctl.count)
{
return;
}
timer = timerctl.t0; // 总是先把第一个代入timer
for(;;)
{
// timer的计时器全部在工作中,因此不用确认flags
if(timer->timeout > timerctl.count)
{
break;
}
// 超时
timer->flags = TIMER_FLAGS_ALLOC;
if(timer != mt_timer)
{
fifo32_put(timer->fifo, timer->data);
}
else
{
ts = 1; // mt_timer超时
}
timer = timer->next_timer; // 将下一个计时器的地址赋给timer
}
timerctl.t0 = timer;
timerctl.next_time = timer->timeout;
if(ts != 0)
{
mt_taskswitch();
}
return;
}
作者提到,为什么不一检测到mt_timer就switch呢?是因为任务切换的时候,中断处理可能还没完成(比如接下来的一些定时器超时),所以立即切换可能会出现一些错误。
挑战任务切换
harib12a
在之前如何切换任务的基础上,我们还要设定一下任务b的TSS的各种状态:
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
tss_b.eip = (int)&task_b_main;
tss_b.eflags = 0x00000202; // IF = 1
tss_b.eax = 0;
tss_b.ecx = 0;
tss_b.edx = 0;
tss_b.ebx = 0;
tss_b.esp = task_b_esp; // 函数的内存地址
tss_b.ebp = 0;
tss_b.esi = 0;
tss_b.edi = 0;
tss_b.es = 1 * 8;
tss_b.cs = 2 * 8;
tss_b.ss = 1 * 8;
tss_b.ds = 1 * 8;
tss_b.fs = 1 * 8;
tss_b.gs = 1 * 8;
然后taskswitch到b之后,任务b目前的函数:
void task_b_main(void)
{
for(;;)
{
io_hlt();
}
}
程序的作用是10s之后会锁死:什么都动不了
任务切换进阶
harib12b
在taskb的main里加点东西
void task_b_main(void)
{
struct FIFO32 fifo;
struct TIMER *timer;
int i, fifobuf[128];
fifo32_init(&fifo, 128, fifobuf);
timer = timer_alloc();
timer_init(timer, &fifo, 1);
timer_settime(timer, 500);
for(;;)
{
io_cli();
if(fifo32_status(&fifo) == 0)
{
io_stihlt();
}
else
{
i = fifo32_get(&fifo);
io_sti();
if(i == 1) // 超时时间为5秒
{
taskswitch3(); //返回任务A
}
}
}
}
taskswitch3是新增的函数,和之前的类似
过5秒锁死之后,又可以动了。。
做个简单的多任务(1)
harib12c
我们在函数里指定要跳转到位置,像这样
_farjmp: ; void farjmp(int eip, int cs);
JMP FAR [ESP+4] ; eip, cs
RET
这样我们可以直接指定cs,就进行对应的进程切换了。
然后,设定一个定时切换的定时器:
timer_ts = timer_alloc();
timer_init(timer_ts, &fifo, 2);
timer_settime(timer_ts, 2);
...
if(i == 2)
{
farjmp(0, 4 * 8);
timer_settime(timer_ts, 2);
}
task_b_main里也有类似的切换。这里就不写了。
这其实就已经有多任务的雏形了。。
做个简单的多任务(2)
harib12d
在task_b_main里继续计数。。那么就不写了。。
不过值得说的一点是,作者用了一个类似全局变量的宏定义,把地址传给了task_b_main。
到这步,就可以看到又在计时,又可以输入了。
提高运行速度
harib12e
还以为提高是真正的进程切换的提高呢)其实是让画面的刷新频率慢一些——因为人眼分辨不出来。这里也不放代码了。
作者为了传参,通过指针往栈地址里放了函数的第一个参数。像这样
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
*((int *) (task_b_esp + 4)) = (int) sht_back;
考虑一下这里的 - 8,其实是地址最后00和fc的区别。
不让用return
作者提到,类似task_b_main, HariMain,都不能return。这是因为这些函数并不是某段程序调用的,因此return的时候使用[ESP]作为返回地址的话,就会出问题(因为本来调用的时候没有用到ESP)
测试运行速度
harib12f
这一节测试了一下上一节改进画面之后的效率,进行了比如,去掉定时器、更改显示频率等等,可以比较接近原性能的50%
多任务进阶
harib12g
看到了自动管理的影子。
设定一个特殊的计时器,定时触发中断:
struct TIMER *mt_timer;
int mt_tr;
void mt_init(void)
{
mt_timer = timer_alloc();
// 这里没有必要用timer_init,因为不需要向缓冲区写入数据
timer_settime(mt_timer, 2);
mt_tr = 3 * 8;
return;
}
void mt_taskswitch(void)
{
if(mt_tr == 3 * 8)
{
mt_tr = 4 * 8;
}
else
{
mt_tr = 3 * 8;
}
timer_settime(mt_timer, 2);
farjmp(0, mt_tr);
return;
}
之后参考上面写的中断程序,一定记得在最后再进行任务切换。
最后,再把各个函数中切换部分删掉,真正意义的“多任务”就初见雏形了。
那么,明天写任务管理程序。