发布: 更新时间:2024-08-11 09:49:50
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:[email protected]
GitHhub:
https://github.com/WindDevil
(目前啥也没有
主要是对
任务
的概念进行进一步扩展和延伸:形成
sys_yield
sys_exit
这里主要看具体实现,这些概念之前学习RTOS的时候使用是会使用了,但是具体怎么实现还不好说.
尽管 CPU 可以一直在跑应用了,但是其利用率仍有上升的空间.
随着应用需求的不断复杂,有的时候会在内核的监督下访问一些外设,它们也是计算机系统的另一个非常重要的组成部分,即
输入/输出
(I/O, Input/Output) .
CPU 会把 I/O 请求传递给外设,待外设处理完毕之后,CPU 便可以从外设读到其发出的 I/O 请求的处理结果.
我们暂时考虑 CPU 只能
单向地
通过读取外设提供的寄存器信息来获取外设处理 I/O 的完成状态。
多道程序的思想在于:
这样的话,只要同时存在的
应用足够多
,就能
一定程度
上隐藏 I/O 外设处理相对于 CPU 的延迟,保证 CPU 不必浪费时间在等待外设上,而是几乎一直在进行计算。
这种任务切换,是让应用
主动
调用
sys_yield
系统调用来实现的,这意味着应用主动交出 CPU 的使用权给其他应用。
这一段的描述相当是一种多任务的轮询,但是在我的脑海中,
外部中断
还是比多任务轮询要好得多的. 但是怎么合理地
利用
外部中断提高实时性,就是一个问题.
至于主动调用
sys_yield
就是一件很难的事情,也就是为啥叫做
协作式
, 就是系统的性能要依赖程序员在设计APP的时候释放CPU.(我自己都想拉满CPU,谁想管你死活捏)
这里提到了
一种多道程序执行的典型情况
:
这张图很好解释:
上面我们是通过“避免无谓的外设等待来提高 CPU 利用率”这一切入点来引入
sys_yield
。但其实调用
sys_yield
不一定与外设有关
。随着内核功能的逐渐复杂,我们还会遇到
其他需要等待的事件
,我们都可以立即调用
sys_yield
来避免等待过程造成的浪费。
这一部分和我最开始考虑的关于实时性问题的思考是有一定关联的.
当应用调用它主动交出 CPU 使用权之后,它下一次再被允许使用 CPU 的时间点与内核的调度策略与当前的总体应用执行情况有关,很有可能远远迟于该应用等待的事件(如外设处理完请求)达成的时间点。这就会造成该应用的响应延迟不稳定或者很长。比如,设想一下,敲击键盘之后隔了数分钟之后才能在屏幕上看到字符,这已经超出了人类所能忍受的范畴。但也请不要担心,我们后面会有更加优雅的解决方案。
思考我们之前提到的两种
syscall
.
在
内核层
实现的:
//os/syscall/mod
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;
mod fs;
mod process;
use fs::*;
use process::*;
/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
在
用户层
实现的:
//user/syscall
use core::arch::asm;
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id
);
}
ret
}
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
pub fn sys_exit(exit_code: i32) -> isize {
syscall(SYSCALL_EXIT, [exit_code as usize, 0, 0])
}
这里如果能理解到这里的同名的
syscall
,
sys_write
,
sys_exit
不是同一个函数,说明才
理解到位
.
现在要
继续实现
一个
系统调用
sys_yield
.
于是要在
用户层
实现接口:
// user/src/syscall.rs
pub fn sys_yield() -> isize {
syscall(SYSCALL_YIELD, [0, 0, 0])
}
// user/src/lib.rs
pub fn yield_() -> isize { sys_yield() }
SYSCALL_YIELD
同样是一个
需要定义
的常量.
这里有个小问题,由于
yield
是rust的
关键字
,因此定义函数名字的时候
增加了一个
_
.
于是在
内核层
的
syscall
里边也需要增加一个判别,现在我只写成伪代码,因为具体我也
不知道
参数怎么填写:
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
// 这里是伪代码
SYSCALL_YIELD => sys_yield(...)
// 这里是伪代码
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
思考上一章实现的
AppManager
,它包含了三部分:
但是考虑当前的任务的状态,可能
不是
简单地如上图两任务的情况一样,而是存在更多的任务和更复杂的情景.
想到我们本节
开头
时候所说,要建立一个
任务运行状态
的概念,把任务归类为如下几种状态:
因此可以使用rust构建这样一个结构体:
// os/src/task/task.rs
#[derive(Copy, Clone, PartialEq)]
pub enum TaskStatus {
UnInit, // 未初始化
Ready, // 准备运行
Running, // 正在运行
Exited, // 已退出
}
#[derive]
这个注解有点类似于
Kotlin
,可以让
编译器自动
帮你实现一些方法:
Clone
clone
PartialEq
Copy
回想起上一节提到的
TaskContext
,我们的
任务控制块
中需要保存的两部分也就知道了:
TaskContext
TaskStatus
因此用rust构建这样一个结构体:
// os/src/task/task.rs
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
}
那么有了
TaskControlBlock
,就可以实现一个任务管理器.
任务管理器需要管理多个任务,于是就需要知道:
这里使用了
常量和变量分离的方法
来实现它.
// os/src/task/mod.rs
pub struct TaskManager {
num_app: usize,
inner: UPSafeCell<TaskManagerInner>,
}
struct TaskManagerInner {
tasks: [TaskControlBlock; MAX_APP_NUM],
current_task: usize,
}
这是因为
num_app
是常量不需要变化,而
inner
是变量,需要用
UPSafeCell
,保证其
内部可变性
和
单核时
安全的借用能力.
这里在
官方文档
里提到了:
AppManager
current_app
TaskManger
TaskManagerInner
current_task
为
TaskManager
创建全局实例
TASK_MANAGER
,仍然使用
懒初始化
的方法:
// os/src/task/mod.rs
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 in 0..num_app {
tasks[i].task_cx = TaskContext::goto_restore(init_app_cx(i));
tasks[i].task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe { UPSafeCell::new(TaskManagerInner {
tasks,
current_task: 0,
})},
}
};
}
这个初始化顺序是:
get_num_app
TaskControlBlock
MAX_APP_NUM
init_app_cx
task
current_task
TaskManagerInner
UPSafeCell
num_app
TaskManager
TASK_MANAGER
类似于上一章实现的
内核层
的
syscall
函数中会根据
函数代码
调用函数.
我们需要理解到的一点就是:
syscall
ecall
syscall
我们现在讲的是
内核层具体实现
调用的函数,其作用是在
syscall
中作为一个
分支
:
// os/src/syscall/process.rs
use crate::task::suspend_current_and_run_next;
pub fn sys_yield() -> isize {
suspend_current_and_run_next();
0
}
这个是
sys_yield
,用于暂停当前的应用并切换到下个应用.
看它的具体实现实际上是
抽象化
了
suspend_current_and_run_next
接口,使得接口名称
一致
.
这时候要考虑我们上一章实现的
sys_exit
:
//! App management syscalls
use crate::loader::run_next_app;
use crate::println;
/// task exits and submit an exit code
pub fn sys_exit(exit_code: i32) -> ! {
println!("[kernel] Application exited with code {}", exit_code);
run_next_app()
}
打印了LOG之后,使用
run_next_app
切换到下一个APP.
那么考虑到现在
run_next_app
已经不适合于当前的有
任务调度
的系统,所以也要对
sys_exit
的具体实现进行修改.
// os/src/syscall/process.rs
use crate::task::exit_current_and_run_next;
pub fn sys_exit(exit_code: i32) -> ! {
println!("[kernel] Application exited with code {}", exit_code);
exit_current_and_run_next();
panic!("Unreachable in sys_exit!");
}
可以看到现在的具体实现是
抽象化
了
exit_current_and_run_next
接口,使得接口名称
一致
.
接下来我们只需要
具体实现
,刚刚提到的两个接口就行了:
// os/src/task/mod.rs
pub fn suspend_current_and_run_next() {
mark_current_suspended();
run_next_task();
}
pub fn exit_current_and_run_next() {
mark_current_exited();
run_next_task();
}
这里摘抄出具体实现,但是具体实现中还是有三个函数
有待实现
:
mark_current_suspended
mark_current_exited
run_next_task
他们的具体实现要和上一章和上一节的实现对比:
这一章的实现是不同的,是通过
修改用户的状态
,解决.
// os/src/task/mod.rs
fn mark_current_suspended() {
TASK_MANAGER.mark_current_suspended();
}
fn mark_current_exited() {
TASK_MANAGER.mark_current_exited();
}
impl TaskManager {
fn mark_current_suspended(&self) {
let mut inner = self.inner.borrow_mut();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Ready;
}
fn mark_current_exited(&self) {
let mut inner = self.inner.borrow_mut();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Exited;
}
}
然后再通过
run_next_task
来(根据状态)
决定(可以叫调度吗?对的...不对...对的对的...不对)
下一步要运行哪个Task.
// os/src/task/mod.rs
fn run_next_task() {
TASK_MANAGER.run_next_task();
}
impl TaskManager {
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!");
}
}
}
这里也是分为两部分:
run_next_task
TASK_MANAGER.run_next_task();
TaskManager
run_next_task
if let
if let
if let
match
None
None
Some
Option
Some
Some
Some(T)
T
Some(i32)
i32
Some(T)
Some(3)
None
self.find_next_task()
None
Some(next)
next
Some()
next
TaskManager.inner
TaskManager.inner
TaskManager.inner
__switch
TaskManager.inner
task.task_cx
__switch
可以看到
find_next_task
是一个重要的方法,它的实现是这样的:
// os/src/task/mod.rs
impl TaskManager {
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
})
}
}
它在获取
TaskManager.inner
的单线程可变借用之后对
current_task
为开头(
不包含它本身
)把整个数组看成一个
环形队列
然后逐个去
查询状态
, 直到找到
第一个
状态为准备的任务.
这里关于Rust语言,每次我们遇到不会了的,不是光把它搞懂,还要把它上一层的偏概念性的东西搞懂.
这里用到的就是
闭包
和
迭代器
的知识:
for
Iterator
map
Iterator
map
map
find
find
Option
None
do...while
这张图太好了:
回想上一章,我们使用
run_next_app
调用了
__restore
调用
sret
回到用户态.
目前我们要第一次进入用户态应该也需要
sret
才可以.
但是思考一下上一章我们学到的
__switch
的实现,显然它是
不改变
特权级的.
因此第一次进入用户态还是要依赖
__restore
.
为了使用
__restore
则需要构建Trap上下文,把
上一节
实现的
init_app_cx
,移动到
loader.rs
:
// os/src/loader.rs
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()),
)
}
再给
TaskContext
构造一个
构建第一次执行任务的上下文
的方法:
// os/src/task/context.rs
impl TaskContext {
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" { fn __restore(); }
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
在这个操作之中,
TaskContext
__restore
__switch
ret
__restore
s0~s12
需要注意的是,
__restore
的实现需要做出变化:它
不再需要
在开头
mv sp, a0
了。因为在
__switch
之后,
sp
就已经正确指向了我们需要的 Trap 上下文地址。
然后在创建
TaskManager
的全局实例
TASK_MANAGER
的时候为
每个任务上下文
, 初始化为由如下内容组成的
TaskContext
:
__restore
s0~s12
为
TaskContext
构建一个
执行第一个任务
的方法:
impl TaskManager {
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!");
}
这段代码可以这样理解:
__switch
__switch
zero_init
这时候这个执行顺序有点乱了,我尝试画一个流程图.
首先是这章实现的结构体
TaskManager
的结构:
初始化
的流程为:
初始化后的
TASK_MANAGER
:
调用
run_fist_app
之后发生了什么:
这时候考虑APP发生挂起的时候会发生什么: