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ブランチ相当)
- ベース: linux-evl commit
-
カーネルコンフィグ:
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.c — sched_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_remote は nohz_full CPU に対して 1Hz hard-coded で実行される delayed work。housekeeping CPU 上で動き、ターゲット CPU の rq lock を取って統計更新と reschedule を行う。
isolated CPU では即 return することで、tick 処理も requeue も完全に止める。これで 1 秒周期の IPI 源が一つ消える。
3. kernel/sched/core.c — sched_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