[Rx86OS-XVI] 实现多任务

平台

处理器:Intel Celeron® Dual-Core CPU  2.10GHz

操作系统:Windows7 专业版 x86

阅读书籍:《30天自制操作系统》—川合秀实[2015.04.18,04.20– 04.21]

工具:../toolset/


多任务

在windows操作系统中,多任务就是多个程序“同时”运行的状态。单个CPU实现多任务是通过反复切换各个运行的程序来实现的。切换的速度越快,会让人觉得程序“同时”运行的效果越好,在一般的操作系统中,执行切换的动作每0.01~ 0.03秒就会进行一次。

切换这个动作本身也需要消耗一定的时间,这个时间大约为0.0001秒,不同的CPU及操作系统所需的时间也有不同。这个时间越短越好。----抄自于“书”。


1 通路

1.1 x86多任务机制

在x86保护模式下实现多任务不可肆意而为,它的实现要围绕x86处理器支持多任务切换的硬件机制。

[Rx86OS-XVI] 实现多任务_第1张图片

Figure 1. 任务切换硬件机制

图中绿色部分表示需要编写程序来实现,蓝色部分是CPU硬件自动完成的。被0标注的部分是使用多任务之前需要做的程序配置;1表示从内存中读到JMP指令,取JMP指令这个过程由CPU完成,JMP指令由程序提供;2表示CPU在0和1的基础上自动完成的动作。同一个标号下的步骤无严格的先后顺序要求。了解TSS(Task Status Segment)需要以GDT为基础,TSS这个概念由0-2共同形成


1.2 实现2个任务的简单跳转

(1) 准备工作

[1] TSS

x86CPU的TSS有16位和32位两个版本。“书”使用32位版本,并用以下结构体描述TSS。

//描述32位版本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;
};

创建这个结构体的初衷是TSS32内的每个元素都表示任务的相关信息。“书”中记载,在切换任务时,backlink内存可能会被修改。在初始化结构体时,要将ldtr设置为0,iomap设置为0x40000000,否则任务无法得到正确的切换。

struct TSS32 tss_a, tss_b;
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;

[2] 注册TSS

//ADR_GDT为GDT的首地址,0x00270000
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;

//将2个任务分别注册到GDT的3,4编号中,AR_TSS32为0x89
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);

[3] TR赋值、任务切换函数

C语言没有语句能够做到,对TR赋值以及任务切换。这部分功能要用汇编编写。

 

1.       GLOBAL _load_tr

2.       _load_tr:             ; void load_tr(int tr);

3.                     LTR        [ESP+4]  ; tr为当前运行任务在GDT中的编号 * 8

4.             RET
 

规定将当前运行任务的TSS在GDT中的编号乘以8(段寄存器的高13位表GDT编号)写往TR寄存器。往TR寄存器赋值只是改变了TR的值,并不会进行任务跳转,任务跳转得靠JMP指令。从A任务跳转到B任务需要用JMP B任务段地址*8:0。
 

1.       GLOBAL _farjmp

2.       _farjmp:       ; void farjmp(int eip, int cs);

3.              JMP       FAR      [ESP+4]  ; [ESP+4]存eip, [ESP+8]存cs(TSS在GDT中的段编号*8)

4.       RET
 

[3] 准备2个任务

将HariMain()设为当前正运行的一个任务

load_tr(3* 8);

这里没有初始化注册在GDT编号3的TSS,CPU会将当前任务的相关信息修改到TR指向的TSS中?


还准备一个任务就可以构成多任务的环境。

void task_b_main(void)
{
	struct SHEET *sht_back;
    //0x0fec在HariMain函数中定义
    sht_back    = sht_back = (struct SHEET *) *((int *) 0x0fec);
    putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, “task_b_main”, 12);
	for (;;) {
	}
}

除HariMain()任务外,其它的任务不能有return语句。因为任务不是由某段程序直接调用的(见CALL、RET指令)。

这个任务在5s之后在屏幕的(0,144)起始位置显示“5[sec]”。


通过TSS给task_b_main任务(程序)设置段地址等相关的信息

