任务排程

本文目标

  • 学习基本的排程演算法
  • 阅读原始码以理解排程器的实际运作

常见的排程演算法

FCFS (First-Come First-Served)

FCFS 是所有排程演算法中最简单的,所有的任务都会被放进 Queue 当中,最先到的任务会优先被处理,直到处理完毕或是该任务主动让出使用权,作业系统才会处理下一个任务。

RR (Round-Robin)

上面提到的 FCFS 排程方法有一个显而易见的缺点,如果有一个任务需要非常长时间才会完成,甚至是该任务不会有结束的时候 (http server, system monitor),这样就会产生霸占问题,让其他进入排程的任务永源分配不到硬体资源。
RR 演算法解决了 FCFS 的霸占问题,在 RR 中,会设定一个基本时间,每个任务都可以获得基本时间的硬体资源,如果任务很长的话就会被分次执行:

任务 所需时间
Task1 3 Units
Task2 1 Unit
Task3 4 Units

假设排程器的基本时间为一个 Unit,此时,在作业系统上的任务执行顺序会像是这样:

Task1 -> OS -> Task2 -> OS -> Task3 -> OS -> Task1 -> OS -> Task3 -> OS -> Task1 -> OS -> Task3 -> OS -> Task3

SJF (Shortest Job First)

最短优先演算法会让所需最短时间的任务被优先执行,如果有复数个任务所需的时间一样短,就会采用 FCFS 的策略执行。

PS (Priority Scheduling)

PS 会为每个任务赋予一个优先权,优先权较高的任务将优先被执行,如果有复数个任务的优先权相同,就会采用 FCFS 的策略执行。
其实 SJF 就可以算是 PS 的一种,只是他以时间单位作为优先权。
不过,PS 不像是 SJF 会有任务所需时间减短的情况产生,这有可能会让优先权较低的任务永远分配不到硬体资源,这个情况又称饥饿 (Starvation) 现象。
为了解决这个问题,PS 会引入老化的机制,让执行过但未完成的任务的优先权降低,确保每个任务都能分配到硬体资源。

原始码观摩: mini-riscv-os

在 mini-riscv-os 的 05-Preemptive 中,透过时间中断的机制完成了简易的 RR 排程器:

void user_task0(void)
{
	lib_puts("Task0: Created!\n");
	while (1) {
		lib_puts("Task0: Running...\n");
		lib_delay(1000);
	}
}

其中的 lib.c 内的 lib_delay() 是个延迟回圈,并不会交还控制权。

void lib_delay(volatile int count)
{
	count *= 50000;
	while (count--);
}

相反的,作业系统会透过时间中断,强制取回控制权。(由於 lib_delay 延迟较久,所以作业系统通常会打断其 while (count--) 的回圈取回控制权)

User application 初始化

作业系统 (os.c) 一开始会呼叫 user_init(),让使用者建立 task (在本范例中会在 user.c 当中建立 user_task0 与 user_task1 )。

#include "os.h"

void user_task0(void)
{
	lib_puts("Task0: Created!\n");
	while (1) {
		lib_puts("Task0: Running...\n");
		lib_delay(1000);
	}
}

void user_task1(void)
{
	lib_puts("Task1: Created!\n");
	while (1) {
		lib_puts("Task1: Running...\n");
		lib_delay(1000);
	}
}

void user_init() {
	task_create(&user_task0);
	task_create(&user_task1);
}

然後作业系统会在 os_start() 里透过 timer_init() 函数设定时间中断,接着就是进入 os_main() 的主回圈里,该回圈采用 Round-Robin 的大轮回排班方法,每次切换就选下一个 task 来执行 (若已到最後一个 task,接下来就是第 0 个 task)。


#include "os.h"

void os_kernel() {
	task_os();
}

void os_start() {
	lib_puts("OS start\n");
	user_init();
	timer_init(); // start timer interrupt ...
}

int os_main(void)
{
	os_start();

	int current_task = 0;
	while (1) {
		lib_puts("OS: Activate next task\n");
		task_go(current_task);
		lib_puts("OS: Back to OS\n");
		current_task = (current_task + 1) % taskTop; // Round Robin Scheduling
		lib_puts("\n");
	}
	return 0;
}

在 05-Preemptive 的中断机制中,笔者修改了中断向量表:

.globl trap_vector
# the trap vector base address must always be aligned on a 4-byte boundary
.align 4
trap_vector:
	# save context(registers).
	csrrw	t6, mscratch, t6	# swap t6 and mscratch
        reg_save t6
	csrw	mscratch, t6
	# call the C trap handler in trap.c
	csrr	a0, mepc
	csrr	a1, mcause
	call	trap_handler

	# trap_handler will return the return address via a0.
	csrw	mepc, a0

	# load context(registers).
	csrr	t6, mscratch
	reg_load t6
	mret

当中断发生时,中断向量表 trap_vector() 会呼叫 trap_handler():

