はじめに
ラズパイ2上でRustを使ってコンテキストスイッチが動作するようになったので仕組みや不明点、課題を記載する。
リポジトリは以下
https://github.com/osdev-rs/minimal-kernel-rpi2/tree/task
例によって「やってみた」系の記事なので多くは望まないで欲しい
IRQハンドラ
コンテキストスイッチ処理の入口となるIRQハンドラから見ていく。
irq_asm_handler:
sub lr, lr, #4
stmfd sp!, {lr}
stmfd sp!, {r0-r12}
bl irq_handler
teq r0, #1
mov r0, sp
bleq demo_context_switch
ldmfd sp!, {r0-r12, pc}^
irq_handler()
を実行して1が返ってきたら現在のsp(スタックポインタ)を引数にしてdemo_context_switch()
を実行する。
現状ではタイマ(1秒毎)とUARTの割込みが発生しており、タイマ割込みの場合irq_handler()
が1を返すようになっている。
余談:
lrを壊してしまっているが現状のコードではIRQハンドラ内で再割り込みは許可せず、IRQハンドラからリターンするときのCPSR上書き(後述)で自動的に割込み許可となるため問題はない(はず)。将来的に再割り込みを許可する際には修正する必要があると思われる。
CPSRとldmfd
の^
について
CPSRはゼロフラグやキャリーフラグ等現在の演算状態を保持するレジスタ。
IRQハンドラが呼び出されるとプロセッサモードはIRQモードとなり、元のモードのCPSRはSPSRへ保存される。
ldmfd
の^
はレジスタリスト内にpc
があるときSPSRをCPSRに書き戻す処理をおこなう。
CPSRはプロセッサモードを決めるフラグも持つためこれにより、IRQハンドラからリターンすると共に元のプロセッサモードに戻ることになる。
なおSPSRからCPSRへの書き戻しはsubs pc, lr, #4
でも行われる。
プロセッサモードとレジスタ
上記のようにプロセッサモード毎に共有するレジスタと別に持つレジスタがある。
今回のコードではタスクはUserモードで動作し、その他IRQモードとSystemモードが登場する。
IRQモードはUserモードとr0-r12は共有するのに対してlr,spは別に持つ。
対してSystemモードはUserモードとr0-r12,lr,spを共有する。
なお、SP_irqのように書いてあるが、アセンブラ上で (sp_irq
と書けるわけではなく、IRQモードでsp
はSP_irqを表すという意味である。MRS
,MSR
を使えばアクセス可能?)
今回の設計
1秒毎(タイマ割込み毎)に2つのタスクを交互に切り替える。
コンテキストスイッチの手順はこうなる
- IRQハンドラの冒頭でr0-r12,lrをspへ退避(上記「IRQハンドラ」参照)
- (タスク切り替え処理では)spに退避したレジスタとSPSRを切替元タスクのTCB(タスクコントロールブロック(後述))に保存
- spの同じ領域を切替先タスクのTCBに保存してあるレジスタで書き換え、SPSRもそのTCBに保存してあるSPSRに書き換える
- 上記の書き換えによりIRQハンドラから
ldmfd sp!, {r0-r12, pc}^
で返る際に切替先タスクに返る
C言語でコンテキストスイッチ書く場合は呼び出した関数の先で適当にsp調整していきなりldmfd sp!, {r0-r12, pc}^
を呼び出すみたいな荒業も見かけるが、Rustだとオブジェクトの後処理が動かなかったり等明らかに問題があるので少なくともIRQハンドラへ戻ってからコンテキストスイッチが走るようにする必要がある。
TCB(タスクコントロールブロック)
TCBは中断されたタスクが再開できるように各レジスタ値を保存しておく領域である。
TCBの定義
pub struct Tcb {
stack_addr: *mut u8,
stack_size: usize,
sp: *mut u32,
r: [u32; 13],
lr: *mut u8,
pc: *mut u8,
cpsr: u32,
}
unsafe impl Send for Tcb {}
unsafe impl Sync for Tcb {}
lazy_static! {
static ref TCBS: Mutex<Vec<Tcb>> = {
Mutex::new(Vec::new())
};
}
static mut current_task:usize = 0;
今回のTCBではsp,r0-r12,lr,pc,cpsrを保存する。
タスク切替のためにTCBのリストをlazy_staticで保持するが、生ポインタを持っているとthread safetyについて怒られてしまうため、unsafe impl
でSend
とSync
を実装して回避している。
#[no_std]
でも使用できるspinのspin::Mutex
を使用している。
current_task
は今1番目のタスクか2番めのタスクかを記憶するために使用する。
なお、lazy_staticは
[dependencies]
lazy_static = {version = "1.2.0", features = ["spin_no_std"]}
このように指定することで#[no_std]
での使用が可能。
TCBの初期化
fn task_exit() {
uart::write(&format!("task_exit\n"));
loop {}
}
const STACK_SIZE: usize = 4096;
impl Tcb {
pub fn new(entry: unsafe extern "C" fn()) -> Tcb {
unsafe {
let addr = alloc::alloc::alloc(Layout::new::<[u8; STACK_SIZE]>());
let sp = (addr.offset(STACK_SIZE as isize) as u32 & !(7u32)) as *mut u32;
Tcb {
stack_addr: addr,
stack_size: STACK_SIZE,
sp: sp,
r: [0xdeadbeef; 13],
lr: task_exit as *mut u8,
pc: entry as *mut u8,
cpsr: 0x10, /* User mode */
}
}
}
}
alloc::alloc::alloc()
により自前のアロケータ(TODO: そのうち記事を書く)でスタックメモリを確保している。
タスクのエントリポイントは引数で渡されてくる関数とし、アドレスをpcにセットする。
CPSRにはUserモードで動作させるため0x10をセットしておく。
今回はタスクが終了することはないが終了先としてlrにtask_exit()
を指定している。(4バイトずらしておくべきだろうか?)
コンテキストスイッチ
task.rsのdemo_context_switch()
の処理について解説していく。
現タスク情報のTCBへの退避
demo_context_switch()
の引数には、現タスクのレジスタ(r0-r12,pc)をIRQハンドラの冒頭でスタックに退避した際のspが渡されてくるので、TCBにそのspから現タスクのレジスタを取得して保存する。また、現タスクのCPSR(IRQモードではSPSRに退避されている)も保存する。
{
let mut sp = sp;
for i in 0..13 {
(*tcbs)[current].r[i] = *sp;
sp = sp.offset(1);
}
(*tcbs)[current].pc = *sp as *mut u8;
let mut cpsr = 0u32;
asm!("mrs $0, spsr" : "=r"(cpsr));
(*tcbs)[current].cpsr = cpsr;
}
cps #31
でSystemモードへ移行して、現タスクのlr,spを取得(SystemモードはUserモードとlr,spを共有しているため)しcps #18
でIRQモードへ復帰している。
cps #31
ではspが変わってしまうため、IRQモードに復帰するまでRustのコードは実行されないようにする必要がある。
let mut lr_tmp: u32 = 0;
let mut sp_tmp: u32 = 0;
asm!("cps #31
mov $0, lr
mov $1, sp
cps #18" : "=r"(lr_tmp), "=r"(sp_tmp));
(*tcbs)[current].lr = lr_tmp as *mut u8;
(*tcbs)[current].sp = sp_tmp as *mut u32;
次タスク情報のSPへの上書き
次タスクのr0-r12,pcをスタックへ上書きし、CPSRもSPSRへ上書きする。
spsr_cxsf
の後ろの_cxsf
は4文字それぞれがSPSRの中でどの領域に書き込むかの指定であり、SPSR全体を書き換える場合は_cxsf
を指定しておけば問題ない。
{
let mut sp = sp;
for i in 0..13 {
*sp = (*tcbs)[next].r[i];
sp = sp.offset(1);
}
*sp = (*tcbs)[next].pc as u32;
let cpsr = (*tcbs)[next].cpsr;
asm!("msr spsr_cxsf, $0" :: "r"(cpsr));
}
取得したときと同様にプロセッサモードを変えながらlr,spを上書きする。
asm!("cps #31
mov lr, $0
mov sp, $1
cps #18" ::"r"((*tcbs)[next].lr), "r"((*tcbs)[next].sp));
あとはIRQハンドラから復帰すればコンテキストスイッチが行われる。
課題
コンテキストスイッチを動作させるためSystemモードでのlrの退避と復帰がなぜ必要なのかわかっていないので調べる必要がある。(当初lrの退避・復帰が実装されておらずコンテキストスイッチが動作してくれなかった)
最初のタスク起動
基本的にはコンテキストスイッチ処理と変わりはない
#[no_mangle]
extern "C" fn demo_setup_switch(sp: *mut u32) {
unsafe {
let tcbs = (*TCBS).lock();
{
let mut sp = sp;
for i in 0..13 {
*sp = (*tcbs)[current_task].r[i];
sp = sp.offset(1);
}
*sp = (*tcbs)[current_task].pc as u32;
let cpsr = (*tcbs)[current_task].cpsr;
asm!("msr spsr_cxsf, $0" :: "r"(cpsr));
}
asm!("cps #31 @ SYSTEM MODE
mov lr, $0
mov sp, $1
cps #19 @ SVC MODE"
::"r"((*tcbs)[current_task].lr), "r"((*tcbs)[current_task].sp));
}
}
pub fn demo_start() {
unsafe {
{
let mut tcbs = (*TCBS).lock();
(*tcbs).push(Tcb::new(entry1));
(*tcbs).push(Tcb::new(entry2));
}
asm!(
"stmfd sp!, {r0-r12, lr}
mov r0, sp
bl demo_setup_switch
ldmfd sp!, {r0-r12, pc}^"
);
}
}
demo_start()
で最初のタスクが起動される。
spをdemo_setup_switch()
に渡して、最初のタスクのレジスタ値で上書きする。pcはタスクのエントリポイントを指定し、SPSRも上書きする。
Systemモードにてlr,spを上書きしてldmfd sp!, {r0-r12, pc}^
で最初のタスクが起動される。
この処理はSVCモードで動作させているため、Systemモードの後戻るプロセッサモードはIRQモード(cps #18
)ではなくSVCモード(cps #19
)になっている点が異なっている。