本篇内容继续多任务的讲解。上一篇中实现了两个任务之间的自动切换,但还不够通用,这里将其优化为多个任务之间的切换。接着引入了任务休眠的概念与休眠的程序实现。最后介绍了任务的优先级,一种用切换时间的长短来衡量,一种用Task Level来衡量。
1. 多任务管理程序优化
对多任务管理程序进行一些优化,首先修改结构体定义。
#define MAX_TASKS 1000 /* 最大任务数量 */
#define TASK_GDT0 3 /* 定义GDT从3号开始分配给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];
};
仿照前面的SHEETCTL修改了当前的TASKCTL。tasks用于存放当前运行的任务指针,初始化函数task_init如下:
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;
}
初始化函数中给每个任务分配了GDT编号,有了自己的TSS。task_init函数最终会返回一个内存地址,表示当前运行的程序已经作为一个任务了。
接下来还需要初始化一个任务结构的函数task_alloc,这里随意地设置了变量的初始值:
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; /* 无空闲 */
}
task_run函数,将task添加到tasks的末尾,并将running变量增加1。这样就将任务添加到了任务列表中,在任务切换时可以被切换到。
void task_run(struct TASK *task)
{
task->flags = 2; /*
task->flags = 2; /* 任务活动中标志 */
taskctl->tasks[taskctl->running] = task;
taskctl->running++;
return;
}
然后是重点,任务切换函数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;
}
此函数中设置了0.02s的超时时间。当任务里列表中有2个以上任务时,在达到超时时间时就依次进行切换。如果当前是列表中最后一个任务在运行,则下一次切换又会切换到列表的首个任务。
切换相关的函数完成,相应地修改主程序:
……
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);
……
超时中断函数inthandler20,在超时处理中调用了task_switch函数实现切换:
void inthandler20(int *esp)
{
struct TIMER *timer;
char ts = 0;
io_out8(PIC0_OCW2, 0x60);
timerctl.count++;
if (timerctl.next > timerctl.count) {
return;
}
timer = timerctl.t0;
for (;;) {
if (timer->timeout > timerctl.count) {
break;
}
timer->flags = TIMER_FLAGS_ALLOC;
if (timer != task_timer) {
fifo32_put(timer->fifo, timer->data);
} else {
ts = 1;
}
timer = timer->next;
}
timerctl.t0 = timer;
timerctl.next = timer->timeout;
if (ts != 0) {
task_switch();
}
return;
}
2. 任务休眠
有些任务实际运行的时间可能并不长,但是需要及时地响应,比如移动鼠标,如果长时间不同鼠标,分配给鼠标监控任务的时间里,该任务基本都处于无事可做的状态,这样就浪费了本可以分配给其他任务的时间,降低了CPU的使用效率。为了改善这种情况,引入了任务休眠的概念。
实现起来也比较简单,只要将该任务从任务列表中删除即可。但同时如果触发了鼠标事件,又要及时将任务唤醒。
首先创建函数task_sleep:
{
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--; /* 需要移动tasks数组成员 */
}
/* 移动成员 */
for (; i < taskctl->running; i++) {
taskctl->tasks[i] = taskctl->tasks[i + 1];
}
task->flags = 1; /* 不工作的状态 */
if (ts != 0) {
/* 进行任务切换 */
if (taskctl->now >= taskctl->running) {
/* now的值如果出现异常,进行修正 */
taskctl->now = 0;
}
farjmp(0, taskctl->tasks[taskctl->now]->sel);
}
}
return;
}
如果需要休眠的任务不是当前运行的任务,使该任务休眠只需要将该任务从任务列表中删除,将tasks列表中后面的任务移位即可;如果需要休眠的任务是当前运行的任务,则处理结束后还必须马上切换到下一个任务。
还要实现在FIFO中写入数据时将任务唤醒的功能。修改FIFO结构体定义,加入用于记录要唤醒任务的信息:
struct FIFO32 {
int *buf;
int p, q, size, free, flags;
struct TASK *task;
};
改写fifo32_init,使其可以在参数中指定一个任务。在不使用唤醒功能时,该参数被设置为了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;
fifo->q = 0;
fifo->task = task; /* 有数据写入时需要唤醒的任务 */
return;
}
实现唤醒功能:
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); /* 通过task_run让任务重新回到任务列表 */
}
}
return 0;
}
相比任务A未休眠的情况,B任务得到了更多运行时间,提高了运行效率。
下面通过增加多个窗口来直观感受多任务的运行情况。
……
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 */
//创建3个窗口
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); /* �����F�Ȃ� */
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); /* �����F�Ȃ� */
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) {
sprintf(s, "%02X", i - 256);
putfonts8_asc_sht(sht_back, 0, 16, COL8_FFFFFF, COL8_008484, s, 2);
if (i < 0x54 + 256) {
if (keytable[i - 256] != 0 && cursor_x < 128) {
s[0] = keytable[i - 256];
s[1] = 0;
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, s, 1);
cursor_x += 8;
}
}
if (i == 256 + 0x0e && cursor_x > 8) {
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, " ", 1);
cursor_x -= 8;
}
boxfill8(sht_win->buf, sht_win->bxsize, cursor_c, cursor_x, 28, cursor_x + 7, 43);
sheet_refresh(sht_win, cursor_x, 28, cursor_x + 8, 44);
} else if (512 <= i && i <= 767) { /* �}�E�X�f�[�^ */
if (mouse_decode(&mdec, i - 512) != 0) {
sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
if ((mdec.btn & 0x01) != 0) {
s[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) {
s[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) {
s[2] = 'C';
}
putfonts8_asc_sht(sht_back, 32, 16, COL8_FFFFFF, COL8_008484, s, 15);
mx += mdec.x;
my += mdec.y;
if (mx < 0) {
mx = 0;
}
if (my < 0) {
my = 0;
}
if (mx > binfo->scrnx - 1) {
mx = binfo->scrnx - 1;
}
if (my > binfo->scrny - 1) {
my = binfo->scrny - 1;
}
sprintf(s, "(%3d, %3d)", mx, my);
putfonts8_asc_sht(sht_back, 0, 0, COL8_FFFFFF, COL8_008484, s, 10);
sheet_slide(sht_mouse, mx, my);
if ((mdec.btn & 0x01) != 0) {
sheet_slide(sht_win, mx - 80, my - 8);
}
}
} else if (i <= 1) {
if (i != 0) {
timer_init(timer, &fifo, 0);
cursor_c = COL8_000000;
} else {
timer_init(timer, &fifo, 1);
cursor_c = COL8_FFFFFF;
}
timer_settime(timer, 50);
boxfill8(sht_win->buf, sht_win->bxsize, cursor_c, cursor_x, 28, cursor_x + 7, 43);
sheet_refresh(sht_win, cursor_x, 28, cursor_x + 8, 44);
}
}
}
}
任务A用于监控鼠标移动与键盘输入,任务B0-B2则用于其他3个窗口的显示,可以看到多个窗口同时运行的现象。
3. 设置任务优先级-切换时间
以上完成的内容中,各个任务都被分配了相同的切换时间。实际上通过给任务分配不同的切换时间,也是调整任务优先级的一种方式。通过设置不同的任务切换时实现了任务优先级的差异。
struct TASK {
int sel, flags;
int priority;
struct TSS32 tss;
};
在task结构体中增加了priority变量,设置从1-10的优先级,对应切换时间0.01s-0.1s。
相应修改任务切换的函数:
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.02s */
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;
}
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;
}
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;
}
int fifo32_put(struct FIFO32 *fifo, int data)
{
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, 0); /* 唤醒时不改变优先级,设置为0 */
}
}
return 0;
}
修改内容还是比较容易理解的。增加了priority参数供task_run设置超时时间时进行应用,实现不同任务使用不同的切换时间。
4. 设置任务优先级-Level
通过设置切换时间,确实可以使某些任务获得更多的运行时间,但是仍然要等待其他任务到达切换时间之后才能进行切换。像键盘与鼠标监控这种对于实时性要求很高的任务,如果不能及时切换,会给用户带来卡顿的使用体验。同样的还有网卡,如果不能及时接收,网络数据就可能丢失。因此这种情况下需要牺牲其他任务,优先保证这些任务的运行。
为了解决这个问题,设计了一种Level模式。将任务从高到低分为几个不同的Level,如果高Level中有任务在运行,就永远不会切换到低Level中的任务,只在高Level中选择任务进行切换。这样就可以确保Level高的任务始终会得到优先处理。
修改TASKCTL:
#define MAX_TASKS_LV 100
#define MAX_TASKLEVELS 10
struct TASK {
int sel, flags;
int level, priority;
struct TSS32 tss;
};
struct TASKLEVEL {
int running; /* 这一Level中运行的任务数量 */
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];
};
在TASKCTL中,我们增加了levle数组,当前设置3个Level。每个数组元素都是TASKLEVEL结构体,其中保存着每一Level的任务信息。此外还增加了lv_change变量,用于记录下一次任务切换时是否需要改变Level。
为了完成优先级Level的相关功能,首先要编写操作TASKLEVEL结构体的函数。
struct TASK *task_now(void)
{
struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
return tl->tasks[tl->now];
}
void task_add(struct TASK *task)
{
struct TASKLEVEL *tl = &taskctl->level[task->level];
tl->tasks[tl->running] = task;
tl->running++;
task->flags = 2;
return;
}
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;
}
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;
}
task_now用于获取当前运行中的任务地址,需要首先根据当前活动的Level获取TASKLEVEL结构体,再获取该Level中正在运行的任务。
task_add用于向TASKLEVEL结构体中添加一个任务。调用task_add函数时,传入的参数task中已经设置好了需要添加到哪个Level的信息。通过task中的Level信息找到该Level的指针,并将该任务添加到此Level的任务列表中。
task_remove用于从某一Level的列表中删除任务。同样显示通过Level信息找到TASKLEVEL的指针,然后在该Level的任务列表中找到此任务进行移除,将此任务设置为休眠。移除之后,需要将列表中其他的任务进行移位。
task_switchsub用于确定任务切换时切换到哪个Level。
完成了操作TASKLEVEL结构体相关的函数,可以进入其他程序的修改了。
task_init与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.02s */
task->level = 0; /* 先设置为最高Level*/
task_add(task);
task_switchsub(); /* 设置Level */
load_tr(task->sel);
task_timer = timer_alloc();
timer_settime(task_timer, task->priority);
return task;
}
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); /* 从旧Level的任务列表中移除 */
}
if (task->flags != 2) {
/* 加入新Level的任务列表 */
task->level = level;
task_add(task);
}
taskctl->lv_change = 1; /* 下次任务切换时检查Level */
return;
}
改写task_sleep与task_switch:
void task_sleep(struct TASK *task)
{
struct TASK *now_task;
if (task->flags == 2) {
/* 当前处于活动状态 */
now_task = task_now();
task_remove(task); /* 将任务从当前Level任务列表中移除 */
if (task == now_task) {
/* 当前运行的任务休眠,需要进行切换 */
task_switchsub();
now_task = task_now(); /* 经过task_switchsub获取Level后,再获取需要切换的任务 */
farjmp(0, now_task->sel);
}
}
return;
}
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与主程序中也需要相应修改,只需在调用task_run函数时加入相应的Level参数即可。这里将A设置为Level1,将B0-B2设置为Level2,这样在A任务处于活动状态时就不会切换到B0-B2。运行效果相同,但如果频繁移动鼠标,可以发现鼠标的响应相比修改之前会有很大改善。
到这里多任务的主要内容就基本完成了。在逐步完善程序的过程中,对于之前理解模糊的优先级的概念也有了更深入的理解。下一篇的主要内容转向交互,敬请期待。