reg_t trap_handler(reg_t epc, reg_t cause)
{
  reg_t return_pc = epc;
  reg_t cause_code = cause & 0xfff;

  if (cause & 0x80000000)
  {
    /* Asynchronous trap - interrupt */
    switch (cause_code)
    {
    case 3:
      lib_puts("software interruption!\n");
      break;
    case 7:
      lib_puts("timer interruption!\n");
      // disable machine-mode timer interrupts.
      w_mie(~((~r_mie()) | (1 << 7)));
      timer_handler();
      return_pc = (reg_t)&os_kernel;
      // enable machine-mode timer interrupts.
      w_mie(r_mie() | MIE_MTIE);
      break;
    case 11:
      lib_puts("external interruption!\n");
      break;
    default:
      lib_puts("unknown async exception!\n");
      break;
    }
  }
  else
  {
    /* Synchronous trap - exception */
    lib_puts("Sync exceptions!\n");
    while (1)
    {
      /* code */
    }
  }
  return return_pc;
}

跳到 trap_handler() 之後,它会针对不同类型的中断呼叫不同的 handler,所以我们可以将它视为一个中断的派发任务中继站:

                         +----------------+
                         | soft_handler() |
                 +-------+----------------+
                 |
+----------------+-------+-----------------+
| trap_handler() |       | timer_handler() |
+----------------+       +-----------------+
                 |
                 +-------+-----------------+
                         | exter_handler() |
                         +-----------------+

trap_handler 可以根据不同的中断类型,将中断处理交给不同的 handler,这样子做就可以大大的提高作业系统的扩充性。

#include "timer.h"

// a scratch area per CPU for machine-mode timer interrupts.
reg_t timer_scratch[NCPU][5];

#define interval 20000000 // cycles; about 2 second in qemu.

void timer_init()
{
  // each CPU has a separate source of timer interrupts.
  int id = r_mhartid();

  // ask the CLINT for a timer interrupt.
  // int interval = 1000000; // cycles; about 1/10th second in qemu.

  *(reg_t *)CLINT_MTIMECMP(id) = *(reg_t *)CLINT_MTIME + interval;

  // prepare information in scratch[] for timervec.
  // scratch[0..2] : space for timervec to save registers.
  // scratch[3] : address of CLINT MTIMECMP register.
  // scratch[4] : desired interval (in cycles) between timer interrupts.
  reg_t *scratch = &timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);
  scratch[4] = interval;
  w_mscratch((reg_t)scratch);

  // enable machine-mode timer interrupts.
  w_mie(r_mie() | MIE_MTIE);
}

static int timer_count = 0;

void timer_handler()
{
  lib_printf("timer_handler: %d\n", ++timer_count);
  int id = r_mhartid();
  *(reg_t *)CLINT_MTIMECMP(id) = *(reg_t *)CLINT_MTIME + interval;
}

看到 timer.c 之中定义的 timer_handler(),它会将 MTIMECMP 做 reset 的动作。

/* In trap_handler() */
// ...
case 7:
      lib_puts("timer interruption!\n");
      // disable machine-mode timer interrupts.
      w_mie(~((~r_mie()) | (1 << 7)));
      timer_handler();
      return_pc = (reg_t)&os_kernel;
      // enable machine-mode timer interrupts.
      w_mie(r_mie() | MIE_MTIE);
      break;
// ...
  • 为了避免 Timer Interrupt 出现巢状中断的情况,在处理中断之前, trap_handler() 会将 timer interrupt 关闭,等到处理完成後再打开。

是说这一步有点多余,因为 RISC-V 处理器进入中断後的预设情况下会暂时停止处理其他 Interrupt,之後我再发个 PR 把它修掉 XD

  • timer_handler() 执行完毕後,trap_handler() 会将 mepc 指向 os_kernel(),做到任务切换的功能。
    换言之,如果中断不属於 Timer Interrupt,Program counter 则会跳回进入中断前的状态,这个步骤定义在 trap_vector() 中:
csrr	a0, mepc # a0 => arg1 (return_pc) of trap_handler()

补充
在 RISC-V 中,函式的参数会被优先存进 a0 - a7 暂存器,如果不够用,才会存入 Stack。
其中,a0 与 a1 暂存器还有作为函式返回值的作用。

最後,记得在 Kernel 开机时导入 trap 以及 timer 的初始化动作:

void os_start()
{
	lib_puts("OS start\n");
	user_init();
	trap_init();
	timer_init(); // start timer interrupt ...
}

透过时间中断强制取回控制权,我们就不用担心有恶霸行程把持 CPU 不放,系统也就不会被恶霸卡住而整个瘫痪了,这就是现代作业系统中最重要的行程管理机制

Reference


<<:  Day20 Socket.io 常用的 API

>>:  [Lesson19] View Binding

1. 新Leader不该事必躬亲

前言 这篇适合给first time leader,特别是刚被promote成team leade...

DAY9 - BFS应用

昨天写了BFS模板&一题模板题,今天放几题比较复杂的~~ 例题实战 909. 蛇梯棋 这题最...

Day 02:专案01 - 超简单个人履历01 | HTML简介

专案介绍 这个专案会带你使用HTML和CSS,打造专属於你的个人履历网页。 预览图: 首先,我们从H...

Day23 jQuery 基本教学(三)

Method 操作方法 在熟悉 selector 後,就可以开始采用物件连结的方式进行各种作业 最基...

Day7-aws或gcp 我选择本机建立k8s

是否该用云端平台 在正式使用k8s的时候,部署k8s最好的情况是使用云端平台。 一来机器规格可以依照...