task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
tss_b.eip = (int) &task_b_main; //此任务对应task_b_main程序
// task_b_main任务信息,栈、段等信息设置
tss_b.eflags = 0x00000202; 
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;

(2) 切换到task_b_main任务中

将准备工作的代码写在HariMain程序中。然后在HariMain程序中设置,3s后切换到task_b_main任务中:

*((int *) 0x0fec) = (int) sht_back;
……
} else if (i == 3) {    //3s时间
				putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
				farjmp(0, 4 * 8);//切换到GDT编号为4的任务中去
} else if (i <= 1) 

声明好各个程序后,打开“!cons_nt.bat”,程序通过编译后,使用“makerun”命令在QEMU中运行程序:

[Rx86OS-XVI] 实现多任务_第2张图片

Figure2. 从HariMain程序切换到task_b_main

HariMain程序运行3s后,CPU切换到task_b_main程序中。


(3) 定时切换任务

当任务切换到task_b_main任务中后,过5s后再切换回HariMain任务中去吧。

void task_b_main(void)
{
	struct FIFO32 fifo;
	struct TIMER *timer;
	int i, fifobuf[128];
	struct SHEET *sht_back;
	
	fifo32_init(&fifo, 128, fifobuf);
	timer = timer_alloc();
	timer_init(timer, &fifo, 1);
	timer_settime(timer, 500);
	sht_back = (struct SHEET *) *((int *) 0x0fec);

	for (;;) {
		io_cli();
		if (fifo32_status(&fifo) == 0) {
			io_sti();
			io_hlt();
		} else {
			i = fifo32_get(&fifo);
			io_sti();
			if (i == 1) { //到?5s
				putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, "5[sec]", 7);
				farjmp(0, 3 * 8);
			}
		}
	}
}

打开“!cons_nt.bat”,程序通过编译后,使用“make run”命令在QEMU中运行程序:

[Rx86OS-XVI] 实现多任务_第3张图片

Figure3. HariMain与task_b_main定时切换

程序运行后,HariMain计数3s后切换到task_b_main中;进入task_b_main后,task_b_main程序计数5s后切换到HariMain中的任务切换语句处(后一语句)……如此反复进行。


2 快速切换

像1.2中定时切换任务的程序那样,将任务切换的语句写在循环语句中,就能够使两个程序来回切换。但任务之间切换的时间较长,就没有x86保护模式下多任务的感觉(边听“一无所有”边敲word,如果这两个任务的切换时间为5s、3s,那么就得先听5s的歌,再敲3s的word这样的间隔动作)。所以,任务间的切换需要小到人能感知的那个(0.02s?),这才能让人觉得多个任务是在“同时”进行。


在两个任务中加入定时0.02s的语句,程序在这个地方跳转。

void HariMain(void)
{
        ……
        struct TIMER *timer *timer_ts;
        ……
        timer_ts = timer_alloc();
        timer_init(timer_ts, &fifo, 2); //到达时间往缓冲区写2
        timer_settime(timer_ts, 2); //定时时长=2 * 定时器周期
        for (;;) {
                io_cli();
                if (fifo32_status(&fifo) == 0) {
                        io_stihlt();
                } else {
                        i = fifo32_get(&fifo);
                        io_sti();
                        //定时到0.02s切换到GDT编号为4的任务中
                        if (i == 2) { 
                                farjmp(0, 4 * 8);
                                //相当于让timer_ts再重新定时0.02s
                                timer_settime(timer_ts, 2);
                }……
        }
}

为了展示两个任务能够“同时”运行的错觉,将task_b_main改为显示计数。

void task_b_main(void)
{
	struct FIFO32 fifo;
	struct TIMER *timer_ts;
	int i, fifobuf[128], count = 0;
	char s[11];
	struct SHEET *sht_back;

	fifo32_init(&fifo, 128, fifobuf);
	timer_ts = timer_alloc();
	timer_init(timer_ts, &fifo, 1);
	timer_settime(timer_ts, 2);
	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();
          //到达0.02s,切换到GDT编号为3的任务中
			if (i == 1) {
				farjmp(0, 3 * 8);
				timer_settime(timer_ts, 2);
			}
		}
	}
}

