link もっと前
   2020年 10月 13日 -
      2020年 10月 4日  
link もっと後

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

2020年 10月 13日

link permalink

link 編集する

Zephyr OS で遊ぼう その 22 - SMP 対応 CPU コア数 1、HART 0 以外で動かす、動作確認環境

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

前回は CONFIG_SMP のビルドエラーと実行時エラーに対応しました。以前書いたとおり、SMP 対応は下記の手順で進めていますので、再掲します。

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

前回までで 2番目の項目が終わったところです。今回はコア数を増やして先頭以外のコアで実行します。

動作確認の環境

Zephyr を書き換える前に、変更した効果が確認できる環境を作りましょう。サンプルの synchronization を少し改造して HART ID を表示します。

スレッド名を表示させる機能を加える

// zephyr/samples/synchronization/src/main.c

void helloLoop(const char *my_name,
	       struct k_sem *my_sem, struct k_sem *other_sem)
{
	const char *tname;

	while (1) {
		int id = z_riscv_hart_id();    //★HART ID を取得★

		/* take my semaphore */
		k_sem_take(my_sem, K_FOREVER);

		/* say "hello" */
		tname = k_thread_name_get(k_current_get());
		if (tname != NULL && tname[0] != '\0') {
			printk("%d: %s: Hello World from %s!\n",
				id, tname, CONFIG_BOARD);    //★HART ID を一緒に表示する★
		} else {
			printk("%d: %s: Hello World from %s!\n",
				id, my_name, CONFIG_BOARD);    //★HART ID を一緒に表示する★
		}

今回は変更してもしなくても構わないですが、カーネルコンフィグを変えると k_thread_name_get() でスレッド名が取得できるようになります。スレッドを多数作成したときに便利です。

スレッド名を表示させる設定
$ ninja menuconfig

General Kernel Options  --->
  Kernel Debugging and Metrics  --->
    [*] Thread name [EXPERIMENTAL]

動作させると下記のような表示になるはずです。

変更前の実行結果
$ mkdir build
$ cd build
$ cmake -G Ninja -DBOARD=qemu_rv32_virt ../samples/synchronization/

...

$ ninja

...

$ qemu-system-riscv32 -nographic -machine virt -net none -chardev stdio,id=con,mux=on -serial chardev:con -mon chardev=con,mode=readline -kernel zephyr/zephyr.elf -cpu rv32 -smp cpus=1 -bios none

** Booting Zephyr OS build zephyr-v2.4.0-546-g720718653f92  ***
0: thread_a: Hello World from QEMU RV32 virt board!
0: thread_b: Hello World from QEMU RV32 virt board!
0: thread_a: Hello World from QEMU RV32 virt board!
0: thread_b: Hello World from QEMU RV32 virt board!

...

HART ID = 0 で実行されていることがわかります。

続きは次回です。

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

コメント一覧

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



2020年 10月 12日

link permalink

link 編集する

Zephyr OS で遊ぼう その 21 - SMP 対応 CPU コア数 1、実行時エラーの対処

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

SMP 対応のうち、ビルドエラーの対処が終わったので、実行時エラーに対処します。

実行時エラー: k_spin_lock

CONFIG_SMP を有効にすると、k_spin_lock() 内で atomic_cas() を呼ぶようになります。すると k_spin_lock() -> atomic_cas() -> z_impl_atomic_cas() -> k_spin_lock() という循環呼び出しが発生し、スタックオーバーフローを起こしてクラッシュします。これは Zephyr のバグではなくコンフィグの設定間違いが原因です。

k_spin_lock() の循環呼び出し

// zephyr/include/spinlock.h

static ALWAYS_INLINE k_spinlock_key_t k_spin_lock(struct k_spinlock *l)
{
	ARG_UNUSED(l);
	k_spinlock_key_t k;

	/* Note that we need to use the underlying arch-specific lock
	 * implementation.  The "irq_lock()" API in SMP context is
	 * actually a wrapper for a global spinlock!
	 */
	k.key = arch_irq_lock();

#ifdef CONFIG_SPIN_VALIDATE
	__ASSERT(z_spin_lock_valid(l), "Recursive spinlock %p", l);
#endif

#ifdef CONFIG_SMP
	while (!atomic_cas(&l->locked, 0, 1)) {    //★CONFIG_SMP が有効だと atomic_cas() を呼ぶ★
	}
#endif

...


// zephyr/include/sys/atomic.h

#ifdef CONFIG_ATOMIC_OPERATIONS_BUILTIN
static inline bool atomic_cas(atomic_t *target, atomic_val_t old_value,
			  atomic_val_t new_value)
{
	return __atomic_compare_exchange_n(target, &old_value, new_value,
					   0, __ATOMIC_SEQ_CST,
					   __ATOMIC_SEQ_CST);
}
#elif defined(CONFIG_ATOMIC_OPERATIONS_C)    //★既存の RISC-V ボードはこちらが有効になっている★
__syscall bool atomic_cas(atomic_t *target, atomic_val_t old_value,
			 atomic_val_t new_value);

#else
extern bool atomic_cas(atomic_t *target, atomic_val_t old_value,
		      atomic_val_t new_value);
#endif

...


// build/zephyr/include/generated/syscalls/atomic.h

static inline bool atomic_cas(atomic_t * target, atomic_val_t old_value, atomic_val_t new_value)
{
#ifdef CONFIG_USERSPACE
	if (z_syscall_trap()) {
		return (bool) arch_syscall_invoke3(*(uintptr_t *)&target, *(uintptr_t *)&old_value, *(uintptr_t *)&new_value, K_SYSCALL_ATOMIC_CAS);
	}
#endif
	compiler_barrier();
	return z_impl_atomic_cas(target, old_value, new_value);    //★ここにくる★
}

...


// zephyr/kernel/CMakeLists.txt

target_sources_ifdef(CONFIG_ATOMIC_OPERATIONS_C   kernel PRIVATE atomic_c.c)    //★CONFIG_ATOMIC_OPERATIONS_C 有効のとき実装は atomic_c.c★

...

// zephyr/kernel/atomic_c.c

bool z_impl_atomic_cas(atomic_t *target, atomic_val_t old_value,
		       atomic_val_t new_value)
{
	k_spinlock_key_t key;
	int ret = false;

	key = k_spin_lock(&lock);    //★循環呼び出し★

	if (*target == old_value) {
		*target = new_value;
		ret = true;
	}

	k_spin_unlock(&lock, key);

	return ret;
}

...

RISC-V の SoC のコンフィグでは大抵 CONFIG_ATOMIC_OPERATIONS_C が有効になっていて、atomic_cas() の実装としてスピンロックを使います。これは SMP と相性が悪く、CONFIG_ATOMIC_OPERATIONS_C と CONFIG_SMP を同時に有効にすると先ほど説明した循環呼び出しが発生してしまいます。

循環呼び出しを防ぐには独自に atomic_cas() を実装する必要がありますが、アトミック操作を自分で実装&検証するのは大変ですから、RISC-V のアトミック命令(Atomic Extension)とコンパイラの機能を頼ります。

以前追加した QEMU RISC-V 32bit virtpc 用のコンフィグを SMP のテスト用に改造します。

ATOMIC_OPERATIONS_* の設定例

# zephyr/soc/riscv/riscv-privilege/rv32-virt/Kconfig.soc

config SOC_QEMU_RV32_VIRT
	bool "QEMU RV32 virt SOC implementation"
	select ATOMIC_OPERATIONS_C if !SMP         # 非 SMP のときは従来通り
	select ATOMIC_OPERATIONS_BUILTIN if SMP    # SMP のときは Atomic Extension に頼る

コンフィグ CONFIG_ATOMIC_OPERATIONS_BUILTIN を有効にすると、Zephyr は atomic_cas() の実装として __atomic_compare_exchange_n() ビルトイン関数を使います。ビルトイン関数を使うにはコンパイラのサポートが必要で、今のところ、サポートしているのは GCC のみだと思います。LLVM でも使えるかもしれませんが、未調査です。

これで CONFIG_SMP を有効にしても、エラーやハングアップすることなく、今までどおりに動作するようになったはずです。

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

コメント一覧

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



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 この記事にコメントする



link もっと前
   2020年 10月 13日 -
      2020年 10月 4日  
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 サイトの情報