コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2020年10月10日 >>> 2020年9月27日
link もっと後

2020年10月10日

Zephyr OSで遊ぼう その19 - SMP対応CPUコア数1、ビルドエラーの対処1

目次: Zephyr

新しい形式のコンテキストスイッチを実装しました。以前書いたとおり、SMP対応は下記の手順で進めています。再掲しておきましょう。

  • SMPの前提条件、新しいコンテキストスイッチ方式に対応する(CONFIG_USE_SWITCH, CONFIG_USE_SWITCH_SUPPORTED)
  • (今ここ)SMPに対応する(CONFIG_SMP)、ただしCPUコア数は1
  • 先頭ではないコア(mhartid != 0)で動作させる、ただしCPUコア数は1
  • CPUコア数を1以上にする(CONFIG_SMP)

やっと最初の項目が終わったところです。いよいよCONFIG_SMPを有効にします。大量のビルドエラーが発生しますので、1つずつやっつけます。

コンパイルエラー: arch_curr_cpu

環境や利用するバージョンによりますが、最初に目にするのはarch_curr_cpu() に関するコンパイルエラーだと思われます。

arch_curr_cpu() が未定義のときに出るエラー
../include/sys/arch_interface.h:367:28: warning: 'arch_curr_cpu' declared 'static' but never defined [-Wunused-function]
 static inline struct _cpu *arch_curr_cpu(void);
                            ^~~~~~~~~~~~~

この関数は、現在のCPU(= 実行中のCPU)の情報を返します。RISC-Vにはmhartidという自身のHART IDを取得できるCSR(Control and Status Registers)が規格で定められており、この手の処理は楽に実装できます。

arch_curr_cpu() の実装

// zephyr/include/arch/riscv/arch_inlines.h

static inline uint32_t z_riscv_hart_id(void)
{
	uint32_t hartid;

	__asm__ volatile ("csrr %0, mhartid" : "=r"(hartid));

	return hartid;
}

static inline struct _cpu *arch_curr_cpu(void)
{
#ifdef CONFIG_SMP
	uint32_t hartid = z_riscv_hart_id();

	return &_kernel.cpus[hartid];
#else
	return &_kernel.cpus[0];
#endif
}

他のアーキテクチャを見る限りarch_inlines.hに定義するのが良さそうですが、RISC-V向けには存在しません。新たに追加しましょう。ヘッダファイルを追加したら、親玉のarch_inlines.hに #includeを追加します。

arch_curr_cpu() の実装(続き)

// zephyr/include/arch/arch_inlines.h

...

#if defined(CONFIG_X86) || defined(CONFIG_X86_64)
#include <arch/x86/arch_inlines.h>
#elif defined(CONFIG_ARC)
#include <arch/arc/arch_inlines.h>
#elif defined(CONFIG_XTENSA)
#include <arch/xtensa/arch_inlines.h>
#elif defined(CONFIG_RISCV)             //★この2行を追加する
#include <arch/riscv/arch_inlines.h>    //★
#endif

このヘッダは明示的に #includeしなくても常にインクルードされます。

リンクエラー: arch_start_cpu

メインCPU以外のCPU(2つ目以降のCPU)を起動するための関数です。SMPモードの他、非SMPモード(※)でも使います。今はCPU 1つで動かすので、とりあえず空関数を定義します。

関数はどこに定義しても動きますが、他アーキテクチャの実装を見るとSMP関連の関数は1つのCソースファイルにまとめた方が良さそうなので、新たにcpu_smp.cを作成します。

arch_start_cpu() の実装(仮)

// zephyr/arch/riscv/core/CMakeLists.txt

zephyr_library_sources(
  cpu_idle.c
  cpu_smp.c    ★足す★
  fatal.c
  irq_manage.c
  isr.S
  prep_c.c
  reset.S
  swap.S
  thread.c
)


// zephyr/arch/riscv/core/cpu_smp.c

void arch_start_cpu(int cpu_num, k_thread_stack_t *stack, int sz,
		    arch_cpustart_t fn, void *arg)
{
}

ZephyrというかCMakeのルールですけども、新たにソースコードを追加した場合、CMakeLists.txtにファイル名を追加しコンパイル対象に指定する必要があります。特定のCONFIG_* が定義されたときだけコンパイルすることも可能ですが、今回は不要です。

(※)Zephyrのマルチプロセッサモードには、SMPモードと非SMPモードがあります。SMPモードは、互いのプロセッサ間でIPI(Inter-Processor Interrupt)を用いて制御します。非SMPモードでは、互いのプロセッサのことは何も考慮せず動作します。

リンクエラー: smp_timer_init

これはSMP用のタイマーの初期化関数です。タイマーのハードウェア構成はアーキテクチャによって様々で、一様に「こう実装すべき」という指針はありません。今はCPU 1つで動かすので、とりあえず空関数を定義します。

smp_timer_init() を追加(仮)

// zephyr/drivers/timer/riscv_machine_timer.c

...

void smp_timer_init(void)
{
}

今回はRISC-VのPrivilege modeのタイマーが実装対象です。タイマードライバはriscv_machine_timer.cになります。

長くなってきたので、続きは次回。

編集者:すずき(2023/09/24 12:10)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年10月5日

Zephyr OSで遊ぼう その18 - SMP対応の準備、コンテキストスイッチの実装、後編3、プリエンプション

目次: Zephyr

前回はRISC-Vの2つあるコンテキストスイッチのうち、明示的なコンテキストスイッチを実装しました。今回はもう一方のプリエンプションを実装します。

対応方針(再掲)

従来と新形式のコンテキストスイッチで大きく異なるのは、下記の要素です。

明示的コンテキストスイッチ 従来 新形式
割り込まれた処理の返り値 設定必要(thread->arch.swap_return_value) 設定不要(do_swap() がやってくれる)
切り替え元スレッド _kernel.cpu[0].current a1レジスタ(引数old_thread->switch_handle)
切り替え先スレッド _kernel.ready_q.cache a0レジスタ(引数new_thread->switch_handle)

