5 设定任务优先级(2)(harib13e)

如果多把玩一下harib13d,会发现鼠标的移动速度好像有点慢,尤其是拖住窗口快速移动的时候,反应很糟糕。模拟器环境下手已经放开鼠标了可它还在动,即使到了真机环境下也还是觉得有点不利索。

问题的原因在于,其他任务的运行造成任务A运行速度变慢,从而导致了上述情形。要解决这个问题,只要把任务A的优先级调高就可以了,调到最高值10的话,情况应该会有所改善吧。我们在HariMain中将启动任务A的部分改为task_run(task_a, 10);来试试看,果然速度回到从前的样子了。

有人可能会担心,如果把优先级设为10,那其他任务的运行速度不就变慢了吗?不会的。任务A的优先级无论设置得多高,也一点都不会浪费时间,因为当任务A空闲的时候会自动休眠,只要进入休眠状态,即从tasks中删除后,优先级的设置就毫无影响了,其他任务的运行速度也就和之前没什么区别了。

■■■■■

上述例子说明,任务优先级是个很好用的东西。我们不妨把这个例子来总结一下:在操作系统中有一些处理,即使牺牲其他任务的性能也必须要尽快完成,否则会引起用户的不满,就比如这次对鼠标的处理。对于这类任务,我们可以让它在处理结束后马上休眠,而优先级则可以设置得非常高。

这种宁可牺牲其他任务性能也必须要尽快处理的任务可不是只有鼠标一个,比如键盘处理也是一样,网络处理应该也属于这类(如果速度太慢的话可能会丢失数据哦)。播放音乐也是,如果音乐播放任务的优先级太低的话,音乐就会一卡一卡的。

我们当然可以把这些任务的优先级都设置成10,不过真这样做的话,当它们之中的两个以上同时运行的时候,可能还是会出问题。如果拿音乐和鼠标做比较,那应该是音乐更重要吧。因为如果发生“在播放音乐的时候移动窗口,音乐卡住”这种情况,用户肯定会觉得超级不爽的。相比之下,哪怕鼠标的反应稍微慢些,我们也要保证音乐播放的质量。

然而按照现在的设计,当优先级为10的两个任务同时运行时,优先哪个就全凭运气了,任务切换先轮到谁谁就赢了。运气好的那个任务可以消耗很多时间来完成它的工作,而另外一个优先级同样是10的任务就只能等待了。也就是说,如果运气不好,音乐播放就会变得一团糟,而这样的操作系统显然不怎么好用。

■■■■■

因此我们需要设计一种架构,使得即便高优先级的任务同时运行,也能够区分哪个更加优先。

其实也没有架构那么复杂,基本上就是创建了几个struct TASKCTL。个数随意,多少都行,我们在这里先按创建3个来讲解。

5 设定任务优先级(2)(harib13e) - 图1

这种架构的工作原理是,最上层的LEVEL 0中只要存在哪怕一个任务,则完全忽略LEVEL 1和LEVEL 2中的任务,只在LEVEL 0的任务中进行任务切换。当LEVEL 0中的任务全部休眠,或者全部降到下层LEVEL,也就是当LEVEL 0中没有任何任务的时候,接下来开始轮到LEVEL 1中的任务进行任务切换。当LEVEL 0和LEVEL 1中都没有任务时,那就该轮到LEVEL 2出场了。

在这种架构下,只要把音乐播放任务设置在LEVEL 0中,就可以保证获得比鼠标更高的优先级。

■■■■■

实际上,我们不需要创建多个TASKCTL,只要在TASKCTL中创建多个tasks[]即可。

本次的bootpack.h节选

  1. #define MAX_TASKS_LV 100
  2. #define MAX_TASKLEVELS 10
  3. struct TASK {
  4. int sel, flags; /* se1用来存放GDT的编号*/
  5. int level, priority;
  6. struct TSS32 tss;
  7. };
  8. struct TASKLEVEL {
  9. int running; /*正在运行的任务数量*/
  10. int now; /*这个变量用来记录当前正在运行的是哪个任务*/
  11. struct TASK *tasks[MAX_TASKS_LV];
  12. };
  13. struct TASKCTL {
  14. int now_lv; /*现在活动中的LEVEL */
  15. char lv_change; /*在下次任务切换时是否需要改变LEVEL */
  16. struct TASKLEVEL level[MAX_TASKLEVELS];
  17. struct TASK tasks0[MAX_TASKS];
  18. };

对于每个LEVEL我们设定最多允许创建100个任务,总共10个LEVEL。至于其余有变更的地方,与其在这里用文字讲解,不如看看在程序中的实际应用更加容易理解。

