进程优先级队列

更详细的讲解和代码调试演示过程,请参看视频
Linux kernel Hacker, 从零构建自己的内核

前几节,我们实现了进程的调度,并给每个进程赋予相应优先级,优先级高的进程,能够得到更多的CPU时间。但在演示时,我们发现一个问题,就是,当进程发送切换时,鼠标键盘的响应会被卡死,这是因为响应鼠标键盘事件的进程被调到后台,无法获得CPU运行时间,从而不能处理鼠标或键盘事件。

这种情况对用户来说,是不可接受的。对用户输入给予及时回应,直接关系到用户体验。所以负责相应用户输入的进程,其重要性要比那些只需要在后台运行,不用和用户直接交互的进程高很多。由此,我们需要实现的是,只要用户有输入,那么负责处理用户输入的进程就不能被打断,为了实现这个目的,我们需要实现进程的优先级队列。

这里写图片描述

如上图,我们把所有进程根据level分成三个队列,进程越重要,它对应的level值越低,这样,当进行进程调度时,进程调度器先查看level等于0的队列是否还有进程,有的话,只执行该队列的进程,如果该队列不止一个进程,那么每个进程根据他们的优先级获得相应的CPU时间。

如果levelw为0的队列没有可调度的进程,那么level为1的队列中的进程才有获得调度的机会,以此类推。

由此,我们看看相关数据结构的定义,首先是multi_task.h:

struct TASK {
    int sel, flags;
    int priority;
    int level;
    struct TSS32 tss;
};

#define  MAX_TASKS  5
#define  MAX_TASKS_LV   3
#define  MAX_TASKLEVELS 3

#define  TASK_GDT0  7
#define  SIZE_OF_TASK  120

struct TASKLEVEL {
    int running;
    int now;
    struct TASK *tasks[MAX_TASKS_LV];
};

#define SIZE_OF_TASKLEVEL  (8+ 4*MAX_TASKS_LV)

struct TASKCTL {
    int  now_lv;
    int  lv_change;
    struct TASKLEVEL  level[MAX_TASKLEVELS];
    struct TASK tasks0[MAX_TASKS];
};

TASK结构体增加了一个level变量,用于表明进程的重要性。TASKLEVEL对应于上图的进程队列,相同重要性的进程,都存储在对应TASKLEVEL的tasks数组中。

TASKCTL 也就是进程管理器,其存储的不再是进程,而是进程的优先级队列,它要找到重要性最高的进程队列,从中取出进程进行调度。

multi_task.c 需要进行相应改动:

struct TASKCTL *get_taskctl() {
    return taskctl;
}

void init_task_level(int level) {
    taskctl->level[level].running = 0;
    taskctl->level[level].now = 0;
    int i;
    for (i = 0; i < MAX_TASKS_LV; i++) {
        taskctl->level[i].tasks[i] = 0;
    }
}

init_task_level 对进程优先级队列进行初始化,running表示改对了中有几个进程,now表示队列中,哪个进程正在被调度到前台进行运行。

struct TASK  *task_init(struct MEMMAN *memman) {
    int  i;
    ....
    
    task = task_alloc();
    task->flags = 2;  //active
    task->priority = 100;
    task->level = 0;
    task_add(task);
    task_switchsub();
    ....
}

上面代码用于初始化运行CMain函数的进程,它把CMain进程的level设置为0,也就是该进程的重要性最高,只要它不被挂起,那么它始终拥有被调度的权利。task_add会根据进程的重要性,将其加入对应的优先级队列,一旦有新进程加入,task_switchsub 则修改相应调度信息,以便进程调度器做出适当调动。

void task_run(struct TASK *task,int level, int priority) {
    if (level < 0) {
        level = task->level;
    }

    if (priority > 0) {
        task->priority = priority;
    }

    if (task->flags == 2 && task->level != level) {
        task_remove(task); //change task flags
    }

    if (task->flags != 2) {
        task->level = level;
        task_add(task);
    }

    taskctl->lv_change = 1;
    return;
} 

task_run 的作用是修改进程重要性或优先级,或者把新的进程根据其重要性添加到相应队列。如果进程的重要性有改动,那么通过task_remove把进程从原有的优先级队列中去除,然后再通过task_add将进程添加到对应的队列。

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 && new_task != 0) {
        farjmp(0, new_task->sel);
    }

    return;
}

task_switch 被时钟中断调用,它的逻辑发送不小变化。taskctl->now_lv表示当前正则被调度的优先级队列,例如,如果now_lv的值是0,那么表示当前level等于0的队列中的进程才可以获得被调度的机会。task_switch 通过taskctl->now_lv得知哪个进程队列应该被调度,然后从该队列中,取出task对象进行执行。如果有新的进程加入,或有进程的重要性被改变了,那么taskctl->lv_change的值就不等于0。假设当前获得调度机会的是level值为1的队列中的进程,但是有level等于0的进程对象添加到了level等于0的队列中,那么此时,调度算法就会停止从level为1的队列中去调度进程,而是切换到level为0的队列,从中获取要调度的进程。

int  task_sleep(struct TASK *task) {
   struct TASK *cur_task = 0;

   if (task->flags == 2) {
       cur_task = task_now();
       task_remove(task);
  
       if (task == cur_task) {
          task_switchsub();
          cur_task = task_now();
        
          if (cur_task != 0)
          {
              farjmp(0, cur_task->sel);
          }
       }
   }

   return 0;
}

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

task_sleep 用于删除进程,如果需要杀掉某个进程,那么可以使用该函数剥夺指定进程对象获得调度的权利。task_remove负责把进程从它所在的队列中删除,如果当前被挂起的进程是正在运行的进程,那么task_sleep会选择下一个合适的进程进行调度执行。

task_now 用于返回当前正在被调用的进程对象,task_add把给定进程加入对应的优先级队列。

void task_remove(struct TASK *task) {
    int i ;
    struct TASKLEVEL *tl = &taskctl->level[task->level];
    for (i = 0; i< tl->running; i++) {
        if (tl->tasks[i] == task) {
            tl->tasks[i] = 0;
            break;
        }
    }

    tl->running--;
    if (i < tl->now) {
        tl->now--;
    }

    if (tl->now >= tl->running) {
        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;
    for (i = 0; i < MAX_TASKLEVELS; i++) {
        if (taskctl->level[i].running > 0) {
           break;
        }
    }

    taskctl->now_lv = i;
    taskctl->lv_change = 0;
}

task_remove主要负责把进程对象从队列中删除,并修改相应的队列数据。一旦有进程删除或添加,那么进程调度需要作出对应的调整,task_switchsub的作用是根据进程的添加或删除,修改进程调度器的相应信息,进而改变进程调度器的调度行为。

最后是主入口函数CMain也做相应改动:

void CMain(void) {
    ....
    for (i = 0; i < 2; i++) {
       ....

       ....
       task_run(task_b[i], 1, (i+1)*5);
    }
    ....
}

主要到,我们把运行两个窗体的进程其重要性设置成1,也就是只要运行主入口函数的进程不挂起,那么运行两个窗体的进程就不能得到执行。

本节代码改动虽多,但逻辑简单,理解起来应该不难,经过上面的改动后,系统运行的情况如下:


这里写图片描述

从上图看出,如果CMain进程运行时,上面两个窗体原来的计数字符串没有显示,这是因为两个窗体的进程得不到调度的机会。由于CMain进程负责相应鼠标键盘,因此,此时我们移动鼠标就不再出现卡顿的情形。

当CMain进程被挂起后,两个窗体的进程获得执行机会,因而窗体中的计算字符串就出现了:


这里写图片描述

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容