打开“!cons_nt.bat”,程序通过编译通过后,使用“make run”命令在QEMU中运行程序:

[Rx86OS-XVI] 实现多任务_第4张图片

Figure4. 快速切换任务

除了左下角的数字是在任务task_b_main中运行的外,其它的显示都是在HariMain中运行的。由于这两个任务切换速度快,它们就像同时在运行一样(windows中的鼠标一闪一闪,左下角的数字也不停的显示)。


3 提升

3.1 (刷新)速度

程序1s内就会计数100多次,如果每计1个数都要在画面上显示一次的话,人眼也分辨不出来。没有必要计数1次就刷新1次,每隔0.01s刷新显示一次计数就足够了。

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

程序增添了一个专门用来标识0.01s的计时器timer_put,往缓冲区内写1和2来表示定时0.01s和0.02s。


另外,任务task_b_main有了参数,但它不会被程序显示调用,怎么给它传递参数呢?

task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
……
tss_b.esp = task_b_esp;
……
*((int *) (task_b_esp + 4)) = (int) sht_back;

task_b_main(structSHEET *sht_back)函数的sht_back参数保存在ESP+ 4内,用来描述TSS的结构体TSS32中,esp元素与ESP对应。TSS32.esp+ 4就是一个任务的第一个参数的内存地址。在QEMU中运行修改后的程序:

[Rx86OS-XVI] 实现多任务_第5张图片

Figure5. 提高运行速度

上图与快速切换程序的运行结果是脚前脚后的截图,但上图的运行速度(从计数看)明显快乐许多。


3.2 真正的多任务切换

如果采用将多任务切换的代码写在每个任务程序中的方式,就有可能切换不成功(切换语句忘记写、因为bug而导致没有运行到任务切换语句处等)。所以,需要编写一种只要时间到达就能够切换多任务的程序。真正的多任务,是要做到在程序本身不知道的情况下进行任务切换。----抄自于“书”。


(1) 创建定时自动切换多任务的函数

struct TIMER *mt_timer;
int mt_tr;

void mt_init(void)
{
	mt_timer = timer_alloc();
	//mt_timer定时0.02s就超时
	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;
}

(2) 修改定时中断函数

void inthandler20(int *esp)
{
	struct TIMER *timer;
	char ts = 0;
	io_out8(PIC0_OCW2, 0x60);	//通知IRQ-0继续监听中断
	timerctl.count++;
	if (timerctl.next > timerctl.count) {
		return;
	}
	timer = timerctl.t0; //timer指向定时时间最小的定时器
	for (;;) {
		//所有的定时器都处于使用当中
		if (timer->timeout > timerctl.count) {
			break;
		}
		//所有小于最小定时时间的定时器改使用状态为TIMER_FLAGS_ALLOC
		timer->flags = TIMER_FLAGS_ALLOC;
		if (timer != mt_timer) {
			fifo32_put(timer->fifo, timer->data);
		} else {
			ts = 1; //任务切换的定时时间到
		}
		timer = timer->next; //指向下一个定时时间最小的定时器
	}
	timerctl.t0 = timer;
	timerctl.next = timer->timeout;
	if (ts != 0) {
		mt_taskswitch();
	}
	return;
}

因为任务切换过程(mt_taskswitch)会修改标志寄存器EFLAG的状态位,而在运行定时中断程序的过程中应该屏蔽中断的产生(若本次定时中断程序未运行完再来定时中断会引起程序不能正确运行)。所以,定时器中断程序使用ts变量来标记任务切换时间是否到达,当产生相同中断不会影响程序正确性时再调用任务切换函数mt_taskswitch。

3.3 管理多任务

制作一个能描述、管理任务的数据结构。然后围绕数据结构来更方便地管理多任务。


(1) 描述多任务的数据结构

