コグノスケ


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

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

2020年10月13日

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で実行されていることがわかります。

続きは次回です。

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

コメント一覧

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



2020年10月12日

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を有効にしても、エラーやハングアップすることなく、今までどおりに動作するようになったはずです。

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

コメント一覧

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



2020年10月11日

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で置き換え。

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

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

コメント一覧

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



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



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

管理用メニュー

link 記事を新規作成

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

最近のコメント5件

  • link 21年3月13日
    すずきさん (03/05 15:13)
    「あー、このプログラムがまずいんですね。ご...」
  • link 21年3月13日
    emkさん (03/05 12:44)
    「キャストでvolatileを外してアクセ...」
  • link 24年1月24日
    すずきさん (02/19 18:37)
    「簡単にできる方法はPowerShellの...」
  • link 24年1月24日
    KKKさん (02/19 02:30)
    「追伸です。\nネットで調べたらマイクロソ...」
  • link 24年1月24日
    KKKさん (02/19 02:25)
    「私もエラーで困ってます\n手動での回復パ...」

最近の記事3件

  • link 24年3月25日
    すずき (03/26 03:20)
    「[Might and Magic Book One TASのその後] 目次: Might and Magicファミコン版以前(...」
  • link 21年10月4日
    すずき (03/26 03:14)
    「[Might and Magicファミコン版 - まとめリンク] 目次: Might and Magicファミコン版TASに挑...」
  • link 24年3月19日
    すずき (03/20 02:52)
    「[モジュラージャックの規格] 古くは電話線で、今だとEthernetで良く見かけるモジュラージャックというコネクタとレセプタク...」
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

最終更新: 03/26 03:20