Multiprog & Time-Sharing OS

在这里插入图片描述

在这里插入图片描述

【总体思路】

  • 编译:应用程序和内核独立编译,合并为一个镜像
  • 编译:应用程序需要各自的起始地址
  • 构造:系统调用服务请求接口,任务的管理与初始化
  • 构造:任务控制块,任务的上下文/状态管理
  • 运行:特权级切换,任务与OS相互切换
  • 运行:任务通过系统调用/中断实现主动/被动切换

为此需要实现

  • 一次性加载所有用户程序,减少任务切换开销;
  • 支持任务切换机制,保存切换前后程序上下文;
  • 支持程序主动放弃处理器,实现 yield 系统调用;
  • 以时间片轮转算法调度用户程序,实现资源的时分复用。

【结构框架】

── os3-ref
   ├── build.rs
   ├── Cargo.toml
   ├── Makefile
   └── src
       ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
       ├── config.rs(新增:保存内核的一些配置)
       ├── console.rs
       ├── logging.rs
       ├── sync
       ├── entry.asm
       ├── lang_items.rs
       ├── link_app.S
       ├── linker.ld
       ├── loader.rs(新增:将应用加载到内存并进行管理)
       ├── main.rs(修改:主函数进行了修改)
       ├── sbi.rs(修改:引入新的 sbi call set_timer)
       ├── syscall(修改:新增若干 syscall)
       │   ├── fs.rs
       │   ├── mod.rs
       │   └── process.rs
       ├── task(新增:task 子模块,主要负责任务管理)
       │   ├── context.rs(引入 Task 上下文 TaskContext)
       │   ├── mod.rs(全局任务管理器和提供给其他模块的接口)
       │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
       │   ├── switch.S(任务切换的汇编代码)
       │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
       ├── timer.rs(新增:计时器相关)
       └── trap
           ├── context.rs
           ├── mod.rs(修改:时钟中断相应处理)
           └── trap.S

【多道程序放置与加载】

😃 多道程序放置

对于Dunkleosteus OS内核让所有的应用都共享同一个固定的起始地址。 正因如此,内存中同时最多只能驻留一个应用。

要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。 为此,我们编写脚本 user/build.py 为每个应用定制各自的起始地址。

import os

base_address = 0x80400000
step = 0x20000
linker = "src/linker.ld"

app_id = 0
apps = os.listdir("build/app")
apps.sort()
chapter = os.getenv("CHAPTER")

for app in apps:
    app = app[: app.find(".")]
    os.system(
        "cargo rustc --bin %s --release -- -Clink-args=-Ttext=%x"
        % (app, base_address + step * app_id)
    )
    print(
        "[build.py] application %s start with address %s"
        % (app, hex(base_address + step * app_id))
    )
    if chapter == '3':
        app_id = app_id + 1
  • 第 8 行:os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。
  • 第 9 行:对文件的列表进行排序。
  • 第 10 行:返回环境变量键的值,在这里就是返回 CHAPTER 这个 KeyValue。
  • 第 13 行:取得app这个文件的名字。
  • 第 14 行:system函数可以将字符串转化成命令在服务器上运行;其原理是每一条system函数执行时,其会创建一个子进程在系统上执行命令行,子进程的执行结果无法影响主进程。
  • 第 15 - 16 行:执行cargo rustc --bin %s --release -- -Clink-args=-Ttext=%x 系统命令,可以举个例子,假设 hello_world为内部运行的第一个函数,则运行 cargo rustc --bin hello_world --release -- -Clink-args=-Ttext=0x80400000
    • cargo rustc [OPTIONS] [–] [args]…
    • cargo rustc --bin:针对这个目录生成二进制文件
    • cargo rustc --release:生成的target文件夹下不再有debug目录,替代的是release目录
    • -Clink-args=-Ttext=0x80420000:使用我们的链接脚本,-Ttext链接时将初始地址重定向为0x80420000。
  • 第 22 - 23 行:在OSmakefile中增加CHAPTER ?= 3,对应于22行的条件判断使得app_id会不停的增加,下个应用的地址就会在之上的偏移0x20000。下一个链接时的地址空间就为0x80420000。

它的思路很简单,对于每一个应用程序,使用 cargo rustc 单独编译, 用 -Clink-args=-Ttext=xxxx 选项指定链接时 .text 段(代码段)的地址为 0x80400000 + app_id * 0x20000 。最后将这些编译好的bin文件放在/user/build/bin目录下,此时这些文件相当于可执行文件,但还没有被加载进内存中执行,所以需要后续的程序加载任务。

😃 多道程序加载

Dunkleosteus OS不同的是将batch.rs拆分为了loader.rstaskloader.rs负责启动时加载应用程序,task负责调度和切换。

//! os/src/loader.rs
//! Loading user applications into memory
//! For chapter 3, user applications are simply part of the data included in the
//! kernel binary, so we only need to copy them to the space allocated for each
//! app to load them. We also allocate fixed spaces for each task's
//! [`KernelStack`] and [`UserStack`].

use crate::config::*;
use crate::trap::TrapContext;

#[repr(align(4096))]
#[derive(Copy, Clone)]
/// kernel stack structure
struct KernelStack {
    data: [u8; KERNEL_STACK_SIZE],
}