■■■■■

首先,我们先写几个用于操作struct TASKLEVEL的函数,如果没有这些函数的话,task_run和task_sleep会变得冗长难懂。

其中task_now函数,用来返回现在活动中的struct TASK的内存地址。

本次的mtask.c节选

  1. struct TASK *task_now(void)
  2. {
  3. struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
  4. return tl->tasks[tl->now];
  5. }

这里面包含很多结构,比较繁琐,不过仔细看看应该就能明白。

■■■■■

task_add函数,用来向struct TASKLEVEL中添加一个任务。

本次的mtask.c节选

  1. void task_add(struct TASK *task)
  2. {
  3. struct TASKLEVEL *tl = &taskctl->level[task->level];
  4. tl->tasks[tl->running] = task;
  5. tl->running++;
  6. task->flags = 2; /*活动中*/
  7. return;
  8. }

实际上,这里应该增加if(tl —> running < MAX_TASKS_LV)等,这可以判断在一个LEVEL中是否错误地添加了100个以上的任务,不过我们把它省略了,不好意思,偷个懒。

■■■■■

task_remove函数,用来从struct TASKLEVEL中删除一个任务。

本次的mtask.c节选

  1. void task_remove(struct TASK *task)
  2. {
  3. int i;
  4. struct TASKLEVEL *tl = &taskctl->level[task->level];
  5. /*寻找task所在的位置*/
  6. for (i = 0; i < tl->running; i++) {
  7. if (tl->tasks[i] == task) {
  8. /*在这里 */
  9. break;
  10. }
  11. }
  12. tl->running--;
  13. if (i < tl->now) {
  14. tl->now--; /*需要移动成员,要相应地处理 */
  15. }
  16. if (tl->now >= tl->running) {
  17. /*如果now的值出现异常,则进行修正*/
  18. tl->now = 0;
  19. }
  20. task->flags = 1; /* 休眠中 */
  21. /* 移动 */
  22. for (; i < tl->running; i++) {
  23. tl->tasks[i] = tl->tasks[i + 1];
  24. }
  25. return;
  26. }

上面的代码基本上是照搬了task_sleep的内容。

■■■■■

task_switchsub函数,用来在任务切换时决定接下来切换到哪个LEVEL。

本次的mtask.c节选

  1. void task_switchsub(void)
  2. {
  3. int i;
  4. /*寻找最上层的LEVEL */
  5. for (i = 0; i < MAX_TASKLEVELS; i++) {
  6. if (taskctl->level[i].running > 0) {
  7. break; /*找到了*/
  8. }
  9. }
  10. taskctl->now_lv = i;
  11. taskctl->lv_change = 0;
  12. return;
  13. }

到目前为止,和struct TASKLEVEL相关的函数已经差不多都写好了,准备工作做到这里,接下来的事情就简单多了。

下面我们来改写其他一些函数,首先是task_init。最开始的任务,我们先将它放在LEVEL 0,也就是最高优先级LEVEL中。这样做在有些情况下可能会有问题,不过后面可以再用task_run重新设置,因此不用担心。

本次的mtask.c节选

  1. struct TASK *task_init(struct MEMMAN *memman)
  2. {
  3. (中略)
  4. for (i = 0; i < MAX_TASKLEVELS; i++) {
  5. taskctl->level[i].running = 0;
  6. taskctl->level[i].now = 0;
  7. }
  8. task = task_alloc();
  9. task->flags = 2; /*活动中标志*/
  10. task->priority = 2; /* 0.02秒*/
  11. task->level = 0; /*最高LEVEL */
  12. task_add(task);
  13. task_switchsub(); /* LEVEL 设置*/
  14. load_tr(task->sel);
  15. task_timer = timer_alloc();
  16. timer_settime(task_timer, task->priority);
  17. return task;
  18. }

开始的时候只有LEVEL 0中有一个任务,因此我们按照这样的方式来进行初始化。

■■■■■

下面是task_run,我们要让它可以在参数中指定LEVEL。

本次的mtask.c节选

  1. void task_run(struct TASK *task, int level, int priority)
  2. {
  3. if (level < 0) {
  4. level = task->level; /*不改变LEVEL */
  5. }
  6. if (priority > 0) {
  7. task->priority = priority;
  8. }
  9. if (task->flags == 2 && task->level != level) { /*改变活动中的LEVEL */
  10. task_remove(task); /*这里执行之后flag的值会变为1,于是下面的if语句块也会被执行*/
  11. }
  12. if (task->flags != 2) {
  13. /*从休眠状态唤醒的情形*/
  14. task->level = level;
  15. task_add(task);
  16. }
  17. taskctl->lv_change = 1; /*下次任务切换时检查LEVEL */
  18. return;
  19. }

