🕒

Xenomai4 で"リアルタイム"プロセスを作った件

に公開

注意

この記事と内容は、Claude Codeと共に作成しました。私のようにAIにアレルギーのある方は、ブラウザバック推奨。Claude Codeがあると、1日でここまでできる、という例としても書いています。

TL;DR

Cyclone V SoC (DE10-Nano) のARM Cortex A9上で Xenomai4 を使い、CPU1 に固定したリアルタイムスレッドのジッタを 計測上ゼロ (10μs を超えるスパイク 0 回 / 5 秒、32M ループ) にできた。鍵は isolcpus=1 nohz_full=1 rcu_nocbs=1 だけでは止まらない以下の周期処理を止めること。

  • EVL の proxy tick (do_clock_tick() 内の RQ_TPROXY セット)
  • スケジューラの sched_tick_remote() (1Hz hard-coded)
  • sched_tick_start() からの remote tick work 起動

パッチは 2 ファイル・3 箇所、計 30 行未満。

CPU1はカーネルによるマルチスレッドの対象外となるため、isolcpusがないとカーネルがクラッシュするので注意。

動作環境

  • ボード: Intel Cyclone V SoC (DE10-Nano)
  • CPU: ARM Cortex-A9 dual-core @ 800 MHz
  • カーネル: Linux 6.12.67 + EVL (Xenomai 4) co-kernel
    • ベース: linux-evl commit a0119f32d (evl/v6.12.y ブランチ相当)
  • カーネルコンフィグ: CONFIG_HZ=1000 / CONFIG_NO_HZ_FULL=y / CONFIG_RCU_NOCB_CPU=y / CONFIG_EVL=y
  • ブートパラメータ: isolcpus=1 nohz_full=1 rcu_nocbs=1

背景: isolcpus + nohz_full でも CPU1 は静かにならない

EVL は Linux カーネル上に co-kernel として動作するリアルタイム拡張。カーネルオプションとしてisolcpus=1 nohz_full=1 rcu_nocbs=1 を設定すれば CPU1 を OOB スレッド専用にできる…はずだった。

実際には CPU1 に固定した OOB スレッドで純粋な計算ループを回すと、約 1 秒周期で 27〜35μs のスパイクが現れる。

=== Spike Timing Analysis (compute only, 32M loops, 5.4 sec) ===
Spikes >10000 ns: 10
  #  loop_idx     offset(ms)   duration(ns)   gap(ms)
   0     5626833       949.96 ms       69703 ns      0.00
   1     5631732       950.84 ms       35165 ns      0.86
   2     5637449       951.84 ms       12717 ns      1.00
   ...

犯人を追っていくと、isolcpus でも nohz_full でも止められない次の 3 つの周期処理(割り込み)に行き着いた。

  • proxy tick が CPU1 で発火 → 27μs 級スパイク
  • sched_tick_remote() が 1Hz で system_unbound_wq 経由で実行 → CPU1 に reschedule IPI
  • dl_task_timer() (SCHED_DEADLINE bandwidth replenishment) が hrtimer から発火 → CPU1 に function call IPI

パッチ

patches/0001-cpu1-rt-isolation.patch は 2 ファイル・3 箇所の最小限の変更。

1. kernel/evl/clock.c — proxy tick の発火抑制

 if (unlikely(timer == &rq->inband_timer)) {
-    rq->local_flags |= RQ_TPROXY;
+    if (!cpu_is_isolated(evl_rq_cpu(rq)))
+        rq->local_flags |= RQ_TPROXY;
     continue;
 }

EVL の do_clock_tick() は inband 用タイマーの期限切れで RQ_TPROXY を立てる。このフラグが立つと evl_core_tick() から evl_notify_proxy_tick() 経由で inband stage に同期 IRQ が配送される。

isolated CPU では inband スケジューラを動かさないので、このフラグを立てる必要がない。これだけで CPU1 上の twd 発火が 12 回/5 秒 → 0 回/5 秒 になる。

2. kernel/sched/core.csched_tick_remote() の停止

 static void sched_tick_remote(struct work_struct *work)
 {
     ...
+    /* Skip entirely for isolated CPUs — no tick, no requeue. */
+    if (cpu_is_isolated(cpu))
+        return;
     ...
 }

