link もっと前
   2020年 10月 14日 -
      2020年 10月 5日  
link もっと後

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

2020年 10月 14日

link permalink

link 編集する

Zephyr OS で遊ぼう その 23 - SMP 対応 CPU コア数 1、HART 0 以外で動かす

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

前回は HART 0 以外で動かす際に、動作確認が必要なので準備を行いました。今回は HART 0 以外で動かします。

実行する HART を一時的にずらす

一番簡単なやり方は、ブート時の判定条件を変えることだと思います。通常は HART ID が 0 だったら起動しますが、0 じゃない HART のときに起動するように変更します。この変更は最終的には不要なので、あとで元に戻すのを忘れないようにしてください。

変更前の実行結果

// zephyr/arch/riscv/core/reset.S

...

SECTION_FUNC(TEXT, __initialize)
	/*
	 * This will boot master core, just halt other cores.
	 * Note: need to be updated for complete SMP support
	 */
	csrr a0, mhartid
	addi a0, a0, -3    //★HART ID - 3 = 0 なら実行する、つまり HART ID 3 で実行する★
	beqz a0, boot_master_core

...

Zephyr の CPU コア数は menuconfig から変更可能です。なぜかは知りませんが、最大 4コアらしいです。

CPU 数の変更
$ ninja menuconfig

General Kernel Options  --->
  SMP Options  --->
    (4) Number of CPUs/cores

実行してみます。QEMU の -smp cpus=1 オプションを cpus=4 に変更して 4コアで実行します。

変更前の実行結果
$ 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=4 -bios none

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

HART ID は変わりました。しかしスレッド A からスレッド B に切り替わらず、ハングアップしてしまいます。原因はタイマー割り込みが HART ID 0 以外に入らないからです。Zephyr はタイマー割り込みによってカーネルの内部時間(Tick)を更新する他、割り込みを契機にコンテキストスイッチを行っています。

タイマーの SMP 対応

Zephyr では通常の定期的なタイマー割り込みと、Tickless Timer という不定期なタイマー割り込みの仕組みがあります。通常のタイマーの場合、一定時間ごとにタイマー割り込みを発生(例えば 10ms ごとなど)させ、1Tick ずつ時間を進めます。実装は単純ですが、用もなくタイマー割り込みが発生するため、消費電力や処理性能に悪影響を及ぼします。

Tickless Timer の場合、各 CPU が「最後に割り込みが発生した時刻」を記録しておいて、タイマー、タイマー以外の割り込みが発生した際に、前回の割り込みからどれだけ時間が経過したか、つまり、何 Tick 経過したか?を計算して、一気に時間を進めます。また「次のタイマー割り込みの設定」は、できるだけ遠く(現在時刻 + 1 Tick)に設定して、無用なタイマー割り込みが発生しないように工夫されています。

「最後に割り込みが発生した時刻」と「次のタイマー割り込みの設定」は CPU が割り込みを受けたタイミングによって値が変わり、全 CPU で共有する値ではありませんから、CPU ごとに専用の場所を用意する必要があります。

タイマーの SMP 対応

// zephyr/drivers/timer/riscv_machine_timer.c(変更前)

static struct k_spinlock lock;
static uint64_t last_count;

static void set_mtimecmp(uint64_t time)
{
#ifdef CONFIG_64BIT
	*(volatile uint64_t *)RISCV_MTIMECMP_BASE = time;
#else
	volatile uint32_t *r = (uint32_t *)RISCV_MTIMECMP_BASE;


// zephyr/drivers/timer/riscv_machine_timer.c(変更後)

#define RISCV_MTIMECMP (RISCV_MTIMECMP_BASE + (uintptr_t)z_riscv_hart_id() * 8)    //★「次のタイマー割り込みの設定」★
#define last_count last_count_mp[z_riscv_hart_id()]    //★「最後に割り込みが発生した時刻」★

static struct k_spinlock lock;
static uint64_t last_count_mp[CONFIG_MP_NUM_CPUS];     //★CPU の数だけ配列を確保★

static void set_mtimecmp(uint64_t time)
{
#ifdef CONFIG_64BIT
	*(volatile uint64_t *)RISCV_MTIMECMP = time;
#else
	volatile uint32_t *r = (uint32_t *)RISCV_MTIMECMP;

今回の SMP 対応では MTIMECMP レジスタの幅が 64bit であることがわかれば、動作の詳細を知らなくても読み進められると思います。

仕様が気になる場合は、SiFive Core Local Interruptor(CLINT)の仕様を参照ください。CLINT は FE310 もしくは FU540 のマニュアルに載っています。FE310 はシングルコア、FU540 はマルチコアです(FE310-G002 Manual, FU540-C000 Manual)。

効果の確認

以上の対応で HART ID 0 以外もタイマー割り込みが入るようになり、スケジューラが動作するようになったはずです。

変更後の実行結果
$ 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=4 -bios none

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

...

HART ID = 3 で実行されています。やったね。以降、実行する HART を一時的にずらす変更は不要なので、元に戻すことを忘れないようにしてください。

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

コメント一覧

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



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



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