Zephyr入門(概要 & (スケジューラ:概要編))


はじめに

OSがどのようにして動作しているか知りたいけれど、よく使われているLinuxカーネルだと複雑すぎてチンプンカンプン…。ファイルが多すぎてどこから見れば良いかも全く分からない…。
こういう人が多いと思います。
また、OSの知識はあるがZephyrは読んだことない、という人もいると思います。
今回、私自身がZephyrを読み始めるにあたって、読解した内容を皆さんと共有できたら良いなと思い、投稿することにしました。

Zephyrとは

Zephyrとは2015年から開発されているRTOS (Real Time Operating System)です。また、Linuxとは別OSですが、The Linux Foundationの一つのプロジェクトでもあり、Intel、SynopsysやNXPなどがプラチナスポンサーです。
日本ではITRONやVxWorksほど知名度が高くないですが、ELC (Embedded Linux Conference)North America & OpenIoT Summit 2017、2018 と2年連続でZephyrが一番セッション数が多くホットな話題でした。
今回、Zephyrを題材にした理由は次の通りです。
- この流れはいずれ日本でも広がる可能性があるので、それに備えて。
- ZephyrのコードはLinuxカーネルとコードやファイル規則が似ている個所が多々あるため、Zephyrをある程度理解できれば、Linuxカーネルのコードリーディングの敷居が低くなる。

使用環境など

Zephyrのライセンス:Apache License 2.0
今回使用したバージョン:1.11.0
公式サイト:https://www.zephyrproject.org/
入手元:https://www.zephyrproject.org/developers/#downloads
github:https://github.com/zephyrproject-rtos/zephyr

ソースコードをどこから読めばよいか

正解はないと思います。業務で必要になった機能、興味のある機能、起動処理、など。今回はOSの役割とは何か、という切り口から考えてみます。
はじめにOSは主に以下の役割を担います。(簡単ですが)

  • ハードウェアの抽象化 & 隠蔽化
     例えばx86向けに開発したアプリを変更せずにArm上で動作させる。
    これは、上位に位置するアプリにハードウェアを意識させなくてすむように抽象化、隠蔽化しています。

  • システムを効率良く利用する
     例えばマルチタスクで動作させることで各処理を効率良く、そしてH/Wを最大限に有効活用する。
     スケジューラが複数のタスクを実行させたいタイミングで実行させる、ただしその実行順序にはルールがあります。このスケジューリングアルゴリズムについては次回記載します。

今回は上記2番目で記載したマルチタスクを効率よく動作させる機能である「スケジューラ」から始めます。
H/Wが絡まないため、ソフト開発者の方もとっつきやすいと思います。
なお、処理を実行するインスタンスの呼称としてタスクやスレッドがありますが、本記事ではZephyrのドキュメントに従って「スレッド」で統一します。

スケジューラ編のあとは割込み処理やメモリ管理などを投稿していく予定です。

第2回:Zephyr入門(スケジューラ:コア編)
第3回:Zephyr入門(スケジューラ:システムコール編)
第4回:Zephyr入門(スケジューラ:システムスレッド編)
第5回:Zephyr入門(スケジューラ:ワークキュー編)
第6回:Zephyr入門(排他制御:mutex編)

なお、Linuxに興味がある方向けにLinuxカーネルの記事も執筆中です。
【読解入門】Linuxカーネル (概要編)

