寫在前面 本隨筆是非常菜的菜雞寫的。如有問題請及時提出。 可以聯繫:[email protected] GitHhub:https://github.com/WindDevil (目前啥也沒有 本節重點 主要是對 任務 的概念進行進一步擴展和延伸:形成 任務運行狀態:任務從開始到結束執行過程中所處的 ...
寫在前面
本隨筆是非常菜的菜雞寫的。如有問題請及時提出。
可以聯繫:[email protected]
GitHhub:https://github.com/WindDevil (目前啥也沒有
本節重點
主要是對 任務 的概念進行進一步擴展和延伸:形成
- 任務運行狀態:任務從開始到結束執行過程中所處的不同運行狀態:未初始化、準備執行、正在執行、已退出
- 任務控制塊:管理程式的執行過程的任務上下文,控製程序的執行與暫停
- 任務相關係統調用:應用程式和操作系統之間的介面,用於程式主動暫停
sys_yield
和主動退出sys_exit
這裡主要看具體實現,這些概念之前學習RTOS的時候使用是會使用了,但是具體怎麼實現還不好說.
多道程式背景與 yield 系統調用
儘管 CPU 可以一直在跑應用了,但是其利用率仍有上升的空間.
隨著應用需求的不斷複雜,有的時候會在內核的監督下訪問一些外設,它們也是電腦系統的另一個非常重要的組成部分,即 輸入/輸出 (I/O, Input/Output) .
CPU 會把 I/O 請求傳遞給外設,待外設處理完畢之後,CPU 便可以從外設讀到其發出的 I/O 請求的處理結果.
我們暫時考慮 CPU 只能 單向地 通過讀取外設提供的寄存器信息來獲取外設處理 I/O 的完成狀態。
多道程式的思想在於:
- 內核同時管理多個應用。如果外設處理 I/O 的時間足夠長,那我們可以先進行任務切換去執行其他應用
- 在某次切換回來之後,應用再次讀取設備寄存器,發現 I/O 請求已經處理完畢了,那麼就可以根據返回的 I/O 結果繼續向下執行了
這樣的話,只要同時存在的 應用足夠多 ,就能 一定程度 上隱藏 I/O 外設處理相對於 CPU 的延遲,保證 CPU 不必浪費時間在等待外設上,而是幾乎一直在進行計算。
這種任務切換,是讓應用 主動 調用 sys_yield
系統調用來實現的,這意味著應用主動交出 CPU 的使用權給其他應用。
這一段的描述相當是一種多任務的輪詢,但是在我的腦海中, 外部中斷 還是比多任務輪詢要好得多的. 但是怎麼合理地 利用 外部中斷提高實時性,就是一個問題.
至於主動調用sys_yield
就是一件很難的事情,也就是為啥叫做 協作式 , 就是系統的性能要依賴程式員在設計APP的時候釋放CPU.(我自己都想拉滿CPU,誰想管你死活捏)
這裡提到了 一種多道程式執行的典型情況 :
這張圖很好解釋:
- 這張圖的 橫軸 是時間軸
- 這張圖的 縱軸 是運行實體(任務和IO硬體)
- 可以看到是有三個運行實體
- I/O Device : 這個是IO硬體
- I/O Task : 這個是請求IO硬體的任務
- Other Task : 這個是不請求IO硬體的其它任務
- 可以看到最開始是 IO Task 在運行.
- 在 I/O Start yield 時刻,IO Task 請求了IO硬體,然後釋放了CPU.
- Other Task 接手CPU,同時 IO Device 繼續處理硬體上的問題.
- 一直執行到 Not Complete yileld again 時段的開頭,Other Task 執行完畢,把CPU釋放.
- 由 IO Task 接手之後檢查IO硬體狀態,仍然沒有處理完畢.
- 在 Not Complete yileld again 時段的結尾, IO Task 釋放CPU.
- Other Task 再次接手CPU,同時 IO Device 繼續處理硬體上的問題.
- 在 Other Task 執行期間,發生了 I/O Complete 時刻,但是此時軟體感知不到.
- 在 Continue 時刻, ,Other Task 執行完畢,把CPU釋放.
- 由 IO Task 接手之後檢查IO硬體狀態,處理完畢,因此繼續執行.
上面我們是通過“避免無謂的外設等待來提高 CPU 利用率”這一切入點來引入 sys_yield
。但其實調用 sys_yield
不一定與外設有關 。隨著內核功能的逐漸複雜,我們還會遇到 其他需要等待的事件 ,我們都可以立即調用 sys_yield
來避免等待過程造成的浪費。
sys_yield 的缺點
這一部分和我最開始考慮的關於實時性問題的思考是有一定關聯的.
當應用調用它主動交出 CPU 使用權之後,它下一次再被允許使用 CPU 的時間點與內核的調度策略與當前的總體應用執行情況有關,很有可能遠遠遲於該應用等待的事件(如外設處理完請求)達成的時間點。這就會造成該應用的響應延遲不穩定或者很長。比如,設想一下,敲擊鍵盤之後隔了數分鐘之後才能在屏幕上看到字元,這已經超出了人類所能忍受的範疇。但也請不要擔心,我們後面會有更加優雅的解決方案。
sys_yield 的標準介面
思考我們之前提到的兩種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
Trait 之後就可以調用clone
函數完成拷貝; - 實現了
PartialEq
Trait 之後就可以使用 == 運算符比較該類型的兩個實例,從邏輯上說只有 兩個相等的應用執行狀態才會被判為相等,而事實上也確實如此。 Copy
是一個標記 Trait,決定該類型在按值傳參/賦值的時候採用移動語義還是複製語義。
回想起上一節提到的TaskContext
,我們的 任務控制塊 中需要保存的兩部分也就知道了:
TaskContext
保存任務上下文TaskStatus
保存任務狀態
因此用rust構建這樣一個結構體:
// os/src/task/task.rs
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
}
任務管理器
那麼有了TaskControlBlock
,就可以實現一個任務管理器.
任務管理器需要管理多個任務,於是就需要知道:
- app 總數
- 當前 的任務
- 每個任務的 控制塊
- 任務 狀態
- 任務 上下文
這裡使用了 常量和變數分離的方法 來實現它.
// 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
來獲取每個 已經載入到記憶體 的任務上下文. - 把所有的任務都 初始化 為 Ready 狀態.
- 然後用 匿名函數 的方式得到的
task
和初始化為0的current_task
創建一個匿名TaskManagerInner
,隨後包裹在UPSafeCell
之中,和num_app
一起創建一個TaskManager
,傳給TASK_MANAGER
.
實現 sys_yield 和 sys_exit 系統調用
類似於上一章實現的 內核層 的syscall
函數中會根據 函數代碼 調用函數.
我們需要理解到的一點就是:
- 應用層 的
syscall
函數只是使用ecall
觸發Trap. - 內核層 的
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
這種模式匹配寫法,最開始沒有掌握rust的開發技術,因此不懂.- 這時候查閱Rust聖經.關於
if let
的部分.- 當只需要進行一次匹配的時候就可以使用這個方法.
- 使用匹配是為瞭解決用簡單的 == 不能解決 複雜類型 匹配的情況.
- 使用
if let
而不是match
是為瞭解決只有None
和非None
兩種情況的簡單寫法.
- 查閱Rust聖經.關於
Some
的部分.Option
枚舉有兩種可能Some
代表有值,Some
包裹的內容就是它的值- 一個在 定義 枚舉類型的時候是
Some(T)
,T
代表的是類型.Some(i32)
就代表可以存儲i32
類型的值. - 在實例的時候
Some(T)
可以被實例化Some(3)
,就代表這個值存在且值為3.
- 一個在 定義 枚舉類型的時候是
None
代表沒值
- 因此這一段的結果意思是:
- 如果
self.find_next_task()
的結果不是None
,那麼對應的返回值應該是Some(next)
. - 下麵的邏輯里的
next
就是返回的Some()
里包裹的next
.代表 下一個任務的任務號 .
- 如果
- 這時候查閱Rust聖經.關於
- 隨後獲取
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
Trait 的map
方法: Rust中的迭代器(Iterator
)有一個map
方法,它接收一個閉包(closure),並將迭代器中的每個元素傳遞給這個閉包。map
方法會生成一個新的迭代器,其中的元素是閉包返回的結果。- 迭代器有一個
find
方法,它接收一個閉包作為參數。該閉包定義了要查找的條件,當迭代器中的元素滿足這個條件時,find
方法就會返回一個Option
類型的結果,其中包含找到的第一個匹配項或者None
如果沒有任何元素滿足條件。
- 閉包一種匿名函數,它可以賦值給變數也可以作為參數傳遞給其它函數,不同於函數的是,它允許捕獲調用者作用域中的值.
- 有點像是某種C里的 函數巨集 ,用
do...while
封裝起來的這種.因此可以偷取別的作用域的變數來用.
- 有點像是某種C里的 函數巨集 ,用
這張圖太好了:
第一次進入用戶態
回想上一章,我們使用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發生掛起的時候會發生什麼: