スケジューラ編はまだ続きますが、ここで一旦一休み。
比較的、楽に読めるスケジューラ関連のシステムコールを見てみましょう。

システムコールとは

RTOS(OS)におけるユーザ空間、カーネル空間や特権モードの話をすると長くなるので、ここではユーザスレッドがカーネルの機能を使用するための関数群とします。

スケジューラが提供しているシステムコール

Zephyrが提供しているシステムコール群の中でスケジューラが提供しているものを紹介します。sched.cで定義している7つのシステムコールを以下に示します。
・k_thread_priority_get ( )
・k_thread_priority_set ( )
・k_yield ( )
・k_sleep ( )
・k_wakeup ( )
・k_current_get ( )
・k_is_preempt_thread ( )

これら7つのシステムコールについて順に見ていきましょう。

どの関数がシステムコールか調べるにはソースコードを_SYSCALL_HANDLERで検索しましょう。システムコールは必ずこのマクロで定義されています。

k_thread_priority_get ( )

kernel/sched.c
int _impl_k_thread_priority_get(k_tid_t thread)
{
        return thread->base.prio;
}

対象スレッドの優先度を取得するシステムコールです。
関数は上記の通り一行のみです。thread->base.prioはそのスレッドの優先度を保持しているのため、この値を返します。

k_thread_priority_set ( )

kernel/sched.c
_SYSCALL_HANDLER(k_thread_priority_set, thread_p, prio)
{
1        struct k_thread *thread = (struct k_thread *)thread_p;

2        _SYSCALL_OBJ(thread, K_OBJ_THREAD);
3        _SYSCALL_VERIFY_MSG(_VALID_PRIO(prio, NULL),
                             "invalid thread priority %d", (int)prio);
4        _SYSCALL_VERIFY_MSG((s8_t)prio >= thread->base.prio,
                            "thread priority may only be downgraded (%d < %d)",prio, thread->base.prio);

5        _impl_k_thread_priority_set((k_tid_t)thread, prio);
6        return 0;
}

対象のスレッドに指定した優先度を設定するシステムコールです。
ポイントを絞って見ます。

3行目:優先度の範囲チェック (_VALID_PRIO ( ))
   対象処理は以下の通りです。(後で説明するために[1]~[3]を記載しています)

kernel/include/ksched.h
#define _VALID_PRIO(prio, entry_point) \
        ([1]((prio) == K_IDLE_PRIO && _is_idle_thread(entry_point)) || \
         [2]       (_is_prio_higher_or_equal((prio), \
                        K_LOWEST_APPLICATION_THREAD_PRIO) && \
         [3]        _is_prio_lower_or_equal((prio), \
                        K_HIGHEST_APPLICATION_THREAD_PRIO)))

以下の条件を満たす時、真を返します。
[1] 設定するprioがidleスレッドの優先度、かつ対象スレッドがidleスレッドの場合
  つまり、idleスレッドをidleスレッド用優先度に設定する場合です。
または
[2] 最低優先度以上の場合 かつ [3] 最高優先度以下の場合
です。

[1]の詳細は以下の通りです。

include/kernel.h
#define K_IDLE_PRIO K_LOWEST_THREAD_PRIO
#define K_LOWEST_THREAD_PRIO CONFIG_NUM_PREEMPT_PRIORITIES←コンフィグで設定

コンフィグ項目NUM_PREEMPT_PRIORITIESで設定した最低値がidleスレッドの優先度になります。_is_idle_thread ( )はentry_pointがidleスレッドか否かを判定します。

[2](最低優先度以上であることのチェック)の詳細は以下の通りです。

kernel/include/ksched.h
static inline int _is_prio_higher_or_equal(int prio1, int prio2)
{
       return _is_prio1_higher_than_or_equal_to_prio2(prio1, prio2);
}                         |
                          
static inline int _is_prio1_higher_than_or_equal_to_prio2(int prio1, int prio2)
{
       return prio1 <= prio2; ★第一引数の優先度が第二引数より高い場合、真を返す。
}