#[repr(align(4096))]
#[derive(Copy, Clone)]
/// user stack structure
struct UserStack {
    data: [u8; USER_STACK_SIZE],
}

/// kernel stack instance
static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack {
    data: [0; KERNEL_STACK_SIZE],
}; MAX_APP_NUM];

/// user stack instance
static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack {
    data: [0; USER_STACK_SIZE],
}; MAX_APP_NUM];

impl KernelStack {
    fn get_sp(&self) -> usize {
        self.data.as_ptr() as usize + KERNEL_STACK_SIZE
    }
    pub fn push_context(&self, trap_cx: TrapContext) -> usize {
        let trap_cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
        unsafe {
            *trap_cx_ptr = trap_cx;
        }
        trap_cx_ptr as usize
    }
}

impl UserStack {
    fn get_sp(&self) -> usize {
        self.data.as_ptr() as usize + USER_STACK_SIZE
    }
}

/// Get base address of app i.
fn get_base_i(app_id: usize) -> usize {
    APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}

/// Get the total number of applications.
pub fn get_num_app() -> usize {
    extern "C" {
        fn _num_app();
    }
    unsafe { (_num_app as usize as *const usize).read_volatile() }
}

/// Load nth user app at
/// [APP_BASE_ADDRESS + n * APP_SIZE_LIMIT, APP_BASE_ADDRESS + (n+1) * APP_SIZE_LIMIT).
pub fn load_apps() {
    extern "C" {
        fn _num_app();
    }
    let num_app_ptr = _num_app as usize as *const usize;
    let num_app = get_num_app();
    let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
    // clear i-cache first
    unsafe {
        core::arch::asm!("fence.i");
    }
    // load apps
    for i in 0..num_app {
        let base_i = get_base_i(i);
        // clear region
        (base_i..base_i + APP_SIZE_LIMIT)
            .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
        // load app from data section to memory
        let src = unsafe {
            core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
        };
        let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
        dst.copy_from_slice(src);
    }
}

/// get app info with entry and sp and save `TrapContext` in kernel stack
pub fn init_app_cx(app_id: usize) -> usize {
    KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
        get_base_i(app_id),
        USER_STACK[app_id].get_sp(),
    ))
}
  • 第 14 - 16 行:声明内核栈结构体,内部大小为8K

  • 第 21 - 23 行:声明用户栈结构体,内部大小为4K

  • 第 26 - 28 行:为内核栈实例化,共有MAX_APP_NUM个内核栈,并初始化内核栈为0。

  • 第 31 - 33 行:为用户栈实例化,共有MAX_APP_NUM个用户栈,并初始化用户栈为0。

  • 第 35 - 46 行:为内核栈实现get_sppush_context方法,get_sp方法获取当前app内核栈的指针。push_context方法将传入的trap_cx保存在当前app内核栈的开头。

    • 第 37 行:返回当前app的内核栈指针,指向8k大小的开头处。
    • 第 40 行:获得trap_cx_ptr,地址就是8k大小的开头处减去TrapContext大小的位置。
    • 第 41 - 43 行:将trap_cx放入的trap_cx_ptr的地址处。所以现在从8k大小的开头处 - trap_cx_ptr之间存放的就是传入进来的trap_cx
    • 第 44 行:返回 trap_cx_ptr 这个位置。
  • 第 48 - 52 行:为用户栈实现get_sp方法,也就是返回栈指针,指向当前app 其用户栈4k大小的开头处。

  • 第 55 - 57 行:获取app_id的基地址,地址为 0x80400000+ app_id * 0x20000。这个地址与我们上面python脚本链接时候的文件地址保持一致,需要从这个地方加载之前编译好的bin文件。

  • 第 60 - 65 行:从link_app.S汇编文件_num_app字段的第一行.quad 12的中获得当前app的总数量。

  • 第 69 - 93 行:load_apps函数,将所有用户程序在内核初始化的时候一并加载进内存。第 i 个应用被加载到以物理地址 base_i 开头的一段物理内存上。

    • 第 70 - 72 行:引出_num_app这个字段。
    • 第 73 行:得到_num_app指针。
    • 第 74 行:获取app的总体数量。
    • 第 75 行:指向app_0_start
    • 第 77 - 79 行:清除icache
    • 第 82 行:获取app_i的基地址。( i 从0-num_app)
    • 第 84 - 85 行:是把从82行获得的基地址到基地址+0x20000这段区域清零。这段地址目的是存放第i+1app
    • 第 87 - 89 行:获取当前app代码段的切片。
    • 第 90 行:获取该app应该存放基地址的切片,大小为0x20000
    • 第 91 行:将该app切片存入进去。
  • 第 96 - 101 行:获取应用程序信息的入口和sp,并保存 TrapContext 在内核堆栈。

    • 第 98 行:get_base_i获取当前app_id运行时的基地址。
    • 第 99 行:获取app_id的用户栈地址。
    • 第 97 行:对TrapContext进行初始化,将传入的app_id运行时的基地址放到sepc(记录 Trap 发生之前执行的最后一条指令的地址)字段。传入的栈指针放到x[2]SPP设为User状态。再将其放到内核栈的开头部分。

load_apps 函数主要是将 user/build/bin 目录下编译好的各个 APP.bin 文件拷贝进以0x80400000为起始的内存地址中,每个app之间相距0x20000大小。这下就可以通过将入口地址改到这些地址分别执行相应的之前编好的app了。

