Help us understand the problem. What is going on with this article?

[Rust] ラズパイ2でベアメタルコンテキストスイッチ

はじめに

ラズパイ2上でRustを使ってコンテキストスイッチが動作するようになったので仕組みや不明点、課題を記載する。

リポジトリは以下
https://github.com/osdev-rs/minimal-kernel-rpi2/tree/task

例によって「やってみた」系の記事なので多くは望まないで欲しい

IRQハンドラ

コンテキストスイッチ処理の入口となるIRQハンドラから見ていく。

boot.S
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^について

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0802bj/Cihcadda.html

CPSRはゼロフラグやキャリーフラグ等現在の演算状態を保持するレジスタ。

IRQハンドラが呼び出されるとプロセッサモードはIRQモードとなり、元のモードのCPSRはSPSRへ保存される。
ldmfd^はレジスタリスト内にpcがあるときSPSRをCPSRに書き戻す処理をおこなう。
CPSRはプロセッサモードを決めるフラグも持つためこれにより、IRQハンドラからリターンすると共に元のプロセッサモードに戻ることになる。

なおSPSRからCPSRへの書き戻しはsubs pc, lr, #4でも行われる

プロセッサモードとレジスタ

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473lj/dom1359731128950_00010.html

Screenshot from 2019-01-30 14-39-38.png

上記のようにプロセッサモード毎に共有するレジスタと別に持つレジスタがある。
今回のコードではタスクは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つのタスクを交互に切り替える。

コンテキストスイッチの手順はこうなる

  1. IRQハンドラの冒頭でr0-r12,lrをspへ退避(上記「IRQハンドラ」参照)
  2. (タスク切り替え処理では)spに退避したレジスタとSPSRを切替元タスクのTCB(タスクコントロールブロック(後述))に保存
  3. spの同じ領域を切替先タスクのTCBに保存してあるレジスタで書き換え、SPSRもそのTCBに保存してあるSPSRに書き換える
  4. 上記の書き換えによりIRQハンドラからldmfd sp!, {r0-r12, pc}^で返る際に切替先タスクに返る

C言語でコンテキストスイッチ書く場合は呼び出した関数の先で適当にsp調整していきなりldmfd sp!, {r0-r12, pc}^を呼び出すみたいな荒業も見かけるが、Rustだとオブジェクトの後処理が動かなかったり等明らかに問題があるので少なくともIRQハンドラへ戻ってからコンテキストスイッチが走るようにする必要がある。

TCB(タスクコントロールブロック)

TCBは中断されたタスクが再開できるように各レジスタ値を保存しておく領域である。

TCBの定義

task.rs
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 implSendSyncを実装して回避している。

#[no_std]でも使用できるspinspin::Mutexを使用している。

current_taskは今1番目のタスクか2番めのタスクかを記憶するために使用する。

なお、lazy_staticは

cargo.toml
[dependencies]
lazy_static = {version = "1.2.0", features = ["spin_no_std"]}

このように指定することで#[no_std]での使用が可能。

TCBの初期化

task.rs
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に退避されている)も保存する。

task.rs
        {
            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のコードは実行されないようにする必要がある。

task.rs
        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を指定しておけば問題ない。

task.rs
        {
            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を上書きする。

task.rs
        asm!("cps #31
              mov lr, $0
              mov sp, $1
              cps #18" ::"r"((*tcbs)[next].lr), "r"((*tcbs)[next].sp));

あとはIRQハンドラから復帰すればコンテキストスイッチが行われる。

課題

コンテキストスイッチを動作させるためSystemモードでのlrの退避と復帰がなぜ必要なのかわかっていないので調べる必要がある。(当初lrの退避・復帰が実装されておらずコンテキストスイッチが動作してくれなかった)

最初のタスク起動

基本的にはコンテキストスイッチ処理と変わりはない

task.rs
#[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)になっている点が異なっている。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away