link もっと前
   2020年 10月 11日 -
      2020年 10月 2日  
link もっと後

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

2020年 10月 11日

link permalink

link 編集する

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

目次: Zephyr を調べる - まとめリンク

SMP 対応の序盤、ビルドエラー対処の続きです。

オフセットマクロ

ビルドの難所です。CONFIG_SMP を有効にすると isr.S で大量にエラーが出ます。

_kernel_offset_* の未定義エラー

zephyr/arch/riscv/core/isr.S:305: Error: illegal operands `lw sp,_kernel_offset_to_irq_stack(t2)'
zephyr/arch/riscv/core/isr.S:316: Error: illegal operands `lw t3,_kernel_offset_to_nested(t2)'
zephyr/arch/riscv/core/isr.S:376: Error: illegal operands `sw t2,_kernel_offset_to_nested(t1)'
zephyr/arch/riscv/core/isr.S:463: Error: illegal operands `lw t1,(0x78+___ready_q_t_cache_OFFSET)(t0)'
zephyr/arch/riscv/core/isr.S:468: Error: illegal operands `sw t1,_kernel_offset_to_current(t0)'

原因は _kernel_offset_* 系のオフセットマクロが未定義になるためです。

_kernel_offset_* の定義箇所

// zephyr/kernel/include/offsets_short.h

#ifndef CONFIG_SMP
/* Relies on _kernel.cpu being the first member of _kernel and having 1 element
 */
#define _kernel_offset_to_nested \r	(___cpu_t_nested_OFFSET)

#define _kernel_offset_to_irq_stack \r	(___cpu_t_irq_stack_OFFSET)

#define _kernel_offset_to_current \r	(___cpu_t_current_OFFSET)
#endif /* CONFIG_SMP */

#define _kernel_offset_to_idle \r	(___kernel_t_idle_OFFSET)

#define _kernel_offset_to_current_fp \r	(___kernel_t_current_fp_OFFSET)

#define _kernel_offset_to_ready_q_cache \r	(___kernel_t_ready_q_OFFSET + ___ready_q_t_cache_OFFSET)

...


// zephyr/include/kernel_offsets.h

...

#ifndef CONFIG_SMP
GEN_OFFSET_SYM(_ready_q_t, cache);
#endif

当たり前ですが、この #ifdef を外すだけでは SMP は動きません。対策方法を理解するには、Zephyr のカーネル構造体の内部に、少しだけ立ち入る必要があります。

カーネル構造体(CPU が 1つの場合)

カーネル構造体は _kernel という名前で何度か出ていましたが、見覚えありますか?なくても全然構わないです。下記のような定義の構造体です。細かい定義はさておき、大事なことは cpus が _kernel の先頭にある、という点です。

_kernel の定義箇所

// zephyr/kernel/sched.c

/* the only struct z_kernel instance */
struct z_kernel _kernel;


// zephyr/include/kernel_structs.h

struct z_kernel {
	struct _cpu cpus[CONFIG_MP_NUM_CPUS];    //★_kernel の先頭に cpus がある★

#ifdef CONFIG_SYS_CLOCK_EXISTS
	/* queue of timeouts */
	sys_dlist_t timeout_q;
#endif

#ifdef CONFIG_SYS_POWER_MANAGEMENT
	int32_t idle; /* Number of ticks for kernel idling */
#endif

	/*
	 * ready queue: can be big, keep after small fields, since some
	 * assembly (e.g. ARC) are limited in the encoding of the offset
	 */
	struct _ready_q ready_q;

...


// zephyr/include/kernel_structs.h

struct _cpu {
	/* nested interrupt count */
	uint32_t nested;

	/* interrupt stack pointer base */
	char *irq_stack;

	/* currently scheduled thread */
	struct k_thread *current;

	/* one assigned idle thread per CPU */
	struct k_thread *idle_thread;

...

CPU が 1つしか存在しない場合、cpus の要素数は 1 であり、_kernel の先頭 = cpus[0] の先頭になります。そのため _kernel.cpus[0].current のオフセット = cpu 構造体の current へのオフセット、です。offsets_short.h の定義はこの性質を利用しています。

C 言語だと cpus[0] と cpus[i] の違いでしかなく、ありがたみがわかりませんが、アセンブラだと非常に単純かつ高速にオフセットを求めることができます。下記は isr.S から持ってきた例ですが、_kernel.cpus[0].current へのアクセスがわずか 2命令で実現できます。

CPU 数 1 のとき、_kernel へのアクセスが最適化できる

	la t0, _kernel
	RV_OP_LOADREG t0, _kernel_offset_to_current(t0)

残念ながら SMP の場合は cpus が 1つではありませんから、上記の最適化は使えません。cpus[n] のオフセット、つまり HART ID * sizeof(struct _cpu) を計算する必要があります。

まずは struct _cpu のオフセットマクロが未定義なので、追加します。Zephyr では GEN_ABSOLUTE_SYM() というマクロが用意されており、アセンブラ用のマクロを生成してくれます。便利ですね。

cpu_t のオフセットマクロの定義

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

#ifdef CONFIG_SMP
GEN_ABSOLUTE_SYM(__cpu_t_SIZEOF, sizeof(_cpu_t));
#endif

次に isr.S のビルドエラーが出ている箇所を直します。

isr.S の修正方針

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

/*
 * xreg0: result &_kernel.cpu[mhartid]
 * xreg1: work area
 */
.macro z_riscv_get_cpu xreg0, xreg1
#ifdef CONFIG_SMP
	csrr xreg0, mhartid
	addi xreg1, x0, __cpu_t_SIZEOF
	mul  xreg1, xreg0, xreg1
	la   xreg0, _kernel
	add  xreg0, xreg0, xreg1
#else
	la   xreg0, _kernel
#endif
.endm


//(変更前)

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

/* Decrement _kernel.cpus[0].nested variable */
lw t2, _kernel_offset_to_nested(t1)
addi t2, t2, -1
sw t2, _kernel_offset_to_nested(t1)


//(変更後)

/* Get reference to _kernel.cpus[n] */
z_riscv_get_cpu t1, t2               //★z_riscv_get_cpu に置き換え★

/* Decrement _kernel.cpus[n].nested variable */
lw t2, ___cpu_t_nested_OFFSET(t1)    //★_kernel_offset_to_* から ___cpu_t_*_OFFSET に置き換え★
addi t2, t2, -1
sw t2, ___cpu_t_nested_OFFSET(t1)

修正方針は 2つあります。

  • cpus[0] -> cpus[n]: _kernel.cpus[n] のアドレスを取得するマクロ z_riscv_get_cpu を作成。_kernel = cpus[0] のアドレスを取得しているところを z_riscv_get_cpu で置き換え。
  • _kernel_offset_to_* は未定義なので、___cpu_t_*_OFFSET で置き換え。

ここまで直すとビルドが通るはずですが、実はビルドが通るだけでは動きません。次回は実行時のエラーを対策します。

[編集者: すずき]
[更新: 2020年 10月 14日 01:18]

コメント一覧

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



2020年 10月 10日

link permalink

link 編集する

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 になります。

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

[編集者: すずき]
[更新: 2020年 10月 14日 20:02]

コメント一覧

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



2020年 10月 5日

link permalink

link 編集する

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 対応に活用するためなので我慢です。

[編集者: すずき]
[更新: 2020年 10月 10日 01:18]

コメント一覧

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



2020年 10月 4日

link permalink

link 編集する

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) の処理を実装しています。

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

[編集者: すずき]
[更新: 2020年 10月 9日 01:16]

コメント一覧

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



2020年 10月 3日

link permalink

link 編集する

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日の日記参照)の紙芝居が参考になるかと思います。

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

[編集者: すずき]
[更新: 2020年 10月 9日 01:13]

コメント一覧

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



link もっと前
   2020年 10月 11日 -
      2020年 10月 2日  
link もっと後

管理用メニュー

link 記事を新規作成

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDF ファイル RSS 1.0
QR コード QR コード

最終更新: 10/18 04:44

カレンダー

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

最近のコメント 5件

  • link 20年09月10日
    すずき 「追加情報。最新の Debian Test...」
    (更新:10/07 16:48)
  • link 20年09月20日
    hdk 「最近は音楽聞く時やビデオ視聴時はミニコン...」
    (更新:09/24 21:43)
  • link 20年09月20日
    すずき 「ありゃー、同じ壊れ方ですね。\n新たなヘ...」
    (更新:09/24 00:23)
  • link 20年09月20日
    hdk 「うちのATH-AD300もやはり頭にプラ...」
    (更新:09/23 12:26)
  • link 20年07月10日
    すずき 「鳥のゲームは知りませんでした。色々やって...」
    (更新:08/11 18:59)

最近の記事 3件

link もっとみる
  • link 20年02月22日
    すずき 「[Zephyr を調べる - まとめリンク] 日記が増えすぎて、一...」
    (更新:10/18 04:44)
  • link 20年10月18日
    すずき 「[Zephyr OS で遊ぼう その 27 - SMP 対応、] ...」
    (更新:10/18 04:43)
  • link 20年10月17日
    すずき 「[Zephyr OS で遊ぼう その 26 - SMP 対応、] ...」
    (更新:10/18 03:24)

こんてんつ

open/close wiki
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 過去日記について

その他の情報

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