init_app_cx函数依据传入进去的app_id初始化对应的Trap上下文到内核栈空间中。首先将app_id对应的app运行起始地址拷贝到sepc字段,再将app_id对应的用户栈sp指针拷贝到x[2]字段。之后将整个TrapContext拷贝到app_id所对应的内核栈头部。

loader.rs中为每个app都声明了对应的内核栈和用户栈。内核栈每个空间大小8k,内核栈每个空间大小4k

【任务切换】

即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致

😃 任务切换的具体实现

任务切换与上一章提及的 Trap 控制流切换相比,有如下异同

  • 与 Trap 切换不同,它不涉及特权级切换(因为完全是在S模式下进行的切换),部分由编译器完成;
  • 与 Trap 切换相同,它对应用是透明的。

我们需要在 __switch 中保存 CPU 的某些寄存器,它们就是 任务上下文 (Task Context)。

# os/src/task/switch.S

.altmacro
.macro SAVE_SN n
    sd s\n, (\n+2)*8(a0)
.endm
.macro LOAD_SN n
    ld s\n, (\n+2)*8(a1)
.endm
    .section .text
    .globl __switch
__switch:
    # __switch(
    #     current_task_cx_ptr: *mut TaskContext,
    #     next_task_cx_ptr: *const TaskContext
    # )
    # save kernel stack of current task
    sd sp, 8(a0)
    # save ra & s0~s11 of current execution
    sd ra, 0(a0)
    .set n, 0
    .rept 12
        SAVE_SN %n
        .set n, n + 1
    .endr
    # restore ra & s0~s11 of next execution
    ld ra, 0(a1)
    .set n, 0
    .rept 12
        LOAD_SN %n
        .set n, n + 1
    .endr
    # restore kernel stack of next task
    ld sp, 8(a1)
    ret
  • 第 4 行:定义SAVE_SN宏,调用携带的参数为n。

  • 第 5 行:SAVE_SN宏具有的汇编语句为sd s\n, (\n+2)*8(a0)

  • 第 6 行:结束宏定义。

  • 第 7 行:定义LOAD_SN宏,调用携带的参数为n。

  • 第 8 行:LOAD_SN宏具有的汇编语句为ld s\n, (\n+2)*8(a1)

  • 第 9 行:结束宏定义。

  • 第 10 行:进入代码段。

  • 第 11 行:声明全局符号__switch

  • 第 18 - 25 行:保存当前的上下文(14个寄存器值)到current_task_cx_ptr这个传入的参数所指向的地址中。

    • 第 18 行:存储 spa0 中的 sp
    • 第 20 行:存储 raa0 中的 ra
    • 第 21 - 25 行:通过 SAVE_SN 这个宏定义来把s0 - s11依次保存到a0中所对应的寄存器中,通过修改保存地址的方式确定保存到对应的位置。

    上述所述之所以将当前的状态保存到a0当中去是因为a0里面现在存在的是当调用__switch时所传入的第一个参数也就是current_task_cx_ptr 。前面的保存就是将当前的这14个寄存器的值保存到所传入的这个结构体中。同理所示,a1保存的是next_task_cx_ptr传入的第二个参数。

  • 第 27 - 34 行:从next_task_cx_ptr 这个传入的参数所指向的地址中取出当前的上下文(14个寄存器值)。

    • 第 27 行:从a1中恢复ra寄存器值。它记录了 __switch 函数返回之后应该跳转到哪里继续执行,return address
    • 第 28 - 32 行:从a1中恢复s0 - s11寄存器值。
    • 第 34 行:从a1中恢复sp寄存器值。
  • 第 35 行:返回

__switch函数主要做的事情就是将依据传入的两个参数,将当前的状态(14个寄存器)保存到第一个参数中,再将第二个参数中所保存的状态(14个寄存器)恢复到当前寄存器中。当返回的时候就会从下一个应用所传入的ra字段加载运行。也就顺利成章的完成了任务的切换。

😃 任务切换的结构体声明

context.rs中保存了任务切换所需要的结构体声明和一些方法

// os/src/task/context.rs
//! Implementation of [`TaskContext`]

#[derive(Copy, Clone)]
#[repr(C)]
/// task context structure containing some registers
pub struct TaskContext {
    ra: usize,
    sp: usize,
    s: [usize; 12],
}

impl TaskContext {
    pub fn zero_init() -> Self {
        Self {
            ra: 0,
            sp: 0,
            s: [0; 12],
        }
    }
    pub fn goto_restore(kstack_ptr: usize) -> Self {
        extern "C" {
            fn __restore();
        }
        Self {
            ra: __restore as usize,
            sp: kstack_ptr,
            s: [0; 12],
        }
    }
}
  • 第 7 - 11 行:为任务切换所需要结构体的声明。
    • 第 8 行:ra寄存器保存的是return address返回后继续执行所需要的地址。
    • 第 9 行:sp寄存器保存的是栈指针。
    • 第 10 行:s0 - s11这12个寄存器为保存寄存器,也就是函数执行的上下文。
  • 第 14 - 20 行:为TaskContext实现了zero_init这个方法,将里面的值初始化为0。
  • 第 21 - 30 行:goto_restore 保存传入的 sp,并将 ra 设置为 __restore 的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。
