TQ2440开发板学习纪实(10)--- 实现多任务处理,最简单OS模型

Keywords: Mutitasking,Context Switch,Thread

0 多任务(多线程,多进程)基本概念

0.1 CPU与多任务

对于“多任务(Multitasking)”,不同的应用领域有不同术语。在操作系统领域,一般称为“多任务”;在应用程序设计领域,一般称为“多线程”;而在Unix领域,更多的人喜欢用“多进程”来表示相同的意思。本文着眼于OS层,所以使用“多任务”这个术语。

所谓的“多任务”,就是让一个CPU能“同时”运行多个程序。注意这里的“同时”表示的是用户(人)的感觉,实际上是多个程序以很高的频率交替运行,给人一种同时运行的感觉。目前大多数操作系统都是支持多任务的,而且大多数CPU都从硬件级别对多任务进行了特别的支持。

在我们的PC上,边浏览网页,边听歌,同时还可以开着QQ等众多软件,这就是最直观的“多任务”,早就被人们认为是理所当然的事情了。

与“多任务”相对应的是“单任务”。最典型的单任务系统就是DOS了,在DOS下,只能一个任务独占CPU,直到其运行结束为止。

0.2 多任务的必要性

一是提高CPU使用效率。对于大多数应用程序而言,它不可能始终利用CPU进行不断的运算,而是需要消耗很多时间在等待I/O完成。在等待I/O的时候,CPU只能做一些无用的循环,白白浪费了CPU运算能力。而“多任务”则可以完美解决这个问题。

二是实时响应的需要。在多任务普及之前,让CPU运行多个程序通常是通过中断处理程序的方式实现。这种方式也可以实现多个程序的调度,只是程序代码在中断处理的环境下运行,为避免中断嵌套导致的复杂性,运行时同类型中断往往是关闭的。这就会造成运行一个中断处理程序期间,中断不再响应,无法满足实时要求。在嵌入式领域,实时性要求普遍存在,为此出现了一大批实时OS,例如uC/OS,RT-Linux,FreeRTOS等等。

三是简化应用程序软件开发。通过多任务的底层支持,应用层软件可以彼此完全独立的进行开发,大大提高了开发效率。

0.3 何时进行任务切换

“多任务”的根本就是任务切换。这是所有OS所使用的的方式。因为CPU在执行一条指令时(软中断指令)或者执行后(外部中断信号),总会检测中断标志位,并据此跳转到中断处理程序。所以在中断上下文中执行任务切换是最小粒度的切换。伪代码:

MOV r0, r2
/* 这里可能会跳转到中断处理程序,然后进行任务切换 */
LDR r1, [r1]
/* 这里可能会跳转到中断处理程序,然后进行任务切换 */

任务切换时,需要保存当前任务的执行环境(CPU的各个寄存器的当前值),然后把下一个要运行的任务的执行环境恢复。

1 ARMv4核心任务切换原理

1.1 ARMv4核对任务切换的支持

ARMv4支持7种运行模式,任务的执行都是在User模式下。其他模式都是“特权模式”,用于异常处理与系统管理。在发生中断时,自动进入相应的特权模式。

这里写图片描述

本文我们以IRQ中断模式为例,说明任务切换的过程。

1.2 上下文的保存与恢复

用户模式程序执行时,发生IRQ中断后,CPU自动完成:
(1)把当前CPSR复制到SPSR_irq
(2)关IRQ中断
(3)进入IRQ模式
(4)把PC-4存入R14_irq
(5)跳转到中断处理程序

任务上下文环境通常存放到一个称为PCB的区域,PCB里包含了R0-R15以及CPSR的当前值。虽然PCB可以存放到任何内存中,但是为了处理方便,通常把PCB存放到每个任务的Stack上。

1.2.1 PCB的保存

因为涉及到CPU寄存器的直接操作,所以C语言无能为力,必须使用汇编来完成。现在是在IRQ模式下,我们对任务执行环境的每一个寄存器进行分析:

  • R0-R12,这个是User和IRQ模式共享的,所以可以直接读取保存。
  • R13,这个在IRQ模式下不可见。
  • R14,这个同样在IRQ模式下不可见。
  • R15,我们关心的是被中断任务的下一条指令地址,CPU已经自动把下一条指令的地址存入了R14_irq。所以,我们实际要保存的是R14_irq的值。
  • CPSR,CPU已经自动存入了SPSR_irq,所以我们需要保存的是SPSR_irq的值。

