【操作系统】30天自制操作系统--(14)多任务1

        本章开始多任务的设计。

一 多任务的说明

        多任务(multitask),指的是操作系统中,多个应用程序同时运行的状态。然而,对于单核CPU来说,同一个瞬间只能处理一个事情,不能做到左右互搏、一心二用的效果,那只能通过快速切换运行任务,来实现这种所谓的多任务状态

        在一般的操作系统中,这个切换动作每0.01-0.03秒进行一次(这样CPU大概只有1%的处理能力消耗在任务切换上,可以忽略不计)。这个切换时间不能太慢(会让人感觉到程序卡顿)、也不能太快(消耗CPU的处理能力,功夫都花在切换上面,没时间处理正事了) 。

二 实现任务切换

        实现任务的切换,有两个步骤:

        (1)将TASK1有关寄存器的值写入到内存中;

        (2)将运行TASK2需要的值从内存中读出到寄存器中

        每个任务包含的状态可以归纳为“任务状态段”(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; /* 32位寄存器 */
	int es, cs, ss, ds, fs, gs;							     /* 16位寄存器 */
	int ldtr, iomap;									     /* 与任务设置有关的信息 */
};

        TSS总计包含26个int成员(104字节),成员的内容主要是一些寄存器,具体可以参考【操作系统】CPU寄存器详解,这边主要关注一下eip指令寄存器( 指令寄存器可以说是CPU中最最重要的寄存器了,它指向了下一条要执行的指令所存放的地址,CPU的工作其实就是不断取出它指向的指令,然后执行这条指令,同时指令寄存器继续指向下面一条指令,如此不断重复,这就是CPU工作的基本日常

        对指令寄存器赋值(MOV EIP,0x1234),就相当于0x1234地址的指令,也就类似于汇编中的JMP 0x1234实现的效果。对任务实现切换,本质上就是从一个指令执行地址跳转到另一个指令的执行地址,这边还得用JMP指令,JMP指令又分为两种模式(near模式、far模式):
        (1)near 模式,只改写 EIP(指令寄存器)
        (2)far 模式,同时改写 EIP(指令寄存器) 和 CS(代码段寄存器)

        如果一条 JMP 指令所指定的目标地段不是可执行的代码,而是 TSS 的话,CPU 就不会执行通常的改写 EIP 和 CS 操作,而是将这条指令理解为任务切换,换句话说,TSS 里面就是用来执行任务切换的代码。

        根据上面的思路,按照下面的步骤就可以进行任务切换了:

【1】首先创建两个TSS:任务A的TSS和任务B的TSS。

struct TSS32 tss_a, tss_b;

【2】向他们的ldtr和iomap赋值:

tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;

【3】将他们两个在GDT中注册(这边可以参考一下【操作系统】30天自制操作系统--(4)显示字体(汉字)以及GDT/IDT中对于段1和段2的GDT注册):

struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
#define AR_TSS32		0x0089

set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);//段号3,基地址&tss_a,段长104-1
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);//段号4,基地址&tss_b,段长104-1

【4】赋值TR寄存器(任务寄存器,存储当前正在运行哪一个任务),对它的赋值必须把GDT的编号乘以8,且对TR寄存器的操作必须在汇编的层面完成:

_load_tr:		; void load_tr(int tr)
		LTR		[ESP+4]
		RET

【5】光改变TR的值还不能任务切换,必须执行far模式的跳转指令才行:

; 跳转到段3(任务b-->任务a)
_taskswitch3:	; void taskswitch3(void);
		JMP		3*8:0
		RET

; 跳转到段4(任务a-->任务b)
_taskswitch4:	; void taskswitch4(void);
		JMP		4*8:0
		RET

【6】上面完成了从tss_a、tss_b的注册和跳转,这边还需要准备一下tss_b的初始化和执行程序,不然跳转到一个空地址上也不顶用:

        初始化如下:

int task_b_esp;

task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;  //为任务b定义新栈

//初始化任务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;

        任务b的执行操作如下(定时5s之后,返回任务a):

void task_b_main(void) {
	struct FIFO32 fifo;
	struct TIMER *timer;
	int i, fifobuf[128];

	fifo_init(&fifo, 128, fifobuf);
	timer = timer_alloc();
	timer_init(timer, &fifo, 1);
	// 设置超时时间为 5 秒
	timer_settime(timer, 500);

	for(;;) {
		io_hlt();
		if (fifo32_status(&fifo) == 0) {
			io_sti();
			io_hlt();
		} else {
			i = fifo32_get(&fifo);
			io_sti();
			if (i == 1) {
				// 返回任务 3
				taskswitch3();
			}
		}
	}
}