😃 任务切换的外部封装

switch.rs 对汇编代码__switch函数进行了封装,封装成rust函数可以供我们方便使用。

// os/src/task/switch.rs
//! Rust wrapper around `__switch`.
//!
//! Switching to a different task's context happens here. The actual
//! implementation must not be in Rust and (essentially) has to be in assembly
//! language (Do you know why?), so this module really is just a wrapper around
//! `switch.S`.

core::arch::global_asm!(include_str!("switch.S"));

use super::TaskContext;

extern "C" {
    /// Switch to the context of `next_task_cx_ptr`, saving the current context
    /// in `current_task_cx_ptr`.
    pub fn __switch(
        current_task_cx_ptr: *mut TaskContext, 
        next_task_cx_ptr: *const TaskContext);
}

【管理多道程序】

而内核为了管理任务,需要维护任务信息,相关内容包括:

  • 任务运行状态:未初始化、准备执行、正在执行、已退出
  • 任务控制块:维护任务状态和任务上下文
  • 任务相关系统调用:程序主动暂停 sys_yield 和主动退出 sys_exit
😃 yield 系统调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5NzMVjp-1663242684630)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220908112436254.png)]

上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。 开始时,蓝色应用向外设提交了一个请求,外设随即开始工作, 但是它要一段时间后才能返回结果。蓝色应用于是调用 sys_yield 交出 CPU 使用权, 内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果, 于是再次 sys_yield 。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。

😃 实现 sys_yield 和 sys_exit
//  os/src/syscall/process.rs
//! Process management syscalls

use crate::config::{MAX_APP_NUM, MAX_SYSCALL_NUM};
use crate::task::{exit_current_and_run_next, suspend_current_and_run_next, TaskStatus};
use crate::timer::get_time_us;

#[repr(C)]
#[derive(Debug)]
pub struct TimeVal {
    pub sec: usize,
    pub usec: usize,
}

pub struct TaskInfo {
    status: TaskStatus,
    syscall_times: [u32; MAX_SYSCALL_NUM],
    time: usize,
}

/// task exits and submit an exit code
pub fn sys_exit(exit_code: i32) -> ! {
    info!("[kernel] Application exited with code {}", exit_code);
    exit_current_and_run_next();
    panic!("Unreachable in sys_exit!");
}

/// current task gives up resources for other tasks
pub fn sys_yield() -> isize {
    suspend_current_and_run_next();
    0
}

/// get time with second and microsecond
pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize {
    let us = get_time_us();
    unsafe {
        *ts = TimeVal {
            sec: us / 1_000_000,
            usec: us % 1_000_000,
        };
    }
    0
}

/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
    -1
}
  • 第 10 - 13 行:TimeVal结构体定义。
  • 第 22 - 26 行:sys_exit 基于 task 子模块提供的 exit_current_and_run_next 接口,它的含义是退出当前的应用并切换到下个应用。
  • 第 29 - 32 行:sys_yield 的实现用到了 task 子模块提供的 suspend_current_and_run_next 接口,这个接口如字面含义,就是暂停当前的应用并切换到下个应用。
  • 第 35 - 44 行:sys_get_time获取us级的时间。并针对时间设置TimeVal结构体的值。
😃 任务控制块与任务运行状态

任务运行状态在os/src/task/task.rs这个文件下:

// os/src/task/task.rs
//! Types related to task management

use super::TaskContext;

#[derive(Copy, Clone)]
/// task control block structure
pub struct TaskControlBlock {
    pub task_status: TaskStatus,
    pub task_cx: TaskContext,
    // LAB1: Add whatever you need about the Task.
}

#[derive(Copy, Clone, PartialEq)]
/// task status: UnInit, Ready, Running, Exited
pub enum TaskStatus {
    UnInit,// 未初始化
    Ready,// 准备运行
    Running,// 正在运行
    Exited,// 已退出
}
  • 第8 - 12行:声明了TCB控制块结构体,内部拥有两个成员分别为当前任务的状态和当前任务的上下文。
  • 第 16 - 21 行:声明了枚举类型的任务运行状态,共有四种任务状态。
😃 任务管理器

/os/src/task/mod.rs这个文件是进行任务调度的上层实现。

//! /os/src/task/mod.rs
//! Task management implementation
//! Everything about task management, like starting and switching tasks is
//! implemented here.
//!
//! A single global instance of [`TaskManager`] called `TASK_MANAGER` controls
//! all the tasks in the operating system.
//!
//! Be careful when you see [`__switch`]. Control flow around this function
//! might not be what you expect.

mod context;
mod switch;
#[allow(clippy::module_inception)]
mod task;

use crate::config::{MAX_APP_NUM, MAX_SYSCALL_NUM};
use crate::loader::{get_num_app, init_app_cx};
use crate::sync::UPSafeCell;
use lazy_static::*;
pub use switch::__switch;
pub use task::{TaskControlBlock, TaskStatus};

pub use context::TaskContext;

/// The task manager, where all the tasks are managed.
///
/// Functions implemented on `TaskManager` deals with all task state transitions
/// and task context switching. For convenience, you can find wrappers around it
/// in the module level.
///
/// Most of `TaskManager` are hidden behind the field `inner`, to defer
/// borrowing checks to runtime. You can see examples on how to use `inner` in
/// existing functions on `TaskManager`.
pub struct TaskManager {
    /// total number of tasks
    num_app: usize,
    /// use inner value to get mutable access
    inner: UPSafeCell<TaskManagerInner>,
}

