🥝

RISC-Vのlinux kernelにおけるcontext switch

2024/06/13に公開

備忘録、riscvのコンテキストスイッチ部のレジスタの保存/復元処理を見ていきます。

arch/ricv/include/asm/switch_to.h

#define switch_to(prev, next, last)			\
do {							\
	struct task_struct *__prev = (prev);		\
	struct task_struct *__next = (next);		\
	if (has_fpu())					\
		__switch_to_fpu(__prev, __next);	\
	if (has_vector())					\
		__switch_to_vector(__prev, __next);	\
	((last) = __switch_to(__prev, __next));		\
} while (0)

このマクロがsched/core.cから呼ばれます。

__switch_to_fpuはfpuのレジスタを保存する関数です。
もしfpuのstateがdirtyで保存する必要があれば__fstate_save()が呼ばれます。

fpu.S
SYM_FUNC_START(__fstate_save)
	li  a2,  TASK_THREAD_F0
	add a0, a0, a2
	li t1, SR_FS
	csrs CSR_STATUS, t1
	frcsr t0
	fsd f0,  TASK_THREAD_F0_F0(a0)
	fsd f1,  TASK_THREAD_F1_F0(a0)
	fsd f2,  TASK_THREAD_F2_F0(a0)
...
	fsd f31, TASK_THREAD_F31_F0(a0)
	sw t0, TASK_THREAD_FCSR_F0(a0)
	csrc CSR_STATUS, t1
	ret
SYM_FUNC_END(__fstate_save)

TASK_THREAD_F0, F1...は
arch/riscv/kernel/asm-offsets.cにおいて定義されています。

void asm_offsets(void) {
...
	OFFSET(TASK_THREAD_F0,  task_struct, thread.fstate.f[0]);
	OFFSET(TASK_THREAD_F1,  task_struct, thread.fstate.f[1]);
...
}

OFFSETマクロの定義は以下の通り。

include/linux/kbuild.h
#define DEFINE(sym, val) \
	asm volatile("\n.ascii \"->" #sym " %0 " #val "\"" : : "i" (val))
...
#define OFFSET(sym, str, mem) \
	DEFINE(sym, offsetof(struct str, mem))

これはTASK_THREAD_F0をstruct task_structにおけるメンバthread.fstate.f[0]のオフセット情報をバイナリに埋め込む処理です。
例えば
OFFSET(TASK_THREAD_F0, task_struct, thread.fstate.f[0]);
は、
->TASK_THREAD_F0 2000 offsetof(struct task_struct, thread.fstate.f[0])
という文字列を埋め込むことになります。
そしてコンパイル時にこれらのバイト列をKbuildが解釈してarch/riscv/include/generated/asm-offsets.hを生成します。

...
#define TASK_TI_CPU_NUM 32 /* offsetof(struct task_struct, thread_info.cpu) */
#define TASK_THREAD_F0 2800 /* offsetof(struct task_struct, thread.fstate.f[0]) */
#define TASK_THREAD_F1 2808 /* offsetof(struct task_struct, thread.fstate.f[1]) */
...

もう一度__fstate_save()を見ます。

csrsはcontrol and status registerをsetするという処理で、CSR_STATUSはセットしたいCSRの番号を、t1(=SR_FS)はセットしたい値を表します。RISC-V priviledged manual,p17を見てみると、SR_FS(=0x300)のCSRはmstatusというものです。SR_FS(=0x00006000)によってmstatusのFS[1:0]を1にセットします。おそらくSet Register Floating Statusの意。

frcsr t0でFCSRという浮動小数点計算のCSRの値をt0に読み込みます。

fsdは浮動小数点の値をsetし、幅はdoubleであることを表します。TASK_THREAD_F0_Fn(a0)はF0からFnまでのオフセットをF0の(task_structからの)オフセットに足している。つまり、struct task_structのレジスタ情報保存用のフィールドに今のfpuの値を格納しています。

最後にsw(store word)命令でFCSRの値を保存し、mstateのFSビットをクリアしています。

dirtyフラグをクリアして、同様に必要があればnextのfpuをロードします。
vectorが有効であれば同様に処理します。

その次の__switch_toが本体。

arch/riscv/kernel/entry.S
SYM_FUNC_START(__switch_to)
	/* Save context into prev->thread */
	li    a4,  TASK_THREAD_RA
	add   a3, a0, a4
	add   a4, a1, a4
	REG_S ra,  TASK_THREAD_RA_RA(a3)
	REG_S sp,  TASK_THREAD_SP_RA(a3)
	REG_S s0,  TASK_THREAD_S0_RA(a3)
	REG_S s1,  TASK_THREAD_S1_RA(a3)
	REG_S s2,  TASK_THREAD_S2_RA(a3)
	REG_S s3,  TASK_THREAD_S3_RA(a3)
	REG_S s4,  TASK_THREAD_S4_RA(a3)
	REG_S s5,  TASK_THREAD_S5_RA(a3)
	REG_S s6,  TASK_THREAD_S6_RA(a3)
	REG_S s7,  TASK_THREAD_S7_RA(a3)
	REG_S s8,  TASK_THREAD_S8_RA(a3)
	REG_S s9,  TASK_THREAD_S9_RA(a3)
	REG_S s10, TASK_THREAD_S10_RA(a3)
	REG_S s11, TASK_THREAD_S11_RA(a3)
	/* Save the kernel shadow call stack pointer */
	scs_save_current
	/* Restore context from next->thread */
	REG_L ra,  TASK_THREAD_RA_RA(a4)
	REG_L sp,  TASK_THREAD_SP_RA(a4)
	REG_L s0,  TASK_THREAD_S0_RA(a4)
	REG_L s1,  TASK_THREAD_S1_RA(a4)
	REG_L s2,  TASK_THREAD_S2_RA(a4)
	REG_L s3,  TASK_THREAD_S3_RA(a4)
	REG_L s4,  TASK_THREAD_S4_RA(a4)
	REG_L s5,  TASK_THREAD_S5_RA(a4)
	REG_L s6,  TASK_THREAD_S6_RA(a4)
	REG_L s7,  TASK_THREAD_S7_RA(a4)
	REG_L s8,  TASK_THREAD_S8_RA(a4)
	REG_L s9,  TASK_THREAD_S9_RA(a4)
	REG_L s10, TASK_THREAD_S10_RA(a4)
	REG_L s11, TASK_THREAD_S11_RA(a4)
	/* The offset of thread_info in task_struct is zero. */
	move tp, a1
	/* Switch to the next shadow call stack */
	scs_load_current
	ret
SYM_FUNC_END(__switch_to)

a0, a1には引数でstruct task_struct *prev, *nextが渡されます。
している処理は殆ど__fstate_save(), __fstate_restore()と同じで、struct task_structの各レジスタに今の値を格納、nextの値をロードするという処理です。scs_save_current, scs_load_currentはtp(スレッドポインタ)をセーブ、ロードします。
move tp, a1はスレッドポインタを新しいものに更新しています。

気が向いたらメモリマップのswitch処理についても書こうと思います。arch/riscv/mm/context.cのswitch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *task)などの関数が行っていて、同じくスケジューラやforkシステムコールなどから呼ばれます。

Discussion