プリエンプション 従来 新形式
割り込まれた処理の返り値 設定必要(thread->arch.swap_return_value) 設定不要(do_swap() がやってくれる)
切り替え元スレッド _kernel.cpu[0].current _kernel.cpu[n].current(※)
切り替え先スレッド _kernel.ready_q.cache z_get_next_switch_handle() の返り値

(※)初めは切り替え元スレッドですが、z_get_next_switch_handle() を呼ぶと、切り替え先のスレッドに変わります。

割り込まれた処理の返り値

明示的プリエンプションと共通の部分のため、改めて直す必要はないです。

切り替え元/切り替え先スレッド

かなり処理が変わるため、#ifdefだとごちゃごちゃしてしまいます。スレッド取得の専用マクロを作ります。

切り替え元、切り替え先スレッドの取得

// zephyr/arch/riscv/core/isr.S

/*
 * xcpu: pointer of _kernel.cpus[n]
 * xold: (result) old thread
 * xnew: (result) next thread to schedule
 *
 * after this function a0 is broken
 */
.macro z_riscv_get_next_switch_handle xcpu, xold, xnew
#ifdef CONFIG_USE_SWITCH
	addi sp, sp, -RV_REGSIZE*2               //★新たな処理★
	RV_OP_STOREREG ra, RV_REGSIZE(sp)
	addi a0, sp, 0                           //★スタックの先頭へのポインタを第一引数old_threadとする★
	jal ra, z_arch_get_next_switch_handle    //★(2) この関数内で _currentがnew_threadに設定される★
	addi xnew, a0, 0                        //★a0が返り値、切り替え先のスレッドが入っている★
	RV_OP_LOADREG xold, 0(sp)               //★スタック先頭に切り替え元のスレッドが入っている★
	RV_OP_LOADREG ra, RV_REGSIZE(sp)
	addi sp, sp, RV_REGSIZE*2
#else
	/* Get pointer to _kernel.current */                           //★従来処理★
	RV_OP_LOADREG xold, _kernel_offset_to_current(xcpu)          //★切り替え元スレッド★

	RV_OP_LOADREG xnew, _kernel_offset_to_ready_q_cache(xcpu)    //★切り替え先スレッド★
#endif
.endm


// zephyr/arch/riscv/core/thread.c

#ifdef CONFIG_USE_SWITCH
void *z_arch_get_next_switch_handle(struct k_thread **old_thread)
{
	*old_thread =  _current;    //★スタックの先頭に現在のスレッド(= 切り替え元のスレッド)を保存★

	return z_get_next_switch_handle(*old_thread);
}
#endif

わざわざスタックのポインタを経由して書き込むなんてややこしいことをせず、RV_OP_LOADREG xold, _kernel_offset_to_current(xcpu) で良いのでは?と思うかもしれませんが、z_arch_get_next_switch_handle() の呼び出しでどのレジスタが壊れるかわかりませんから、結局 xoldをスタックに退避する必要があります。

明示的コンテキストスイッチと処理を共有しているため、ちょっとわかりにくいですが、プリエンプションの中心となる処理はこの辺りです。

プリエンプション処理の差分

// zephyr/arch/riscv/core/isr.S

 #ifdef CONFIG_PREEMPT_ENABLED
-	/*
-	 * Check if we need to perform a reschedule
-	 */
-
-	/* Get pointer to _kernel.current */
-	RV_OP_LOADREG t2, _kernel_offset_to_current(t1)
-
 	/*
 	 * Check if next thread to schedule is current thread.
 	 * If yes do not perform a reschedule
 	 */
-	RV_OP_LOADREG t3, _kernel_offset_to_ready_q_cache(t1)
+	z_riscv_get_next_switch_handle t1, t2, t3
 	beq t3, t2, no_reschedule
+
+#ifdef CONFIG_USE_SWITCH
+	/* Set old thread to t1 */
+	addi t1, t2, 0             //★(1) t1レジスタにold_threadを設定する★
+#endif
+

前回決めたとおり、合流地点(reschedule)に辿り着く前に切り替え元(old_thread)、切り替え先スレッド(new_thread)を取得し _currentにnew_threadを設定し, t1レジスタにold_threadを設定します。

処理 (1) でnew_threadを _currentに設定していて、処理 (2) でold_threadをt1レジスタに設定してから、rescheduleに到達します。プリエンプション処理のすぐ後にrescheduleラベルがあるので、ジャンプは不要です。

とても長くなってしまいましたが、新しい形式のコンテキストスイッチを実装できました。苦労の割に動作の見た目は何も変わりませんが、本命のSMP対応に活用するためなので我慢です。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年10月4日

Zephyr OSで遊ぼう その17 - SMP対応の準備、コンテキストスイッチの実装、後編2、明示的コンテキストスイッチ

目次: Zephyr

前回はRISC-Vの明示的なコンテキストスイッチの既存実装を調べました。今回は新しいコンテキストスイッチを実装します。

対応方針

従来と新形式のコンテキストスイッチで大きく異なるのは、下記の要素です。

明示的コンテキストスイッチ 従来 新形式
割り込まれた処理の返り値 設定必要(thread->arch.swap_return_value) 設定不要(do_swap() がやってくれる)
切り替え元スレッド _kernel.cpu[0].current a1レジスタ(引数old_thread->switch_handle)
切り替え先スレッド _kernel.ready_q.cache a0レジスタ(引数new_thread->switch_handle)

プリエンプション 従来 新形式
割り込まれた処理の返り値 設定必要(thread->arch.swap_return_value) 設定不要(do_swap() がやってくれる)
切り替え元スレッド _kernel.cpu[0].current _kernel.cpu[n].current(※)
切り替え先スレッド _kernel.ready_q.cache z_get_next_switch_handle() の返り値

(※)初めは切り替え元スレッドですが、z_get_next_switch_handle() を呼ぶと、切り替え先のスレッドに変わります。

割り込まれた処理の返り値

割り込まれた処理の返り値を -EINTRに設定するために必要な処理は、do_swap() がやるため実装は必要ありません。従来の処理が間違って発動しないように #ifdefで消しておきます。

swap_return_valueは不要

// zephyr/arch/riscv/core/isr.Sの差分

+	/* Save stack pointer of current thread. */
+	RV_OP_STOREREG sp, _thread_offset_to_sp(t1)    //★スタックポインタ保存は新しい形式でも必要★
+
+#ifndef CONFIG_USE_SWITCH
 	/*
-	 * Save stack pointer of current thread and set the default return value
-	 * of z_swap to _k_neg_eagain for the thread.
+	 * Set the default return value of z_swap to _k_neg_eagain for
+	 * the thread.
 	 */
-	RV_OP_STOREREG sp, _thread_offset_to_sp(t1)
 	la t2, _k_neg_eagain
 	lw t3, 0x00(t2)
 	sw t3, _thread_offset_to_swap_return_value(t1)    //★返り値設定は不要★
+#endif /* !CONFIG_USE_SWITCH */


// zephyr/arch/riscv/core/offsets/offsets.c

//★_thread_offset_to_swap_return_value() マクロを使えるようにする仕掛け★

#ifndef CONFIG_USE_SWITCH
GEN_OFFSET_SYM(_thread_arch_t, swap_return_value);    //★いらない★
#endif /* !CONFIG_USE_SWITCH */


// zephyr/include/arch/riscv/thread.h

struct _thread_arch {
#ifndef CONFIG_USE_SWITCH
	uint32_t swap_return_value; /* Return value of z_swap() */    //★いらない★
#endif /* !CONFIG_USE_SWITCH */
};

処理を消すだけでも動きますが、swap_return_valueを間違って使うとバグの元なので、変数宣言ごと消します。

切り替え元/切り替え先スレッド

従来は常に _kernel変数を見れば良かったので楽でした。新形式では明示的コンテキストスイッチとプリエンプションで切り替え元/切り替え先スレッドの取得方法が異なります。よって、明示的コンテキストスイッチとプリエンプションで、スレッドの扱いを揃える必要があります。

設計する人の自由で決めて構いませんが、今回は合流地点(reschedule)に辿り着く前に切り替え元(old_thread)、切り替え先スレッド(new_thread)を取得し _currentにnew_threadを設定し, t1レジスタにold_threadを設定することとします。

明示的コンテキストスイッチの場合、切り替え元と切り替え先スレッドは引数で渡されます。引数はハンドラの先頭でスタックに保存されますので、スタックからロードできます。

切り替え元/切り替え先スレッドの取得

// zephyr/arc/riscv/core/isr.S

#ifdef CONFIG_USE_SWITCH
	/*
	 * Get new_thread and old_thread from stack.
	 *   - a0 = new_thread->switch_handle  -> _current
	 *   - a1 = &old_thread->switch_handle -> t1
	 */

	/* Get reference to _kernel */
	la t2, _kernel

	/* Get new_thread from stack */
	RV_OP_LOADREG t1, __z_arch_esf_t_a0_OFFSET(sp)    //★スタックから切り替え先スレッド取得★

	/* Set new thread to _current */
	RV_OP_STOREREG t1, ___cpu_t_current_OFFSET(t2)    //★(2) この関数内でnew_threadを _currentに設定★

	/* Get old_thread from stack and set it to t1 */
	RV_OP_LOADREG t1, __z_arch_esf_t_a1_OFFSET(sp)    //★(1)スタックから切り替え元スレッド取得、t1レジスタにold_threadを設定★
	addi t1, t1, -___thread_t_switch_handle_OFFSET    //★(A) old_thread->switch_handleの更新★
#endif

	/*
	 * Go to reschedule to handle context-switch
	 */
	j reschedule


// zephyr/arch/riscv/core/thread.c

void arch_new_thread(struct k_thread *thread, k_thread_stack_t *stack,
		     char *stack_ptr, k_thread_entry_t entry,
		     void *p1, void *p2, void *p3)
{
	struct __esf *stack_init;

...

#ifdef CONFIG_USE_SWITCH
	thread->switch_handle = thread;    //★(B) switch_handleの初期値設定★
#endif
}

処理 (1) でnew_threadを _currentに設定していて、処理 (2) でold_threadをt1レジスタに設定してから、rescheduleにジャンプします。

wait_for_switch() への対処

スレッドと直接関係ないもののold_thread->switch_handleを更新する処理も重要です。あとでハマりやすいポイントですので、補足しておきます。

以前、少し言及しましたが(2020年9月30日の日記参照)、switch_handleの更新を実装し忘れるとCONFIG_SMPを有効にしたときにwait_for_switch() で無限ループに陥ってハマります。

Zephyrのドキュメントにてarch_switch() を見ると(Zephyr Project: arch_switch)、スレッドにレジスタを退避後、old_thread->switch_handleをNULL以外の値で書き換える必要があります。これはコンテキストスイッチ処理内で行う (A) の処理に相当します。

実はこれだけではダメです。wait_for_switch() はコンテキストスイッチの「前」に呼ばれるからです。一番最初に発生するコンテキストスイッチのold_thread->switch_handleは誰も書き換えてくれないのでハングします。この問題の対処としてスレッド生成時にswitch_handleを初期化する (B) の処理を実装しています。

次回はプリエンプションの実装をします。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年10月3日

Zephyr OSで遊ぼう その16 - SMP対応の準備、コンテキストスイッチの実装、後編1、RISC-Vのコンテキストスイッチ

目次: Zephyr

前回は、AArch64の実装を調べました。いよいよ新しい方式のコンテキストスイッチを実装したいところですが、その前にもう一つだけRISC-Vの既存実装を調べます。

RISC-Vの2つのコンテキストスイッチ経路

RISC-V向け実装において、コンテキストスイッチが行われる条件は2つあります。1つはスリープしたときなどに呼ばれる明示的なコンテキストスイッチです。do_swap() を経由します。もう1つは割り込み発生時に行われるプリエンプションです。

明示的コンテキストスイッチ
do_swap() -> arch_switch() ラッパー関数 -> z_riscv_switch() -> ecall -> __irq_wrapper -> is_syscall -> reschedule -> no_reschedule -> mret
プリエンプション
(任意の場所) -> __irq_wrapper -> is_interrupt -> on_irq_stack -> 割り込みハンドラ(isr_timer() など) -> on_thread_stack -> reschedule -> no_reschedule -> mret

明示的コンテキストスイッチについては、以前(2020年9月29日の日記参照)実装したラッパー関数がスタート地点となります。コードを変更する前に、従来のコンテキストスイッチがどんな経路を通るか確認します。

arch_switch() ラッパー関数 -> z_riscv_switch() -> ecall -> __irq_wrapper -> is_syscall

// zephyr/arch/riscv/include/kernel_arch_func.h

static inline void arch_switch(void *switch_to, void **switched_from)
{
	z_riscv_switch(switch_to, switched_from);
}


// zephyr/arch/riscv/core/swap.S

/*
 * void z_riscv_switch(void *switch_to, void **switched_from)
 */
SECTION_FUNC(exception.other, z_riscv_switch)

	/* Make a system call to perform context switch */
	ecall    //★例外を発生させる★

	jalr x0, ra


// zephyr/arch/riscv/core/isr.S

/*
 * Handler called upon each exception/interrupt/fault
 * In this architecture, system call (ECALL) is used to perform context
 * switching or IRQ offloading (when enabled).
 */
SECTION_FUNC(exception.entry, __irq_wrapper)
	/* Allocate space on thread stack to save registers */
	addi sp, sp, -__z_arch_esf_t_SIZEOF

...

	/*
	 * Check if exception is the result of an interrupt or not.
	 * (SOC dependent). Following the RISC-V architecture spec, the MSB
	 * of the mcause register is used to indicate whether an exception
	 * is the result of an interrupt or an exception/fault. But for some
	 * SOCs (like pulpino or riscv-qemu), the MSB is never set to indicate
	 * interrupt. Hence, check for interrupt/exception via the __soc_is_irq
	 * function (that needs to be implemented by each SOC). The result is
	 * returned via register a0 (1: interrupt, 0 exception)
	 */
	jal ra, __soc_is_irq

	/* If a0 != 0, jump to is_interrupt */
	addi t1, x0, 0
	bnez a0, is_interrupt    //★割り込みの場合はこちらにジャンプする★

	/*
	 * If the exception is the result of an ECALL, check whether to
	 * perform a context-switch or an IRQ offload. Otherwise call _Fault
	 * to report the exception.
	 */
	csrr t0, mcause
	li t2, SOC_MCAUSE_EXP_MASK
	and t0, t0, t2
	li t1, SOC_MCAUSE_ECALL_EXP

	/*
	 * If mcause == SOC_MCAUSE_ECALL_EXP, handle system call,
	 * otherwise handle fault
	 */
	beq t0, t1, is_syscall    //★ecallの場合はこちらにジャンプする★

	/*
	 * Call _Fault to handle exception.
	 * Stack pointer is pointing to a z_arch_esf_t structure, pass it
	 * to _Fault (via register a0).
	 * If _Fault shall return, set return address to no_reschedule
	 * to restore stack.
	 */
	addi a0, sp, 0
	la ra, no_reschedule
	tail _Fault    //★いずれでもなければ停止させる★

...

Zephyr RISC-V向け実装では、割り込み・例外ハンドラは1つだけです。割り込みも例外も全て __irq_wrapperに飛んできますから、最初の方で要因をチェックして仕分けしています。RISC-Vの規格としては割り込み要因ごとに別の割り込みハンドラに飛べる形式(ベクタ形式)もありますが、Zephyrは使っていません。

明示的コンテキストスイッチの実行経路: is_syscall -> reschedule -> no_reschedule -> mret


// zephyr/arch/riscv/core/isr.S

is_syscall:
	/*
	 * A syscall is the result of an ecall instruction, in which case the
	 * MEPC will contain the address of the ecall instruction.
	 * Increment saved MEPC by 4 to prevent triggering the same ecall
	 * again upon exiting the ISR.
	 *
	 * It's safe to always increment by 4, even with compressed
	 * instructions, because the ecall instruction is always 4 bytes.
	 */
	RV_OP_LOADREG t0, __z_arch_esf_t_mepc_OFFSET(sp)
	addi t0, t0, 4
	RV_OP_STOREREG t0, __z_arch_esf_t_mepc_OFFSET(sp)

...

	/*
	 * Go to reschedule to handle context-switch
	 */
	j reschedule  //★コンテキストスイッチ★

...

reschedule:

...

	/* Get reference to _kernel */
	la t0, _kernel

	/* Get pointer to _kernel.current */
	RV_OP_LOADREG t1, _kernel_offset_to_current(t0)

	/*
	 * Save callee-saved registers of current thread
	 * prior to handle context-switching
	 */
	RV_OP_STOREREG s0, _thread_offset_to_s0(t1)
	RV_OP_STOREREG s1, _thread_offset_to_s1(t1)
...
	RV_OP_STOREREG s10, _thread_offset_to_s10(t1)
	RV_OP_STOREREG s11, _thread_offset_to_s11(t1)

...

	/*
	 * Save stack pointer of current thread and set the default return value
	 * of z_swap to _k_neg_eagain for the thread.
	 */
	RV_OP_STOREREG sp, _thread_offset_to_sp(t1)
	la t2, _k_neg_eagain
	lw t3, 0x00(t2)
	sw t3, _thread_offset_to_swap_return_value(t1)

	/* Get next thread to schedule. */
	RV_OP_LOADREG t1, _kernel_offset_to_ready_q_cache(t0)

	/*
	 * Set _kernel.current to new thread loaded in t1
	 */
	RV_OP_STOREREG t1, _kernel_offset_to_current(t0)

	/* Switch to new thread stack */
	RV_OP_LOADREG sp, _thread_offset_to_sp(t1)

	/* Restore callee-saved registers of new thread */
	RV_OP_LOADREG s0, _thread_offset_to_s0(t1)
	RV_OP_LOADREG s1, _thread_offset_to_s1(t1)
...
	RV_OP_LOADREG s10, _thread_offset_to_s10(t1)
	RV_OP_LOADREG s11, _thread_offset_to_s11(t1)