#define MAX_TASKS	1000	//最大的任务数量
#define TASK_GDT0	3	    //最开始运行的任务在GDT中的编号
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;
};
struct TASK {//描述一个任务的结构体
	int sel, flags; //保存任务在GDT中的编号,任务状态
	struct TSS32 tss;
};
struct TASKCTL {//管理任务的结构体
	int running; //运行的任务的个数
	int now; //当前运行的任务
	struct TASK *tasks[MAX_TASKS];//保存各任务的地址
	struct TASK tasks0[MAX_TASKS];
};

(2) 管理多任务的函数

初始化一个任务。

struct TASKCTL *taskctl;
struct TIMER *task_timer;

/* 为管理多任务结构体分配内存,并初始化各个任务,
 * 并置一个多任务处于被使用的状态并对其初始化
 * memman为管理内存的结构体
 * 返回获取的多任务的结构体的地址
 */
struct TASK *task_init(struct MEMMAN *memman)
{
	int i;
	struct TASK *task;
	struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
	taskctl = (struct TASKCTL *) memman_alloc_4k(memman, sizeof (struct TASKCTL));
	for (i = 0; i < MAX_TASKS; i++) {
		taskctl->tasks0[i].flags = 0;
		taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
		set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);
	}
	task = task_alloc(); //将一个多任务置于被使用的状态
	task->flags = 2; 
	taskctl->running = 1;
	taskctl->now = 0;
	taskctl->tasks[0] = task;
	load_tr(task->sel);
	task_timer = timer_alloc();
	timer_settime(task_timer, 2);
	return task;
}

置一个多任务于被使用状态并初始化其TSS。

/* 从管理多任务结构体中开启一个未运行的多任务为运行状态
 * 开启一个任务后称为运行状态后返回这个任务的地址,若
 * 所有的任务都处于运行状态则返回0
 */
struct TASK *task_alloc(void)
{
	int i;
	struct TASK *task;
	for (i = 0; i < MAX_TASKS; i++) {
		if (taskctl->tasks0[i].flags == 0) {
			task = &taskctl->tasks0[i];
			task->flags = 1; //此任务处于被使用状态
			task->tss.eflags = 0x00000202; /* IF = 1; */
			task->tss.eax = 0; //此任务的TSS的初始值
			task->tss.ecx = 0;
			task->tss.edx = 0;
			task->tss.ebx = 0;
			task->tss.ebp = 0;
			task->tss.esi = 0;
			task->tss.edi = 0;
			task->tss.es = 0;
			task->tss.ds = 0;
			task->tss.fs = 0;
			task->tss.gs = 0;
			task->tss.ldtr = 0;
			task->tss.iomap = 0x40000000;
			return task;
		}
	}
	return 0; //描述任务的结构体被全部使用
}

让一个处于被使用状态的任务处于运行状态。

//让task任务处于运行状态
void task_run(struct TASK *task)
{
	task->flags = 2; //task处于运行状态
	taskctl->tasks[taskctl->running] = task; //管理任务的结构体增一个运行的任务
	taskctl->running++;
	return;
}

切换在运行的各个任务,在定时中断函数中调用task_switch()函数进行任务切换。

//重新定时任务切换的时间,
//若有1个以上运行的任务就依次切换各个运行的任务
void task_switch(void)
{
	timer_settime(task_timer, 2);
	if (taskctl->running >= 2) {
		taskctl->now++;
		if (taskctl->now == taskctl->running) {
			taskctl->now = 0;
		}
        //通过管理任务的结构体将任务切换到下一个任务
		farjmp(0, taskctl->tasks[taskctl->now]->sel);
	}
	return;
}

当任务较多时,每个任务得以CPU运行的时间间隔会相应的变长。最好使“任务切换时间* 最大任务数量”都比较小。这样,就算任务数量较多,也能让人产生多个任务同时运行的错觉。


在HariMain()主函数中通过调用管理多任务的数据结构和函数来实现多任务。使用这些结构体和函数需要按照先后顺序,调用管理多任务的函数要符合设计的流程。

