上一篇

小源:從零開始寫 OS (8) —— 創建頁表?

zhuanlan.zhihu.com圖標

本章代碼對應 commit :de86ae6e1e8bdfe3388530f82b2081fe29e40b12

概要

內核可以看作一個服務進程,管理軟硬體資源,響應用戶進程的種種合理以及不合理的請求。為了防止可能的阻塞,支持多線程是必要的。內核線程就是內核的分身,一個分身可以處理一件特定事情。內核線程的調度由內核負責,一個內核線程處於阻塞狀態時不影響其他的內核線程。本章我們的任務有:

  1. 實現內核線程相關結構體。
  2. 為新線程構造這個結構體。
  3. 實現線程切換。

線程相關結構體

創建 process/structs.rs ,並在 lib.rs 中加入 mod process

描述一個線程,需要知道其全部的寄存器信息以及棧的使用情況。

// in process/structs.rs

use crate::context::Context;

pub struct Thread {
pub context: Context, // 線程相關的上下文
pub kstack: KernelStack, // 線程對應的內核棧
}

在切換線程時,我們會將上下文直接保存在棧上,因此 Context 只需要保存最終的棧指針 sp,也就是上下文的起始地址 content_addr 即可,詳細的上下文內容使用新的結構體 ContextContent 描述。還記得 Trap 中我們創建的棧幀結構體嗎?與當時的處理方式類似,我們需要一個規定好格式的結構體來保存寄存器的內容:

// in context.rs

#[repr(C)]
pub struct Context {
content_addr: usize // 上下文內容存儲的位置
}

#[repr(C)]
struct ContextContent {
ra: usize, // 返回地址
satp: usize, // 二級頁表所在位置
s: [usize; 12], // 被調用者保存的寄存器
}

由於接下來需要在彙編中讀寫這個結構體的內容,因此我們需要在它的 內存布局 上和 rust 編譯器達成一致。這裡我們使用 #[repr(C)] 標記,表示讓編譯器對結構體使用 C 語言標準 的內存布局,否則 rust 默認的內存布局是未定義的,可能在變數順序上有所調整。

在發生函數調用時, riscv32 約定了 調用者保存寄存器(caller saved)被調用者保存寄存器(callee saved) ,保存前者的代碼由編譯器悄悄的幫我們生成,保存後者的代碼則需要我們自己編寫,所以結構體中只包含部分寄存器。

接下來我們定義 內核棧(KernelStack) 的結構:

// in struct.rs

pub struct KernelStack(usize);
const STACK_SIZE: usize = 0x8000;

其實內核棧就是一片固定大小的內存空間,因此我們只需要在 KernelStack 中記錄棧的起始地址。

為了實現簡單,棧空間就直接從內核堆中分配了。我們需要在它的構造函數(new)中分配內存,並在析構函數(Drop)中回收內存。具體而言,我們使用 rust 的 alloc API 實現內存分配和回收:

// in lib.rs

#![feature(alloc)]

extern crate alloc;

// in struct.rs

use alloc::alloc::{alloc, dealloc, Layout};
impl KernelStack {
pub fn new() -> KernelStack {
let bottom =
unsafe {
alloc(Layout::from_size_align(STACK_SIZE, STACK_SIZE).unwrap())
} as usize;
KernelStack(bottom)
}

pub fn top(&self) -> usize {
self.0 + STACK_SIZE
}
}

impl Drop for KernelStack {
fn drop(&mut self) {
unsafe {
dealloc(
self.0 as _,
Layout::from_size_align(STACK_SIZE, STACK_SIZE).unwrap()
);
}
}
}

Drop trait 包含在 prelode 中,所以無需手動引入他。

為新線程構造結構體

在操作系統中,有一個特殊的線程,其編號為 0 。其作用是初始化一些信息,並且在沒有其他線程需要運行的時候運行他。為此我們需要對這個特殊的內核線程和內核其他的線程進行一些區分:

use riscv::register::satp;
impl Thread {
pub fn new_idle() -> Thread {
unsafe {
Thread {
context: Context::null(),
kstack: KernelStack::new(),
}
}
}

pub fn new_kernel(entry: extern "C" fn(usize) -> !, arg: usize) -> Thread {
unsafe {
let _kstack = KernelStack::new();
Thread {
context: Context::new_kernel_thread(entry, arg, _kstack.top(), satp::read().bits()),
kstack: _kstack,
}
}
}
}

內核線程的 kstack 除了存放線程運行需要使用的內容,還需要存放 ContextContent 。因此在創建 Thread 的時候,需要為其分配 kstack ,將 ContextContext 內容複製到 kstack 的 top 。而 Context 只保存 ContextContent 首地址 content_addr:

Context 構造函數

impl Context {
pub unsafe fn null() -> Context {
Context { content_addr: 0 }
}

pub unsafe fn new_kernel_thread(
entry: extern "C" fn(usize) -> !,
arg: usize,
kstack_top: usize,
satp: usize ) -> Context {
ContextContent::new_kernel_thread(entry, arg, kstack_top, satp).push_at(kstack_top)
}
}

第 0 個內核線程的 content_addr 賦值為 0 的原因稍後說明

ContextContent 構造函數

