自制操作系统日志——第十五天

自制操作系统日志——第十五天

从今天开始,让我们进入到多任务的制作。



前言

多任务,在英文中为 multitask,即多个任务同时进行。即在宏观上来说,我们感觉到这些应用程序是同时运行的。但是微观上来看,若我们只有一个cpu的情况下,这些应用程序是按串行形式进行的,只是我们要求在尽可能短的时间内进行切换任务,这才会使得我们看起来是同时运行的状况。

一般来说,切换动作大约每秒0.01~0.03左右。太短的话会导致cpu的处理能力主要在切换任务上面,而使得程序本身无法良好的运行了!(当然随着cpu性能的提高,可能切换时间会越来越短吧)


一、任务切换

当cpu发出任务切换指令时,cpu会先将当前寄存器中的值全部写入内存之中,然后为了运行下一个程序,又会从对应的内存地址中,读入该程序存储的寄存器等等的相关信息。

因此,以下我们将建立一个结构体 TSS 任务状态段,以便进行任务切换时所需的信息:

struct TSS32//共26个int成员,104字节。摘自cpu技术资料里的设定
{
	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;//段寄存器的值。段寄存器16位,但是预先设定32,以防止为来说不定是32了
	int ldtr, iomap;//任务设置部分。先设置ldtr=0 ,iomap=0x40000000
};

然后,要想进行任务切换的话还需要我们使用jmp指令。 jmp这条指令我们在第0天的汇编基础知识里讲过,当它近转移的时候,仅改变eip的值,当它远转移的时候会改变cs和eip的值。

若一条jmp指令指向的目标段不是代码段而是TSS的话,则代表进行的是任务切换!也就是说转去到一个任务当中了。。
cpu每一次执行带有段地址的指令时,都会去GDT中确认相关设置,以判断是普通的跳转还是执行任务切换!!不过, 在汇编来看是一样的指令,即jmp far xxx。但是cpu会有所区分的!

创建两个任务窗口,并赋予初值,以及在gdt中注册:

struct TSS32 tss_a, tss_b;
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
略:
    tss_a.ldtr = 0;
	tss_a.iomap = 0x40000000;
	tss_b.ldtr = 0;
	tss_b.iomap = 0x40000000;
	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寄存器,记录当前运行哪一个任务,赋值时一定是GDT编号乘以8
  	task_b_esp = memman_alloc_4k(memman, 64*1024) + 64 * 1024;//栈空间开始地址,栈的末尾地址
	tss_b.eip = (int) &task_b_main;//切换后从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; //给代码段设置的是和bootpack.c一样的段地址
	tss_b.ss = 1 * 8;
	tss_b.ds = 1 * 8;
	tss_b.fs = 1 * 8;
	tss_b.gs = 1 * 8;

	for(;;)   
	{else if (i == 10) { /*10秒计时器 */
	 putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
	 taskswitch4();
	 }}

然后我们设置tss_b_main的代码,以便进行切换:

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

以上Bootpack.c的添加的代码意思是,在运行10s后切到任务b,在任务b中5s后返回任务a(即主函数中),接下来我们增加以下naskfunc.nas:

_load_tr: ;void load_tr(int tr)
    LTR [esp+4] ;LTR改变TR寄存器的值
	RET

_taskswitch3: ; void taskswotch3(void)
    JMP 3*8:0
	RET

_taskswitch4: ; void taskswotch4(void)
    JMP 4*8:0
	RET

TR寄存器,用于让cpu记住当前执行的是哪一个任务。其值必须是GDT号 x 8 。

这里,有一点的是我们进行切换的代码里 jmp后的偏移地址写的是0 。但是在切换任务的里这一点并不重要!!这是因为当我们切换过后,再回到原来的任务时,会回到当初的地点。比如我们使用taskswitch4切换的跳转后,再用taskswitch3切换回去时,就会从taskswitch4的jmp后的指令继续执行。任何,执行ret回到c语言当中!!!
这里就是识别到了是任务切换,因此切换回去后,会先从在切换时保存相关信息的那个地址处取出先前的数据的!

除此之外,需要解释一点的是当我们切换到任务b的那个代码时候,此时已经不在任务a的代码段中了,因此在b中设置的任务缓冲区,也不会影响到a。因此,当我们切换到b后,即使输入字符也不会有任何反应(但是键盘的中断已经发生,并将数据发送到了对应的键盘缓冲区域),只是b里没有对应的对中断做出反应的代码,因此不会有任何反应。 但是在5s之后,我们回到了主程序里,此时之前写入的缓冲数据就会被读出!!

在这里插入图片描述
不过这里可能看不出来(笑)。。。

二、简单的多任务

在前面,我们已经学会了如何去切换任务的手法了,那么接下来让我们彻底的实现一下简易的多任务运行吧!!!

这里,我们拟采用tss_a与tss_b之间进行交互的运行,这样子我们就可以切实的感受到程序确实是同时在运行了!!

首先,为方便切换任务,我们摒弃前面的switch这个函数,采用一个统一的切换函数:
naskfunc.nas:

_farjmp: ; void farjmp(int eip, int cs)
    JMP far [ESP+4] ;高地址是cs,低地址是eip
	RET

至此,taskswitch4 = farjmp(0, 4*8)