显然这里的主要问题就是如何读取保存被中断时,用户模式下的R13,R14。有两种方式:
(1)修改CPSR,让CPU重新进入User模式,从而可以操作R13,R14。
这种方式看起来简单,其实非常繁琐。

(a) IRQ模式下入栈保存r0,因为r0接下来用于修改cpsr。

stmfd sp!, {r0}

(b)进入System模式(System模式和User模式共享所有寄存器,如果进入User模式则无法返回到IRQ模式了)

 mrs r0, cpsr
 bic r0, 0x1F
 orr r0, 0x1F
 msr cpsr, r0

(c)保存r1-r14入栈

stmfd sp, {r1-r14}
sub sp, sp, #56

(d)返回IRQ模式

mrs r0, cpsr
bic r0, r0, 0x1F
orr r0, r0, 0x12
msr cpsr, r0

(e)恢复中断前r0的值,并把r14_irq和SPSR_irq存入r2,r3备用

ldmfd sp!, {r0}
mov r2, r14
mov r3, spsr

(f)再次进入System模式,这次我们用r1完成

 mrs r1, cpsr
 bic r1, 0x1F
 orr r1, 0x1F
 msr cpsr, r1

(g)把r0,r2(此时为R14_irq的值),r3(此时为SPSR_irq的值)入栈保存

stmfd sp!, {r0,r2,r3}

可以看出这要涉及到多次模式切换,非常繁琐低效。为此,ARMv4提供了专门的指令,用来避免模式切换。

(2)ARMv4提供了专门的指令用于在特权模式下操作用户模式下的寄存器。那就是stm和ldm,例如:

ldm sp, {r13,r14}^ /* 此时操作的是r13, r14 */
ldm sp, {r13,r14} /* 此时操作的是r13_irq, r14_irq */

这里起作用的就是指令中最后的^符号。
这个^符号作用很大,除了上述功能外,如果寄存器列表里有R15,那么^符号还会自动从SPSR_irq中还原CPSR。

ldm sp, {pc}^   /* 这将导致SPSR_irq复制到CPSR */

这样就会非常的简单:

stmfd sp!, {r0} /* r0将来存sp_usr,所以先保存 */

stmfd sp, {sp}^ /* 读取r13_user入栈 */
nop
ldmfd sp, {r0}  /* 此时r0代表了PCB */

stmfd r0!, {lr} /* PCB存入被中断任务的下一条指令地址 */
mov lr, r0      /* 改用lr代表PCB */
ldmfd sp!, {r0} /* 恢复r0 */
stmfd lr, {r0-r14}^ /* PCB存入Usermode的r0-r14 */
nop
sub lr, lr, #60

mrs r0, spsr
stmfd lr!, {r0}  /* PCB存入SPSR_irq */

ldr r0, =cur_pcb /* 更新PCB指针 */
str lr, [r0]

保存PCB后,把PCB新指针保存到任务结构中,然后把下一个要执行的任务的PCB写入cur_pcb处,然后执行恢复。

1.2.2 PCB的恢复

通常会把PCB指针存放到一个变量中,假设为cur_pcb。

    .data
cur_pcb:
    .word 0x0000000
    .text
    ldr r0, =cur_pcb
    ldr r0, [r0]  /* r0代表了PCB */
    ldmfd r0!, {r1} /* r1代表了SPSR_irq */
    msr spsr, r1  /* 恢复SPSR_irq */
    mov lr, r0    /* 改用lr_irq代表PCB */

    ldmfd lr, {r0-r14}^ /* 恢复r0-r14 */
    nop
    add lr, lr, #60

    ldmfd lr, {pc}^   /* 恢复PC,同时恢复CPSR(进入User模式),执行新任务 */

2 任务管理及调度策略

2.1 任务管理

除PCB外,每个任务一般还有自身的额外属性,如优先级,运行时间等。所以一般使用一个结构来表示任务:

struct Task {
    void* PCB;
    int priority;  /* 任务优先级 */
};

系统需要管理多个任务,如果数量固定或者不大,可以用数组管理。如果数量很大且变化,则用链表较为合适。如果数量极大,则可以考虑使用树、哈希表等其他容器来提高管理效率。

本文的测试环境,尚不具备动态内存分配功能,所以采用了数组了管理。

struct Task[TASK_MAX+1];

