(7)从1开始写一个操作系统

第七章

时间片概念

时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个任务被分配一个时间段,称作它的时间片,即该任务允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个任务。如果任务在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张任务先后运行的列表,当任务用完它的时间片后,它被插入到列表的前边,后面的任务顺序移动到队列的末尾。

当任务5的时间片用完时,中断中进行任务调度,开始遍历任务就绪表,假设上面任务都是就绪状态,并且任务2,4,5的优先级相同,由于4的任务比较靠后说明任务4更久没有被运行,所以此时切换到任务4。将任务4放入已运行列表前面。

如果下次时间片用完的时候再切换就会切换到任务2。如果任务4由于挂起释放cpu也会优先切换到任务2。

我们在中断中需要对就绪的任务进行判断,遍历就绪任务找到:任务优先级最高,并且不在已运行列表中的或者在已运行列表中较靠后的任务。

这里有一个需要注意的地方就是有时候可能会由于某些情况导致无效的任务切换,也就是当前运行的任务的时间片没有到,也没有更高优先级的任务就绪导致的任务调度,这个时候需要增加当前运行的任务的时间片是否用完的逻辑判断,如果时间片还有则只查找优先级比当前任务高的任务。

 

时间片轮询调度法

首先在任务控制块中添加时间片设置和剩余时间片记录变量。