...

no_reschedule:

...

	/* Restore MEPC register */
	RV_OP_LOADREG t0, __z_arch_esf_t_mepc_OFFSET(sp)
	csrw mepc, t0

	/* Restore SOC-specific MSTATUS register */
	RV_OP_LOADREG t0, __z_arch_esf_t_mstatus_OFFSET(sp)
	csrw mstatus, t0

...

	/* Restore caller-saved registers from thread stack */
	RV_OP_LOADREG ra, __z_arch_esf_t_ra_OFFSET(sp)
	RV_OP_LOADREG gp, __z_arch_esf_t_gp_OFFSET(sp)
	RV_OP_LOADREG tp, __z_arch_esf_t_tp_OFFSET(sp)
	RV_OP_LOADREG t0, __z_arch_esf_t_t0_OFFSET(sp)
...
	RV_OP_LOADREG a6, __z_arch_esf_t_a6_OFFSET(sp)
	RV_OP_LOADREG a7, __z_arch_esf_t_a7_OFFSET(sp)

	/* Release stack space */
	addi sp, sp, __z_arch_esf_t_SIZEOF

	/* Call SOC_ERET to exit ISR */
	SOC_ERET

コメントが丁寧に書いてあって素晴らしいですね。コンテキストスイッチの手順はAArch64の実装とほぼ同じですが、AAarch64は明示的なコンテキストスイッチとプリエンプションが独立して実装されており、RISC-Vはrescheduleで両者が合流する点が違います。コンテキストスイッチの説明は先日(2020年10月1日の日記参照)の紙芝居が参考になるかと思います。

明示的なコンテキストスイッチとプリエンプションの部分が大体仕分けできました。いよいよ実装に挑みます。続きはまた。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年10月1日

Zephyr OSで遊ぼう その15 - SMP対応の準備、コンテキストスイッチの実装、中編2、既存実装調査

目次: Zephyr

前回は、新しいコンテキストスイッチ関数に対応しているAArch64の実装のうち、共通部分からアーキテクチャ依存部分に至るまでを調べました。引き続きアーキテクチャ依存部分を調べます。

アーキ依存部分の処理(AArch64)

コンテキストスイッチのアーキテクチャ依存部分(AArch64向けarch_switch())の実装はほぼ全てアセンブラで実装されています。

コンテキストスイッチの本体はz_arm64_context_switchです。x0がnew_thread, x1がold_threadだと思って動きます。コンテキストスイッチの仕事はレジスタの値を切り替え前のスレッド構造体(old_thread)に退避し、切り替え後のスレッド構造体(new_thread)からレジスタの値を復旧させることです。

AArch64の新しい形式のコンテキストスイッチarch_switch() 実装

// zephyr/arch/arm/core/aarch64/switch.S

GTEXT(z_arm64_svc)
SECTION_FUNC(TEXT, z_arm64_svc)
	z_arm64_enter_exc x2, x3, x4    /* ★レジスタをスタックに退避(切り替え前のスレッド)★ */

	switch_el x1, 3f, 2f, 1f
3:
	mrs	x0, esr_el3
	b	0f
2:
	mrs	x0, esr_el2
	b	0f
1:
	mrs	x0, esr_el1
0:
	lsr	x1, x0, #26

	cmp	x1, #0x15 /* 0x15 = SVC */
	bne	inv

	/* Demux the SVC call */
	and	x1, x0, #0xff
	cmp	x1, #_SVC_CALL_CONTEXT_SWITCH
	beq	context_switch    /* ★以下参照★ */

...