然后,我们进一步的修改运行主函数:
建立一个timer_ts变量,以便每个0.02s切换一次任务!!

略
	timer_ts = timer_alloc();
	timer_init(timer_ts, &fifo, 2);
	timer_settime(timer_ts, 2);//每隔0.02s切换一次
	*((int *) 0x0fec) = (int) sht_back;for(;;)   
	{
		io_cli(); //IF=0
		if (fifo32_status(&fifo) == 0)
		{
			io_stihlt();
		}else 
		{ 
			i = fifo32_get(&fifo);
			io_sti();
			if(i == 2)
			{
				farjmp(0, 4 *8);
				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);//任务A首先将自己的bufback地址写入到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();
			io_hlt();
		}else {
			i = fifo32_get(&fifo);
			io_sti();
			if(i ==1)
			{
				farjmp(0, 3 *8 );
				timer_settime(timer_ts, 2);
			}
		}
	}
}

在这里插入图片描述
成功了!!!这两个任务真的同时在进行, 有点兴奋 嘿嘿嘿!!

优化速度:

虽然在上面,我们已经成功的实现了多任务的运行,但是这里我们自信看看会发现这里的计数实在是太慢了! 因此,我们接下来优化一下,并解决前面的不同任务间的sht_back的传递问题。

  • 上述代码之所以计数较慢,是因为我们每计一个数就在画面上显示一次,但是一秒钟刷新上百次我们根本就看不出来,因此我们需要更改一下,设置每个0.01s刷新一次即可。

  • 还记得我们前面部分曾经说过,c语言的函数传参在汇编语言里实际上是放在一段内存空间空间中,而后利用[esp]这个寄存器指向该内存位置进行读取传递的!!既然如此,这里我们也可以利用这种思想: 由于我们建立的TSS,在进行任务切换时,cpu会读取该结构体中的esp内容,因此若我们事先将sht_bak的地址事先放在esp指向的那个地址,那么我们就可以利用这种传参的方式欺骗到了tss_b_main函数了。

以下进行修改对应的代码:

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[11];

	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, 10);
			    timer_settime(timer_put, 1);
			}else if (i ==2)
			{
				farjmp(0, 3 *8 );
				timer_settime(timer_ts, 2);
			}
		}
	}
}

主函数:

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

然后,运行:
在这里插入图片描述
发现这速度飞涨啊!!!

三、真正的多任务

此前,我们所作的多任务机制是一问题!!! 问题就在于,我们做到简易的多任务系统,其实是靠程序本身所带动的,但是这种情况不太好。譬如,假设我们的b程序,如果卡死了那么我们就无法切换到a程序运行了,这样子就很有可能因为一个任务而导致全盘崩溃。

因此,我们下面要做一个与程序无关的,是让程序本身不知道的情况下进行的多任务管理机制。那么我们就需要利用到中断程序的20号,也就是计时器的功能了:

首先,让我们建立mtask.c文件,并写入:

//多任务
#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;
    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;
}

然后,对timer.c进行改写:

void inthandler20(int *esp)
{
    struct TIMER *timer;
    char ts = 0;
    io_out8(PIC0_OCW2, 0x60);//IRQ-0信号接收完后告知PIC
    timerctl.count++;
    if(timerctl.next_time > timerctl.count)
    {
        return; //还不到下一个时刻,因此返回
    }
    timer = timerctl.t0;//先把最前面的赋予给timer
	for (;;) {// timers的定时器都是活动中的因此不需要确认flags
        if(timer->timeout > timerctl.count )
        {
            break;
        }
        //超时
        timer->flags = TIMER_FLAGS_ALLOC;
        if(timer != mt_timer)//如果产生超时的是mt_timer 就ts = 1
        {
            fifo32_put(timer->fifo, timer->data);
        }else{
            ts = 1 ;//mt_timer超时
        }
		timer = timer->next_timer;//将下一定时器地址赋予给timer
    }
    //新移位
    timerctl.t0 = timer;
    //timerctl.next的设定
	timerctl.next_time = timer->timeout;
    if(ts != 0){
		mt_taskswitch();
    }
	return;
}

这里,我们之所以利用一个ts变量来进行切换,目的是为了防止如果直接在else那里进行切换的话,可能会导致下面的移位操作无法完成(因为可能会有别的中断进来打扰)。

最后修改以下bootpack.c , 将之前的有关任务切换的代码都删掉:



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

	fifo32_init(&fifo, 128, fifobuf);
	timer_put = timer_alloc();
	timer_init(timer_put, &fifo, 1);
	timer_settime(timer_put, 1);
	timer_1s = timer_alloc();
	timer_init(timer_1s, &fifo, 100);
	timer_settime(timer_1s, 100);


	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, 10);
			    timer_settime(timer_put, 1);
			}else if (i == 100) {
			sprintf(s, "%11d", count - count0);
			putfonts8_asc_sht(sht_back, 0, 128, COL8_FFFFFF, COL8_008484, s, 11);
			count0 = count;
			timer_settime(timer_1s, 100);
		     }
	     }
    }
}

主函数里的就不列出来了,自行查看源代码。
在这里插入图片描述


总结

以上就是我多任务的第一部分的内容,感觉越来越有趣了,啊哈哈哈!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值