Zephyrのスケジューラ

  • 関連コード:kernel/sched.c、kernel/include/ksched.h、kernel_struct.h、kswap.h、include/kernel.h
  • スケジューラの概要
    1. 優先度に基づく制御
    2. スレッドの分類としてcooperativeスレッドとpreemptibleスレッドの2種類ある。
    3. 割込みを明示的にマスクしない場合、スレッド実行中に割り込まれる可能性がある。
    4. スレッドの状態としてはready状態とunready状態がある。
    5. 各スレッドはタイムスライスによって制御する。
  1. 優先度に基づく制御  
    各スレッドが優先度を持ち、スケジューラは優先度の高いスレッドを実行する。 次に示すcooperativeスレッドとpreemptibleスレッドではcooperativeスレッドの方が優先度が高い。
  2. スレッドの種類としてcooperativeスレッドとpreemptibleスレッドの2種類ある。
    リアルタイム処理用途のcooperativeスレッドクラスと優先度が高いスレッド(例えばcooperatibveスレッド)にCPUの横取り(以後プリエンプションと記載)を許すpreemptibleスレッドクラスが存在する。
      それぞれの優先度の数はビルド時のconfigで変更可能であり、デフォルトではcooperativeスレッドは16、preemptibleスレッドは15となっている。(cooperativeスレッド→CONFIG_NUM_PRIORITIESで設定、preemptibleスレッド→CONFIG_NUM_PREEMPT_PRIORITIESで設定。)  
    Zephyrのスケジューラではcooperativeスレッドを負値、preemptibleスレッドを正値で管理する。次のようなイメージ。 ユーザーは負値は直接使用するのではなく、cooperativeスレッドの場合はK_PRIO_COOPマクロで0から128、preemptibleスレッドもK_PRIO_PREEMPTで0から128のように設定する。(小さい方が高優先度)

  また、それぞれのクラスで設定できる値は0から128のため、最大で-128から127までの256段階の優先度を設定できる。

 3. 割込みを明示的にマスクしない場合、スレッド実行中に割り込まれる可能性がある。    
  irq_lock()を用いて割込みをマスクしないとスレッド実行中に割込み発生契機で対応する割込みハンドラが実行されるため、スレッドの実行は遅延する。  
  これを防ぐためにはirq_lock()/irq_unlock()を用いてプロセッサレベルで割込みを禁止/許可する。例えばx86の場合、cli命令を発行することでEFLAGSレジスタのIFフラグをクリアして割込みを禁止する。同様にArmの場合はcpsid命令を発行してCPSRのIビットをセットして割込みを禁止する。

 4. スレッドの状態としてはready状態とunready状態がある。    
  実行可能なready状態、I/O待ちや何らかの条件を満たすのを待つunready状態の2つの状態を持つ。
  次回説明するが、スケジューラは次に実行するスレッドを決定するためにreadyキューというキューで管理しており、ready状態のスレッドはこのキューにつながっている。 
 5. 各スレッドはタイムスライスによって制御する。  
cooperativeスレッド用のタイムスライスとpreemptiveスレッド用のタイムスライスがある。  

ここまで、コードを載せていないため、「ふーん」という感想を持たれる方もいると思います。
なので、今回は一例としてスケジューラの核心部であるスレッド切り替え(コンテキストスイッチ)のコードを載せます。

kernel/include/kswap.h  
1 static inline unsigned int _Swap(unsigned int key)
2 {
3         struct k_thread *new_thread, *old_thread;
4         int ret;

5         old_thread = _current;

6         _check_stack_sentinel();
7         _update_time_slice_before_swap();

8  #ifdef CONFIG_KERNEL_EVENT_LOGGER_CONTEXT_SWITCH
9         _sys_k_event_logger_context_switch();
10 #endif

11        new_thread = _get_next_ready_thread();

12        old_thread->swap_retval = -EAGAIN;

13#ifdef CONFIG_SMP
14        old_thread->base.active = 0;
15        new_thread->base.active = 1;

16        new_thread->base.cpu = _arch_curr_cpu()->id;
17#endif

18        _current = new_thread;
19        _arch_switch(new_thread->switch_handle,
20                     &old_thread->switch_handle);

21        ret = _current->swap_retval;

22        irq_unlock(key);

23        return ret;
24 }

順に見ていきます。

まず、引数のkeyは本関数実行時の割込み状態を保持した変数です。
この変数は割込み操作のネスト化を許すために利用しますが、詳細は割込み編で述べることとし、本稿では図だけ示し、詳細は割愛します。

5行目で_currentポインタをold_threadポインタに保持しておきます。この_currentポインタは常に現在実行しているスレッド(k_thread構造体)をポイントしています。余談ですが、Linuxカーネルではcurrentポインタを同じ役割で使用しています。
7行目の_update_time_slice_before_swap()でその時点のタイムスライス値を0にリセットします。(TICKLESS_KERNELが無効の場合)
11行目で次に実行するスレッドを決定します。決定するアルゴリズムの説明は次回述べます。この関数の復帰値new_threadが次に実行されますのでそのスレッドを、_currentに設定します。(18行目)
19行目がこの関数のコア部分です。
_arch_switchで現在実行していたスレッドのコンテキスト(レジスタ値など)の退避した後に、new_threadのコンテキストを復元し、new_threadを実行します。元々実行していたスレッドの実行時はこの関数から復帰しません。次にこのスレッドを実行するときに復帰します。図で示すと次の通りです。

  
いかがでしたでしょうか。コンテキストスイッチの核の部分はシンプルですよね。大雑把に言えばこの部分はどのOSでも同じ処理を行います。

ではこの調子で次回からは本格的にコードリーディングしていきます。

それでは、また。

次回::Zephyr入門(スケジューラ:コア編)

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