/// The task manager inner in 'UPSafeCell'
struct TaskManagerInner {
    /// task list
    tasks: [TaskControlBlock; MAX_APP_NUM],
    /// id of current `Running` task
    current_task: usize,
}

lazy_static! {
    /// a `TaskManager` instance through lazy_static!
    pub static ref TASK_MANAGER: TaskManager = {
        let num_app = get_num_app();
        let mut tasks = [TaskControlBlock {
            task_cx: TaskContext::zero_init(),
            task_status: TaskStatus::UnInit,
        }; MAX_APP_NUM];
        for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
            t.task_cx = TaskContext::goto_restore(init_app_cx(i));
            t.task_status = TaskStatus::Ready;
        }
        TaskManager {
            num_app,
            inner: unsafe {
                UPSafeCell::new(TaskManagerInner {
                    tasks,
                    current_task: 0,
                })
            },
        }
    };
}

impl TaskManager {
    /// Run the first task in task list.
    ///
    /// Generally, the first task in task list is an idle task (we call it zero process later).
    /// But in ch3, we load apps statically, so the first task is a real app.
    fn run_first_task(&self) -> ! {
        let mut inner = self.inner.exclusive_access();
        let task0 = &mut inner.tasks[0];
        task0.task_status = TaskStatus::Running;
        let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
        drop(inner);
        let mut _unused = TaskContext::zero_init();
        // before this, we should drop local variables that must be dropped manually
        unsafe {
            __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
        }
        panic!("unreachable in run_first_task!");
    }

    /// Change the status of current `Running` task into `Ready`.
    fn mark_current_suspended(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Ready;
    }

    /// Change the status of current `Running` task into `Exited`.
    fn mark_current_exited(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Exited;
    }

    /// Find next task to run and return task id.
    ///
    /// In this case, we only return the first `Ready` task in task list.
    fn find_next_task(&self) -> Option<usize> {
        let inner = self.inner.exclusive_access();
        let current = inner.current_task;
        (current + 1..current + self.num_app + 1)
            .map(|id| id % self.num_app)
            .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
    }

    /// Switch current `Running` task to the task we have found,
    /// or there is no `Ready` task and we can exit with all applications completed
    fn run_next_task(&self) {
        if let Some(next) = self.find_next_task() {
            let mut inner = self.inner.exclusive_access();
            let current = inner.current_task;
            inner.tasks[next].task_status = TaskStatus::Running;
            inner.current_task = next;
            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
            drop(inner);
            // before this, we should drop local variables that must be dropped manually
            unsafe {
                __switch(current_task_cx_ptr, next_task_cx_ptr);
            }
            // go back to user mode
        } else {
            panic!("All applications completed!");
        }
    }

    // LAB1: Try to implement your function to update or get task info!
}

/// Run the first task in task list.
pub fn run_first_task() {
    TASK_MANAGER.run_first_task();
}

/// Switch current `Running` task to the task we have found,
/// or there is no `Ready` task and we can exit with all applications completed
fn run_next_task() {
    TASK_MANAGER.run_next_task();
}

/// Change the status of current `Running` task into `Ready`.
fn mark_current_suspended() {
    TASK_MANAGER.mark_current_suspended();
}

/// Change the status of current `Running` task into `Exited`.
fn mark_current_exited() {
    TASK_MANAGER.mark_current_exited();
}

/// Suspend the current 'Running' task and run the next task in task list.
pub fn suspend_current_and_run_next() {
    mark_current_suspended();
    run_next_task();
}

/// Exit the current 'Running' task and run the next task in task list.
pub fn exit_current_and_run_next() {
    mark_current_exited();
    run_next_task();
}

