任务切换

任务简介

多任务与上下文

任务就是一个指令执行流。

Responsive Image

如果有多个 HART,那就可以同时执行多个指令执行流。

协作式多任务

协作式环境下,下一个任务被调度的前提是当前任务主动放弃处理器。

抢占式多任务

抢占式环境下,操作系统完全决定任务调度方案,操作系统可以剥夺当前任务对处理器的使用,将处理器提供给其它任务。

协作式多任务

上下文切换

Responsive Image

切换过程需要完成:

  • 保存上文(保存上一个任务的寄存器信息)
  • 恢复下文(恢复下一个任务的寄存器信息)

CPU 中有 32 个寄存器,保存各种状态,在切换过程中我们主要关注两个寄存器:ra(x1) 存放返回地址mscratch 一个特权寄存器,指向当前处理的任务

切换过程

初始化寄存器,根据调用约定,ra都初始化为任务的第一条指令地址。mscratch开始指向 Task A。

Responsive Image

Task A 稳定执行,当他想要放弃 CPU 时,就会执行 call swithc_to指令。执行call的过程中,就会把当前指令的下一条指令的地址放到 CPU 的ra寄存器。

Responsive Image

接下里跳转到swithc_to函数执行,该函数是切换上下文的核心函数。首先保存上文,将 CPU 中的寄存器信息全部保存:

Responsive Image

切换mscratch指针到下一个任务 Task B:

Responsive Image

恢复下文

Responsive Image

swithc_to函数执行到return时,接下来执行的指令就是 CPU 中ra保存的那条指令,也就是地址为j指令,这就是 Task B 的第一条指令,这样就完成了任务的切换。

Responsive Image

源码分析

切换核心函数 switch_to

switch_to:
 csrrw t6, mscratch, t6 # swap t6 and mscratch
 beqz t6, 1f   # Notice: previous task may be NULL
 reg_save t6   # save context of prev task
                        # 把CPU的信息保存到内存

 # Save the actual t6 register, which we swapped into
 # mscratch
 mv t5, t6  # t5 points to the context of current task
 csrr t6, mscratch # read t6 back from mscratch
 sw t6, 120(t5) # save t6 with t5 as base

1:
 # switch mscratch to point to the context of the next task
 csrw mscratch, a0

 # Restore all GP registers
 # Use t6 to point to the context of the new task
 mv t6, a0
 reg_restore t6      # 把内存里的信息恢复到CPU

 # Do actual context switching.
 ret

创建和初始化第一号任务

使用结构体context保存上下文中寄存器的信息:

struct context {
 /* ignore x0 */
 reg_t ra;
 reg_t sp;
 reg_t gp;
 reg_t tp;
 reg_t t0;
 reg_t t1;
 reg_t t2;
 reg_t s0;
 reg_t s1;
 reg_t a0;
 reg_t a1;
 reg_t a2;
 reg_t a3;
 reg_t a4;
 reg_t a5;
 reg_t a6;
 reg_t a7;
 reg_t s2;
 reg_t s3;
 reg_t s4;
 reg_t s5;
 reg_t s6;
 reg_t s7;
 reg_t s8;
 reg_t s9;
 reg_t s10;
 reg_t s11;
 reg_t t3;
 reg_t t4;
 reg_t t5;
 reg_t t6;
};

#define STACK_SIZE 1024
uint8_t task_stack[STACK_SIZE];
struct context ctx_task;

写一个任务函数,功能就是每隔1000 滴答打印一句话。

void user_task0(void)
{
 uart_puts("Task 0: Created!\n");
 while (1) {
  uart_puts("Task 0: Running...\n");
  task_delay(1000);
 }
}

初始化任务,需要初始化栈,并把任务的首地址保存到contextra寄存器。

void sched_init()
{
 w_mscratch(0);

 ctx_task.sp = (reg_t) &task_stack[STACK_SIZE - 1];
 ctx_task.ra = (reg_t) user_task0;
}

切换到第一个用户任务