prio1は指定した優先度、prio2はK_LOWEST_APPLICATION_THREAD_PRIOです。
このK_LOWEST_APPLICATION_THREAD_PRIOは下記[2]'の通りで、
CONFIG_NUM_PREEMPT_PRIORITIESに16を設定している場合、K_LOWEST_APPLICATION_THREAD_PRIOは15となります。

[2]'

include/kernel.h
 #define K_LOWEST_APPLICATION_THREAD_PRIO (K_LOWEST_THREAD_PRIO - 1)
 #define K_LOWEST_THREAD_PRIO CONFIG_NUM_PREEMPT_PRIORITIES

即ち、最低優先度はidleスレッド用でユーザスレッドは最低優先度+1以上の範囲で設定可能ということです。

[3](最高優先度以下であることのチェック)の詳細は以下の通りです。

kernel/include/ksched.h
static inline int _is_prio_lower_or_equal(int prio1, int prio2)
{
        return _is_prio1_lower_than_or_equal_to_prio2(prio1, prio2);
}                         |
                          
static inline int _is_prio1_lower_than_or_equal_to_prio2(int prio1, int prio2)
{
        return prio1 >= prio2;★第一引数の優先度が第二引数より低い場合、真を返す。
}

prio1は指定した優先度、prio2はK_HIGHEST_APPLICATION_THREAD_PRIOです。
このK_HIGHEST_APPLICATION_THREAD_PRIOは下記[3]'の通りで、
CONFIG_NUM_COOP_PRIORITIESに16を設定している場合、K_HIGHEST_APPLICATION_THREAD_PRIOは-16となります。

[3]'

include/kernel.h
#define K_HIGHEST_APPLICATION_THREAD_PRIO (K_HIGHEST_THREAD_PRIO)
#define K_HIGHEST_THREAD_PRIO (-CONFIG_NUM_COOP_PRIORITIES)

k_thread_priority_set ( )に戻ります。
4行目:優先度を低くする場合にメッセージ出力します。
5行目:この_impl_k_thread_priority_set ()が、本システムコールの核でこの関数を以下に示します。

kernelo/sched.c
void _impl_k_thread_priority_set(k_tid_t tid, int prio)
{
        /*
         * Use NULL, since we cannot know what the entry point is (we do not
         * keep track of it) and idle cannot change its priority.
         */
1        _ASSERT_VALID_PRIO(prio, NULL);
2        __ASSERT(!_is_in_isr(), "");

3        struct k_thread *thread = (struct k_thread *)tid;
4        int key = irq_lock();

5        _thread_priority_set(thread, prio);
6        _reschedule_threads(key);
}

4行目で割込み禁止後、5行目で優先度を設定します。この_thread_priority_set ( )を以下に示します。

kernel/include/ksched.h
static inline void _thread_priority_set(struct k_thread *thread, int prio)
{
1        if (_is_thread_ready(thread)) {
2                _remove_thread_from_ready_q(thread);
3                thread->base.prio = prio;
4                _add_thread_to_ready_q(thread);
5        } else {
6                thread->base.prio = prio;
7        }
}

1-4行目:対象スレッドがready状態の場合、一度readyキューから削除し、優先度を設定後、新たな優先度に対応するreadyキューの位置に追加します。
5-7行目:対象スレッドがunready状態の場合、スレッドのbase.prioに指定した優先度を設定します。

_impl_k_thread_priority_set ( )の6行目については以下の通りです。

kernel/sched.c
void _reschedule_threads(int key)
{
#ifdef CONFIG_PREEMPT_ENABLED
1        K_DEBUG("rescheduling threads\n");

2        if (_must_switch_threads()) {
3                K_DEBUG("context-switching out %p\n", _current);
4                _Swap(key);
5        } else {
6                irq_unlock(key);
7        }
#else
8        irq_unlock(key);
#endif
}

CONFIG_PREEMPT_ENABLEDが有効の場合を考えましょう。
本関数の引数keyは割込みの状態を保持しています。
深くなりますが、重要なので2行目の_must_switch_threads関数を以下に示します。
この関数の復帰値としてコンテキストスイッチが必要(最高優先度のスレッドが_currentスレッドより優先度が高い)の場合、真を返すため、第一回で見た_Swap関数を呼び出します。コンテキストスイッチが不要な場合はirq_unlock ( )を実行して復帰します。