2.2 任务初始化

2.2.1 尚未执行的任务的初始化

一个任务执行之前,必须进行初始化,也就是为PCB赋值。这样在首次执行时,才能从PCB中进行恢复。

必须要正确赋值的寄存器是:
* R13,这是新任务的Stack指针,必须给予正确赋值,保证每个任务的堆栈不会重叠。
* R15,这是第一条要执行的指令地址。
* SPSR_irq,这会被恢复到CPSR。
对于其他的寄存器,可以全部赋值为0。

int task_add(void(*start)(void*), void* state, void* sp, int priority)
{
    asm("push {r0-r3} \n");
    asm("mrs r0, cpsr \n");
    asm("bic r0, r0, #0x1F \n");
    asm("orr r0, r0, #0x10 \n");
    asm("str r0, [r2, #-68] \n"); /* r2 = sp */
    asm("pop {r0-r3} \n");

    void** pcb = ((void**)sp)-17;
    *(pcb+1) = state;   /* R0 */
    *(pcb+2) = 0;
    *(pcb+3) = 0;
    *(pcb+4) = 0;
    *(pcb+5) = 0;
    *(pcb+6) = 0;
    *(pcb+7) = 0;
    *(pcb+8) = 0;
    *(pcb+9) = 0;
    *(pcb+10) = 0;
    *(pcb+11) = 0;
    *(pcb+12) = 0;
    *(pcb+13) = 0;
    *(pcb+14) = sp; /* R13 */
    *(pcb+15) = 0;  /* R14 */
    *(pcb+16) = start; /* R15 */

    static int i = 1;
    if(i>9) {
        return -1;
    }
    task_array[i].PCB = pcb;
    task_array[i].priority = priority;
    task_count++;
    i++;
    return (i-1);
}

2.2.2 任务0的初始化

系统启动以后进入SVC模式,初始化完毕后,切换自身到User模式,成为第一个任务,也叫作任务0。这是一个特殊的任务:

  • 它一开始就存在,由启动代码进化而来,是第一个运行的任务
  • 它无需初始化,因为其堆栈已在启动时设置好了
  • 系统第一次任务切换时,会把其当前状态恢复到PCB
 /*--------------- task 0 ----------*/
    uart_send_str("Enter User mode as task 0...");
    asm("mrs r0, cpsr");
    asm("bic r0, r0, #0x1F");
    asm("orr r0, r0, #0x10");
    asm("msr cpsr_cxsf, r0");
    uart_send_str("[OK]\x0A\x0D");


    while(1) {
        puts("hello from task 0\n");
        delay(100000);
    }

除此之外,任务0和其他任务并无区别。

2.3 调度策略

如何确定下一个要执行的任务?这是一个策略问题。不同的操作系统支持不同的任务调度策略,同一个操作系统也可能支持不同的调度策略,例如Linux就允许用户动态改变线程调度算法。调度策略大概分为:
(1)按照时间平均调度
也就是轮流执行所有任务,目标是所有任务平等的占用CPU。
(2)按照优先级调度
优先级高的任务先执行,优先级低的会被优先级高的任务抢占CPU。
(3)综合调度
综合考虑优先级、时间片等因素,行程较为复杂的调度策略。

作为小小实验,本文仅对轮流调度进行测试。也就是每接收到一个IRQ中断,就会切换到下一个任务,循环执行。

   task = cur_task+1;
    if (task >= task_count) {
        task -= task_count;
    }

    if (task != cur_task) {
        task_array[cur_task].PCB = cur_pcb; /* save old pcb */
        cur_pcb = task_array[task].PCB;     /* set new pcb */
        cur_task = task;
    }

3 测试结果与结论

测试代码中,我们添加了5个任务,这样加上任务0,一共是6个任务了。

 /*-------------- add tasks ---------*/
    task_init();
    for(int i=0; i<5; i++) {
        task_add(task, (void*)(i+1), (void*)(0x33000000+0x1000*i), i);
    }

第一个任务运行后,通过外部中断(按键或者UART0)轮流切换下一个任务执行。

4 完整源码及注意

完整源代码,请点击startcode v1.1下载。

因为涉及到C和汇编混合编程,C引用汇编标签的方法请参考我的另一篇博文《C语言中通过全局变量引用汇编语言中的全局标签》

猜你喜欢

转载自blog.csdn.net/smstong/article/details/54405220