Zephyr×Rustのインテグレーションにチャレンジ!⑦~システムコール調査~

はじめに

ZephyrとRustのインテグレーションに挑戦しています。

これまでで、アプリケーションをRustで書いてきました。 ここからは、Driverを書く方法を調査していきます。

前回までのあらすじ

Rustで書いたDriverの初期化関数から、文字が出力できました。

前回のDriverでは、ユーザーアプリケーションから、Driver APIを使う方法がありません。 そこで、ユーザーアプリケーションからDriver APIを利用できるように、システムコールを実装します。 実装する前には、調査が必要です。

Zephyrでどのようにシステムコールが実装されているか、見ていきます。

System Calls

少し古いドキュメントですが、下記にシステムコールについて書かれています (1.13以降はZephyr Kernel Primerの章が削除されています…)。

docs.zephyrproject.org

Components

  • __syscallプレフィックスのついたプロトタイプ宣言。ビルドプロセス内でgen_syscall.pyによって処理されます。
  • 実装関数。実際のシステムコール実装です。
  • ハンドラ関数。引数を検証してから、実装関数に渡すためのラッパーです。

プロトタイプ宣言

システムコールにする関数は、__syscallを付けて宣言されています。

__syscall void k_sem_init(struct k_sem *sem, unsigned int initial_count,
                          unsigned int limit);

__syscall属性は、非常に特殊です。コンパイラは、単純にstatic inlineに展開します。 post-buildで実行されるparse_syscalls.pyは、__syscall属性がついたAPIを解析します。細かい制限はありますが、一旦置いておきます。

呼び出しコンテキスト

システムコールを呼び出しているソースコードが、ユーザーモードで実行されるか、スーパーバイザモードで実行されるか、でシステムコールの呼び出し方を変更します。 基本、スーパーバイザモードからのシステムコール呼び出しは、単純に実装関数を呼び出す簡易の実装になります。

Cソースファイルごとに実行モードを判定するために、ビルドプロセスで、__ZEPHYR_SUPERVISOR____ZEPHYR_USER__というマクロをコンパイラフラグとして追加します。

  • CONFIG_USERSPACEが有効になっていない場合、全てのシステムコールAPIは、直接、実装関数を呼び出します。
  • __ZEPHYR_SUPERVISOR__が定義されていると、直接、実装関数を呼び出します。
  • __ZEPHYR_USER__が定義されていると、システムコールが無条件に発行されます。

実装の詳細

__syscallを使ってAPIを宣言すると、scripts/gen_syscalls.pyによって、Cソースファイルとヘッダファイルが生成されます。 生成されたファイルは、include/generated下にあります。

  • システムコールシステムコールID型で番号付けされます。include/generated/syscall_list.hK_SYSCALL_から始まるAPIとして表現されます。
  • include/generated/syscall_list.hにハンドラ関数のプロトタイプが生成されます。
  • include/generated/syscall_dispatch.cシステムコール発行テーブル (_k_sycall_table) にエントリが生成されます。
  • ハンドラ関数は、未実装のシステムコールハンドラのエイリアスである、weakハンドラ関数として宣言されます。

k_sem_init()は、build/zephyr/include/generated/syscalls/kernel.hで次のようになっています。

K_SYSCALL_DECLARE3_VOID(K_SYSCALL_K_SEM_INIT, k_sem_init, struct k_sem *,
                        sem, unsigned int, initial_count,
                        unsigned int, limit);

この場合のマクロは、次のように実装されています。上述した通り、システムコールの呼び出しコンテキストによって実装が変わります。

#if !defined(CONFIG_USERSPACE) || defined(__ZEPHYR_SUPERVISOR__)

#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        extern void _impl_##name(t0 p0, t1 p1, t2 p2); \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                _impl_##name(p0, p1, p2); \
        }

#elif defined(__ZEPHYR_USER__)
#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                _arch_syscall_invoke3((u32_t)p0, (u32_t)p1, (u32_t)p2, id); \
        }

#else /* mixed kernel/user macros */
#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        extern void _impl_##name(t0 p0, t1 p1, t2 p2); \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                if (_is_user_context()) { \
                        _arch_syscall_invoke3((u32_t)p0, (u32_t)p1, (u32_t)p2, id); \
                } else { \
                        compiler_barrier(); \
                        _impl_##name(p0, p1, p2); \
                } \
        }
#endif

あー、やっと、アーキテクチャ固有のシステムコール呼び出しが見えました。

だいぶ、長くなってきたので、次回に続きます。