// LAB1: Public functions implemented here provide interfaces.
// You may use TASK_MANAGER member functions to handle requests.
  • 第 35 - 40 行:创建一个TaskManager 结构体,这里用到了变量与常量分离的编程风格:字段 num_app 表示应用数目,它在 TaskManager 初始化后将保持不变; 而包裹在 TaskManagerInner 内的任务控制块数组 tasks,以及正在执行的应用编号 current_task 会在执行过程中变化。TaskManager 结构体为控制TCB的最外围的结构体
  • 第 43 - 48 行:第一个字段为任务控制块数组,其为每一个任务创建了一个TCB,第二个字段表明当前正在运行的任务。方便内核管理这些任务。TaskManagerInner结构体为TaskManager结构体的子集,包含了所有任务的TCB
  • 第 50 - 72 行:声明了TASK_MANAGER,在第一次使用的时候对其进行初始化,主要对TCB进行初始化操作
    • 第 53 行:调用 loader 子模块提供的 get_num_app 接口获取链接到内核的应用总数。
    • 第 54 - 57 行:创建tasks数组,含有config.rsMAX_APP_NUM数量的TCB
    • 第 58 - 61 行:依次对每个任务控制块进行初始化,将其运行状态设置为 Ready ,并在它的内核栈栈顶压入一些初始化上下文(trap上下文),然后更新它的 task_cx
    • 第 62 -70 行:返回TaskManager结构体。并把上文所初始化的tasks字段加入进来。
  • 第 74 - 140 行:为TaskManager实现了run_first_taskmark_current_suspendedmark_current_exitedfind_next_taskrun_next_task这5个方法
    • 第 79 - 91 行:实现了run_first_task这个方法,这个函数将第一个任务加载进内核中执行。

      • 第 80 行:调用 exclusive_access 方法获取inner其内部对象的可变引用,也就是TaskManagerInner这个数据段。
      • 第 81 行:获取tasks0第一个应用的TCB,类型为TaskControlBlock
      • 第 82 行:修改task0TCB中当前状态为running态。
      • 第 83 行:设置next_task_cx_ptrtask0task_cx
      • 第 84 行:drop inner。切换任务之前,我们要手动 drop 掉我们获取到的 TaskManagerInner 可变引用。 因为函数还没有返回, inner 不会自动销毁。我们只有令 TASK_MANAGERinner 字段回到未被借用的状态,下次任务切换时才能再借用。
      • 第 85 行:初始化一个_unusedTaskContext类型的上下文为0。
      • 第 88 行:通过 __switch 进行上下文的切换,将当前状态保存在 _unused中,并载入next_task_cx_ptr所表明的第一个任务的上下文。
    • 第 94 - 98 行:实现了mark_current_suspended这个方法,这个方法设置当前app的状态为ready

    • 第 101 - 105 行:实现了mark_current_exited这个方法,这个方法设置当前app的状态为exited

    • 第 110 - 116 行:实现了find_next_task这个方法,这个方法查找之后的第一个找到的当前状态为ready的方法。判断依据为每个app所对应的task_status字段。

    • 第 120 - 137 行:实现了run_next_task这个方法,这个方法将找到下一个处于Ready状态的app将其进行运行(上下文切换、状态修改、保存上一个app的上下文)。

      • 第 121 行:判断find_next_task方法返回的值是否存在,表明是否还有处于Readyapp
      • 第 122 行:调用 exclusive_access 方法获取inner其内部对象的可变引用,也就是TaskManagerInner这个数据段。
      • 第 123 行:获取当前正在运行的任务ID。
      • 第 124 行:设置下一个任务的当前状态为Running
      • 第 125 行:设置current_task字段的ID为下一个即将运行的任务。
      • 第 126 行:设置当前任务的上下文为current_task_cx_ptr
      • 第 127 行:设置下一个任务的上下文为next_task_cx_ptr
      • 第 128 行:drop inner
      • 第 131 行:通过 __switch 进行上下文的切换,将当前状态保存在当前任务的上下文中,并载入next_task_cx_ptr所表明的下一个任务的上下文。
    • 第 143 - 145 行:创建run_first_task函数,运行第一个app,为TaskManager方法run_first_task的外部封装。

    • 第 149 - 151 行:创建run_next_task函数,运行下一个app,为TaskManager方法run_next_task的外部封装

    • 第 154 - 156 行:创建mark_current_suspended函数,设置当前app的状态为Ready,为TaskManager方法mark_current_suspended的外部封装

    • 第 159 - 161 行:创建mark_current_exited函数,设置当前app的状态为Exited,为TaskManager方法mark_current_exited的外部封装

    • 第 164 - 167 行:创建suspend_current_and_run_next函数,设置当前app的状态为Ready并开始运行下一个app

    • 第 170 - 173 行:创建exit_current_and_run_next函数,设置当前app的状态为exited并开始运行下一个app

此文件主要是由内核使用的负责管理任务之间的切换还有各个任务的状态。每个任务通过他的task_status字段来判断其处的状态。任务之间的切换首先修改下一个任务与当前任务的运行状态,其次保存将要运行任务的ID,最后进行上下文的切换也就是寄存器的保存与恢复

【分时多任务系统】

现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 一般将 时间片 (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度。

😃 时钟中断与计时器

实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。

os/src/sbi.rs这个文件在原来sbi的基础上新增了SBI_SET_TIMER 这个ID,依据OpenSBI文档ID对应为0。用于设置mtimecmp的值

//  os/src/sbi.rs
//! SBI call wrappers

#![allow(unused)]

const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_SHUTDOWN: usize = 8;

#[inline(always)]
/// general sbi call
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret;
    unsafe {
        core::arch::asm!(
            "li x16, 0",
            "ecall",
            inlateout("x10") arg0 => ret,
            in("x11") arg1,
            in("x12") arg2,
            in("x17") which,
        );
    }
    ret
}

/// use sbi call to set timer
pub fn set_timer(timer: usize) {
    sbi_call(SBI_SET_TIMER, timer, 0, 0);
}

/// use sbi call to putchar in console (qemu uart handler)
pub fn console_putchar(c: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}

/// use sbi call to getchar from console (qemu uart handler)
pub fn console_getchar() -> usize {
    sbi_call(SBI_CONSOLE_GETCHAR, 0, 0, 0)
}

/// use sbi call to shutdown the kernel
pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    panic!("It should shutdown!");
}
  • 第 6 行:SBI_SET_TIMERID为0。
  • 第 29 - 31 行:sbi 子模块有一个 set_timer 调用,用来设置 mtimecmp 的值。

此文件在原有基础上新增了SBI_SET_TIMER这个ecall指令主要设置定时器阈值,为M模式下的SBI

运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 get_time 函数可以取得当前 mtime 计数器的值;