switch_to函数的参数就是上下文,当执行到ret时也就切换到了user_task0

void schedule()
{
 struct context *next = &ctx_task;
 switch_to(next);
}

以上是单任务的情况,如果是多任务时,就用数组保存多个context,最大支持 10 个任务。

#define MAX_TASKS 10
#define STACK_SIZE 1024
uint8_t task_stack[MAX_TASKS][STACK_SIZE];
struct context ctx_tasks[MAX_TASKS];

使用简单的求模取余的方式确定下一个任务是哪一个:

/*
 * _top is used to mark the max available position of ctx_tasks
 * _current is used to point to the context of current task
 */
static int _top = 0;
static int _current = -1;

/*
 * implment a simple cycle FIFO schedular
 */
void schedule()
{
 if (_top <= 0) {
  panic("Num of task should be greater than zero!");
  return;
 }

 _current = (_current + 1) % _top;
 struct context *next = &(ctx_tasks[_current]);
 switch_to(next);
}

因为多个任务协作,需要一个函数来表示主动放弃 CPU:

/*
 * DESCRIPTION
 *  task_yield()  causes the calling task to relinquish the CPU and a new 
 *  task gets to run.
 */
void task_yield()
{
 schedule();
}

调用关系

Responsive Image

抢占式多任务

抢占式多任务:抢占式环境下,操作系统完全决定任务调度方案,操作系统可以剥夺当前任务对处理器的使用,将处理器提供给其他任务。

寄存器

Responsive Image

对 MSIP 写入 1 时触发 软中断,写入 0 时表示对中断进行应答,也就是处理完了软中断。

任务同步与锁

并发与同步

并发:多个控制流同时执行

  • 多处理器多任务
  • 单处理器多任务
  • 单处理器任务 + 中断

同步:为了保证在并发执行的环境中各个控制流可以有效执行而采用的一种编程技术

临界区、锁与死锁

临界区:在并发的程序执行环境中,所谓临界区指的是一个会访问共享资源指令片段,而且当这样的多个指令片段同时访问某个共享资源时可能会引发问题。

在并发环境下为了有效控制临界区的执行(同步),我们要做的是当有一个控制流进入临界区时,其他相关控制流必须等待。

锁:一种常见的用来实现同步的技术

  • 不可睡眠锁
  • 可睡眠锁

Responsive Image

当发生中断时,右边的任务获取 CPU 资源,开始执行,但是获取锁时发现当前已经处于锁定状态,所以就处于等待状态。

当下一个中断发生,左侧任务回去 CPU 后会继续执行,实际上左侧任务也不必等待,他可以一直执行,因为右侧任务一直无法获取锁。

当然,右侧任务也可以一直触发中断,让左侧任务让出 CPU。也就是左侧任务逻辑上可以一直运行,但是实际还是会被打断。

Responsive Image

当左侧任务执行完释放锁,右侧任务就可以获取锁,并正常执行下去。

Responsive Image

死锁:当控制流执行路径中会涉及多个锁,并且这些控制流执行路径获取的顺序不同时就可能发送死锁。

解决死锁:

  • 调整获取锁的顺序,比如保持一致
  • 尽可能防止任务在持有一把锁同时申请其他锁

自旋锁

Responsive Image

不能从 C 语言的层面去理解锁,应该要从指令级别去理解。上面的这种上锁方式是有问题的。

如果两个控制流同时加锁,就可能同时获取了锁,因为在汇编指令级别,每条指令执行也是需要时间的:

Responsive Image

AMOSWAP

loop:
    lw a4, -20(s0)  # 参数1
    li a5, 1        # 参数 2
    amoswap.w.aq a5, a5, (a4)   # 将a5与a4指向的内存的值进行交换
                                # 将 1 与 a4 交换,表示如果原来上锁(1)那就什么都没做
                                # 如果原来没上锁(0)那就立即上锁 
    mv a3, a5
    bnez a3,loop

![](https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2022/08/28/21-38-57-b7cece2166dba14bd128970cefdd2702-20220828213857-b116cd.png)