自制操作系统日志——第十五天
从今天开始,让我们进入到多任务的制作。
前言
多任务,在英文中为 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);
}
}
}
}
主函数里的就不列出来了,自行查看源代码。
总结
以上就是我多任务的第一部分的内容,感觉越来越有趣了,啊哈哈哈!