//  os/src/timer.rs
//! RISC-V timer-related functionality

use crate::config::CLOCK_FREQ;
use crate::sbi::set_timer;
use riscv::register::time;

const TICKS_PER_SEC: usize = 100;
const MICRO_PER_SEC: usize = 1_000_000;

/// read the `mtime` register
pub fn get_time() -> usize {
    time::read()
}

/// get current time in microseconds
pub fn get_time_us() -> usize {
    time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
}

/// set the next timer interrupt
pub fn set_next_trigger() {
    set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
  • 第 12 - 14 行:获取当前mtime 计数器的值。time计数器统计自CPU复位以来共运行了多少时间,驱动time计数器是已知的固定频率的时钟。
  • 第 16 - 19 行:依据get_time的值进行计算得到us值。
  • 第 22 - 24 行:timer 子模块的 set_next_trigger 函数对 set_timer 进行了封装, 它首先读取当前 mtime 的值,然后计算出 10ms 之内计数器的增量,再将 mtimecmp 设置为二者的和。 这样,10ms 之后一个 S 特权级时钟中断就会被触发。

主要为时钟部分的上层封装,增加了读取当前time值和设置下一个中断的函数。

😃 增加获取当前时间的系统调用

文件夹位置os/src/syscall/process.rs具体内容参考:) 实现 sys_yield 和 sys_exit

😃 RISC-V 架构中的嵌套中断问题

默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。

  • 当 Trap 发生时,sstatus.sie 会被保存在 sstatus.spie 字段中,同时 sstatus.sie 置零, 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断;
  • 当 Trap 处理完毕 sret 的时候, sstatus.sie 会恢复到 sstatus.spie 内的值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e12HVk6H-1663242684630)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220913155807437.png)]

也就是说,如果不去手动设置 sstatus CSR ,在只考虑 S 特权级中断的情况下,是不会出现 嵌套中断 (Nested Interrupt) 的。

嵌套中断与嵌套 Trap

嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。

嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。

😃 抢占式调度

总体思路就是当发生时钟中断的时候就保存上下文,运行下一个app

//! Trap handling functionality
//!
//! For rCore, we have a single trap entry point, namely `__alltraps`. At
//! initialization in [`init()`], we set the `stvec` CSR to point to it.
//!
//! All traps go through `__alltraps`, which is defined in `trap.S`. The
//! assembly language code does just enough work restore the kernel space
//! context, ensuring that Rust code safely runs, and transfers control to
//! [`trap_handler()`].
//!
//! It then calls different functionality based on what exactly the exception
//! was. For example, timer interrupts trigger task preemption, and syscalls go
//! to [`syscall()`].

mod context;

use crate::syscall::syscall;
use crate::task::{exit_current_and_run_next, suspend_current_and_run_next};
use crate::timer::set_next_trigger;
use riscv::register::{
    mtvec::TrapMode,
    scause::{self, Exception, Interrupt, Trap},
    sie, stval, stvec,
};

core::arch::global_asm!(include_str!("trap.S"));

/// initialize CSR `stvec` as the entry of `__alltraps`
pub fn init() {
    extern "C" {
        fn __alltraps();
    }
    unsafe {
        stvec::write(__alltraps as usize, TrapMode::Direct);
    }
}

/// timer interrupt enabled
pub fn enable_timer_interrupt() {
    unsafe {
        sie::set_stimer();
    }
}

#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
    let scause = scause::read(); // get trap cause
    let stval = stval::read(); // get extra value
    match scause.cause() {
        Trap::Exception(Exception::UserEnvCall) => {
            cx.sepc += 4;
            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
        }
        Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
            error!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.", stval, cx.sepc);
            exit_current_and_run_next();
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            error!("[kernel] IllegalInstruction in application, core dumped.");
            exit_current_and_run_next();
        }
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            set_next_trigger();
            suspend_current_and_run_next();
        }
        _ => {
            panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );
        }
    }
    cx
}

pub use context::TrapContext
  • 第 29 - 36 行:修改 stvec 寄存器来指向正确的 Trap 处理入口点
    • 第 31 行:引入外部符号__alltraps
    • 第 34 行:将 stvec (trap处理代码的地址)设置为 Direct 模式指向__alltraps的地址。
  • 第 39 - 43 行:设置 sie.stie(s-mode下时钟中断的enable比特位), 使得 S 特权级时钟中断不会被屏蔽。
  • 第 63 - 66 行:新增分支,触发了 S 特权级时钟中断时,重新设置计时器, 调用 suspend_current_and_run_next 函数暂停当前应用并切换到下一个。

分时多任务系统在此主要是用了定时器中断进行任务的时间片轮流执行。通过设置定时器的阈值与处理中断函数进行任务切换达到这一目的,为OS层面的。

【实验设计 chapter3】

【获取任务信息】

我们希望引入一个新的系统调用 sys_task_info 以获取当前任务的信息,定义如下:

fn sys_task_info(ti: *mut TaskInfo) -> isize
  • syscall ID: 410
  • 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)任务使用的系统调用及调用次数任务总运行时长(单位ms)
struct TaskInfo {
    status: TaskStatus,
    syscall_times: [u32; MAX_SYSCALL_NUM],
    time: usize
}
  • 参数:
    • ti: 待查询任务信息
  • 返回值:执行成功返回0,错误返回-1
  • 说明:
    • 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。
    • 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为 MAX_SYSCALL_NUM=500 的数组做桶计数。
    • 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。
