はじめに
本稿はスケジューラ編よりスレッド編の方が適切なような気もしますが、Zephyrで予め用意されているスレッドはどのようにスケジューリングされているか、という観点で、スケジューラ編に含めました。
システムスレッドとは
Zephyrは予め下記2スレッドを具備しています。
- mainスレッド
- idleスレッド
これら以外にシステム起動後にワークキューのスレッドを生成したりしますが、これはワークキュー編で述べます。
それぞれのスレッドを一言でいうと
・mainスレッドは初期化処理及びユーザが作成したmain ( )を含むプログラムを実行するスレッド
・idleスレッドはready状態のスレッドが存在しない場合に実行するスレッド
です。
なお、本稿でもUP(Uni-Processor)システム向けのコードを見ることにします。
はじめにこれらのスレッドを生成する関数を見てみましょう。
mainスレッド、idleスレッドともに生成する関数はprepare_multithreading ( )です。
カーネルの初期化処理を担う_Cstart ( )という関数があり、その関数から呼び出されます。
理解しやすいように重要でないと判断したマクロや処理は省略します。
_Cstart ( )はLinuxでいうとstart_kernel ( )に相当します。
static void prepare_multithreading(struct k_thread *dummy_thread)
{
/*
* Initialize the current execution thread to permit a level of
* debugging output if an exception should happen during kernel
* initialization. However, don't waste effort initializing the
* fields of the dummy thread beyond those needed to identify it as a
* dummy thread.
*/
1 _current = dummy_thread;
2 dummy_thread->base.user_options = K_ESSENTIAL;
3 dummy_thread->base.thread_state = _THREAD_DUMMY;
#ifdef CONFIG_THREAD_STACK_INFO
4 dummy_thread->stack_info.start = 0;
5 dummy_thread->stack_info.size = 0;
#endif
#ifdef CONFIG_USERSPACE
6 dummy_thread->mem_domain_info.mem_domain = 0;
#endif
/* _kernel.ready_q is all zeroes */
/*
* The interrupt library needs to be initialized early since a series
* of handlers are installed into the interrupt table to catch
* spurious interrupts. This must be performed before other kernel
* subsystems install bonafide handlers, or before hardware device
* drivers are initialized.
*/
7 _IntLibInit();
/* ready the init/main and idle threads */
8 for (int ii = 0; ii < K_NUM_PRIORITIES; ii++) {
9 sys_dlist_init(&_ready_q.q[ii]);
10 }
/*
* prime the cache with the main thread since:
*
* - the cache can never be NULL
* - the main thread will be the one to run first
* - no other thread is initialized yet and thus their priority fields
* contain garbage, which would prevent the cache loading algorithm
* to work as intended
*/
11 _ready_q.cache = _main_thread;
12 _setup_new_thread(_main_thread, _main_stack,
13 MAIN_STACK_SIZE, bg_thread_main,
14 NULL, NULL, NULL,
15 CONFIG_MAIN_THREAD_PRIORITY, K_ESSENTIAL);
16 _mark_thread_as_started(_main_thread);
17 _ready_thread(_main_thread);
18 init_idle_thread(_idle_thread, _idle_stack);
19 initialize_timeouts();
/* perform any architecture-specific initialization */
20 kernel_arch_init();
}
本関数に渡されるdummy_threadに対しては、本関数実行時には有効な設定処理は行われていません。
このdummy_threadの役割は_currentポインタをNULLの状態にしないためです。
1行目:dummy_threadを_currentポインタで指定します。
2-6行目:dummy_threadのメンバに設定していますが、状態が_THREAD_DUMMYであったり、スタックアドレス、サイズが0であったり、有効な値ではありません。mainスレッドに切り替えるときに_currentポインタが必要となり、そのために適当な値で初期化しています。
7行目:割込みの初期化などを必要に応じて行います。
8-10行目:readyキューのqメンバを初期化しています。(headとtailの初期化)
11行目:readyキューのcacheメンバに_main_threadを設定します。
_main_threadは以下のようにk_thread構造体のポインタで定義されています。
static struct k_thread _main_thread_s;
k_tid_t const _main_thread = (k_tid_t)&_main_thread_s;
12-15行目:_setup_new_thread ( )を用いて、mainスレッドを作成します。
16行目:_mark_thread_as_started ( )を用いてmainスレッドのthread_stateの_THREAD_PRESTARTビットをクリアしています。
static inline void _mark_thread_as_started(struct k_thread *thread)
{
thread->base.thread_state &= ~_THREAD_PRESTART;
}
なお、_THREAD_PRESTARTビットはスレッド生成時にセットされ、実行開始直前で実行するためにクリアします。詳細はスレッド編で述べます。
17行目:mainスレッドをreadyキューにつなぎます。
18行目:以下 (init_idle_thread( ))の通り、mainスレッド同様に_setup_new_thread ( )でidleスレッドを作成し、readyキューにつなぐ処理を実施しています。
static void init_idle_thread(struct k_thread *thr, k_thread_stack_t *stack)
{
_setup_new_thread(thr, stack,
IDLE_STACK_SIZE, idle, NULL, NULL, NULL,
K_LOWEST_THREAD_PRIO, K_ESSENTIAL);
_mark_thread_as_started(thr);
_ready_thread(thr);
}
19行目:ワークキュー(次回述べます)に登録されたエントリがタイマ満了時に繋ぐ_timeout_qキューを初期化しています。(head、tailの初期化)
20行目:アーキテクチャ固有の初期化処理をば実施します。
ここまで、mainスレッドとidleスレッドの生成される部分を見てきました。
次にmainスレッド、idleスレッドの詳細について述べます。
それぞれのスレッドの優先度
- mainスレッド:preemptibleスレッドの最高優先度
- idleスレッド:preemptibleスレッドの最低優先度(idleスレッド専用優先度)
以下にreadyキューのprio_bmapメンバを用いて示します。
mainスレッド
- _setup_new_thread ( )の第8引数で優先度CONFIG_MAIN_THREAD_PRIORITYを指定していました。このCONFIG_MAIN_THREAD_PRIORITYはデフォルトで0です。
config MAIN_THREAD_PRIORITY
int
prompt "Priority of initialization/main thread"
default 0 ★
:
- 前述の通り、mainスレッド生成時には以下の引数で_setup_new_thread()を実行していました。
_setup_new_thread(_main_thread, _main_stack,
MAIN_STACK_SIZE, bg_thread_main,
NULL, NULL, NULL,
CONFIG_MAIN_THREAD_PRIORITY, K_ESSENTIAL);
ここから、以下が判ります。
- 優先度はCONFIG_MAIN_THREAD_PRIORITYで、コンフィグ設定可能。
前述の通り、デフォルトはpreemptibleスレッドの最高優先度。
- 実行する関数はbg_thread_main ( )。
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
:
1 _sys_device_do_config_level(_SYS_INIT_LEVEL_POST_KERNEL);
2 if (boot_delay > 0) {
3 printk("***** delaying boot " STRINGIFY(CONFIG_BOOT_DELAY)
4 "ms (per build configuration) *****\n");
5 k_busy_wait(CONFIG_BOOT_DELAY * USEC_PER_MSEC);
6 }
7 PRINT_BOOT_BANNER();
/* Final init level before app starts */
8 _sys_device_do_config_level(_SYS_INIT_LEVEL_APPLICATION);
9 _init_static_threads();
10 extern void main(void);
11 main();
/* Terminate thread normally since it has no more work to do */
12 _main_thread->base.user_options &= ~K_ESSENTIAL;
}
1行目と8行目はそれぞれのレベルに応じたドライバの初期化を行います。
とりうる値はこのようになっています。
#define _SYS_INIT_LEVEL_PRE_KERNEL_1 0
#define _SYS_INIT_LEVEL_PRE_KERNEL_2 1
#define _SYS_INIT_LEVEL_POST_KERNEL 2
#define _SYS_INIT_LEVEL_APPLICATION 3
指定可能なレベルは上記のとおりです。1行目の場合はPOST_KERNELレベルで、8行目はAPPLICATIONレベルで実行します。これは実行されるタイミングが異なります。
もう少し細かく述べると、リンカが各レベルごとに定義された関数をそれぞれのセクションに配置し、初期化時は当該セクションに定義された初期化処理群を適切なタイミングで順に実行します。
Linuxのドライバを定義するときに使用する下記も上記同様、リンカによって各セクションに配置されます。
#define early_initcall(fn)
#define core_initcall(fn)
#define core_initcall_sync(fn)
#define postcore_initcall(fn)
#define postcore_initcall_sync(fn)
#define arch_initcall(fn)
#define subsys_initcall(fn)
#define subsys_initcall_sync(fn)
#define fs_initcall(fn)
#define fs_initcall_sync(fn)
#define rootfs_initcall(fn)
#define device_initcall(fn)
#define device_initcall_sync(fn)
#define late_initcall(fn)
#define late_initcall_sync(fn)
7行目:"***** " BOOT_BANNER " ****"を出力します。
9行目:
void _init_static_threads(void)
{
1 unsigned int key;
2 _FOREACH_STATIC_THREAD(thread_data) {
3 _setup_new_thread(
4 thread_data->init_thread,
5 thread_data->init_stack,
6 thread_data->init_stack_size,
7 thread_data->init_entry,
8 thread_data->init_p1,
9 thread_data->init_p2,
10 thread_data->init_p3,
11 thread_data->init_prio,
12 thread_data->init_options);
13 thread_data->init_thread->init_data = thread_data;
14 }
#ifdef CONFIG_USERSPACE
15 grant_static_access();
#endif
16 _sched_lock();
17 key = irq_lock();
18 _FOREACH_STATIC_THREAD(thread_data) {
19 if (thread_data->init_delay != K_FOREVER) {
20 schedule_new_thread(thread_data->init_thread,
21 thread_data->init_delay);
22 }
23 }
24 irq_unlock(key);
25 k_sched_unlock();
}
2-12行目:_FOREACH_STATIC_THREADはK_THREAD_DEFINE ( )で静的に定義、初期化するしたスレッド群を対象とします。これらのスレッド群を_setup_new_thread ( )でセットアップします。
18行目:_FOREACH_STATIC_THREADでスレッドのinit_delayがK_FOREVER(無限のタイムアウト遅延(要求された操作が完了するまで待ち続ける))を指定していなければ、schedule_new_thread ( )内のk_thread_start ( )で対象スレッドをreadyキューに繋ぎます。
元のbg_thread_main ( )に戻ります。
11行目:これが本関数の核でmain()を含んだアプリを実行します。なお、アプリからは復帰しないことを想定しています。
idleスレッド
前述の通り、idle生成時には以下の引数で_setup_new_thread()を実行していました。
_setup_new_thread(thr, stack,
IDLE_STACK_SIZE, idle, NULL, NULL, NULL,
K_LOWEST_THREAD_PRIO, K_ESSENTIAL);
ここから、以下が判ります。
- 優先度はK_LOWEST_THREAD_PRIOで、preemptibleスレッドの最低優先度である。
- 実行する関数はidle ( )。
void idle(void *unused1, void *unused2, void *unused3)
{
1 ARG_UNUSED(unused1);
2 ARG_UNUSED(unused2);
3 ARG_UNUSED(unused3);
4 for (;;) {
5 (void)irq_lock();
6 sys_power_save_idle(_get_next_timeout_expiry());
7 IDLE_YIELD_IF_COOP();
8 }
}
4-8行目:コアは6行目になります。
省電力機能(CONFIG_SYS_POWER_LOW_POWER_STATE、CONFIG_SYS_POWER_DEEP_SLEEP、CONFIG_SYS_POWER_MANAGEMENTを非選択)を無効にした場合を載せます。なお、7行目のIDLE_YIELD_IF_COOP ( )はidleスレッドでk_yield ( )を実行します。
static void sys_power_save_idle(s32_t ticks)
{
1 if (_must_enter_tickless_idle(ticks)) {
/*
* Stop generating system timer interrupts until it's time for
* the next scheduled kernel timer to expire.
*/
/*
* In the case of tickless kernel, timer driver should
* reprogram timer only if the currently programmed time
* duration is smaller than the idle time.
*/
2 _timer_idle_enter(ticks);
3 }
4 k_cpu_idle();
}
1行目:CONFIG_TICKLESS_KERNELが有効の場合は真を返します。これはシステムアイドル時にtick割り込みを一定周期であげないようにする省電力機能です。この処理を大まかにいえば、タイマーデバイスにtick周期の時刻でなく、次に満了するタイマーイベントの時刻を設定します。
4行目:実際のアイドル処理を行います。例えば、Armの場合はwfi命令(Wait For Interrupt)を発行してシステムは割込みが入るまでスリープ状態に入ります。
ここまで、システムが用意しているスレッドについて見てきました。この他にもワークキュー用スレッドがありますが、これはワークキュー編で述べたいと思います。
それでは、また。
前回:Zephyr入門(スケジューラ:システムコール編)
次回:Zephyr入門(スケジューラ:ワークキュー編)
『各種製品名は、各社の製品名称、商標または登録商標です。本記事に記載されているシステム名、製品名には、必ずしも商標表示((R)、TM)を付記していません。』