void HariMain(void)
{
        ……
        struct TASK *task_b;
        ……
        task_init(memman);
        task_b = task_alloc();
        task_b->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
        task_b->tss.eip = (int) &task_b_main;
        task_b->tss.es = 1 * 8;
        task_b->tss.cs = 2 * 8;
        task_b->tss.ss = 1 * 8;
        task_b->tss.ds = 1 * 8;
        task_b->tss.fs = 1 * 8;
        task_b->tss.gs = 1 * 8;
        *((int *) (task_b->tss.esp + 4)) = (int) sht_back;
        task_run(task_b);
        ……
}

通过这样的数据结构和围绕数据结构编写的函数来实现多任务对编写程序的人来说更有条理。其运行结果跟快速切换下的运行结果差不多。


3.4 任务休眠

将闲着没事干的任务的时间分配给具有频繁事务做的任务。将没事干的任务A从管理任务的数据结构中删除(不参与任务切换),就可以将时间留给其它任务了;当任务A又有事情干的时候,再将任务A添加到管理任务的数据结构中(加入任务切换),像这样的过程在多任务的术语中叫“休眠”。


编写删除任务函数。

//从管理多任务的数据结构中删除task任务
void task_sleep(struct TASK *task)
{
	int i;
	char ts = 0;
	if (task->flags == 2) {//如果task处于运行状态
		if (task == taskctl->tasks[taskctl->now]) {
			ts = 1; //如果task是当前运行的任务则需要切换掉当前任务
		}
		//找到task在所有运行任务中的位置
		for (i = 0; i < taskctl->running; i++) {
			if (taskctl->tasks[i] == task) {
				break;
			}
		}
		taskctl->running--;
		if (i < taskctl->now) {
			taskctl->now--;
		}
		//任务前移,删除task
		for (; i < taskctl->running; i++) {
			taskctl->tasks[i] = taskctl->tasks[i + 1];
		}
		task->flags = 1; //task不再处于运行状态
		if (ts != 0) {//切换任务运行
			if (taskctl->now >= taskctl->running) {
				taskctl->now = 0;
			}
			farjmp(0, taskctl->tasks[taskctl->now]->sel);
		}
	}
	return;
}

往管理缓冲区的FIFO结构体中增加一个task成员。当缓冲区中来数据时,唤醒task(将task添加进管理任务的结构体中)。

struct FIFO32 { //管理缓冲区的FIFO结构体
	int *buf;    //所管理缓冲区的首地址
	int p, q, size, free, flags;  //管理buf代表的缓冲区的变量
	struct TASK *task; //当缓冲区中有(task的)数据来临时要唤醒的任务
};

//初始化管理缓冲区的结构体
void fifo32_init(struct FIFO32 *fifo, int size, int *buf, struct TASK *task)
{
	fifo->size = size;
	fifo->buf = buf;
	fifo->free = size;//缓冲区剩余空间
	fifo->flags = 0;
	fifo->p = 0; //写入位置
	fifo->q = 0; //读取位置
	fifo->task = task; //有task的数据写入时所需唤醒的task
	return;
}
//往缓冲区内写入数据,并唤醒对应的任务
int fifo32_put(struct FIFO32 *fifo, int data)
{
	if (fifo->free == 0) {
		fifo->flags |= FLAGS_OVERRUN;
		return -1;
	}
	fifo->buf[fifo->p] = data;
	fifo->p++;
	if (fifo->p == fifo->size) {
		fifo->p = 0;
	}
	fifo->free--;
	if (fifo->task != 0) {
		if (fifo->task->flags != 2) { //任务处于非运行状态
			task_run(fifo->task);//让在fifo中的task处于运行状态
		}
	}
	return 0;
}

在HariMain()和task_b_main()中调用修改过的fifo结构体和函数。

void HariMain(void)
{
        ……
        struct TASK *task_a, *task_b;
        ……
        fifo32_init(&fifo, 128, fifobuf, 0);
        ……
        task_a = task_init(memman);
        fifo.task = task_a; //需要休眠、唤醒的任务为task_a
        ……
        for (;;) {
                io_cli();
                if (fifo32_status(&fifo) == 0) {
                        task_sleep(task_a);
                        io_sti();
                } else {
                        ……
                }
        }
}