kernel/sched.c
int __must_switch_threads(void)
{
#ifdef CONFIG_PREEMPT_ENABLED
1        K_DEBUG("current prio: %d, highest prio: %d\n",
2                 _current->base.prio, _get_highest_ready_prio());

#ifdef CONFIG_KERNEL_DEBUG
3        dump_ready_q();
#endif  /* CONFIG_KERNEL_DEBUG */

4        return _is_prio_higher(_get_highest_ready_prio(),
_current->base.prio);
#else
5        return 0;
#endif
}

この関数は、4行目で現在readyキューに登録されている最高優先度のスレッドと対象スレッドの優先度を比較して最高優先度のスレッドの方が優先度が高い場合、真を返します。

k_yield ( )

k_yield ( )は前回出てきましたね。
本システムコールは現在実行中のスレッドをreadyキュー内で繋ぎ直します。
下図のスレッドAに対する操作です。
Zephyr7r2.jpg

kernel/sched.c
void _impl_k_yield(void)
{
1        __ASSERT(!_is_in_isr(), "");

2        int key = irq_lock();

3        _move_thread_to_end_of_prio_q(_current);

4        if (_current == _get_next_ready_thread()) {
5                irq_unlock(key);
#ifdef CONFIG_STACK_SENTINEL
6                _check_stack_sentinel();
#endif
7        } else {
8                _Swap(key);
9        }
}

この関数のポイントは3行目ですね。[3]'として以下に示します。
対象スレッドをreadyキューの最後に繋ぎ直します。
高優先度または同一優先度のスレッドが存在しない場合、対象スレッドは実行継続し、そうでない場合は_Swap ( )でスレッドを切り替えます。

[3]'

kernel/sched.c
void _move_thread_to_end_of_prio_q(struct k_thread *thread)
{
#ifdef CONFIG_MULTITHREADING
1        int q_index = _get_ready_q_q_index(thread->base.prio);
2        sys_dlist_t *q = &_ready_q.q[q_index];

3        if (sys_dlist_is_tail(q, &thread->base.k_q_node)) {
4                return;
5        }

6        sys_dlist_remove(&thread->base.k_q_node);
7        sys_dlist_append(q, &thread->base.k_q_node);

# ifndef CONFIG_SMP
8        struct k_thread **cache = &_ready_q.cache;

9        *cache = *cache == thread ? get_ready_q_head() : *cache;
# endif
#endif
}

3-4行目:対象スレッドがすでにキューの最後の場合、何もせずに復帰します。
6-7行目:readyキューqから削除し、最後に繋ぎ直しています。
8行目:_currentスレッドの次に実行するスレッドを示すcacheを更新しています。

k_sleep ( )

k_sleep ( )は以下の通りです。
現在実行中のスレッドを指定した時間、休眠させるシステムコールです。
この休眠中の間は低優先度のスレッドも実行可能です。

kernel/sched.c
void _impl_k_sleep(s32_t duration)
{
#ifdef CONFIG_MULTITHREADING
        /* volatile to guarantee that irq_lock() is executed after ticks is
         * populated
         */
1        volatile s32_t ticks;
2        unsigned int key;

3        __ASSERT(!_is_in_isr(), "");
4        __ASSERT(duration != K_FOREVER, "");

5        K_DEBUG("thread %p for %d ns\n", _current, duration);

        /* wait of 0 ms is treated as a 'yield' */
6        if (duration == 0) {
7                k_yield();
8                return;
9        }

10        ticks = _TICK_ALIGN + _ms_to_ticks(duration);
11        key = irq_lock();

12        _remove_thread_from_ready_q(_current);
13        _add_thread_timeout(_current, NULL, ticks);

14        _Swap(key);
#endif
}