context_switch:
	/*
	 * Retrieve x0 and x1 from the stack:
	 *  - x0 = new_thread->switch_handle = switch_to thread
	 *  - x1 = x1 = &old_thread->switch_handle = current thread
	 */
	ldp	x0, x1, [sp, #(16 * 10)]    /* ★x1 = 切り替え前、x0 = 切り替え後のスレッド★ */

	/* Get old thread from x1 */
	sub	x1, x1, ___thread_t_switch_handle_OFFSET

	/* Switch thread */
	bl	z_arm64_context_switch    /* ★コンテキストスイッチ本体(スタックが切り替わる)★ */

exit:
	z_arm64_exit_exc x0, x1, x2    /* ★レジスタをスタックから復旧(切り替え後のスレッド)★ */

...

/**
 * @brief Routine to handle context switches
 *
 * This function is directly called either by _isr_wrapper() in case of
 * preemption, or z_arm64_svc() in case of cooperative switching.
 */

GTEXT(z_arm64_context_switch)
SECTION_FUNC(TEXT, z_arm64_context_switch)
	/* addr of callee-saved regs in thread in x2 */
	ldr	x2, =_thread_offset_to_callee_saved
	add	x2, x2, x1

	/* Store rest of process context including x30 */
	stp	x19, x20, [x2], #16
	stp	x21, x22, [x2], #16
	stp	x23, x24, [x2], #16
	stp	x25, x26, [x2], #16
	stp	x27, x28, [x2], #16
	stp	x29, x30, [x2], #16

	/* Save the current SP */
	mov	x1, sp
	str	x1, [x2]

	/* addr of callee-saved regs in thread in x2 */
	ldr	x2, =_thread_offset_to_callee_saved
	add	x2, x2, x0

	/* Restore x19-x29 plus x30 */
	ldp	x19, x20, [x2], #16
	ldp	x21, x22, [x2], #16
	ldp	x23, x24, [x2], #16
	ldp	x25, x26, [x2], #16
	ldp	x27, x28, [x2], #16
	ldp	x29, x30, [x2], #16

	ldr	x1, [x2]
	mov	sp, x1    /* ★ここで切り替え後のスレッドのスタックに変わる★ */

#ifdef CONFIG_TRACING
	stp	xzr, x30, [sp, #-16]!
	bl	sys_trace_thread_switched_in
	ldp	xzr, x30, [sp], #16
#endif

	/* We restored x30 from the process stack. There are three possible
	 * cases:
	 *
	 * - We return to z_arm64_svc() when swapping in a thread that was
	 *   swapped out by z_arm64_svc() before jumping into
	 *   z_arm64_exit_exc()
	 * - We return to _isr_wrapper() when swapping in a thread that was
	 *   swapped out by _isr_wrapper() before jumping into
	 *   z_arm64_exit_exc()
	 * - We return (jump) into z_thread_entry_wrapper() for new threads
	 *   (see thread.c)
	 */
	ret


// zephyr/include/arm/aarch64/thread.h

struct _callee_saved {
	uint64_t x19;
	uint64_t x20;
	uint64_t x21;
	uint64_t x22;
	uint64_t x23;
	uint64_t x24;
	uint64_t x25;
	uint64_t x26;
	uint64_t x27;
	uint64_t x28;
	uint64_t x29; /* FP */
	uint64_t x30; /* LR */
	uint64_t sp;
};

コードを見て一発で理解するのは厳しいので、処理の概要を書いておきます。


Zephyr AArch64のコンテキストスイッチ概要

コンテキストスイッチの際にスタックが切り替わります。z_arm64_exit_exc x0, x1, x2はシステムコールを呼んだスレッド(=切り替え前のスレッド)ではなく、コンテキストスイッチ後のスレッドのスタックからレジスタを復旧します。

これも文章だと何だかわからないので、紙芝居を書いておきます。


Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ先頭


Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ入り口、スタックへレジスタ退避


Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ前半、切り替え前スレッド構造体へレジスタ退避


Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ前半、切り替え前スレッド構造体へスタックポインタ退避


Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ後半、切り替え後スレッド構造体からレジスタ復旧


Zephyr AArch64のコンテキストスイッチ: コンテキストスイッチ後半、切り替え後スレッド構造体からスタックポインタ復旧(=スタック切り替え)


Zephyr AArch64のコンテキストスイッチ: 例外ハンドラ入り口、切り替え後のスタックからレジスタ復旧

図には書きませんでしたが、例外からリターンする命令(eret命令)が参照するレジスタ(SPSR, ELRレジスタ)もスタックに退避、復旧しています。従ってeretが戻る先は切り替え後のスレッドのコードです。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年9月30日

Zephyr OSで遊ぼう その14 - SMP対応の準備、コンテキストスイッチの実装、中編1、既存実装調査

目次: Zephyr

前回は、新しい形式のコンテキストスイッチ関数arch_switch() のラッパー関数まで実装しました。RISC-V向け実装をする前に、既に新しいコンテキストスイッチ関数に対応しているAArch64の実装を調べます。

共通部分からアーキ依存部分

コンテキストスイッチ処理の共通部分(do_swap())から、アーキテクチャ依存部分(AArch64のarch_switch())に至るまでを見ます。

Zephyrの新しい形式のコンテキストスイッチ関数(共通部分)

// zephyr/kernel/include/kswap.h

/* New style context switching.  arch_switch() is a lower level
 * primitive that doesn't know about the scheduler or return value.
 * Needed for SMP, where the scheduler requires spinlocking that we
 * don't want to have to do in per-architecture assembly.
 *
 * Note that is_spinlock is a compile-time construct which will be
 * optimized out when this function is expanded.
 */
static ALWAYS_INLINE unsigned int do_swap(unsigned int key,
					  struct k_spinlock *lock,
					  int is_spinlock)
{

...

	if (new_thread != old_thread) {
#ifdef CONFIG_TIMESLICING
		z_reset_time_slice();
#endif

		old_thread->swap_retval = -EAGAIN;

#ifdef CONFIG_SMP
		_current_cpu->swap_ok = 0;

		new_thread->base.cpu = arch_curr_cpu()->id;

		if (!is_spinlock) {
			z_smp_release_global_lock(new_thread);
		}
#endif
		sys_trace_thread_switched_out();
		_current_cpu->current = new_thread;
		wait_for_switch(new_thread);
		arch_switch(new_thread->switch_handle,
			     &old_thread->switch_handle);    //★コンテキストスイッチ★

	}

...

前々回に紹介した新しいコンテキストスイッチの方式(arch_switch())を使っていることがわかります。

AArch64の新しい形式のコンテキストスイッチarch_switch() 実装、入り口

// zephyr/arch/arm/include/aarch64/arch_func.h

static inline void arch_switch(void *switch_to, void **switched_from)
{
	z_arm64_call_svc(switch_to, switched_from);    //★アセンブラ実装へ★

	return;
}


// zephyr/arch/arm/core/aarch64/switch.S

GTEXT(z_arm64_call_svc)
SECTION_FUNC(TEXT, z_arm64_call_svc)
	svc	#_SVC_CALL_CONTEXT_SWITCH    //★スーパーバイザーコール命令★
	ret

アセンブラ側の実装では、いきなりスーパーバイザーコール命令(svc命令)をぶっ放して、スーパーバイザーコール例外を起こすだけになっています。AArch64は例外ハンドラ内でコンテキストスイッチを行う仕組みのようです。

arch_switch() の引数

関数arch_switch() はスレッドを2つ受け取ります。old_threadは切り替え元のスレッド、new_threadは切り替え先のスレッドです。old_thread -> new_threadにコンテキストスイッチするわけです。ただし直接スレッド構造体を受け取るわけではなく、若干クセのある渡し方をします。

第1引数new_thread->switch_handle(void * 型)は切り替え先のスレッド構造体へのポインタ(struct k_thread * 型)new_threadが入っています。このポインタをキャストするとnew_threadが求められます。

実はここには罠があり、何も実装せずにいるとnew_thread->switch_handleにはNULLが入ります。すると、あとでCONFIG_SMPを有効にした段階でwait_for_switch() 関数にてハングアップします。こんなのパッと見ではわかりません……。

switch_handleの初期化

// zephyr/kernel/include/kswap.h

/* New style context switching.  arch_switch() is a lower level
 * primitive that doesn't know about the scheduler or return value.
 * Needed for SMP, where the scheduler requires spinlocking that we
 * don't want to have to do in per-architecture assembly.
 *
 * Note that is_spinlock is a compile-time construct which will be
 * optimized out when this function is expanded.
 */
static ALWAYS_INLINE unsigned int do_swap(unsigned int key,
					  struct k_spinlock *lock,
					  int is_spinlock)
{

...

	if (new_thread != old_thread) {

...

		sys_trace_thread_switched_out();
		_current_cpu->current = new_thread;
		wait_for_switch(new_thread);    //★これ★
		arch_switch(new_thread->switch_handle,
			     &old_thread->switch_handle);

	}

	if (is_spinlock) {
		arch_irq_unlock(key);
	} else {
		irq_unlock(key);
	}

	return _current->swap_retval;
}


/* There is an unavoidable SMP race when threads swap -- their thread
 * record is in the queue (and visible to other CPUs) before
 * arch_switch() finishes saving state.  We must spin for the switch
 * handle before entering a new thread.  See docs on arch_switch().
 *
 * Note: future SMP architectures may need a fence/barrier or cache
 * invalidation here.  Current ones don't, and sadly Zephyr doesn't
 * have a framework for that yet.
 */
static inline void wait_for_switch(struct k_thread *thread)
{
#ifdef CONFIG_SMP
	volatile void **shp = (void *)&thread->switch_handle;

	while (*shp == NULL) {    //★CONFIG_SMP有効でnew_thread->switch_handleがNULLだとこのループでハング★
		k_busy_wait(1);
	}
#endif
}

AArch64向けのスレッド作成する関数を良く見ると、しれっとswitch_handleを初期化しています。この処理はRISC-V向けには存在しないため、追加する必要がありそうです。

switch_handleの初期化

// zephyr/arch/arm/core/aarch64/thread.c

/*
 * An initial context, to be "restored" by z_arm64_context_switch(), is put at
 * the other end of the stack, and thus reusable by the stack when not needed
 * anymore.
 */
void arch_new_thread(struct k_thread *thread, k_thread_stack_t *stack,
		     char *stack_ptr, k_thread_entry_t entry,
		     void *p1, void *p2, void *p3)
{

...

	/*
	 * We are saving:
	 *
	 * - SP: to pop out entry and parameters when going through
	 *   z_thread_entry_wrapper().
	 * - x30: to be used by ret in z_arm64_context_switch() when the new
	 *   task is first scheduled.
	 */

	thread->callee_saved.sp = (uint64_t)pInitCtx;
	thread->callee_saved.x30 = (uint64_t)z_thread_entry_wrapper;

	thread->switch_handle = thread;    //★ここで初期化している★
}

関数arch_switch() の第2引数 &old_thread->switch_handle(void ** 型)は切り替え元のスレッド構造体のswitch_handleのポインタです。switch_handleの値(たいていNULL)自体には意味がなくて、このポインタからold_threadが計算できることが大事です。

長くなってきたので続きは次回。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年9月29日

Zephyr OSで遊ぼう その13 - SMP対応の準備、コンテキストスイッチの実装、前編、ラッパー関数まで実装

目次: Zephyr

CONFIG_USE_SWITCHを有効にするとビルドエラーが発生しますので、都度対処します。

arch_thread_return_value_set()

最初はarch_thread_return_value_set() が二重定義だと怒られます。

arch_thread_return_value_set() が二重定義
../kernel/include/kernel_internal.h: At top level:
../kernel/include/kernel_internal.h:84:1: error: redefinition of 'arch_thread_return_value_set'
 arch_thread_return_value_set(struct k_thread *thread, unsigned int value)
 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

USE_SWITCHを有効にした場合、arch_thread_return_value_set() はカーネル側で定義されるので、アーキテクチャ側では実装不要です。

arch_thread_return_value_set() カーネル側の定義

// zephyr/arch/riscv/include/kernel_arch_func.h

static ALWAYS_INLINE void
arch_thread_return_value_set(struct k_thread *thread, unsigned int value)
{
	thread->arch.swap_return_value = value;
}


// zephyr/include/arch/riscv/thread.h

struct _thread_arch {
	uint32_t swap_return_value; /* Return value of z_swap() */
};


// zephyr/arch/riscv/core/offsets/offsets.c

GEN_OFFSET_SYM(_thread_arch_t, swap_return_value);

この辺りは要らないので #ifndef CONFIG_USE_SWITCHで囲って無効化します。

arch_switch

次はarch_switch() が未定義だと言われます。

arch_switch() が未定義
../kernel/include/kernel_arch_interface.h:126:20: warning: 'arch_switch' declared 'static' but never defined [-Wunused-function]
 static inline void arch_switch(void *switch_to, void **switched_from);
                    ^~~~~~~~~~~

この関数がUSE_SWITCHの本体です。他のアーキテクチャを見ると、arch_switch() はラッパー関数で、本体はアセンブラで書かれていることが多いようです。他の流儀に習っておきます。

arch_switch() ラッパー

// arch/riscv/include/kernel_arch_func.h

#ifdef CONFIG_USE_SWITCH
extern void z_riscv_switch(void *switch_to, void **switched_from);

static inline void arch_switch(void *switch_to, void **switched_from)
{
	z_riscv_switch(switch_to, switched_from);
}
#else

...

肝心の中身はまた今度実装します。

編集者:すずき(2023/09/24 12:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2020年9月28日

Zephyr OSで遊ぼう その12 - SMP対応の準備、コンテキストスイッチのコンフィグ

目次: Zephyr

以前(2020年2月21日の日記参照)ご紹介したとおり、ZephyrのRISC-V向け実装はSMPに対応していません。Zephyrのマルチコア対応を有効にするコンフィグはCONFIG_SMPですが、有効にすると大量のエラーが出て、何から直したら良いのかわからなくなります。

コンフィグや実装を見た限りでは、下記の4つのステップで対応していくと良さそうです。

  • (今ここ)SMPの前提条件、新しいコンテキストスイッチ方式に対応する(CONFIG_USE_SWITCH, CONFIG_USE_SWITCH_SUPPORTED)
  • SMPに対応する(CONFIG_SMP)、ただしCPUコア数は1
  • 先頭ではないコア(mhartid != 0)で動作させる、ただしCPUコア数は1
  • CPUコア数を1以上にする(CONFIG_SMP)

このコンフィグを有効にすその前に対応すべきコンフィグがあります。CONFIG_USE_SWITCHとCONFIG_USE_SWITCH_SUPPORTEDです。

CONFIG_USE_SWITCH

// zephyr/kernel/Kconfig

config USE_SWITCH
	bool "Use new-style _arch_switch instead of arch_swap"
	depends on USE_SWITCH_SUPPORTED

...

config SMP
	bool "Enable symmetric multithreading support"
	depends on USE_SWITCH

既存の各アーキテクチャの対応状況を見ると、

CONFIG_USE_SWITCH対応状況(一例)

// ARM
// Aarch32はUSE_SWITCHは未サポート
// AArch64はCortex-AのみUSE_SWITCHをサポート

// zephyr/arch/arm/core/aarch64/Kconfig

config CPU_CORTEX_A
	bool
	select CPU_CORTEX
	select HAS_FLASH_LOAD_OFFSET
	select USE_SWITCH
	select USE_SWITCH_SUPPORTED


// ARC
// ARCV2はUSE_SWITCHをサポート

// zephyr/arch/arc/Kconfig

config CPU_ARCV2
	bool
	select ARCH_HAS_STACK_PROTECTION if ARC_HAS_STACK_CHECKING || ARC_MPU
	select ARCH_HAS_USERSPACE if ARC_MPU
	select USE_SWITCH
	select USE_SWITCH_SUPPORTED
	default y

他は載せませんがAArch64, ARC, x86_64, xtensaが対応しているようです。

Zephyrのコンテキストスイッチ

Zephyrにはコンテキストスイッチが2種類存在しています。違いは下記の通りです。

  • arch_swap: シングルコア専用のコンテキストスイッチ、RISC-Vは対応済み
  • arch_switch: SMPに対応したコンテキストスイッチ、シングルコアだと若干効率が悪い、RISC-Vは未対応

CONFIG_USE_SWITCH_SUPPORTEDを有効にするにはarch_switchを実装する必要がありますが、RISC-Vではシングルコアもマルチコアもあり得ますから、CONFIG_USE_SWITCHを無条件に有効にするのは得策ではないと考えられます。

SoC側のKconfigで有効にしても良いですし、ユーザーにmenuconfigから選んでもらう手もあります。あまり悩んでも仕方ないので、ユーザーが選べるようにして先に進めます。RISC-VのKconfigにSMP対応かどうか?を選べるコンフィグを一つ追加します。

CONFIG_USE_SWITCHを有効にするRISC-V向けコンフィグを追加

// zephyr/arch/riscv/Kconfig

config RISCV_SMP
	bool "Does SOC has SMP"
	select USE_SWITCH
	select USE_SWITCH_SUPPORTED

有効にするとビルドエラーだらけになります。続きはまた今度。

編集者:すずき(2023/09/24 12:08)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2020年10月10日 >>> 2020年9月27日
link もっと後

管理用メニュー

link 記事を新規作成

<2020>
<<<10>>>
----123
45678910
11121314151617
18192021222324
25262728293031

最近のコメント20件

  • link 21年9月20日
    すずきさん (11/19 01:04)
    「It was my pleasure.」
  • link 21年9月20日
    whtさん (11/17 23:41)
    「This blog solves my ...」
  • link 24年10月1日
    すずきさん (10/06 03:41)
    「xrdpで十分動作しているので、Wayl...」
  • link 24年10月1日
    hdkさん (10/03 19:05)
    「GNOMEをお使いでしたら今はWayla...」
  • link 24年10月1日
    すずきさん (10/03 10:12)
    「私は逆にVNCサーバーに繋ぐ使い方をした...」
  • link 24年10月1日
    hdkさん (10/03 08:30)
    「おー、面白いですね。xrdpはすでに立ち...」
  • link 14年6月13日
    2048player...さん (09/26 01:04)
    「最後に、この式を出すのに紙4枚(A4)も...」
  • link 14年6月13日
    2048playerさん (09/26 01:00)
    「今のところ最も簡略化した式です。\n--...」
  • link 14年6月13日
    2048playerさん (09/16 01:00)
    「返信ありがとうございます。\nコメントが...」
  • link 14年6月13日
    すずきさん (09/12 21:19)
    「コメントありがとうございます。同じ結果に...」
  • link 14年6月13日
    2048playerさん (09/08 17:30)
    「私も2048の最高スコアを求めたのですが...」
  • link 14年6月13日
    2048さん (09/08 17:16)
    「私も2048の最高スコアを求めたのですが...」
  • link 14年6月13日
    2048playerさん (09/08 16:10)
    「私も2048の最高スコアを求めたのですが...」
  • link 02年8月4日
    lxbfYeaaさん (07/12 10:11)
    「555」
  • link 24年6月17日
    すずきさん (06/23 00:12)
    「ありがとうございます。バルコニーではない...」
  • link 24年6月17日
    hdkさん (06/22 22:08)
    「GPSの最初の同期を取る時は見晴らしのい...」
  • link 24年5月16日
    すずきさん (05/21 11:41)
    「あー、確かにdpkg-reconfigu...」
  • link 24年5月16日
    hdkさん (05/21 08:55)
    「システム全体のlocale設定はDebi...」
  • link 24年5月17日
    すずきさん (05/20 13:16)
    「そうですねえ、普通はStandardなの...」
  • link 24年5月17日
    hdkさん (05/19 07:45)
    「なるほど、そういうことなんですね。Exc...」

最近の記事3件

  • link 23年4月10日
    すずき (11/15 23:48)
    「[Linux - まとめリンク] 目次: Linux関係の深いまとめリンク。目次: RISC-V目次: ROCK64/ROCK...」
  • link 24年11月6日
    すずき (11/15 23:47)
    「[Ubuntu 24.04 LTS on ThinkPad X1 Carbon Gen 12] 目次: Linux会社ではTh...」
  • link 24年11月11日
    すずき (11/15 23:26)
    「[Pythonのテストフレームワーク] 目次: Python最近Pythonを触ることが増えたのでテストについて調べようと思い...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 11/19 01:04