void task_b_main(struct SHEET *sht_back)
{
        ……
        fifo32_init(&fifo, 128, fifobuf, 0);//task_b_main任务不需要休眠
        ……
}

当HariMain()的缓冲区内无数据时就将task_a任务休眠;当缓冲区内有数据时就将task_a重新激活。理解这个过程需要联系3.4中的函数以及键盘中断函数(中断、内存、CPU管理的多任务之间是独立的;键盘中断程序运行时就会调用fifo32_put函数往缓冲区内写入数据,此函数会调用task_run激活记录在管理任务结构体中的task)。打开“!cons_nt.bat”,整个程序通过编译后,用“make run”直接在QEMU中运行:

[Rx86OS-XVI] 实现多任务_第6张图片

Figure 6. 使用任务休眠的程序

和3.1相比,如果HariMain()程序的缓冲区内无数据则相当于只有task_b_main()一个程序在运行,所以task_b_main的速度会显得更快。


3.5 任务优先级

多设置几个任务。“书”中为每个任务准备了窗口(16.3),任务和画面设置的代码由空行隔开。在某些情况下,我们需要提升或者降低某个应用程序的优先级,在多窗口的基础上,来设定任务的优先级。


(1) 为每个任务设定不同的切换时间

将任务切换时间设置在0.01s~ 0.1s的范围内,就可以实现最大10倍的优先级差异(原来优先级指这个)。


修改数据结构。

struct TASK {
	int sel, flags; 
	int priority; //任务的优先级(对应被切换的时间)
	struct TSS32 tss;
};

围绕新数据结构的函数。

struct TASK *task_init(struct MEMMAN *memman)
{
    ……
	task = task_alloc();
	task->flags = 2; //运行状态
	task->priority = 2; /* 0.02秒 */
	taskctl->running = 1;
	taskctl->now = 0;
	taskctl->tasks[0] = task;
	load_tr(task->sel);
	task_timer = timer_alloc();
	timer_settime(task_timer, task->priority);
	return task;
}

//通过参数修改task任务的优先级
void task_run(struct TASK *task, int priority)
{
	if (priority > 0) {
		task->priority = priority;
	}
	if (task->flags != 2) {
		task->flags = 2; 
		taskctl->tasks[taskctl->running] = task;
		taskctl->running++;
	}
	return;
}

//将当前运行的切换时间设置到定时器中
void task_switch(void)
{
	struct TASK *task;
	taskctl->now++;
	if (taskctl->now == taskctl->running) {
		taskctl->now = 0;
	}
	task = taskctl->tasks[taskctl->now];
	timer_settime(task_timer, task->priority);//隔task->priority时长再切换
	if (taskctl->running >= 2) {
		farjmp(0, task->sel);
	}
	return;
}

改写当HariMain()的缓冲区内有数据时,往缓冲区内写数据并唤醒task_a的函数。

int fifo32_put(struct FIFO32 *fifo, int data)
{
    ……
	if (fifo->task != 0) {
		if (fifo->task->flags != 2) { //未处于运行状态
			task_run(fifo->task, 0); //不修改task任务的优先级
		}
	}
	return 0;
}

数据结构和函数都修改完了,只差在主函数HariMain()中调用了。这里需要调用的是用一个循环语句来修改各个任务的优先级。各个任务的优先级数字越大(它代表此任务被切换的时间),优先级越高。


(2) 设置高优先级任务的优先级

提升HariMain的优先级能够使HariMain任务中的操作更加流畅,同时也不会影响其它任务的运行,因为只要HariMain程序的缓冲区内无数据(代表有操作)写入时就会得到休眠。对于像处理鼠标这样的任务,在无数据时就将其休眠,同时还能够将此任务的优先级设置得很高。


当优先级为10的两个任务同时运行时,优先哪个全凭运气(程序代码所决定)。需要设计一种架构,来决定同是高优先级的任务哪个优先运行。抄书中的一幅图来展现此架构。

[Rx86OS-XVI] 实现多任务_第7张图片

Figure7. 多任务优先级架构