在此之前,task_run中下一个要切换到的任务是固定不变的,不过现在情况就不同了。例如,如果用task_run启动了一个比现在活动中的任务LEVEL更高的任务,那么在下次任务切换时,就必须无条件地切换到该LEVEL中的该任务去。

5 设定任务优先级(2)(harib13e) - 图2

此外,如果当前活动中的任务LEVEL被下调,那么此时就必须将其他LEVEL的任务放在优先的位置(同样以上图来说的话,比如当LEVEL 0的任务被降级到LEVEL 2时,任务切换的目标就需要从LEVEL 0变为LEVEL 1)。

综上所述,我们需要在下次任务切换时先检查LEVEL,因此将lv_change置为1。

■■■■■

接下来是task_sleep,在这里我们可以调用task_remove,因此代码会大大缩短。

本次的mtask.c节选

  1. void task_sleep(struct TASK *task)
  2. {
  3. struct TASK *now_task;
  4. if (task->flags == 2) {
  5. /*如果处于活动状态*/
  6. now_task = task_now();
  7. task_remove(task); /*执行此语句的话flags将变为1 */
  8. if (task == now_task) {
  9. /*如果是让自己休眠,则需要进行任务切换*/
  10. task_switchsub();
  11. now_task = task_now(); /*在设定后获取当前任务的值*/
  12. farjmp(0, now_task->sel);
  13. }
  14. }
  15. return;
  16. }

这样看上去清清爽爽。

■■■■■

mtask.c的最后是task_switch,除了当lv_change不为0时的处理以外,其余几乎没有变化。

  1. void task_switch(void)
  2. {
  3. struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
  4. struct TASK *new_task, *now_task = tl->tasks[tl->now];
  5. tl->now++;
  6. if (tl->now == tl->running) {
  7. tl->now = 0;
  8. }
  9. if (taskctl->lv_change != 0) {
  10. task_switchsub();
  11. tl = &taskctl->level[taskctl->now_lv];
  12. }
  13. new_task = tl->tasks[tl->now];
  14. timer_settime(task_timer, new_task->priority);
  15. if (new_task != now_task) {
  16. farjmp(0, new_task->sel);
  17. }
  18. return;
  19. }

对比前面内容来读的话,应该很容易理解。到此为止,我们对mtask.c的改写就完成了。

■■■■■

fifo.c也需要改写一下,不过和上一节一样,只是将唤醒休眠任务的task_run稍稍修改一下而已。优先级和LEVEL都不需要改变,只要维持原状将任务唤醒即可。

本次的fifo.c节选

  1. int fifo32_put(struct FIFO32 *fifo, int data)
  2. /*向FIFO写入数据并累积起来*/
  3. {
  4. (中略)
  5. fifo->free--;
  6. if (fifo->task != 0) {
  7. if (fifo->task->flags != 2) { /*如果任务处于休眠状态*/
  8. task_run(fifo->task, -1, 0); /*将任务唤醒*/
  9. }
  10. }
  11. return 0;
  12. }

■■■■■

最后我们来改写HariMain,可到底该怎么改呢?我们就暂且将任务A设为LEVEL 1,任务B0~B2设为LEVEL 2吧。这样的话,当任务A忙碌的时候就不会切换到任务B0~B2,鼠标操作的响应应该会有不小的改善。

本次的bootpack.c节选

  1. void HariMain(void)
  2. {
  3. (中略)
  4. init_palette();
  5. shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
  6. task_a = task_init(memman);
  7. fifo.task = task_a;
  8. task_run(task_a, 1, 0); /*这里! */
  9. (中略)
  10. /* sht_win_b */
  11. for (i = 0; i < 3; i++) {
  12. (中略)
  13. task_run(task_b[i], 2, i + 1); /*这里! */
  14. }
  15. (中略)
  16. }

■■■■■

好,我们来“make run”。画面看上去和harib13d一模一样,但如果用鼠标不停地拖动窗口的话,就会感到响应速度和之前有很大不同。相对地,拖动窗口时任务B0~B2会变得非常慢,这就代表我们的设计成功了,撒花!

多任务的基础部分到这里就算完成了。明天我们还会补充一些代码来完善一下,然后就开始制作一些更有操作系统范儿的东西了。大家晚安!