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;
}

之后参考上面写的中断程序,一定记得在最后再进行任务切换。
最后,再把各个函数中切换部分删掉,真正意义的“多任务”就初见雏形了。

那么,明天写任务管理程序。