15.1、挑战任务切换
15.1.1、TSS
任务切换主要就是轮转占有CPU。
当任务A在执行时,切换到B,需要保存当前任务A的环境(寄存器值),再恢复B的寄存器的值,就是一次任务切换了。
那么,怎么保存环境呢?内存中有个TSS(task status segment 任务状态段),这个段用来保存环境,他也是一个段,所以也需要在GDT中注册后才能使用。TSS有16位和32位,后面讲使用32位:
// TSS内存段
struct TSS32 {
// 任务设置相关信息,任务切换的时候不会被写入(backlink有时候会被写入)
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
// 32位寄存器值
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
// 16位段寄存器
int es, cs, ss, ds, fs, gs;
// 任务设置相关信息,任务切换的时候不会被写入;ldtr、iomap先设置为0、0x40000000
int ldtr, iomap;
};
15.1.2、任务切换方法
任务切换需要用到JMP指令。
JMP指令有两种(其实有三种包含short短跳转):
near模式:只修改EIP (jmp ax)
far模式:修改CS和EIP (jmp 2*8:0)
如果JMP指令所指定的目标地址段不是可执行代码,而是TSS的话,CPU就不会执行修改CS和EIP的操作,而是理解为任务切换,切换到目标TSS所指定的任务。CPU每次执行带有段地址的指令时,都会到GDT中确认,是执行普通的far-JMP,还是任务切换。
总结:far-JMP跳转到TSS才会任务切换。TSS中存着每个任务的环境呢。
15.1.3、开始切换
- 准备
// 创建两个TSS,初始化ldtr,iomap
struct TSS32 tss_a, tss_b;
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
// 在GDT注册
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);
- 切换
load_tr(3 * 8); // 让TR(task register 任务寄存器)指向3号段(TSS)
/*
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
*/
TR就是指向正在运行的任务
现在设置TR为 3 号段,等到切换到4后,当前运行的bootpack就会保存到 3 号TSS中。
// 10秒后切换到任务4
} else if (i == 10) { /* 10秒计时器 */
putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
taskswitch4();
} else if (i == 3) {
/*
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
*/
现在就要切换到任务4了,任务4的环境得准备一下:
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; // 刚开辟的栈,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;
/*
void task_b_main(void)
{
for (;;) { io_hlt(); }
}
*/
关于EFLAGS,STI后就是0x00000202:
CS设置的还是二号段,跟bootpack一个段。
10秒后,切换到任务4,但任务4啥也没有,就让CPU睡眠了。鼠标不能动了
15.2、切换回任务3
刚刚切换过去就让CPU睡眠了,那再多做点事吧。例如5秒后切换回任务3。
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_sti();
io_hlt();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 1) {
taskswitch3();
}
}
}
}
/*
_taskswitch3: ; void taskswitch3(void);
JMP 3*8:0
RET
*/
5秒后鼠标又能动了。
15.3、任务快速切换
如果任务切花的够快,那么就发现不了鼠标卡死等现象了。
首先把 taskswitch 函数换一下,因为不能一个任务要写一个吧,搞个通用的:
_farjmp: ; void farjmp(int eip, int cs);
JMP FAR [ESP+4] ; eip, cs
RET
JMP FAR 会读取四个字节(第一个参数)放入EIP,读取两个字节(第二个参数)放入CS,达到跳段的目的。
taskswitch3() ---> farjmp(0, 3*8)
taskswitch4() ---> farjmp(0, 4*8)
在HariMain和task_b_main都设置一个0.02秒的定时器,到时间自动切换。
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
timer_ts = timer_alloc();
timer_init(timer_ts, &fifo, 2);
timer_settime(timer_ts, 2);
for (;;) {
io_cli();
if (fifo32_status(&fifo) == 0) {
io_stihlt();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 2) { // 0.02秒切换
farjmp(0, 4 * 8);
timer_settime(timer_ts, 2);
} else if (256 <= i && i <= 511) { /* 键盘数据 */
(略)
} else if (512 <= i && i <= 767) { /* 鼠标数据 */
(略)
} else if (i == 10) { /* 10秒定时器 */
putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
} else if (i == 3) { /* 3秒定时器 */
putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
} else if (i <= 1) { /* 光标定时器 */
(略)
}
}
}
}
void task_b_main(void)
{
struct FIFO32 fifo;
struct TIMER *timer_ts;
int i, fifobuf[128];
fifo32_init(&fifo, 128, fifobuf);
timer_ts = timer_alloc();
timer_init(timer_ts, &fifo, 1);
timer_settime(timer_ts, 2);
for (;;) {
io_cli();
if (fifo32_status(&fifo) == 0) {
io_sti();
io_hlt();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 1) { /* 任务切换 */
farjmp(0, 3 * 8);
timer_settime(timer_ts, 2);
}
}
}
}
15.3.1、让task_b_main在背景图层上显示内容
这就存在一个问题,task_b_main函数不知道sht_back的地址,怎么办呢?
先把sht_back的地址存放在一个地址里,task_b_main使用的时候再去取。
bootpack.c运行在二号段,asmhead.nas在内存的c200处,asmhead指示了bootinfo地址为0x0ff0处,那么就把sht_back放到bootinfo前面0x0fec处。
// 在HariMain函数中添加
*((int *) 0x0fec) = (int) sht_back;
// 在task_b_main函数中添加
struct SHEET *sht_back;
sht_back = (struct SHEET*) *((int*)0x0fec);
task_b_main在背景上显示计数:
void task_b_main(void)
{
(略)
sht_back = (struct SHEET *) *((int *) 0x0fec);
for (;;) {
count++;
sprintf(s, "%10d", count);
// 显示计数
putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 10);
io_cli();
if (fifo32_status(&fifo) == 0) {
io_sti();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 1) { /* 任务切换 */
farjmp(0, 3 * 8);
timer_settime(timer_ts, 2);
}
}
}
}
15.3.2、提高运行速度
现在能够显示计数了,但是太慢了,为什么呢?
分析:count每增长一次,就要刷新,CPU计算很快,计算一次刷新一次,确实很浪费,人眼可能还跟不上CPU速度。那么利用计时器,0.01秒刷新一次就好了。
void task_b_main(struct SHEET *sht_back)
{
struct FIFO32 fifo;
struct TIMER *timer_ts, *timer_put;
int i, fifobuf[128], count = 0;
char s[12];
fifo32_init(&fifo, 128, fifobuf);
timer_ts = timer_alloc();
timer_init(timer_ts, &fifo, 2);
timer_settime(timer_ts, 2);
timer_put = timer_alloc();
timer_init(timer_put, &fifo, 1);
timer_settime(timer_put, 1);
for (;;) {
count++;
io_cli();
if (fifo32_status(&fifo) == 0) {
io_sti();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i == 1) { // 0.01秒刷新一次
sprintf(s, "%11d", count);
putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 11);
timer_settime(timer_put, 1);
} else if (i == 2) {
farjmp(0, 3 * 8);
timer_settime(timer_ts, 2);
}
}
}
}
发现sht_back也是不是从 0xfec取得了,是从参数传递了。怎么传递呢?
在HariMain申请栈空间时做了一下动作:
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
*((int *) (task_b_esp + 4)) = (int) sht_back;
这样以来,sht_back 的地址就存在了任务B的栈底的最后四个字节里了,切换到任务B后,EIP就从task_b_main函数开始执行,当task_b_back函数使用参数时,就会从栈里 ESP+4 取出来。正好时切换之前留下的sht_back的地址。
之后继续提高运行速度的建议:
1、继续增加任务B刷新计数的间隔,真是不刷新
2、适当增大任务切换时间间隔(增加太大鼠标还是有明显卡顿)
15.4、真正的多任务
目前为止,任务切换都通过执行任务的程序自己切换的,真正的多任务应该是程序本身是没有感知的。
做改进:
/* mtask.c */
/* 多任务管理 */
#include "bootpack.h"
struct TIMER *mt_timer;
int mt_tr;
// mt_tr是tr寄存器要设定的值;mt_timer设定任务切换的计时器,没有设置定时器的data,具体原因后面说明
void mt_init(void)
{
mt_timer = timer_alloc();
/*这里没有必要使用timer_init */
timer_settime(mt_timer, 2);
mt_tr = 3 * 8;
return;
}
// 根据mt_tr任务切换,设定计时器
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;
}
// timer.c
void inthandler20(int *esp)
{
struct TIMER *timer;
char ts = 0;
io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00接收信号结束的信息通知给PIC */
timerctl.count++;
if (timerctl.next > timerctl.count) {
return;
}
timer = timerctl.t0; /* 首先把最前面的地址赋给timer */
for (;;) {
/* 因为timers的定时器都处于运行状态,所以不确认flags */
if (timer->timeout > timerctl.count) {
break;
}
/* 超时 */
timer->flags = TIMER_FLAGS_ALLOC;
//这里就是没有给mt_timer设定data的原因,没有用data判断,而是通过判断两个定时器地址是不是一样的
if (timer != mt_timer) {
fifo32_put(timer->fifo, timer->data);
} else {
ts = 1; /* mt_timer超时*/
}
timer = timer->next; /* 将下一个定时器的地址赋给timer*/
}
timerctl.t0 = timer;
timerctl.next = timer->timeout;
if (ts != 0) {
mt_taskswitch();
}
return;
}