前言
大家好!昨天我们已经实践了很多关于多任务的内容,不过今天我们还得继续讲多任务。可 “老是讲多任务都听腻了啊!”,但多任务真的非常重要(当然,如果你不想做一个多任务的操作系统那就不重要啦)。从咱们制作的操作系统角度来说, 希望大家能够在充分做好多任务机制的基础上,再利用多任务逐步完善操作系统本身。因此,大家再稍微忍耐一下吧。 在昨天,我们已经实现了真正的多任务,不过这样还不够完善,或者说不太好用。如果我们想要运行三个任务的话,就必须改写mt_taskswitch的代码。这样的设计实在太逊了,如果能像当初定时器和窗口背景的做法一样(具体如下),是不是觉得更好呢?
task=task_alloc()
task->tss.eip = OX;
tagk->tss.esp = OI;
像上面这样设定各种考存器的初始值
task_run(task):
对代码进行改造吧。 我们就先以此为目标,
一、任务管理自动化
于是我们写了下面这样一段程序,struct TASKCTL是仿照struct SHTCTL写出来的,首先我们来看结构定义。
/* mtask.c */
#define MAX_TASKS 1000 /* 最大任务数量 */
#define TASK_GDT0 3 /* 定义从GDT的几号开始分配给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;
};
struct TASK {
int sel, flags; /* sel用来存放GDT的编号 */
struct TSS32 tss;
};
struct TASKCTL {
int running; /* 正在运行的任务数量 */
int now; /* 记录当前正在运行的是哪个任务 */
struct TASK *tasks[MAX_TASKS];
struct TASK tasks0[MAX_TASKS];
};
下面我们来创建用来对struct TASKCTL及其当中包含的struct TASK进行初始化的函数 task_init。由于struct TASKCTL是一个很庞大的结构, 因此我们让它从memman_alloc来申请内存空间。这个函数是用来替代mt_init使用的。我们使用sel这个变量来存放GDT编号,sel是“selector”的缩写,意为选择符。因为英特尔的巨佬管段地址叫做selector,也就是代表“应该从GDT里面选择哪 个编号”的意思。
struct TASKCTL *taskctl;
struct TIMER *task_timer;
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;
}
调用task_init, 会返回一个内存地址,意思是 “现在正在运行的这个程序,已经变成一个任务了” 。可能大家不是很能理解这个说法,在调用init之后,所有程序的运行都会被当成任务来进行管理,而调用init的这个程序,我们也要让它所属于某个任务,这样一来,通过调用任务的设置函数,就可以对任务进行各种控制, 比如说修改优先级等。
下面我们来创建用来初始化一个任务结构的函数。
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; /* 先置为 0 */
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;
}
关于寄存器的初始值,这里先随便设置了一下。如果不喜欢这个值,可以在bootpack.c里面 设置一下。
接下来是task_run, 这个两数非常短, 大家看看这样写如何。
void task_run(struct TASK *task)
{
task->flags = 2; /* 处在活动中的标志 */
taskctl->tasks[taskctl->running] = task;
taskctl->running++;
return;
}
最后是task_switch, 这个离数用来代替mt_taskswitch。 在timer.c中对mt_taskswitch的调用, 也相应地修改为调用task_switch()。
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;
}
当running是1时,不需要进行任务切换, 函数直接结束。 当running大于等于2时, 当running为1时, 先把now加1,判断是不是最后一个任务,是的话就返回到最前面,从头开始。然后jump到对应任务。
现在我们用以上这些结构和函数, 将bootpack.c改写一下:
void HariMain(void)
{
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);
}
行数变少了,不过相应地mtask.c却变长了,好像也不能说是非常好。不过好在,在HariMain中,就再也不用管GDT到底怎样、任务B的tss要分配到GDT的几号等。
这些麻烦的事情,全部交给mtask.c来处理了。当需要增加任务数量的时候,不用再像之前那样修改task_ switch了,只要先task_alloc,然后再task_run就行了。
好了,我们来运行一下, make run – 不错,貌似成功了。
二、让任务休眠
直到harib13a为止,我们所实现的多任务,是为两个任务分配大约相同的运行时间。这也不 能说是不好,不过相比之下,任务A明显空闲的时间比较多。没有键盘输入、没有鼠标操作也不 会经常出现定时器中断这样的情况,这个时候任务A没什么事做,就只好HLT了。 HLT的时候能省电,也不错嘛!不过当任务B全力以赴拼命干活的时候,任务A却在无所事事, 这样好像不太好。与其让任务A闲着没事干,还不如把这些时间分给繁忙的任务B呢。 那么怎样才能避免任务A浪费时间呢?如果我们不让任务A去HLT,而是把它从taskctl →tasks[]中删掉的话,嗯,这应该是一个不错的主意。如果把任务A从tasks中删掉,只保留任务 B,那任务B就可以全力以赴工作了。像这样将一个任务从tasks中删除的操作,用多任务中的术语来说叫做“休眠”(sleep)。
不过这样也有一个问题,当任务A休眠时,即便FIFO有数据过来,也无法响应了,这可不行, 当FIFO有数据过来的时候,必须要把任务A唤酯 。怎么唤醒呢?其实只要再运行一次task_run就可以了。
首先我们创建task_sleep:
void task_sleep(struct TASK *task)
{
int i;
char ts = 0;
if (task->flags == 2) { /* 如果指定任务处于唤醒状态 */
if (task == taskctl->tasks[taskctl->now]) {
ts = 1; /* 让自己休眠的话,稍后需要进行任务切换 */
}
/* 寻找task所在的住置 */
for (i = 0; i < taskctl->running; i++) {
if (taskctl->tasks[i] == task) {
/* 这里找到 */
break;
}
}
taskctl->running--;
if (i < taskctl->now) {
taskctl->now--; /* 需要移动成员,要相应地处理 */
}
/* 移动成员 */
for (; i < taskctl->running; i++) {
taskctl->tasks[i] = taskctl->tasks[i + 1];
}
task->flags = 1; /* 不工作的状态 */
if (ts != 0) {
/* 任务切换 */
if (taskctl->now >= taskctl->running) {
/* 到最后一个,从头开始 */
taskctl->now = 0;
}
farjmp(0, taskctl->tasks[taskctl->now]->sel);
}
}
return;
}
接下来是当FIFO中写入数据的时候将任务唤醒的功能。首先 我们要在FIFO的结构定义中, 添加用于记录要唤醒任务的信息的成员。
struct FIFO32 {
int *buf;
int p, q, size, free, flags;
struct TASK *task;
};
让它可以在参数中指定一个任务。如果不想使用任务自动唤醒功 然后我们改写ffo32_init, 能的话,只要将task置为0即可。
void fifo32_init(struct FIFO32 *fifo, int size, int *buf, struct TASK *task)
/* FIFO缓冲区初始化 */
{
fifo->size = size;
fifo->buf = buf;
fifo->free = size; /* 剩余 */
fifo->flags = 0;
fifo->p = 0; /* 写入位置u */
fifo->q = 0; /* 读取位置 */
fifo->task = task; /* 有数据写入时需要唤醒的任务 */
return;
}
接着,我们来实现当向FIFO写人数据时,唤醒某个任务的功能。
int fifo32_put(struct FIFO32 *fifo, int data)
/* 向FIFO写入数据并累积起来 */
{
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); /* 将任务唤醒 */
}
}
return 0;
}
我们追加了5行代码。在这里如果任务已经处于唤醒状态的话,再次对其task run是不行的(会 造成任务重复注册),因此我们需要先确认该任务是否处于休眠状态,然后再将其唤醒。
最后我们来改写HariMain:
void HariMain(void)
{
struct TASK *task_a, *task_b;
task_a = task_init(memman);
fifo.task = 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)
{
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, 0);
...
}
最开始的fifo32_init中指定任务的参数,我们用0代替了。因为我们在任务A中应用了休眠,也就需要使用让FIFO来唤醒的功能,不过在这个 时间点上多任务的初始化还没有完成,因此无法指定任务,只能先在这里用0代替, 也就是禁用自动唤醒功能。 随后, 在task init中会返回自己的构造地址,我们将这个地址存入fifo.task。 这样一来,当FIFO为空的时候,任务A将执行task_sleep来替代之前的HLT。关于io_sti和task_sleep的顺序,需要稍微动点脑筋。如果先STI的话,在进行休眠处理的时候可能会发生中断请求,FIFO里面就会写入数据,这样有可能会发 生无法成功唤醒等异常情况。因此,我们需要在禁止中断请求的状态下进行休眠处理,然后在唤醒之后马上执行STI。 task_b_main不需要让FIFO唤醒,因此任务参数指定为0。
那么, 这样做是否能成功呢, “make run”,哇, 速度很快。请注意看速度显示的数字。 我们来试试看:
三、增加窗口数量
刚刚我们已经对任务的新增做了简化,因此接下来我们要为系统增加更多的任务,即形成任务A、任务B0、任务B1和任务B2的格局。任务B0~B2各自拥有自己的窗口,它们的功能都一样,即进行计数,这有点像在 Windows 中启动了一个应用程序及其2个副本的感觉。对了,任务A的3秒定时和10秒定时我们已经不需要了,因此将它们删去。
void HariMain(void)
{
unsigned char *buf_back, buf_mouse[256], *buf_win, *buf_win_b;
struct SHEET *sht_back, *sht_mouse, *sht_win, *sht_win_b[3];
struct TASK *task_a, *task_b[3];
struct TIMER *timer;
...
init_palette();
shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
task_a = task_init(memman);
fifo.task = task_a;
/* sht_back */
sht_back = sheet_alloc(shtctl);
buf_back = (unsigned char *) memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1);
init_screen8(buf_back, binfo->scrnx, binfo->scrny);
/* sht_win_b */
for (i = 0; i < 3; i++) {
sht_win_b[i] = sheet_alloc(shtctl);
buf_win_b = (unsigned char *) memman_alloc_4k(memman, 144 * 52);
sheet_setbuf(sht_win_b[i], buf_win_b, 144, 52, -1);
sprintf(s, "task_b%d", i);
make_window8(buf_win_b, 144, 52, s, 0);
task_b[i] = task_alloc();
task_b[i]->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
task_b[i]->tss.eip = (int) &task_b_main;
task_b[i]->tss.es = 1 * 8;
task_b[i]->tss.cs = 2 * 8;
task_b[i]->tss.ss = 1 * 8;
task_b[i]->tss.ds = 1 * 8;
task_b[i]->tss.fs = 1 * 8;
task_b[i]->tss.gs = 1 * 8;
*((int *) (task_b[i]->tss.esp + 4)) = (int) sht_win_b[i];
task_run(task_b[i]);
}
/* sht_win */
sht_win = sheet_alloc(shtctl);
buf_win = (unsigned char *) memman_alloc_4k(memman, 160 * 52);
sheet_setbuf(sht_win, buf_win, 144, 52, -1);
make_window8(buf_win, 144, 52, "task_a", 1);
make_textbox8(sht_win, 8, 28, 128, 16, COL8_FFFFFF);
cursor_x = 8;
cursor_c = COL8_FFFFFF;
timer = timer_alloc();
timer_init(timer, &fifo, 1);
timer_settime(timer, 50);
/* sht_mouse */
sht_mouse = sheet_alloc(shtctl);
sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99);
init_mouse_cursor8(buf_mouse, 99);
mx = (binfo->scrnx - 16) / 2;
my = (binfo->scrny - 28 - 16) / 2;
sheet_slide(sht_back, 0, 0);
sheet_slide(sht_win_b[0], 168, 56);
sheet_slide(sht_win_b[1], 8, 116);
sheet_slide(sht_win_b[2], 168, 116);
sheet_slide(sht_win, 8, 56);
sheet_slide(sht_mouse, mx, my);
sheet_updown(sht_back, 0);
sheet_updown(sht_win_b[0], 1);
sheet_updown(sht_win_b[1], 2);
sheet_updown(sht_win_b[2], 3);
sheet_updown(sht_win, 4);
sheet_updown(sht_mouse, 5);
sprintf(s, "(%3d, %3d)", mx, my);
putfonts8_asc_sht(sht_back, 0, 0, COL8_FFFFFF, COL8_008484, s, 10);
sprintf(s, "memory %dMB free : %dKB",
memtotal / (1024 * 1024), memman_total(memman) / 1024);
putfonts8_asc_sht(sht_back, 0, 32, COL8_FFFFFF, COL8_008484, s, 40);
for (;;) {
io_cli();
if (fifo32_status(&fifo) == 0) {
task_sleep(task_a);
io_sti();
} else {
i = fifo32_get(&fifo);
io_sti();
if (256 <= i && i <= 511) {
} else if (512 <= i && i <= 767) {
} else if (i <= 1) {
}
}
void make_window8(unsigned char *buf, int xsize, int ysize, char *title, char act)
{
int x, y;
char c, tc, tbc;
if (act != 0) {
tc = COL8_FFFFFF;
tbc = COL8_000084;
} else {
tc = COL8_C6C6C6;
tbc = COL8_848484;
}
...
}
void task_b_main(struct SHEET *sht_win_b)
{
struct FIFO32 fifo;
struct TIMER *timer_1s;
int i, fifobuf[128], count = 0, count0 = 0;
char s[12];
fifo32_init(&fifo, 128, fifobuf, 0);
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 == 100) {
sprintf(s, "%11d", count - count0);
putfonts8_asc_sht(sht_win_b, 24, 28, COL8_000000, COL8_C6C6C6, s, 11);
count0 = count;
timer_settime(timer_1s, 100);
}
}
}
}
这次的代码相当长, 不过我们只是对之前的程序进行了整理, 难度并没有提高。不要被代码的长度吓倒, 只要静下心来仔细读一读, 应该很快就会理解的。
在make_ window8中我们增加了一个act变量。 当act为1时, 颜色不变,当为 0 时,窗口的标题栏(就是显示窗口名称的地方)会变成灰色。task b_main中, 去掉了每0.01秒显示一次count的部分,只保留每1秒显示速度的功能。
我们来运行一下, “make run”。怎么样,是不是感觉更像操作系统了呢?对了,现在只有任务A的窗口可以移动(因为移动窗口的部分没有改写进去)。任务B0~B2这3个任务基本上是以同样的速度在运行。在模拟器环境下它们的速度会有点差异。
四、设定任务优先级
1.0版本
任务B0 ~ B2以同样的速度运行, 从公平竞争的角度来说确实不错, 不过在某些情况下,我们需要提升或者降低某个应用程序的优先级, 因此接下来我们要来实现这样的功能。在此之前,任务切换间隔都固定为了0.02秒,我们把它修改一下,使得可以为每个任务在0.01 秒~0.1秒的范围内设定不同的任务切换间隔, 这样一来,我们就能实现最大10倍的优先级差异。
struct TASK {
int sel, flags; /* sel代表GDT编号 */
int priority; /* 这里 */
struct TSS32 tss;
};
变量名 priority 是 “优先级” 一词的英文写法。
为了应用上面的新结构, 我们需要改写mtask.c
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; /* 处于活动中的标志 */
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_init的改写很简单,没有什么需要特别说明的地方。在这里, 我们给最开始的任务设定了0.02秒这个标准值。
接下来是用来运行任务的task_run, 我们让它可以通过参数来设定优先级。
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;
}
上面的代码中,一开始我们先判断了priority的 值,当为0时则表示不改变当前已经设定的优先级。这样的设计主要是为了在唤醒休眠任务的时候使用。此外,即使该任务正在运行, 我们也能使用task_run仅改变任务的优先级。
接着是task_ switch,我们要让它在设置定时器的时候,应用priority的值。
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);
if (taskctl->running >= 2) {
farjmp(0, task->sel);
}
return;
}
当只有一个任务的时候,如果执行farjmp(0, task->sel);的话, 虽然不会真的切换但确实是发出了任务切换的指令。这时CPU会认为“操作系统怎么会做这种毫无意义的事情呢?这一定是操 作系统的bug!”因而拒绝执行该指令,程序运行就会乱套。所以我们需要在farjmp之前,判断任 务数量是否在2个以上。 这样一来,对mtask.c的改写就OK了。
现在我们来改写fifo.c。从休眠状态唤醒任务的时候需要调用task_run, 我们这次主要就是改写这个地方。说白了,其实我们只是将任务唤醒 ,并不改变其优先级,因此只要将优先级设置为 0 就可以了。
int fifo32_put(struct FIFO32 *fifo, int data)
{
...
fifo->free--;
if (fifo->task != 0) {
if (fifo->task->flags != 2) { /* 处于活动中的标志 */
task_run(fifo->task, 0); /* 唤醒任务 */
}
}
return 0;
}
最后我们来改写一下HarMain, 做一做改变优先级的实验。
ivoid HariMain(void)
{
...
/* sht_win_b */
for (i = 0; i < 3; i++) {
sht_win_b[i] = sheet_alloc(shtctl);
buf_win_b = (unsigned char *) memman_alloc_4k(memman, 144 * 52);
sheet_setbuf(sht_win_b[i], buf_win_b, 144, 52, -1); /
sprintf(s, "task_b%d", i);
make_window8(buf_win_b, 144, 52, s, 0);
task_b[i] = task_alloc();
task_b[i]->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
task_b[i]->tss.eip = (int) &task_b_main;
task_b[i]->tss.es = 1 * 8;
task_b[i]->tss.cs = 2 * 8;
task_b[i]->tss.ss = 1 * 8;
task_b[i]->tss.ds = 1 * 8;
task_b[i]->tss.fs = 1 * 8;
task_b[i]->tss.gs = 1 * 8;
*((int *) (task_b[i]->tss.esp + 4)) = (int) sht_win_b[i];
task_run(task_b[i], i + 1);/* 这里 */
}
...
}
我们为任务B0设置为优先级1,任务B1为2, 任务B2为3, 因此B2应该是最快的, 而B0应该 是最慢的,其速度的差异应该正好是3倍的关系。马上“make run”
如果以任务B0为基准来看,任务B1为1.96 倍,任务B2为2.7倍。虽然不是完美的整数倍这一点令人有些不爽,不过用C语言写的程序,多多少少会有些误差。总之,这样的结果已经达到我们的目的了。
2.0版本
如果多把玩一下harib13d, 会发现鼠标的移动速度好像有点慢,尤其是拖住窗口快速移动的时候,反应很糟糕。模拟器环境下手已经放开鼠标了可它还在动。 问题的原因在于,其他任务的运行造成任务A运行速度变慢,从而导致了上述情形。要解决 这个问题,只要把任务A的优先级调高就可以了,调到最高值10的话,情况应该会有所改善吧。 我们在HariMain中将启动任务A的部分改为task_run(task_a, 10);来试试看,果然速度回到从前的样子了。 有人可能会担心,如果把优先级设为10,那其他任务的运行速度不就变慢了吗?不会的。因为当任务A空闲的时候会自动休眠, 任务A的优先级无论设置得多高,也一点都不会浪费时间, 只要进入休眠状态,即从tasks中删除后,优先级的设置就毫无影响了,其他任务的运行速度也就和之前没什么区别了。
上述例子说明,任务优先级是个很好用的东西。我们不妨把这个例子来总结一下:在操作系统中有一些处理,即使牺牲其他任务的性能也必须要尽快完成,否则会引起用户的不满,就比如 这次对鼠标的处理。对于这类任务,我们可以让它在处理结束后马上休眠,而优先级则可以设置 得非常高。 这种宁可牺牲其他任务性能也必须要尽快处理的任务可不是只有鼠标一个,比如键盘处理也 是一样,网络处理应该也属于这类(如果速度太慢的话可能会丢失数据哦)。播放音乐也是,如 果音乐播放任务的优先级太低的话,音乐就会一卡一卡的。 我们当然可以把这些任务的优先级都设置成10,不过真这样做的话,当它们之中的两个以上 同时运行的时候,可能还是会出问题。如果拿音乐和鼠标做比较,那应该是音乐更重要吧。因为 如果发生“在播放音乐的时候移动窗口,音乐卡住”这种情况,用户肯定会觉得超级不爽的。相 比之下, 哪怕鼠标的反应稍微慢些,我们也要保证音乐播放的质量。 然而按照现在的设计,当优先级为10的两个任务同时运行时,优先哪个就全凭运气了,任务 切换先轮到谁谁就赢了。运气好的那个任务可以消耗很多时间来完成它的工作,而另外一个优先 级同样是10的任务就只能等待了。也就是说, 如果运气不好, 音乐播放就会变得一团糟,而这样的操作系统显然不怎么好用。
因此我们需要设计一种架构,使得即便高优先级的任务同时运行,也能够区分哪个更加优先。 其实也没有架构那么复杂,基本上就是创建了几个struct TASKCTL。 个数随意,多少都行, 我们在这里先按创建3个来讲解。
这种架构的工作原理是,最上层的LEVEL0中只要存在哪怕一个任务,则完全忽略LEVEL1 和LEVEL2中的任务,只在LEVEL0的任务中进行任务切换。 当LEVEL0中的任务全部休眠,或也就是当LEVEL0中没有任何任务的时候, 者全部降到下层LEVEL, 接下来开始轮到LEVEL 1 中的任务进行任务切换。 当LEVEL 0和LEVEL1中都没有任务时,那就该轮到LEVEL2出场了。 在这种架构下,只要把音乐播放任务设置在LEVEL0中,就可以保证获得比鼠标更高的优先级。
实际上,我们不需要创建多个TASKCTL,只要在TASKCTL中创建多个tasks[]即可。
struct TASK {
int sel, flags; /* se1用来存放GDT的编号 */
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 TASKLEVEL的函数, 首先, task_run 和task_sleep会变得冗长难懂。其中task_now函数,用来返回现在活动中的struct TASK的内存地址。
struct TASK *task_now(void)
{
struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
return tl->tasks[tl->now];
}
task_add函数,用来向struct TASKLEVEL中添加一个任务
void task_add(struct TASK *task)
{
struct TASKLEVEL *tl = &taskctl->level[task->level];
tl->tasks[tl->running] = task;
tl->running++;
task->flags = 2; /* 活动标志 */
return;
}
实际上,这里应该增加if(tl->running < MAX_TASKS_LV)等,这可以判断在一个LEVEL 中是否错误地添加了100个以上的任务,不过我们把它省略了,不好意思,偷个懒。
task_remove函数,用来从struct TASKLEVEL中删除一个任务。
void task_remove(struct TASK *task)
{
int i;
struct TASKLEVEL *tl = &taskctl->level[task->level];
/* 寻找task所在的位置 */
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) {
/* 如果now的值出现异常,则进行修正 */
tl->now = 0;
}
task->flags = 1; /* 休眠 */
/* 移动 */
for (; i < tl->running; i++) {
tl->tasks[i] = tl->tasks[i + 1];
}
return;
}
上面的代码基本上是照搬了task sleep的内容。
task_switchsubs函数,用来在任务切换时决定接下来切换到哪个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 TASKLEVEL相关的函数已经差不多都写好了,准备工作做到这里,接下来的事情就简单多了。下面我们来改写其他一些函数, 首先是task_ init。最开始的任务,我们先将它放在LEVEL0, 也就是最高优先级LEVEL中。这样做在有些情况下可能会有问题,不过后面可以再用task_run重新设置, 因此不用担心。
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);
}
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; /* 0.02秒 */
task->level = 0; /* 最高LEVEL */
task_add(task);
task_switchsub(); /* KEVEL设置 */
load_tr(task->sel);
task_timer = timer_alloc();
timer_settime(task_timer, task->priority);
return task;
}
开始的时候只有LEVEL0中有一个任务,因此我们按照这样的方式来进行初始化。
我们要让它可以在参数中指定LEVEL。下面是task_run
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) { /* 改变活动中的LEVEL */
task_remove(task); /* 这里执行之后flag的值会变为1,于是下面的if语句块也会被执行 */
}
if (task->flags != 2) {
/* 从休眠状态唤醒的情形 */
task->level = level;
task_add(task);
}
taskctl->lv_change = 1; /* 下次任务切换时检查LEVEL */
return;
}
在此之前,task_run中下一个要切换到的任务是固定不变的,不过现在情况就不同了。例如, 如果用task_run启动了一个比现在活动中的任务LEVEL更高的任务,那么在下次任务切换时,就必须无条件地切换到该LEVEL中的该任务去。
此外,如果当前活动中的任务LEVEL被下调,那么此时就必须将其他LEVEL的任务放在优先的位置(同样以上图来说的话,比如当LEVEI 0的任务被降级到LEVEL2时,任务切换的目标就需要从LEVEL0变为LEVEL 1)。
所以哦,我们需要在下次任务切换时先检查LEVEL,因此将Iv_change置为1。
接下来是task sleep, 在这里我们可以调用task_remove, 因此代码会大大缩短。
void task_sleep(struct TASK *task)
{
struct TASK *now_task;
if (task->flags == 2) {
/* 如果处于活动状态 */
now_task = task_now();
task_remove(task); /* 执行此语句的话flags将变为1 */
if (task == now_task) {
/* 如果是让自己休眠,则需要进行任务切换 */
task_switchsub();
now_task = task_now(); /* 在设定后获取当前任务的值 */
farjmp(0, now_task->sel);
}
}
return;
}
mtask.c的最后是task_switch,除了当lv_ change不为0时的处理以外,其余几乎没有变化。
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;
}
fifo.c也需要改写一下,不过和上一节一样, 只是将唤醒休眠任务的task run稍稍修改一下而 已。 优先级和LEVEL都不需要改变,只要维持原状将任务唤醒即可。
int fifo32_put(struct FIFO32 *fifo, int data)
{
...
fifo->free--;
if (fifo->task != 0) {
if (fifo->task->flags != 2) { /* 如果任务处于休眠状态 */
task_run(fifo->task, -1, 0); /* 将任务唤醒 */
}
}
return 0;
}
最后我们来改写HariMain,可到底该怎么改呢? 我们就暂且将任务A设为LEVEL1,任务B0 B2设为LEVEL2吧。这样的话,当任务A忙碌的 时候就不会切换到任务B0~B2,鼠标操作的响应应该会有不小的改善。
好,我们来“make run”。画面看上去和harib13d一模一样,但如果用鼠标不停地拖动窗口的话,就会感到响应速度和之前有很大不同。相对地,拖动窗口时任务B0~B2会变得非常慢,这就代表我们的设计成功了!
总结
多任务的基础部分到这里就算完成了。明天我们还会补充一些代码来完善一下,然后就开始制作一些更有操作系统范儿的东西了。大家晚安!
标签:task,struct,16,--,taskctl,30,int,任务,tss From: https://blog.csdn.net/suy123/article/details/145268106