还是要先看
官方手册
.
学过DMA的同志可能比较好理解,一句话,
释放CPU总线
:
如果把应用程序执行的整个过程进行进一步分析,可以看到,当程序访问 I/O 外设或睡眠时,其实是不需要占用处理器的,于是我们可以把应用程序在不同时间段的执行过程分为两类,占用处理器执行有效任务的计算阶段和不必占用处理器的等待阶段。这些阶段就形成了一个我们熟悉的“暂停-继续…”组合的控制流或执行历史。从应用程序开始执行到结束的整个控制流就是应用程序的整个执行过程。
本节的重点是操作系统的核心机制——
任务切换
,在内核中这种机制是在
__switch
函数中实现的。 任务切换支持的场景是:一个应用在运行途中便会主动或被动交出 CPU 的使用权,此时它只能暂停执行,等到内核重新给它分配处理器资源之后才能恢复并继续执行。
这里直接看
官方手册
.
这里主要是提到了一些概念,我把它们摘抄出来:
这部分之前第二章复习第一章的知识的时候我们就重复过关于第一章的
函数调用栈
和第二章的
内核栈/用户栈
的类比和区别.
这里直接看
官方手册
回顾一下就行,应该是为
任务切换
打基础.
在我的脑海里,任务切换是一个直接用
sp
指针进行操作的过程,按照我们上一章学到的知识,只需要在切换之前把
上下文
保存到用户栈就行.可能会增加的功能:
__restore
Trap
官方手册
中提到的异同和我自己脑子里总结的异同还是非常不同的:
这个
编译器帮忙
和
对应用透明
是需要在后续学习过程中注意的.
任务切换的流程:
__switch
问题
:既然不需要特权级切换,那它为什么还要进入Trap呢?是怎么进行的Trap吗?还是通过
ecall
吗?
从实现的角度讲,
__switch
函数和一个普通的函数之间的核心差别仅仅是它会
换栈
。
说起栈的上下文切换,我们不得不想到上一章我们保存的包含
CSR
和
X0~X31
的上下文,那么同样地,在
任务切换
的过程中也有任务的上下文:
认真看这个图,左侧写得是
运行
状态的一个任务,它的内核栈里保存了两部分的东西:
TrapContext
Trap
sp
TrapContext
TrapHandler
右侧写得是
准备
状态的一个任务(可以看到一个细节
sp
寄存器
没有指向
这个栈).
为了保证
sp
重新指向右侧的内核栈的时候能够
恢复现场
, 因此一定有一些东西是需要保存的,那么它就是任务上下文.
这里定义
任务上下文
: CPU 当前的某些寄存器.
可以看到左侧和右侧的图的下面都有一个
TASK_MANAGER
,它是一个类似于我们上一章实现的
APP_MANAGER
的东西,是一个结构体
TaskManager
的一个
全局实例
.
可以看到它保存了
sp
,
ra
,
s0~s11
等寄存器.
为什么
这些寄存器要保存才能
保证
任务能够继续运行,是我们接下去学习的重点.
对于
TaskManager
的具体实现
官方手册
提供了思路和细节,
TaskControlBlock
TaskContext
TaskManager
TaskControlBlock
对于当前正在执行的任务的 Trap 控制流,我们用一个名为
current_task_cx_ptr
的变量来保存放置当前任务上下文的地址;而用
next_task_cx_ptr
的变量来保存放置下一个要执行任务的上下文的地址.
这里直接看示意图,可以看到实现了一个以
current_task_cx_ptr
和
next_task_cx_ptr
为参数的
swtich
函数用以切换上下文.
这里也说明了一件事,就是控制流本身在进行切换之前就可以感知到:
官方手册
为我们描述了任务切换的四个阶段:
__switch
next_task_cx_ptr
ra
s0~s11
sp
__switch
sp
__switch
ret
__switch
__switch
这里我们可以直接看
__switch
的具体实现:
# 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:
# 阶段 [1]
# __switch(
# current_task_cx_ptr: *mut TaskContext,
# next_task_cx_ptr: *const TaskContext
# )
# 阶段 [2]
# 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
# 阶段 [3]
# 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)
# 阶段 [4]
ret
这里应该没什么看不懂的部分,我画了一张图来表述
TaskContext
的内存情况:
对应
rust
的代码:
// os/src/task/context.rs
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
这里提一下:
ra
x1
__swtich
ret
ra
ra
ra
__swtich
s0~s11
__switch
__switch
s0~s11
对应
第三点
,我们应该理解,要使用
Rust
调用才能使得编译器自动帮我们
保存/恢复调用者保存寄存器
:
// os/src/task/switch.rs
global_asm!(include_str!("switch.S"));
use super::TaskContext;
extern "C" {
pub fn __switch(
current_task_cx_ptr: *mut TaskContext,
next_task_cx_ptr: *const TaskContext
);
}