【7】主函数中10s超时缓存处理中执行跳转(任务a---->任务b),实现10s之后跳转任务b:

/* 中略 */
else if (i == 10) { 
    putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
    taskswitch4();  //切换段4(任务a-->任务b)
}
/* 中略 */

        综上,便实现了一个简单的多任务操作,程序运行10s之后光标停止闪烁、鼠标键盘也失效—(任务a---->任务b成功!)。再隔了5s之后,光标恢复正常,刚鼠标键盘没反应的时候存进缓存的数据也一股脑全部冒了出来(任务b---->任务a成功!)

三 多任务操作优化

【1】优化点一:重写一个farjump函数来替代原来的taskswitch3、taskswitch4:

_farjump:		; void farjump(int eip, int cs);
		JMP		FAR [ESP + 4]		; eip, cs
		RET

        同时在任务a和任务b中,分别准备一个timer变量叫做timer_ts(task switch),以便每隔 0.02s 切换一次:

/* HariMain */
if (i == 2) {
	farjmp(0, 4 * 8);
    // 任务返回之后,都将计时器设定为 0.02s 之后
	timer_settime(timer_ts, 2);
}

/* task_b_main */
if (i == 1) { 
    // task switch
    farjmp(0, 3 * 8);
    timer_settime(timer_ts, 2);
}

【2】任务b中增加打印来显示确实任务切换了过来:

/* HariMain 中使用 0x0fec 这个地址将 sht_back 这个变量传给任务 B */
*((int *) 0x0fec) = (int) sht_back;

/* task_b_main */
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);
    // ...
}

【3】上面任务b的操作,每计一个数都会打印,这个伴随有操作内存的操作,过于频繁地执行会是程序变慢,处理办法是设置定时器,达到超时才会执行打印:

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);
	// every 0.01s
	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);
				}
			}
		}
	}
}

四 多任务操作进阶

        截至目前,多任务的一些基本的切换操作已经具备,但是设想一下这个场景,比如说程序切换到任务b,但在任务b运行的过程中程序挂死,那么任务b就不会切换回其他的任务,这样单个任务异常,会使得整个程序挂死,这样会影响到程序的稳定性

        解决办法是,将跳转的逻辑封装之后从HariMain与task_b_main中抽离出来,放到定时器中断inthandler20中(关于该中断的描述参考【操作系统】30天自制操作系统--(11)定时器1),定时器中断中检查是否是多任务定时器,如果是,则直接进行切换,这样就避免了各个子任务异常对于程序整体的影响:

【1】封装子任务切换逻辑 mt_taskswitch

#include"bootpack.h"

struct TIMER *mt_timer;
int mt_tr;

void mt_init(void){
    mt_timer = timer_alloc();
    // 没必要加上 timer_init() 是因为超时时不需要向 FIFO 写数据
    timer_settime(mt_timer, 2);
    mt_tr = 3 * 8;                //初始化为子任务a
    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】定时器中断 inthandler20 中调用 mt_taskswitch(这边在中断的尾巴调用的原因在于:调用 mt_taskswitch() 进行任务切换的时候,即便中断处理还没有完成,IF(interrupt flag) 也有可能被重置为 1 (因为任务切换时同时也会切换 EFLAGS),而中断处理还没完成的时候产生中断显然是不允许的):

void inthandler20(int *esp){
    // 把IRQ-00接收信号结束的信息通知给PIC 
    io_out8(PIC0_OCW2, 0x60);	
	timerctl.count++;
	if (timerctl.next > timerctl.count) {
		return;
	}
    
    struct TIMER *timer = timerctl.t0;
    char ts = 0;
    while (1) {
        // timers的计时器全部在工作中,因此不用确认flags 
        if (timer->timeout > timerctl.count) {
            break;
        }
        timer->flags = TIMER_FLAGS_ALLOC;

		if (timer != mt_timer) {
            fifo32_put(timer->fifo, timer->data);  //(1)不是mt_timer就往缓存中写数据
        } else {
            ts = 1;                                //(2)是mt_timer就是达了切换的时候
        }

        timer = timer->next;
    } 
    timerctl.t0 = timer;
    // fucking irritating ...
    timerctl.next = timer->timeout;

    if (ts != 0) {
        mt_taskswitch();    //子任务切换
    }

    return;
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值