再抄“书”中一段文字来说明这个图示架构:这种架构的原理是,最上层的LEVEL0中只要存在一个任务,就完全忽略LEVEL1和LEVEL2中的任务,只在LEVEL0的任务中进行任务切换。当LEVEL0中的任务全部休眠,或者全部降到下层LEVEL,也就是当LEVEL0中没有任何任务的时候,接下来开始轮到LEVEL1中的任务进行任务切换。当LEVEL0和LEVEL1中都没有任务时,那就该轮到LEVEL2出场了。


实现多任务的这种优先级架构的数据结构。

#define MAX_TASKS_LV	100
#define MAX_TASKLEVELS	10
struct TASK {//描述任务的结构体
	int sel, flags; 
	int level, priority;
	struct TSS32 tss;
};
struct TASKLEVEL {//管理多任务优先级架构的结构体
	int running; 
	int now; 
	struct TASK *tasks[MAX_TASKS_LV];
};
struct TASKCTL {//管理多任务的结构体
	int now_lv; //任务现在所在的LEVEL
	char lv_change; //下次切换任务时是否需要改任务的LEVEL
	struct TASKLEVEL  level[MAX_TASKLEVELS];
	struct TASK tasks0[MAX_TASKS];
};

对每个LEVEL允许有100个任务,最多可以有10 LEVEL。


围绕数据结构实现多任务的这种架构的函数。

//返回在运行的任务的地址
struct TASK *task_now(void)
{
	struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
	return tl->tasks[tl->now];
}
//往struct TASKLEVEL结构体中添加任务task
void task_add(struct TASK *task)
{
	struct TASKLEVEL *tl = &taskctl->level[task->level];
	tl->tasks[tl->running] = task;
	tl->running++;
	task->flags = 2; 
	return;
}

//从struct TASKLEVEL中删除task
void task_remove(struct TASK *task)
{
	int i;
	struct TASKLEVEL *tl = &taskctl->level[task->level];
    
	for (i = 0; i < tl->running; i++) {
		if (tl->tasks[i] == task) {
			break;
		}
	}

	tl->running--;
	if (i < tl->now) {
		tl->now--; 
	}
	if (tl->now >= tl->running) {
       //优先级的架构超过最大值则回到0
		tl->now = 0;
	}
	task->flags = 1; 

	//任务的优先级架构往前移
	for (; i < tl->running; i++) {
		tl->tasks[i] = tl->tasks[i + 1];
	}

	return;
}
//将task切换到哪个LEVEL中
void task_switchsub(void)
{
	int i;
	//寻找最上层的LEVEL
	for (i = 0; i < MAX_TASKLEVELS; i++) {
		if (taskctl->level[i].running > 0) {
			break; 
		}
	}
	taskctl->now_lv = i;
	taskctl->lv_change = 0;
	return;
}

修改其它的函数。

struct TASK *task_init(struct MEMMAN *memman)
{
    ……
	for (i = 0; i < MAX_TASKLEVELS; i++) {
		taskctl->level[i].running = 0;
		taskctl->level[i].now = 0;
	}
	task = task_alloc();
	task->flags = 2;	
	task->priority = 2; 
	task->level = 0;	
	task_add(task);
	task_switchsub();
	load_tr(task->sel);
	task_timer = timer_alloc();
	timer_settime(task_timer, task->priority);
	return task;
}
//在参数中指定任务运行在哪个LEVEL中
void task_run(struct TASK *task, int level, int priority)
{
	if (level < 0) {
		level = task->level; //不改变LEVEL
	}
	if (priority > 0) {
		task->priority = priority;
	}

	if (task->flags == 2 && task->level != level) { 
		task_remove(task); //在task原在的LEVEL中移除task
	}
	if (task->flags != 2) {
		//从休眠中唤醒
		task->level = level;
		task_add(task);
	}

	taskctl->lv_change = 1; //下次切换任务时检查LEVEL
	return;
}
//调用task_move函数的task_sleep
void task_sleep(struct TASK *task)
{
	struct TASK *now_task;
	if (task->flags == 2) {
		now_task = task_now();
		task_remove(task); //flag = 1
		if (task == now_task) {
			//如果自己休眠则进行任务转换
			task_switchsub();
			now_task = task_now(); //获取当前运行任务的值
			farjmp(0, now_task->sel);
		}
	}
	return;
}
//判断lv_change的任务切换函数
void task_switch(void)
{
	struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
	struct TASK *new_task, *now_task = tl->tasks[tl->now];
	tl->now++;
	if (tl->now == tl->running) {
		tl->now = 0;
	}
	if (taskctl->lv_change != 0) {
		task_switchsub();
		tl = &taskctl->level[taskctl->now_lv];
	}
	new_task = tl->tasks[tl->now];
	timer_settime(task_timer, new_task->priority);
	if (new_task != now_task) {
		farjmp(0, new_task->sel);
	}
	return;
}
//往缓冲区写数据并激活任务
int fifo32_put(struct FIFO32 *fifo, int data)
{
	if (fifo->free == 0) {
		fifo->flags |= FLAGS_OVERRUN;
		return -1;
	}
	fifo->buf[fifo->p] = data;
	fifo->p++;
	if (fifo->p == fifo->size) {
		fifo->p = 0;
	}
	fifo->free--;
	if (fifo->task != 0) {
		if (fifo->task->flags != 2) { 
			task_run(fifo->task, -1, 0); //激活任务
		}
	}
	return 0;
}