typedef struct os_tcb {
    u8              OSTCBStkPtr;            /* 指向堆栈栈顶的指针                                      */
    u16             OSTCBDly;               /* 任务等待的节拍数                                        */
    u16             OSTCBStatus;            /* 任务状态                                                */
    u8              OSTCBPrio;              /* 任务优先级                                              */
    u8              OSTCBTimeQuanta;        /* 任务时间片总数                                          */
u8              OSTCBTimeQuantaCtr;     /* 当前时间片的剩余长度                                    */

然后添加一个已运行任务队列,默认值为0xFF。

xdata u8 os_task_run[TASK_SIZE] = {0xFF};

然后我们修改任务创建函数,要求用户设置任务的时间片。

    if ( time_quanta == 0 )
        os_tcb[taskID].OSTCBTimeQuanta = OS_DEFAULT_TIME_QUANTA;
    else
        os_tcb[taskID].OSTCBTimeQuanta = time_quanta;

在设置任务为就绪态的时候需要检查时间片剩余是否为0,如果为0则重新计时。并把当前运行的任务添加到已运行任务队列的头部。

任务切换部分代码

//切换任务栈
    for (; i<TASK_SIZE; i++) { //找到优先级最高任务,并且是在已运行任务队列的最后,如果已运行任务队列中没有则优先运行
        if (os_tcb[i].OSTCBStatus == OS_STAT_RDY) {
            if (os_tcb[i].OSTCBPrio > os_tcb[highest_prio_id].OSTCBPrio) {
                highest_prio_id = i;
                //查找高优先级在已运行队列中的排位
                task_sequence = 0xFF;//先假设这个排在最最最后面
                for (j=0; j<TASK_SIZE; j++) {
                    if (os_task_run[j] == i) {
                        task_sequence = j;
                        break;
                    }
                    if (os_task_run[j] == 0xFF)
                         break;
                }
            } else if (os_tcb[i].OSTCBPrio == os_tcb[highest_prio_id].OSTCBPrio) {
                //查找新找到的高优先级在已运行队列中的排位
                u8 temp_task_sequence = 0xFF;//同优先级使用的临时任务序列
                for (j=0; j<TASK_SIZE; j++) { //查找新的同级任务在任务队列中的排位
                    if (os_task_run[j] == i) {
                        temp_task_sequence = j;
                        break;
                    }
                    if (os_task_run[j] == 0xFF)
                         break;
                }
                if (temp_task_sequence > task_sequence) { //此处我们没有考虑两个相同优先级都没在已运行任务队列中的情况,这种情况下运行第一个被找到的任务
                    highest_prio_id = i;
                    task_sequence = temp_task_sequence;
                }
            }
        }
    }
    os_task_running_ID = highest_prio_id;
    //把当前任务插入已运行任务队列中
    {
        u8 temp_id = os_task_running_ID, temp_temp_id;
        if (task_sequence == 0xFF) { //不在任务队列中,直接头插
            for (j=0; j<TASK_SIZE; j++) {
                if (os_task_run[j] == 0xFF) {
                    os_task_run[j] = temp_id;
                    break;
                }
                temp_temp_id = os_task_run[j];
                os_task_run[j] = temp_id;
                temp_id = temp_temp_id;
            }
        } else { //已在任务队列中,在所在位置前移
            for (j = task_sequence; j>0; j--) {
                os_task_run[j] = os_task_run[j-1];
            }
            os_task_run[0] = os_task_running_ID;
        }
    }
    if (os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr == 0)//给当前运行的时间片赋值
        os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr = os_tcb[os_task_running_ID].OSTCBTimeQuanta;
    os_tcb[os_task_running_ID].OSTCBStatus = OS_STAT_RUNNING;
    SP = os_tcb[os_task_running_ID].OSTCBStkPtr;

现在我们需要完成最重要的部分,在中断中判断任务时间片,并且需要切换任务时,在中断中切换任务。

我们需要把任务切换中的逻辑在中断中在实现一次,并且判断时间片是否为0,如果为0则进行任务切换的逻辑,如果不为0仅仅是时间片自减操作。

首先我们来看一下中断函数。

void timer0_int (void) interrupt TIMER0_VECTOR {
    u8 i;
    for(i=0; i<TASK_SIZE; i++) { //任务时钟
        if(os_tcb[i].OSTCBDly) {
            os_tcb[i].OSTCBDly--;
            if(os_tcb[i].OSTCBDly == 0) { //当任务时钟到时,必须是由定时器减时的才行
                //os_rdy_tbl |= (0x01<<i); //使任务在就绪表中置位
                os_tcb[i].OSTCBStatus = OS_STAT_RDY;
            }
        }
    }
    //时间片轮转计数逻辑
    if (os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr == 0) { //当前运行任务时间片耗尽,执行中断下任务调度
        char i = 0, j = 0;
        u8 highest_prio_id = 0;
        u8 task_sequence = 0;//当前任务的已运行队列中一定是0
        //SP -= 2;
        os_tcb[os_task_running_ID].OSTCBStkPtr = SP;
        os_tcb[os_task_running_ID].OSTCBStatus = OS_STAT_RDY;
        //切换任务栈
        for (; i<TASK_SIZE; i++) { //找到优先级最高任务,并且是在已运行任务队列的最后,如果已运行任务队列中没有则优先运行
            if (os_tcb[i].OSTCBStatus == OS_STAT_RDY) {
                if (os_tcb[i].OSTCBPrio > os_tcb[highest_prio_id].OSTCBPrio) {
                    highest_prio_id = i;
                    //查找高优先级在已运行队列中的排位
                    task_sequence = 0xFF;//先假设这个排在最最最后面
                    for (j=0; j<TASK_SIZE; j++) {
                        if (os_task_run[j] == i) {
                            task_sequence = j;
                            break;
                        }
                    }
                } else if (os_tcb[i].OSTCBPrio == os_tcb[highest_prio_id].OSTCBPrio) {
                    //查找新找到的高优先级在已运行队列中的排位
                    u8 temp_task_sequence = 0xFF;//同优先级使用的临时任务序列
                    for (j=0; j<TASK_SIZE; j++) { //查找新的同级任务在任务队列中的排位
                        if (os_task_run[j] == i) {
                            temp_task_sequence = j;
                            break;
                        }
                    }
                    if (temp_task_sequence > task_sequence) { //此处我们没有考虑两个相同优先级都没在已运行任务队列中的情况,这种情况下运行第一个被找到的任务
                        highest_prio_id = i;
                        task_sequence = temp_task_sequence;
                    }
                }
            }
        }
        os_task_running_ID = highest_prio_id;
        //把当前任务插入已运行任务队列中
        {
            u8 temp_id = os_task_running_ID, temp_temp_id;
            if (task_sequence == 0xFF) { //不在任务队列中,直接头插
                for (j=0; j<TASK_SIZE; j++) {
                    if (os_task_run[j] == 0xFF) {
                        os_task_run[j] = temp_id;
                        break;
                    }
                    temp_temp_id = os_task_run[j];
                    os_task_run[j] = temp_id;
                    temp_id = temp_temp_id;
                }
            } else { //已在任务队列中,在所在位置前移
                for (j = task_sequence; j>0; j--) {
                    os_task_run[j] = os_task_run[j-1];
                }
                task_sequence = os_task_run[0];
            }
        }
        if (os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr == 0) //给当前运行的时间片赋值
            os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr = os_tcb[os_task_running_ID].OSTCBTimeQuanta;
        os_tcb[os_task_running_ID].OSTCBStatus = OS_STAT_RUNNING;
        SP = os_tcb[os_task_running_ID].OSTCBStkPtr;
    }
    else { //时间片未到,进行自减
        os_tcb[os_task_running_ID].OSTCBTimeQuantaCtr--;
    }
}

我们来看一下中断函数的汇编。

             ; FUNCTION timer0_int (BEGIN)
0000 C0E0              PUSH    ACC
0002 C0F0              PUSH    B
0004 C083              PUSH    DPH
0006 C082              PUSH    DPL
0008 C0D0              PUSH    PSW
000A 75D000            MOV     PSW,#00H
000D C000              PUSH    AR0
000F C004              PUSH    AR4
0011 C005              PUSH    AR5
0013 C006              PUSH    AR6
0015 C007              PUSH    AR7
                                           ; SOURCE LINE # 40

我们看到在中断函数开始的时候只是把ACC,B,DPH,DPL,PSW,R0,R4,R5,R6,R7入栈了,如果我们没有在中断中切换任务是没有关系的,因为在中断退出的时候还会出栈,但是一旦我们要在中断中切换任务,任务的栈就会切换,而且还会涉及到在中断外切换任务OS_TASK_SW(),在中断外切换任务时可是将所有寄存器都进行了入栈,这样就会导致中断与非中断时的入栈出栈寄存器对不上,会发生非常不好的事情。所以我们需要让中断也将全部寄存器入栈。我们需要做的事情只有一个,就是在中断函数开头处调用汇编语言进行一次寄存器的使用,keil就会将所有的寄存器在函数进入时入栈。

添加

#pragma asm
    MOV AR1,AR1
#pragma endasm

汇编结果如下:

; void timer0_int (void) interrupt TIMER0_VECTOR {

       RSEG  ?PR?timer0_int?RT_OS_ASM
       USING    0
timer0_int:
       PUSH     ACC
       PUSH     B
       PUSH     DPH
       PUSH     DPL
       PUSH     PSW
       MOV     PSW,#00H
       PUSH     AR0
       PUSH     AR1
       PUSH     AR2
       PUSH     AR3
       PUSH     AR4
       PUSH     AR5
       PUSH     AR6
       PUSH     AR7
       USING    0
                     ; SOURCE LINE # 100

到这里我们基本的工作都准备完成了,有一些细节没有在这里一一讲解,需要自己去看源码。我们还要准备3个同优先级任务,这里不使用sleep函数,而是改用delay,因为delay函数不会释放cpu,只是在那里占用cpu进行计数。

void app_task_1(void)
{
    while (1) {
        debug_print("app_task_1\r\n");
        delay_ms(200);
        delay_ms(200);
        delay_ms(200);
        delay_ms(200);
        delay_ms(200);
    }
}

运行的时候差不多每隔3s会出现一次,这里有个思考,为什么代码中写的delay1s,运行时3s一个周期呢?

运行结果如下,时间并不是非常准确地3s,因为delay函数本身不准确导致的:

[2019-09-09 00:33:45.496]# RECV ASCII>
STC15F2K60S2 RT-OS Test Prgramme!
app_task_1
[2019-09-09 00:33:45.713]# RECV ASCII>
app_task_2
[2019-09-09 00:33:45.823]# RECV ASCII>
app_task_3
[2019-09-09 00:33:48.148]# RECV ASCII>
app_task_1
[2019-09-09 00:33:48.588]# RECV ASCII>
app_task_2
[2019-09-09 00:33:48.920]# RECV ASCII>
app_task_3
[2019-09-09 00:33:50.912]# RECV ASCII>
app_task_1
[2019-09-09 00:33:51.352]# RECV ASCII>
app_task_2
[2019-09-09 00:33:51.682]# RECV ASCII>
app_task_3
[2019-09-09 00:33:54.447]# RECV ASCII>
app_task_3
[2019-09-09 00:33:54.557]# RECV ASCII>
app_task_1
[2019-09-09 00:33:54.997]# RECV ASCII>
app_task_2

 

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页