DAY15:多任务(1)
挑战任务切换
CPU处理多任务的原理:
当向CPU发出任务切换指令时,CPU会先把寄存器的值全部写入内存中,为了运行下一个程序,CPU会把所有寄存器中的值从内存中读出来,这样就完成了一次切换。而任务切换所需要的时间,就是对内存进行写入和读取操作所消耗的时间。
寄存器中的内容写入内存:
补充“任务状态段”(task status segment) ,简称“TSS”
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;
}
EIP(extended instruction pointer):是CPU用来记录下一条需要执行的指令位于内存中哪个地址的寄存器。
JMP 0x1234;即“MOV EIP,0x1234”(不推荐汇编语言中编写)
JMP指令
在进行任务切换时,还得勤用JMP指令,而JMP指令分为俩种:
①只改写EIP的为near模式
②同时改写EIP和CS的为far模式
far模式样例:
JMP DWORD 2*8:0x0000001b
这条指令在向EIP存入0x1b的同时,将CS置为2*8(=16)。像这样在JMP目标地址中带冒号(:)的,就是far模式的JMP指令。
JMP指令所指定的目标地址段不是可执行的代码,而是TSS的话,CPU就不会执行通常的改写EIP和CS的操作,而是将这条指令理解为任务切换。也就是说,CPU会切换到目标TSS所指定的任务,说白了,就是JMP到一个任务那里去了。
事例:从任务A切换到任务B
首先创建俩个TSS:
struct TSS32 tss_a.tss_b;
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
然后让其在GDT中进行定义:
struct SEGMENT_DESCRIPTOR *gdt=(struct SEGMENT_DESCRIPTOR *)ADR_GDT;
set_segmedes(gdt+3,103,(int)&tss_a,AR_TSS32);
set_segmedes(gdt+4,103,(int)&tss_a,AR_TSS32);
tss_a定义在gdt的3号,段长限制为103字节(tss_b类似)
TR寄存器(task register)
其作用是让CPU记住当前正在运行哪一个任务。
每次给TR寄存器赋值的时候,必须把GDT的编号乘以8!
而且给TR寄存器赋值需要使用LTR指令!
向TR寄存器存入 3*8这个值,因为刚才我们把当前运行的任务定义为GDT的3号。
load_tr(3 * 8);
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
执行far跳转指令:
_taskswitch4: ;void taskswitch4(void);
JMP 4*8:0
RET
far-JMP指令是用作任务切换的话,地址段(冒号前面的4*8的部分)要指向TSS这一点比较重要,而偏移量(冒号后面的0的部分)并没有什么实际作用,会被忽略掉,一般来说像这样写0就可以了。
初始化tss_b:
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;
给cs置为GDT的2号,其他寄存器都置为GDT的1号.
在eip中,需要定义切换任务时,要从哪里开始运行,在这里先把task_b_main这个函数的内存地址赋予给它。
void task_b_main(void)
{
for(;;){ io_hlt();}
}
任务切换进阶
上文只是将任务B切换到A,现在尝试再切换回任务A:
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函数:
_taskswitch3: ;void taskswitch3(void);
JMP 3*8:0
RET
做个简单的多任务
为了能够同时执行多个任务,新创建如下函数:
_farjmp: ;void farjmp(int eip,int cs);
JMP FAR[ESP+4] ;eip,cs
RET
“JMP FAR”指令的功能是执行far跳转。。在JMP FAR指令中,可以指定一个内存地址,CPU会从指定的内存地址中读取4个字节的数据,并将其存入EIP寄存器,再继续读取2个字节的数据.并将其存入CS寄存器。
farjmp(eip,cs);,在[ESP+4]这个位置就存放了eip的值,而[ESP+8]则存放了cs的值,这样就可以实现far跳转了。
taskswitch3(); → farjmp(0, 3* 8);
taskswitch4(); → farjmp(0, 4 * 8);
接下来缩短切换的间隔,在任务A和任务B中,分别准备一个timer_ts变量,以便每隔0.02s执行一次任务切换。
运行速度改进
提高运行速度
降低页面刷新频率,人眼要求不是很高
多任务进阶
现在准备实现真正的多任务,即在程序本身不知道的情况下进行任务切换。
于是创建如下函数:
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;
}
mt_init函数的功能是初始化mt_timer和mt_tr的值,并将计时器设置为0.02秒之后。
变量mt_tr实际上代表了TR寄存器,而不需要使用timer_init是因为在发生超时的时候不需要向FIFO缓冲区写入数据。