然后在HariMain函数中将HariMain设为LEVEL1,任务B0~ B2设为LEVEL2。这样的话,当任务A忙碌的时候就不会切换到任务B0~ B2,鼠标操作的相应应该会有不少的改善。

void HariMain(void)
{
    ……
    task_a = task_init(memman);
	fifo.task = task_a;
	task_run(task_a, 1, 2);
    ……
    /* sht_win_b */
	for (i = 0; i < 3; i++) {
		……
		task_run(task_b[i], 2, i + 1);
	}
    ……
}

“书”中说根据运行结果,此设计很成功,我相信“书”中所说的。


3.6 闲置任务

当B0~ B2任务未被开启且FIFO为空时,程序会去找下层LEVEL中的任务(无任务),这样会导致程序出现异常(相信“书”说的是事实)。所以,一般情况下可以让任务休眠,当所有LEVEL中都没有任务存在的时候,就执行HLT让CPU休眠。


创建一个只运行HLT指令的任务,将其置于最底层的LEVEL中。就是标题中所提到的闲置任务。

void task_idle(void)
{
	for (;;) {
		io_hlt();
	}
}

这样,即便任务A进入休眠状态,系统也会自动切换到上面这个闲置任务中执行HLT。当有像移动鼠标这样的中断发生时,像任务A这样的程序就会被唤醒而被继续执行。


将闲置任务放在最下层的LEVEL中。

//初始化一个任务将其地址返回,并将闲置任务添加到最底层的LEVEL中
struct TASK *task_init(struct MEMMAN *memman)
{
	struct TASK *task, *idle;
	……

	idle = task_alloc();
	idle->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
	idle->tss.eip = (int) &task_idle;
	idle->tss.es = 1 * 8;
	idle->tss.cs = 2 * 8;
	idle->tss.ss = 1 * 8;
	idle->tss.ds = 1 * 8;
	idle->tss.fs = 1 * 8;
	idle->tss.gs = 1 * 8;
	task_run(idle, MAX_TASKLEVELS - 1, 1);

	return task;
}

往最底层加入闲置任务后,就算只剩下HariMain任务,当HariMain任务进入休眠程序往底层LEVEL搜寻任务也会搜到闲置任务,不会出现什么异常。


总结

过程:(通路--最底层的程序需要遵循硬件机制)-->组织(设计能描述和管理事务的数据结构)-->提升(优化)。

实现多任务涉及到的几个方面(3)。

中断、内存、CPU管理的多任务之间是独立的;键盘中断程序运行时就会调用fifo32_put函数往缓冲区内写入数据,此函数会调用task_run激活记录在管理任务结构体中的task。


[x86OS] Note Over.

[2015.04.29]

你可能感兴趣的:([Rx86OS-XVI] 实现多任务)