第15天:多任务(1)

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:
EFLAGS
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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值