引数durationは休眠する時間をmsで指定します。
6~9行目:durationが0の場合はk_yield ( )と同じ挙動をします。
10行目:引数msをもとにtickに変換し、タイムアウト満了時のtickを変数ticksに設定します。
12行目:readyキューから_currentスレッドを削除します。
13行目:_currentスレッドのタイムアウト値をもとにwaitキューに登録します。
    waitキューについては今後、詳細を述べますので今回は割愛します。
14行目:スケジューリングしてready状態にある最高優先度のスレッドをディスパッチします。

k_wakeup ( )

k_wakeup ( )は以下の通りです。
指定したスレッドを起床させる関数です。

kernel/sched.c
void _impl_k_wakeup(k_tid_t thread)
{
1        int key = irq_lock();

        /* verify first if thread is not waiting on an object */
3        if (_is_thread_pending(thread)) {
4                irq_unlock(key);
5                return;
6        }

7        if (_abort_thread_timeout(thread) == _INACTIVE) {
8                irq_unlock(key);
9                return;
10        }

11        _ready_thread(thread);

12        if (_is_in_isr()) {
13                irq_unlock(key);
14        } else {
15                _reschedule_threads(key);
16        }
}

3-6行目:対象スレッドが意図的に保留状態(ペンディング)されている場合は本関数から復帰します。
7-10行目:_abort_thread_timeout ( )はタイマーがすでに満了時刻を過ぎてキューに登録済みの場合_INACTIVEを返すため、7行目は真となり、それ以降は何もせずに復帰します。
11行目:本システムコールの核はこの_ready_thread ( )です。
    _ready_thead ( )を以下に示します。

kernel/include/ksched.h
static inline void _ready_thread(struct k_thread *thread)
{
1        if (_is_thread_ready(thread)) {
2                _add_thread_to_ready_q(thread);
3        }

#ifdef CONFIG_KERNEL_EVENT_LOGGER_THREAD
4        _sys_k_event_logger_thread_ready(thread);
#endif
}

1-3行目:_is_thread_ready関数で対象スレッドをready状態にできるかどうかをチェックし、可能な場合、readyキューに登録します。
ready状態にできない例としてはスレッドのステートが以下の場合であるケースです。
・_THREAD_PENDING
・_THREAD_PRESTART
・_THREAD_DEAD
・_THREAD_DUMMY
・_THREAD_SUSPENDED

k_current_get ( )

現在実行中のスレッドを返します。
以下の通り、_currentを返すだけのシステムコールです。

kernel/sched.c
k_tid_t _impl_k_current_get(void)
{
        return _current;
}

k_is_preempt_thread ( )

現在実行中のスレッドがプリエンプション可能か否かを返します。
可能な場合、真を返します。

kernel/sched.c
int _impl_k_is_preempt_thread(void)
{
        return !_is_in_isr() && _is_preempt(_current);
}

割込み処理中でなく、かつ_currentがプリエンプション可能な場合、真を返します。
真を返さない場合として以下が考えられます。
・割込み処理中の場合
・_currentがcooperativeスレッドの場合
・k_sched_lock ( )によりスケジューラロックされている場合

その他

本稿ではスケジューラ関連のシステムコールを記載しました。

述べるのが遅くなりましたが、カーネルのコードリーディングのコツとして私の場合は、対象の関数で実行する全ての関数を深く追いません。ポイントとなるであろう関数のみを追うことでその関数の役割を理解します。 その後、時間に余裕がある場合は各処理に対して「なぜこの処理が必要なのか」ということを意識して細部まで見ます。
Linuxカーネルではコード量が多いため、この読み方がより重要であると思っています。
対象の関数の先頭にある重要でない役割でない関数を細部まで追っていき、それで疲弊してしまい、カーネルを読むのを諦めてしまう人もいると思います。こういうケースを防ぐためにも対象の関数の大枠を理解し、まずはポイントとなる処理を追うことが、コードリーディングのコツだと思います。

それでは、今回はここまでにします。

では、また。

前回:Zephyr入門(スケジューラ:コア編)
次回:Zephyr入門(スケジューラ:システムスレッド編)

『各種製品名は、各社の製品名称、商標または登録商標です。本記事に記載されているシステム名、製品名には、必ずしも商標表示((R)、TM)を付記していません。』

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.