sched_tick_remotenohz_full CPU に対して 1Hz hard-coded で実行される delayed work。housekeeping CPU 上で動き、ターゲット CPU の rq lock を取って統計更新と reschedule を行う。

isolated CPU では即 return することで、tick 処理も requeue も完全に止める。これで 1 秒周期の IPI 源が一つ消える。

3. kernel/sched/core.csched_tick_start() の早期 return

 static void sched_tick_start(int cpu)
 {
     if (housekeeping_cpu(cpu, HK_TYPE_TICK))
         return;

+    if (cpu_is_isolated(cpu))
+        return;
     ...
 }

タスク enqueue 時に呼ばれて新しい remote tick work を起動する関数。isolated CPU では起動自体をスキップする。

効果

条件 10μs+ スパイク (5秒間) proxy tick (CPU1) twd (CPU1)
パッチなし 10〜14 +10〜12 +12〜14
パッチ適用 0 +0 +0
=== Spike Timing Analysis (compute only, 32M loops, 5.4 sec) ===
Spikes >10000 ns: 0

CPU1 上では:

  • ✅ proxy tick 発火: ゼロ
  • ✅ scheduler tick (twd): ゼロ
  • ✅ Function call IPI: ゼロ
  • ✅ kworker / softirq: ゼロ (元から)
  • ⚠️ Reschedule IPI: evl_switch_oob/inband() 遷移時のみ (計測ループ中はゼロ。当然ですけど……)

デメリット・注意事項

動作するが意味を失うもの

CPU1 を OOB 専用 にする運用なら全て問題ない:

  • CPU1 上の CFS タスクのタイムスライス管理が機能しない
  • CPU1 上の load average 計算が止まる (監視ツール表示が不正確)
  • CPU1 上の kworker / ksoftirqd が動かない (isolcpus=1 で元から動かない)

壊れる前提

  • isolcpus=1 を外すと壊れる: パッチは isolated CPU 前提
  • CPU1 で inband タスクを走らせると壊れる: tick がないのでスケジューリングが機能しない

CPU1 は完全に「OOB 専用カーネル」のように扱う必要がある。

適用方法

cd /path/to/linux-evl
patch -p1 < /path/to/0001-cpu1-rt-isolation.patch
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8 zImage modules

計測の再現方法

CCNT (Cortex-A9 PMU cycle counter) をユーザーランドから読めるように ccnt_enable.ko をロードした上で、CPU1 に固定した OOB スレッドで純粋な計算ループを回し、ccnt_read() を 32M 回サンプリングして 10μs を超える区間をスパイクとして記録した。

他のマルチコア環境への展望

今回は Cyclone V (Cortex-A9 dual-core) で確認したが、パッチが触っているのは アーキ非依存のレイヤ (kernel/sched/core.c と EVL の clock.c) だけで、ARM 固有のコードは一切いじっていない。判定にも cpu_is_isolated() という汎用 API を使っているだけ。

そのため、原理上は次のような環境でもそのまま通用するはず:

  • Cortex-A53 / A72 (Raspberry Pi, i.MX8 など): ARMv8 系の他のマルチコア SoC
  • x86_64 マルチコア: デスクトップ・サーバ環境で 1 コアを RT 専用に切り出すケース
  • 3 コア以上: housekeeping CPU を CPU0 に残しつつ、CPU1 以降を全部 isolated にする運用

要するに「isolcpus + nohz_full で実現できるはずの"リアルタイム"の意味論を最後まで貫くパッチ」なので、デュアルコア固有の話ではない。手元に Raspberry Pi 4 や x86 環境がある人は、ぜひ試してほしい。

ただし環境を問わず、CONFIG_NO_HZ_FULL=y / CONFIG_RCU_NOCB_CPU=y を有効にしたカーネルで、ブート時に isolcpus=N nohz_full=N rcu_nocbs=N (N は隔離したい CPU 番号) を指定することは前提。これを忘れるとパッチを当てても効果が出ない (cpu_is_isolated() が常に false を返すので何も変わらない)。

参考資料


Discussion