【思路】
  • status:由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running

  • syscall_times:记录当前app使用系统调用的次数。

    1. TCB中添加一个数组用来记录系统调用的次数

      pub struct TaskControlBlock {
          pub task_status: TaskStatus,
          pub task_cx: TaskContext,
          // LAB1: Add whatever you need about the Task.
          pub syscall_count: [u32; MAX_SYSCALL_NUM],
      }
      
    2. app实现刷新syscall计数,获取当前info的方法

      // LAB1: Try to implement your function to update or get task info!
          fn update_task_info(&self , syscall_id:usize) {
              let mut inner = self.inner.exclusive_access();
              let current = inner.current_task;
              inner.tasks[current].syscall_count[syscall_id] = inner.tasks[current].syscall_count[syscall_id]+ 1;
              
          }
      
          fn git_task_info(&self) -> num:[u32; MAX_SYSCALL_NUM]{
              let mut inner = self.inner.exclusive_access();
              let current = inner.current_task;
              sys_count:[u32; MAX_SYSCALL_NUM] = inner.tasks[current].syscall_count;
              sys_count
          }
      
    3. 对方法进行外部封装

      // LAB1: Public functions implemented here provide interfaces.
      // You may use TASK_MANAGER member functions to handle requests.
      fn update_task_info(syscall_id : usize) {
          TASK_MANAGER.update_task_info(syscall_id);
      }
      
      fn git_task_info() -> num:[u32; MAX_SYSCALL_NUM]{
          num:[u32; MAX_SYSCALL_NUM] = TASK_MANAGER.git_task_info();
          num
      }
      
    4. 在系统调用处进行系统调用的计数

      	/// handle syscall exception with `syscall_id` and other arguments
      pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
          // LAB1: You may need to update syscall info here.
          update_task_info(syscall_id);
          match syscall_id {
              SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
              SYSCALL_EXIT => sys_exit(args[0] as i32),
              SYSCALL_YIELD => sys_yield(),
              SYSCALL_GET_TIME => sys_get_time(args[0] as *mut TimeVal, args[1]),
              SYSCALL_TASK_INFO => sys_task_info(args[0] as *mut TaskInfo),
              _ => panic!("Unsupported syscall_id: {}", syscall_id),
          }
      }
      
    5. 新增sys_task_info系统调用处理函数

      static mut old_time: usize = 0;
      /// YOUR JOB: Finish sys_task_info to pass testcases
      pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
          unsafe {
              let cur_time:usize = get_time_us();
              let cha = cur_time - old_time;
              old_time = cur_time;
      
              *ti = TaskInfo {
                  status: TaskStatus::Running,
                  syscall_times: git_task_info(),
                  time: cha,
              };
          }
          0
      }
      
  • time:记录任务总运行时长,在第一次切换到相应任务的时候进行记录时间。时间是ms为单位。

    • 在切换下一个app的时候查看它是否time未被记录过,没有则记录一下时间

      fn run_next_task(&self) {
              if let Some(next) = self.find_next_task() {
                  let mut inner = self.inner.exclusive_access();
                  let current = inner.current_task;
                  inner.tasks[next].task_status = TaskStatus::Running;
                  inner.current_task = next;
                  let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
                  let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
                  if inner.tasks[current].first_time == 0{
                      inner.tasks[current].first_time = get_time_us()/1000;
                  }
                  drop(inner);
                  // before this, we should drop local variables that must be dropped manually
                  unsafe {
                      __switch(current_task_cx_ptr, next_task_cx_ptr);
                  }
                  
                  // go back to user mode
              } else {
                  panic!("All applications completed!");
              }
          }
      
    • 增加获取当前任务时间的方法

      fn git_task_time_info(&self) -> usize{
              let mut inner = self.inner.exclusive_access();
              let current = inner.current_task;
              let sys_init_time:usize = inner.tasks[current].first_time;
              sys_init_time
          }
      
    • 对此方法进行外部封装

      pub fn git_task_time_info() -> usize{
          let num:usize = TASK_MANAGER.git_task_time_info();
          num
      }
      
    • sys_task_infotime部分将当前时间与第一次存入的时间差值进行保存

      /// YOUR JOB: Finish sys_task_info to pass testcases
      pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
          unsafe {
              let cur_time:usize = get_time_us()/1000;
              *ti = TaskInfo {
                  status: TaskStatus::Running,
                  syscall_times: git_task_info(),
                  time: cur_time - git_task_time_info(),
              };
          }
          0
      }
      

😃 个人总结

  1. 多道程序系统是在Dunkleosteus OS的基础上将多个APP加载到紧挨着的不同的地址空间从而实现多个app同时处理。
  2. 多道程序的切换呢主要是依据TCB的控制类似于状态机,内部的切换实质上是上下文的保存与恢复(保存一些寄存器的值简称上下文)。
  3. 分时多任务系统的实现主要在多道程序基础上增加了时间片轮转调度算法,内部的实现是依据中断进行的,通过中断处理函数进行多道程序的调度。
  4. 截至当前时间节点,从裸机开始,到应用与系统分开再到现在可以多任务分时调度运行。特权级分层的效果越来越明显,我们也是越来越不信任应用程序。😃

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值