use core::mem::zeroed;
use riscv::register::sstatus;
impl ContextContent {
fn new_kernel_thread(entry: extern "C" fn(usize) -> !, arg: usize , kstack_top: usize, satp: usize) -> ContextContent {
let mut content: ContextContent = unsafe { zeroed() };
content.ra = entry as usize;
content.satp = satp;
content.s[0] = arg;
let mut _sstatus = sstatus::read();
_sstatus.set_spp(sstatus::SPP::Supervisor); // 代表 sret 之後的特權級仍為 S
content.s[1] = _sstatus.bits();
content
}

unsafe fn push_at(self, stack_top: usize) -> Context {
let ptr = (stack_top as *mut ContextContent).sub(1);
*ptr = self; // 拷貝 ContextContent
Context { content_addr: ptr as usize }
}
}

新線程的 s0, s1 暫時沒用,所以用他來暫存參數 arg ,稍後通過彙編代碼將 a0 賦值為 s0

線程切換

創建好線程之後,則需要有辦法能夠在多個線程中相互切換。 線程切換 也叫 上下文切換 。切換的過程需要兩步:

  1. 保存當前寄存器狀態。
  2. 載入另一線程的寄存器狀態。

// in process/structs.rs

impl Thread {
pub fn switch_to(&mut self, target: &mut Thread) {
unsafe {
self.context.switch(&mut target.context);
}
}
}

// in lib.rs
#![feature(naked_functions)]

// in context.rs
impl Context {
#[naked]
#[inline(never)]
pub unsafe extern "C" fn switch(&mut self, target: &mut Context) {
asm!(include_str!("process/switch.asm") :::: "volatile");
}
}

由於我們要完全手寫彙編實現 switch 函數,因此需要給編譯器一些特殊標記: #[inline(never)] 表示禁止函數內聯, #[naked] 標籤表示不希望編譯器產生多餘的彙編代碼。這裡最重要的是 extern "C" 修飾,這表示該函數使用 C 語言的 ABI ,所以規範中所有調用者保存的寄存器(caller-saved)都會保存在棧上。

至此,我們只剩下最後一個任務:編寫 process/switch.asm

.equ XLENB, 4
.macro Load reg, mem
lw reg, mem
.endm
.macro Store reg, mem
sw reg, mem
.endm

addi sp, sp, (-XLENB*14)
Store sp, 0(a0)
Store ra, 0*XLENB(sp)
Store s0, 2*XLENB(sp)
Store s1, 3*XLENB(sp)
Store s2, 4*XLENB(sp)
Store s3, 5*XLENB(sp)
Store s4, 6*XLENB(sp)
Store s5, 7*XLENB(sp)
Store s6, 8*XLENB(sp)
Store s7, 9*XLENB(sp)
Store s8, 10*XLENB(sp)
Store s9, 11*XLENB(sp)
Store s10, 12*XLENB(sp)
Store s11, 13*XLENB(sp)
csrr s11, satp
Store s11, 1*XLENB(sp)

Load sp, 0(a1)
Load s11, 1*XLENB(sp)
csrw satp, s11
Load ra, 0*XLENB(sp)
Load s0, 2*XLENB(sp)
Load s1, 3*XLENB(sp)
Load s2, 4*XLENB(sp)
Load s3, 5*XLENB(sp)
Load s4, 6*XLENB(sp)
Load s5, 7*XLENB(sp)
Load s6, 8*XLENB(sp)
Load s7, 9*XLENB(sp)
Load s8, 10*XLENB(sp)
Load s9, 11*XLENB(sp)
Load s10, 12*XLENB(sp)
Load s11, 13*XLENB(sp)
mv a0, s0
csrw sstatus, s1
addi sp, sp, (XLENB*14)

Store zero, 0(a1)
ret

Store sp, 0(a0)content_addr 賦值給 sp ,然後保存 callee saved 寄存器。保存完畢後通過 Load sp, 0(a1) 將目標線程的 content_addr 賦值給 sp ,然後恢複目標進程的寄存器。 mv a0, s0 用於傳入新線程的參數, csrw sstatus, s1 設置新進程的 sstatus 寄存器。

對於新創建的線程,因為沒有 caller 為其保存 caller-saved ,所以 s0a0 賦的值可以被保留下來。而對於就舊進程, a0 將被恢復為 caller saved 中的值,不受 s0 的影響; sstatus 同理。

最後在 process/mod.rs 中創建第 0 個和第 1 個內核線程,然後進行切換:

// in process/mod.rs

mod structs;
use structs::Thread;

pub fn init() {
let mut loop_thread = Thread::new_idle();
let mut hello_thread = Thread::new_kernel(hello_thread, 666);
loop_thread.switch_to(&mut hello_thread);
}

#[no_mangle]
pub extern "C" fn hello_thread(arg: usize) -> ! {
println!("hello thread");
println!("arg is {}", arg);
loop{
}
}

// in init.rs

use crate::process::init as process_init;
#[no_mangle]
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
...
process_init();
loop {}
}

執行 make run ,屏幕列印出:

hello thread
arg is 666
100 ticks!
100 ticks!
...

表示我們已經成功創建並切換至 hello_thread


推薦閱讀:
相关文章