アクター
Lambda OS の各 VM タスクは、自分のスタック・グローバル領域・ヒープを持つ自己完結した Lambda C VM インスタンスです。どのタスクもアクターで、状態を隔離し、メッセージで通信し、コアに固定されます。その隣では、ネットワークスタックや制御ループといった ネイティブの Zig タスクが走ります。
ノードの役割
ノードには役割があります。
オーケストレータノード(x86_64 / riscv64 / aarch64)は、多数のアクターを抱えます。実行時にアクターを生成し、監視し、死ねば作り直します。
エッジノード(MMU 無しの MCU)は、1〜2 個のアクターを持ち、ネットワークで協調します。
**多数のアクターを 1 チップで抱えて管理する機構は、設計上、オーケストレータにしか置きません。**264 KB の部品にそれを載せるのは、その機構が何も生まない場所で回すことであり、分散アクターがそもそも避けるために存在する当のものです。
アクター API
型付きメールボックスと send_remote は全ターゲットにあります。残りは対応範囲が分かれます。
| x86_64 | riscv64 | aarch64 | MMU 無し MCU | |
|---|---|---|---|---|
send / recv / msg_from / msg_a / msg_b | ✓ | ✓ | ✓ | ✓ |
msg_tag | ✓ | ✓ | ✓ | — |
send_remote | ✓ | ✓ | ✓ | ✓ |
spawn(動的生成) | ✓ | ✓ | ✓ | — |
monitor / die(監視) | ✓ | ✓ | ✓ | — |
register_name / whereis(ローカル名前解決) | ✓ | — | — | — |
has_msg(非ブロッキング peek) | ✓ | — | — | — |
node_ip / send_node / reply_node | ✓ | — | — | — |
**型付き要求/応答。**メッセージは {from, tag, a, b} です。send(pid, tag, a, b) は送信者の pid を自動で刻むので、ハンドラは誰が呼んだかを知らずに send(msg_from(), …) で答えられます。recv() は dispatch 用に tag を返します。
動的生成と監視(オーケストレータ)。spawn(role) が、走行中の FFI の中から実行時に新しいアクターを作ります。monitor(pid) はアクターが死んだときに知らせるよう要求し、die() は予約タグ MSG_DOWN でモニタに通知して終了します。MSG_DOWN を受けたスーパバイザは、空いたスロットにワーカーを作り直します。こうして、システムは自分で立て直します。
**エッジノードには、これがありません。**その代わり、フレーム単位の障害復帰があります。
- 暴走したフレーム — VM のウォッチドッグが、命令数の上限を超えたフレームを巻き戻します(longjmp)。
- ハードウェアフォルト — VM のサンドボックスを抜けたフォルト(C スタックのオーバーフロー等)も、カーネルが壊れたフレームをスキップし、アクターの状態(ミッションの進捗)を保ったまま、次のフレームから再開させます。再起動ではなく再開です。再起動は保ちたい状態を消してしまうからです。
実機(Pico W)で、飛行中にフォルトを注入して確認済みです。ミッションアクターが兄弟のコンテキストへ書き込み、MPU がそれを拒否し、カーネルがフレームをスキップし、配送ミッションは完走しました。制御ループも兄弟アクターも、それに気づきません。
ノードを跨いだ監視(別ノードの監視役がエッジノードの死を検知して再供給する)は、未実装です。
フレーム駆動の実行
スケジューラは、各スクリプトの main() を毎 tick 呼び直します。グローバル領域の値は呼び出しをまたいで残り、オペランドスタックは毎フレーム捨てられます。だからスクリプトは、内部に無限ループを書きません。1 フレームで一区切りの仕事をして、返します。
フレームが、協調的な進行の単位です。そして行儀の悪いフレームが機械を固めないことを、プリエンプトするタイマが保証します。加えて x86・riscv64・aarch64・MMU 無し MCU では、タスク毎の命令数ウォッチドッグが 1 フレームを有界化します。
グローバルロックの無い並行性
VM は、プロセス全体の実行状態を、ふつうのグローバル変数に持ちます。VM の実行を 1 つのロックで直列化する代わりに、スケジューラが、コンテキストスイッチのたびに、その実行状態をタスクごとに退避・復元します。
これが大きく効きます。**VM のフレームは、その周りに割り込み禁止区間を持たず、完全にプリエンプトできます。**割り込みをマスクする区間は、本当に共有されるデータ——アクターのメールボックス、コンソール、NIC の送信経路、タスク生成——を守る、短いものだけ。あとはタスクごとのスタック・グローバル領域・ヒープが、競合を防ぎます。
SMP では、コアごとの VM グローバルを、コアごとのベースアドレスで切り分けます。x86 は GS ベース、riscv64 はスレッドポインタ、aarch64 は TPIDRRO_EL0、RP2040 の 2 コアはコア ID レジスタ。この 4 つでは、VM が全コアで走ります。RP2350 はまだ 1 つのブロックを共有しているので、VM は単一コアのままです。
位置透過:機械を跨ぐメッセージ
リモートのメッセージが、ローカルのメールボックスに届き、ローカルのものと同じ recv() で読めます。
実機で。Raspberry Pi Pico 2 W(RP2350、MMU 無し)で走る非特権・PMP 隔離のアクターが、実 Wi-Fi・実 LAN 越しに、x86 の Lambda OS で走るアクターへ型付きメッセージを送ります。相手はそれをふつうのメールボックスで受けて返信し、その返値が、送った側のアクター自身のメールボックスに戻ってきます。
そのイメージでは、隔離されたアクターの隣でネットワークスタックが特権タスクとして走っています。隔離と分散が、同じイメージで両立しています。
透過性がどこまでか
**受信経路は透過的です。**リモートのメッセージは、ローカルのものと同じメールボックス、同じ recv()、同じ tag 分岐で処理されます。
**返信経路は透過的ではありません。送信元が別ノードなので、msg_from() は -1 を返します。**したがって、ふつうの send(msg_from(), …) で返すことができません。返信は reply_node() を通ります。カーネルが受信パケットから記録した返信先を使います。
**リモートに答えるスクリプトは、それと承知で書くことになります。**位置透過は、受信については完全で、返信については部分的です。
実測されたこと
- 送っている側は実機(実 Wi-Fi、実 LAN)です。
- 受けている側の x86 Lambda OS は、**エミュレータの中で動いています。**ベアメタルの x86 をピアにした構成は未検証です。
ノードを跨ぐマイクロ秒協調:未実装
アクターのメッセージ配送はベストエフォートです。ノードを跨いで共通のクロックで動くこと(隊形を保つスワームのような、真にマイクロ秒協調した動作)は実装していません。
それには共有の時間基準(有線なら PTP、無線ならノード毎の GPS/PPS 規律)と、ローカルの時刻駆動実行が要ります。エミュレーションでは検証できません。2 つのゲストは、各々がホストに独立に乱されるため、共通のクロックを保てないからです。