From f53ad3f21af36b135c8e03025b799b53e5ec6a5b Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 14:47:46 +0900 Subject: [PATCH 1/9] add roadmap Signed-off-by: Yuuki Takano --- improve_nw.md | 489 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 improve_nw.md diff --git a/improve_nw.md b/improve_nw.md new file mode 100644 index 000000000..ce5ad61c2 --- /dev/null +++ b/improve_nw.md @@ -0,0 +1,489 @@ +# ネットワークスタック改善ロードマップ + +## 1. 概要 + +awkernel の TCP/UDP ネットワークスタックは、多数のコネクションを同時処理する際に +グローバルな `RwLock` 書き込みロックがボトルネックとなる。 + +ソケット生成・ポート割り当て・ソケット破棄のたびに `NET_MANAGER` グローバルロックを +取得するため、並列度が上がるほどスループットが頭打ちになる。本文書では問題箇所を +優先度順に整理し、段階的な改善フェーズを定義する。 + +--- + +## 2. 現状のロック構造 + +### 2.1 グローバル静的変数 + +``` +net.rs +├── NET_MANAGER: RwLock +│ ├── interfaces: BTreeMap> +│ ├── interface_id: u64 +│ ├── tcp_ports_ipv4: BTreeMap (port → 参照カウント) +│ ├── tcp_port_ipv4_ephemeral: u16 +│ ├── udp_ports_ipv4: BTreeSet +│ ├── udp_port_ipv4_ephemeral: u16 +│ └── ... (IPv6 も同様) +├── IRQ_WAKERS: Mutex> +└── POLL_WAKERS: Mutex> + +tcp_stream_no_std.rs +└── CLOSED_CONNECTIONS: Mutex>> + +udp_socket_no_std.rs +└── NUM_MULTICAST_JOIN_IPV4: Mutex> +``` + +### 2.2 インターフェースごとの変数 (if_net.rs) + +``` +IfNet +├── inner: Mutex ← LOCK #3 +│ ├── interface: Interface (smoltcp 本体) +│ ├── default_gateway_ipv4 +│ ├── multicast_addr_ipv4: BTreeSet +│ └── multicast_addr_mac: BTreeMap +├── socket_set: RwLock +├── tx_only_ringq: Vec> ← LOCK #2 +└── rx_irq_to_driver: BTreeMap + └── NetDriver::rx_ringq: Mutex ← LOCK #1 +``` + +### 2.3 ドキュメント化されたロック順序 + +``` +1. NetDriver::rx_ringq +2. IfNet::tx_ringq (tx_only_ringq[i]) +3. IfNet::inner +``` + +### 2.4 主要パスのロック取得チェーン + +**TCP 接続確立 (connect)** +``` +NET_MANAGER.write() ← グローバル書き込みロック (ポート確保) + IfNet::inner.lock() ← interface.poll 用 + IfNet::socket_set.write() ← ソケット追加 +``` + +**TCP/UDP データ送受信** +``` +NET_MANAGER.read() + socket_set.read() + socket.lock() (MCS ロック) +``` + +**ポーリング (poll_rx_tx)** +``` +rx_ringq.lock() ← パケット受信バッファ + tx_ringq.lock() ← パケット送信バッファ + inner.lock() ← interface.poll() 実行 +``` + +--- + +## 3. 特定されたボトルネック + +### 優先度一覧 + +| 優先度 | 場所 | ロック種別 | 問題 | +|---|---|---|---| +| CRITICAL | `net.rs:NET_MANAGER` | `RwLock::write()` | 全 TCP/UDP 接続確立がグローバル書き込みロックを取得。エフェメラルポート探索は最悪 O(16384) 反復しながらロック保持 | +| CRITICAL | `net.rs:get_ephemeral_port_tcp_ipv4` | 同上 | `entry(i)` (ループインデックス) で検索するバグ。正しくは `entry(port)` | +| HIGH | `if_net.rs:IfNet::inner` | `Mutex::lock()` | `interface.poll()` 呼び出し全体でロック保持。複数コアが同一インターフェースをポーリングするとシリアライズ | +| HIGH | `if_net.rs:IfNet::socket_set` | `RwLock::write()` | ソケット追加・削除で書き込みロック。接続レートが高いと write lock 競合が多発 | +| MEDIUM | `tcp_stream_no_std.rs:CLOSED_CONNECTIONS` | `Mutex::lock()` | グローバル Mutex。Drop ハンドラで毎回取得 | +| MEDIUM | `tcp_stream_no_std.rs:connect()` | 二重ロック | `inner.lock()` 保持中に `socket_set.write()` を取得。ドキュメントのロック順序に反する | + +### 3.1 NET_MANAGER グローバル書き込みロック (CRITICAL) + +`TcpStream::connect`、`TcpListener::bind_on_interface`、`TcpListener::accept`(2 回)、 +`UdpSocket::bind_on_interface`、`UdpSocket::drop`、`TcpPort::drop` がすべて +`NET_MANAGER.write()` を取得する。 + +多数のコネクションが並行して確立・破棄されると、全スレッドがこの単一ロックで +逐次化される。 + +### 3.2 エフェメラルポート探索バグ (CRITICAL) + +`net.rs` 内の `get_ephemeral_port_tcp_ipv4`(および IPv6 版)は次のような実装になっている。 + +```rust +for i in 0..(u16::MAX >> 2) { + let port = self.tcp_port_ipv4_ephemeral.wrapping_add(i); + let entry = self.tcp_ports_ipv4.entry(i); // ← バグ: entry(port) が正しい + ... +} +``` + +ループインデックス `i` でキーを検索しているため、ポートの空き判定が正しく動作しない。 +`NET_MANAGER.write()` を保持したまま最悪 16384 回反復するため、パフォーマンス上も問題。 + +### 3.3 IfNet::inner のポーリング中ロック保持 (HIGH) + +`poll_rx_tx` / `poll_tx_only` はいずれも `inner.lock()` を保持したまま +`interface.poll()` を呼ぶ。smoltcp の `Interface::poll` はパケット処理・TCP 状態機械 +更新など相当量の作業を行うため、同一インターフェースの別ポーリングスレッドが +ロック待ちとなる。 + +さらに `IfNetInner` はポーリングに不要なマルチキャスト状態 +(`multicast_addr_ipv4`、`multicast_addr_mac`)も同一 Mutex に含めているため、 +マルチキャスト操作がポーリングを遅延させる。 + +### 3.4 socket_set 書き込みロックの競合 (HIGH) + +ソケット追加(connect/bind/accept)・削除(drop)は `socket_set.write()` を必要とする。 +これは全既存ソケットの `socket_set.read()` を保持しているスレッドをブロックする。 +高接続レートではデータ転送が接続確立によって阻害される。 + +### 3.5 CLOSED_CONNECTIONS グローバル Mutex (MEDIUM) + +`TcpStream::Drop` が毎回 `CLOSED_CONNECTIONS.lock()` を取得する。 +接続破棄が集中するとグローバル Mutex で逐次化される。 + +### 3.6 connect() の二重ロック (MEDIUM) + +`TcpStream::connect` では `inner.lock()` を保持したまま `socket_set.write()` を取得する。 +`socket.connect()` に `interface.context()` を渡す必要があるためだが、 +これによりロック順序(inner → socket_set)がドキュメントと逆になっている。 + +--- + +## 4. ロードマップ + +### Pre-Phase: バグ修正(即日対応) + +**変更内容:** `entry(i)` → `entry(port)` の修正 + +**対象ファイル:** +- `awkernel_lib/src/net.rs` + - `get_ephemeral_port_tcp_ipv4` 内の `self.tcp_ports_ipv4.entry(i)` → `entry(port)` + - `get_ephemeral_port_tcp_ipv6` 内の同様の箇所 + +**効果:** 正確なポート空き判定が可能になる。ポート探索が O(1) に近づく( +既使用ポートが少ない通常ケース)。 + +**リスク:** 低。1 行修正。 + +--- + +### Phase 1: PortAllocator 分離 — NET_MANAGER 書き込みロック除去 + +**目的:** ポート割り当て・解放操作を `NET_MANAGER` から分離し、 +グローバル書き込みロックをホットパスから除去する。 + +#### 1.1 PortAllocator 新設 + +`NetManager` からポート関連フィールドをすべて抽出し、独立した構造体に移動する。 + +```rust +// 新設: awkernel_lib/src/net/port_alloc.rs +pub struct PortAllocator { + tcp_ipv4: Mutex>, + tcp_ipv4_ephemeral: AtomicU16, + tcp_ipv6: Mutex>, + tcp_ipv6_ephemeral: AtomicU16, + udp_ipv4: Mutex>, + udp_ipv4_ephemeral: AtomicU16, + udp_ipv6: Mutex>, + udp_ipv6_ephemeral: AtomicU16, +} + +static PORT_ALLOC: PortAllocator = PortAllocator::new(); +``` + +エフェメラルカーソルは `AtomicU16::fetch_add` で前進させる(ロック不要)。 +実際のポート占有確認・挿入は各プロトコルの個別 Mutex だけを取得する。 + +これにより 4 つのプロトコル名前空間が独立し、TCP IPv4 の割り当てが +TCP IPv6 や UDP をブロックしない。 + +**変更対象ファイル:** +- `awkernel_lib/src/net.rs` — `NetManager` からポートフィールドを削除 +- `awkernel_lib/src/net/port_alloc.rs` — 新規作成 +- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` — `NET_MANAGER.write()` → `PORT_ALLOC` 呼び出し +- `awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs` — 同様(accept 内の 2 箇所含む) +- `awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs` — 同様 +- `awkernel_lib/src/net/tcp.rs` — `TcpPort::drop` 内の `NET_MANAGER.write()` → `PORT_ALLOC` + +**期待効果:** +- N スレッドが並行して接続確立する際、グローバル書き込みロック待ちがなくなる +- `NET_MANAGER` はインターフェース参照取得の読み込みロックのみになる +- 接続確立スループットがコア数に対しほぼ線形スケール + +**リスク:** 中。`TcpPort::drop` のポート参照解放先変更に注意。 + +#### 1.2 NET_MANAGER の読み取り専用化確認 + +Phase 1.1 完了後、`NET_MANAGER.write()` の残存箇所を監査し、 +`add_interface`(初期化パス)以外に書き込みロックがないことを確認する。 + +**変更対象ファイル:** `net.rs`(コード変更なし、確認のみ) + +--- + +### Phase 2: IfNetInner 分割 — ポーリングとマルチキャストの分離 + +**目的:** `IfNet::inner` ロックの保持時間を短縮し、マルチキャスト操作が +ポーリングを遅延させないようにする。 + +#### 2.1 IfNetInner を IfNetCore と IfNetMulticast に分割 + +```rust +// 変更後 +pub(super) struct IfNet { + inner: Mutex, // smoltcp Interface + ゲートウェイ + multicast: Mutex, // マルチキャスト管理(独立) + socket_set: RwLock, + ... +} + +struct IfNetCore { + interface: Interface, + default_gateway_ipv4: Option, +} + +struct IfNetMulticast { + multicast_addr_ipv4: BTreeSet, + multicast_addr_mac: BTreeMap<[u8; 6], u32>, +} +``` + +`poll_rx_tx` / `poll_tx_only` は `inner.lock()` のみ取得し、 +`multicast` は触れない。`join_multicast_v4` / `leave_multicast_v4` は +`multicast.lock()` のみ取得する。 + +**ロック順序の更新(ドキュメント要更新):** +``` +1. NetDriver::rx_ringq +2. IfNet::tx_ringq +3. IfNet::inner (IfNetCore) + ※ multicast は inner と同時に保持しない +``` + +**変更対象ファイル:** +- `awkernel_lib/src/net/if_net.rs` — 構造体分割、各メソッドの lock 先変更 +- `awkernel_lib/src/net.rs` — `if_net.inner` でマルチキャスト参照している箇所を `if_net.multicast` に変更 + +**期待効果:** +- マルチキャスト操作中にポーリングが止まらなくなる +- `join_multicast_v4` / `leave_multicast_v4` がポーリングパスを阻害しない + +**リスク:** 低〜中。smoltcp の `Interface::join_multicast_group()` は +`IfNetCore` 内の `interface` にアクセスするため、マルチキャストの smoltcp 側操作は +`inner.lock()` で、awkernel 側のブックキーピングは `multicast.lock()` で行う必要がある。 +両方を同時保持しないよう注意(ロック順序ドキュメントを更新すること)。 + +**前提条件:** Phase 1 + +--- + +### Phase 3: 二重ロック解消・Drop キュー導入 + +#### 3.1 connect() の二重ロック修正 + +**現状:** +```rust +inner.lock() { + socket_set.write() { // ← inner を保持したまま write + socket.connect(interface.context(), ...) + } +} +``` + +`socket.connect()` が `interface.context()` を必要とするため `inner` を保持するが、 +`socket_set.write()` は進行中の送受信(`socket_set.read()`)をブロックする。 + +**変更方針:** +`inner.lock()` を短命な読み取りに変えて、接続に必要な情報(ローカル IP アドレス、 +現在時刻)だけを取り出してロック解放後に `socket_set.write()` を取得する。 + +```rust +// inner を短時間だけロックして必要情報を取り出す +let ctx = { inner.lock().extract_connect_context() }; +// inner 解放後に socket_set を書き込みロック +socket_set.write().add(...); +socket.connect(ctx, ...); +``` + +smoltcp のフォーク版に `Interface::extract_connect_context()` または同等の +軽量アクセサを追加する。 + +**変更対象ファイル:** +- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` +- `awkernel_lib/smoltcp/src/iface/interface/mod.rs` — コンテキスト取り出しアクセサ追加 + +**期待効果:** +- connect 中に進行中の送受信がブロックされなくなる +- ロック順序がドキュメントと一致する + +**リスク:** 中。smoltcp フォークへの変更を伴う。 + +#### 3.2 Drop キューによる socket_set.write() 遅延解放 + +**現状:** `TcpStream::Drop` が `socket_set.write()` を取得してソケットを即座に削除。 +接続破棄が集中すると write lock がデータ転送をブロックする。 + +**変更方針:** `IfNet` にインターフェースごとの非同期削除キューを追加する。 + +```rust +pub(super) struct IfNet { + ... + drop_queue: Mutex>, // 新規追加 +} +``` + +`TcpStream::Drop` では `socket_set.write()` の代わりに `drop_queue` に +ハンドルを積む(`Mutex` のロック取得のみ)。 +既存の `tcp_garbage_collector`(100ms 周期)がキューを消費して +`socket_set.write()` でソケットを削除する。 + +`CLOSED_CONNECTIONS` グローバル Mutex の代替にもなる。 +インターフェースごとに独立したキューなので、複数インターフェース間の +競合が生じない。 + +**変更対象ファイル:** +- `awkernel_lib/src/net/if_net.rs` — `drop_queue` フィールド追加、drain 処理追加 +- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` — Drop を drop_queue push に変更 +- `awkernel_services/src/network_service.rs` — `tcp_garbage_collector` を更新 + +**期待効果:** +- 接続破棄が集中しても `socket_set.write()` 競合が発生しない(GC レートに平滑化) +- `CLOSED_CONNECTIONS` グローバル Mutex のボトルネック解消 + +**リスク:** 低〜中。遅延削除のため、Drop 後 100ms 以内にハンドルが +再利用されないことを保証する必要がある(smoltcp の SocketSet はスロット再利用を +GC 後にのみ行うため問題なし)。 + +**前提条件:** Phase 2 + +--- + +### Phase 4: インターフェースごとのスケールアウト + +#### 4.1 PortAllocator のインターフェースごとへの移動 + +Phase 1 で作成したグローバル `PORT_ALLOC` を `IfNet` 内に移動する。 +TCP/UDP ソケットはすでに単一インターフェースに紐付いているため、 +ポート名前空間をインターフェースごとに独立させることができる。 + +```rust +pub(super) struct IfNet { + ... + port_alloc: PortAllocator, // グローバル PORT_ALLOC を廃止 +} +``` + +`TcpPort::drop` はポート返却先インターフェースへの参照(`Weak`)を持つ必要がある。 + +**変更対象ファイル:** +- `awkernel_lib/src/net/if_net.rs` — `port_alloc` フィールド追加 +- `awkernel_lib/src/net/tcp.rs` — `TcpPort` に `Weak` を追加 +- `awkernel_lib/src/net/port_alloc.rs` — グローバル静的変数を削除 + +**期待効果:** +- 異なるインターフェース間でポート割り当て競合がゼロになる +- 複数 NIC 構成でポート確保スループットが NIC 数に比例してスケール + +**リスク:** 中。`TcpPort` に `Weak` を持たせる設計変更。 +インターフェース削除前にポートが解放される通常ケースでは問題ない。 + +**前提条件:** Phase 1、Phase 2 + +#### 4.2 マルチキュー RX のフェーズ分離ポーリング + +複数の独立した RX キューを持つ NIC では、パケット受信(RX ドレイン)を +コアごとに並列実行し、smoltcp の `interface.poll()` のみ逐次化する +2 フェーズポーリングを導入する。 + +``` +Phase A (並列, per-queue): hardware → rx_ringq (rx_ringq.lock() のみ) +Phase B (逐次, per-iface): rx_ringq → interface.poll() (inner.lock() が必要) +``` + +現在の `will_poll` AtomicUsize による協調排他を拡張し、Phase A と Phase B を +明示的に分離する。 + +**変更対象ファイル:** +- `awkernel_lib/src/net/if_net.rs` — `poll_rx_tx` のフェーズ分離リファクタリング + +**注意:** SPIN モデル(`specification/awkernel_lib/src/net/if_net/if_net.pml`)の +LTL プロパティ(全パケットが最終的に送信される)をこのフェーズ変更に対して +再検証すること。実装前にモデルを更新し、`spin -a` で再チェックすること。 + +**期待効果:** +- 4 キュー NIC を 4 コアでポーリングする場合、RX 受信スループットが 4 倍近く向上 +- `interface.poll()` のシリアライズ区間のみに競合を限定 + +**リスク:** 高。SPIN モデルの再検証が必要。Phase A/B の境界で +パケットの可視性保証を維持する必要がある。 + +**前提条件:** Phase 2(`IfNetCore` が分離済み)、Phase 3.1(二重ロック解消済み) + +--- + +## 5. フェーズ依存関係 + +``` +Pre-Phase (エフェメラルポートバグ修正) + │ + ▼ +Phase 1.1 (PortAllocator 分離) + │ + ▼ +Phase 1.2 (NET_MANAGER 読み取り専用化確認) + │ + ▼ +Phase 2.1 (IfNetInner → IfNetCore + IfNetMulticast) + │ + ├──────────────────────┐ + ▼ ▼ +Phase 3.1 Phase 3.2 +(connect 二重ロック解消) (Drop キュー導入) + │ │ + └──────────┬───────────┘ + ▼ + Phase 4.1 Phase 4.2 + (per-iface PortAllocator) (2 フェーズポーリング) + │ │ + └───────────┬───────────┘ + ▼ + (完全実装) +``` + +--- + +## 6. 各フェーズの期待効果 + +| フェーズ | 解消するボトルネック | 機構 | 期待効果 | +|---|---|---|---| +| Pre | ポート探索バグ | 1 行修正 | 正確性回復、探索 O(1) 化 | +| 1.1 | NET_MANAGER.write() 全接続逐次化 | AtomicU16 + 4 独立 Mutex | 並行接続確立がほぼ線形スケール | +| 1.2 | 残存 write lock 確認 | 監査のみ | 安全性確認 | +| 2.1 | inner lock がマルチキャストとポーリングを混在 | 構造体分割 | マルチキャスト操作がポーリングを阻害しない | +| 3.1 | connect の二重ロック | コンテキスト事前取り出し | connect 中も送受信が継続 | +| 3.2 | Drop 時の socket_set.write() 競合と CLOSED_CONNECTIONS | per-iface 遅延削除キュー | 接続破棄集中時も送受信が継続 | +| 4.1 | 複数 iface 間のポート割り当て競合 | per-iface PortAllocator | 複数 NIC 構成でポート確保が完全独立 | +| 4.2 | マルチキュー RX がシリアライズ | 2 フェーズポーリング | N キュー NIC で受信スループット N 倍近く改善 | + +--- + +## 7. 変更しない不変条件 + +- **ロック順序:** rx_ringq → tx_ringq → inner (IfNetCore) の順序を全フェーズで維持する。 + `multicast` および `drop_queue` は葉ロックとし、`inner` と同時保持しない。 + 各フェーズでロック順序コメント(`if_net.rs` 冒頭)を更新すること。 + +- **公開 API:** `TcpStream`、`UdpSocket`、`TcpListener`、`SockTcpStream`、 + `SockTcpListener`、`SockUdp` トレイトの型シグネチャを変更しない。 + +- **smoltcp フォーク変更の最小化:** Phase 3.1 のアクセサ追加、Phase 4.2 の + ポーリング構造リファクタリングに限定する。それ以外のフェーズは awkernel 側のみ変更。 + +- **SPIN モデル:** Phase 4.2 の実装前に + `specification/awkernel_lib/src/net/if_net/if_net.pml` の LTL プロパティを + 2 フェーズポーリングモデルに合わせて更新し、`spin -a` で再検証すること。 + +- **no_std 制約:** 全フェーズで `std` API に依存しない。追加する構造体は + `Mutex`、`AtomicU16`、`BTreeMap`、`VecDeque` など no_std 互換の型のみ使用する。 From e9fc3eb620d96a7688ecf84c2e0a0d39a74e570b Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 15:08:24 +0900 Subject: [PATCH 2/9] update improve_nw.md Signed-off-by: Yuuki Takano --- improve_nw.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) diff --git a/improve_nw.md b/improve_nw.md index ce5ad61c2..b62a8431a 100644 --- a/improve_nw.md +++ b/improve_nw.md @@ -487,3 +487,326 @@ Phase 3.1 Phase 3.2 - **no_std 制約:** 全フェーズで `std` API に依存しない。追加する構造体は `Mutex`、`AtomicU16`、`BTreeMap`、`VecDeque` など no_std 互換の型のみ使用する。 + +--- + +## 8. テスト手法 + +### 8.1 テスト環境の構成 + +現状のプロジェクトは `make qemu-x86_64-net` で QEMU を直接起動し、 +ホスト↔ゲスト間の通信のみをテストしている。ここでは **VM 間通信** と +**多数コネクション負荷テスト** を可能にするため、libvirt/KVM を用いた +2-VM 構成を追加する。 + +``` +Host Machine (libvirt / KVM) +├── virbr-awk (NAT bridge, 192.168.100.0/24) ← virsh net-define +├── VM: awkernel (awkernel カーネル, QEMU/KVM, OVMF) +│ ├── vnet0: e1000e, 192.168.100.10 +│ └── vnet1: virtio, 192.168.100.11 +└── VM: counterpart (Fedora/Ubuntu, iperf3 / netperf サーバ) + └── vnet0: 192.168.100.2 +``` + +`awkernel` 側のネットワーク設定は既存の QEMU 引数と同じ +(e1000e + virtio-net-pci、MAC アドレスは Makefile の値を維持)。 + +--- + +### 8.2 環境セットアップ + +#### 8.2.1 仮想ネットワーク定義 + +```xml + + + virbr-awk + + + + + + + + +``` + +```bash +virsh net-define awkernel-net.xml +virsh net-start virbr-awk +virsh net-autostart virbr-awk +virsh net-list --all # 起動確認 +``` + +#### 8.2.2 カウンタパート VM の作成 + +```bash +# Fedora/Ubuntu など標準 Linux を 1 台用意する +virt-install \ + --name counterpart \ + --memory 2048 --vcpus 2 \ + --os-variant fedora40 \ + --network network=virbr-awk,model=virtio \ + --disk path=/var/lib/libvirt/images/counterpart.qcow2,size=20,format=qcow2 \ + --location /path/to/fedora.iso \ + --extra-args 'console=ttyS0' \ + --nographics --noautoconsole + +virsh start counterpart +virsh console counterpart # インストール後にログイン +# インストール完了後 +sudo dnf install -y iperf3 netperf nc # または apt install +``` + +#### 8.2.3 awkernel VM の定義 + +Makefile の QEMU 引数を virsh の `` XML に変換して登録する。 +NIC を既存の QEMU usermode ネットワークから `virbr-awk` ブリッジに切り替える。 + +```xml + + + awkernel + 4 + 16 + + hvm + /usr/share/OVMF/OVMF_CODE.fd + /var/lib/libvirt/qemu/nvram/awkernel_VARS.fd + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +```bash +virsh define awkernel-vm.xml +virsh start awkernel +virsh console awkernel # シリアルコンソールでカーネルログ確認 +``` + +#### 8.2.4 VM の停止・削除 + +```bash +virsh destroy awkernel # 強制停止 +virsh destroy counterpart +virsh undefine awkernel # 定義削除 +virsh undefine counterpart +virsh net-destroy virbr-awk +virsh net-undefine virbr-awk +``` + +--- + +### 8.3 テスト項目 + +| # | テスト名 | 目的 | ツール | 計測値 | +|---|---|---|---|---| +| T1 | 基本疎通確認 | TCP/UDP が到達すること | nc, ping | 成功/失敗 | +| T2 | UDP スループット | NET_MANAGER 改善前後の比較 | iperf3 -u | Gbps | +| T3 | TCP スループット | socket_set 競合改善の確認 | iperf3 | Gbps | +| T4 | 並列 TCP 接続 | 多数コネクション時スケーリング | iperf3 -P N | Gbps, CPU% | +| T5 | 接続確立レート | Phase 1 前後の比較 | netperf TCP_CRR | conn/s | +| T6 | リクエスト/レスポンス RTT | Phase 3 前後の比較 | netperf TCP_RR | μs | +| T7 | パケットキャプチャ解析 | ロスト・再送の確認 | tcpdump, tshark | ロス率% | +| T8 | PCAP ベースの手動確認 | awkernel 側パケット検査 | 既存 filter-dump | - | + +--- + +### 8.4 テストコマンド詳細 + +#### 基本疎通 (T1) + +```bash +# counterpart VM 上 +nc -l -p 9000 # TCP リスナー +nc -u -l -p 9001 # UDP リスナー + +# ホストから awkernel を経由した疎通確認(awkernel がエコーを返す場合) +nc 192.168.100.10 26099 +echo "hello" | nc -u 192.168.100.10 26099 +``` + +#### UDP スループット (T2) + +```bash +# counterpart VM でサーバ起動 +iperf3 -s + +# ホストまたは counterpart から awkernel 向けに送信 +iperf3 -c 192.168.100.10 -u -b 0 -t 30 -l 1400 # 帯域無制限, 30 秒 +iperf3 -c 192.168.100.10 -u -b 0 -t 30 -P 32 # 32 並列ストリーム +``` + +#### TCP スループット・並列接続 (T3, T4) + +```bash +# counterpart VM でサーバ起動 +iperf3 -s -p 5201 + +# N 並列 TCP ストリーム +for N in 1 4 8 16 32 64 128; do + echo "=== -P $N ===" + iperf3 -c 192.168.100.10 -p 5201 -P $N -t 30 -J \ + | tee result_tcp_P${N}.json +done +``` + +#### 接続確立レート (T5) — Phase 1 比較に重要 + +```bash +# counterpart VM で netperf サーバ起動 +netserver -p 12865 + +# TCP 接続生成レートの計測(30 秒間) +netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 +``` + +期待値: Phase 1 適用後は Phase 0 比 **2〜4 倍**の接続/秒を記録する。 + +#### RTT レイテンシ (T6) — Phase 3 比較に重要 + +```bash +netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 +``` + +#### パケットキャプチャ (T7, T8) + +```bash +# awkernel VM は QEMU filter-dump で自動キャプチャ済み +# counterpart 側で補足する場合 +virsh domifstat counterpart vnet0 # パケット統計 +virsh qemu-monitor-command awkernel --hmp \ + 'info network' # QEMU ネットワーク状態確認 + +# キャプチャ解析 +tcpdump -vvv -XXnr packets_net0.pcap | head -100 +tshark -r packets_net0.pcap -z io,stat,1 "tcp" # 1 秒ごとの TCP 統計 +``` + +--- + +### 8.5 ベースライン取得手順 + +各フェーズの実装前に必ずベースラインを記録する。 + +```bash +#!/bin/bash +# baseline.sh — 変更前に実行してベースライン保存 +DATE=$(date +%Y%m%d_%H%M%S) +DIR="bench_baseline_${DATE}" +mkdir -p $DIR + +# T3: TCP スループット +iperf3 -c 192.168.100.10 -p 5201 -t 30 -J > $DIR/tcp_single.json + +# T4: 並列 TCP +for N in 4 16 64; do + iperf3 -c 192.168.100.10 -p 5201 -P $N -t 30 -J > $DIR/tcp_P${N}.json +done + +# T5: 接続確立レート +netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 \ + > $DIR/tcp_crr.txt + +# T6: RTT +netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 \ + > $DIR/tcp_rr.txt + +echo "Baseline saved to $DIR" +``` + +フェーズ実装後に同スクリプトを `bench_after_phaseN_${DATE}` として再実行し、 +数値を比較する。 + +--- + +### 8.6 フェーズ別テスト手順 + +#### Pre-Phase(バグ修正)後 + +```bash +# ポート割り当てが正しく動作することを確認 +# 1. 64 並列 TCP 接続を確立し、全て成功することを確認 +iperf3 -c 192.168.100.10 -p 5201 -P 64 -t 10 +# 2. エフェメラルポート番号が想定範囲内かシリアルコンソールで確認 +virsh console awkernel +``` + +#### Phase 1(PortAllocator 分離)後 + +```bash +# T5: 接続確立レートがベースライン比 2 倍以上であることを確認 +netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 +# T4: 64 並列接続でのスループットがベースライン比 1.5 倍以上 +iperf3 -c 192.168.100.10 -p 5201 -P 64 -t 30 -J +``` + +#### Phase 2(IfNetInner 分割)後 + +```bash +# マルチキャスト join/leave 中にデータ転送が止まらないことを確認 +# 別ターミナルで転送継続 +iperf3 -c 192.168.100.10 -p 5201 -t 60 & +# awkernel コンソールでマルチキャスト join/leave を繰り返す +virsh console awkernel +# iperf3 のスループットが join/leave 中も維持されていることを確認 +``` + +#### Phase 3(二重ロック解消・Drop キュー)後 + +```bash +# T6: RTT がベースライン比 改善していることを確認 +netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 +# 接続破棄集中テスト: 短命コネクションを高レートで生成 +for i in $(seq 1 1000); do + nc -z 192.168.100.10 26099 & +done +wait +# iperf3 の転送が阻害されていないことを並行確認 +``` + +#### Phase 4(per-iface スケールアウト)後 + +```bash +# 複数インターフェース経由の並列転送 +iperf3 -c 192.168.100.10 -p 5201 -P 32 -t 30 -J & # net0 経由 +iperf3 -c 192.168.100.11 -p 5201 -P 32 -t 30 -J & # net1 経由 +wait +# 合算スループットが単独 NIC の 2 倍近いことを確認 +``` + +--- + +### 8.7 既存テストとの併用 + +上記 VM テストは既存のテスト体系を置き換えるものではなく、補完する。 + +| テスト種別 | 用途 | 実行タイミング | +|---|---|---| +| `make test` | 単体テスト(ロック・データ構造) | 毎コミット (CI) | +| SPIN モデル検証 (`make run` in specification/) | ポーリングアルゴリズムの正しさ | Phase 4.2 実装前後 | +| `make qemu-x86_64-net` + `scripts/udp.py` | 基本 UDP/TCP 疎通 | 各フェーズの動作確認 | +| VM テスト (本セクション) | 多数コネクション負荷・スループット計測 | フェーズ前後のベースライン比較 | From b5632a09f0b75987e5efa2cd7f8078365ac4cad0 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 17:17:21 +0900 Subject: [PATCH 3/9] refactor(net): extract port allocation into dedicated PortAllocator (Phase 1.1) Move TCP/UDP port tracking out of NetManager into a standalone port_alloc module with per-protocol Mutex guards, converting NET_MANAGER write locks to read locks in connect/bind/accept paths. Co-Authored-By: Claude Sonnet 4.6 --- awkernel_lib/src/net.rs | 233 +---------------- awkernel_lib/src/net/port_alloc.rs | 237 ++++++++++++++++++ awkernel_lib/src/net/tcp.rs | 5 +- .../net/tcp_listener/tcp_listener_no_std.rs | 68 +++-- .../src/net/tcp_stream/tcp_stream_no_std.rs | 27 +- .../src/net/udp_socket/udp_socket_no_std.rs | 46 ++-- improve_nw.md | 95 +++++++ 7 files changed, 396 insertions(+), 315 deletions(-) create mode 100644 awkernel_lib/src/net/port_alloc.rs diff --git a/awkernel_lib/src/net.rs b/awkernel_lib/src/net.rs index b8446a086..59af53ed1 100644 --- a/awkernel_lib/src/net.rs +++ b/awkernel_lib/src/net.rs @@ -14,12 +14,6 @@ use self::{ net_device::{LinkStatus, NetCapabilities, NetDevice}, }; -#[cfg(not(feature = "std"))] -use self::tcp::TcpPort; - -#[cfg(not(feature = "std"))] -use alloc::collections::BTreeSet; - #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; @@ -34,6 +28,7 @@ pub mod ip_addr; pub mod ipv6; pub mod multicast; pub mod net_device; +pub(self) mod port_alloc; pub mod tcp; pub mod tcp_listener; pub mod tcp_stream; @@ -132,30 +127,6 @@ impl Display for IfStatus { static NET_MANAGER: RwLock = RwLock::new(NetManager { interfaces: BTreeMap::new(), interface_id: 0, - - #[cfg(not(feature = "std"))] - udp_ports_ipv4: BTreeSet::new(), - - #[cfg(not(feature = "std"))] - udp_port_ipv4_ephemeral: u16::MAX >> 2, - - #[cfg(not(feature = "std"))] - udp_ports_ipv6: BTreeSet::new(), - - #[cfg(not(feature = "std"))] - udp_port_ipv6_ephemeral: u16::MAX >> 2, - - #[cfg(not(feature = "std"))] - tcp_ports_ipv4: BTreeMap::new(), - - #[cfg(not(feature = "std"))] - tcp_port_ipv4_ephemeral: u16::MAX >> 2, - - #[cfg(not(feature = "std"))] - tcp_ports_ipv6: BTreeMap::new(), - - #[cfg(not(feature = "std"))] - tcp_port_ipv6_ephemeral: u16::MAX >> 2, }); static IRQ_WAKERS: Mutex> = Mutex::new(BTreeMap::new()); @@ -164,208 +135,6 @@ static POLL_WAKERS: Mutex> = Mutex::new(BTreeMap::new()) pub struct NetManager { interfaces: BTreeMap>, interface_id: u64, - - #[cfg(not(feature = "std"))] - udp_ports_ipv4: BTreeSet, - - #[cfg(not(feature = "std"))] - udp_port_ipv4_ephemeral: u16, - - #[cfg(not(feature = "std"))] - udp_ports_ipv6: BTreeSet, - - #[cfg(not(feature = "std"))] - udp_port_ipv6_ephemeral: u16, - - #[cfg(not(feature = "std"))] - tcp_ports_ipv4: BTreeMap, - - #[cfg(not(feature = "std"))] - tcp_port_ipv4_ephemeral: u16, - - #[cfg(not(feature = "std"))] - tcp_ports_ipv6: BTreeMap, - - #[cfg(not(feature = "std"))] - tcp_port_ipv6_ephemeral: u16, -} - -impl NetManager { - #[cfg(not(feature = "std"))] - fn get_ephemeral_port_udp_ipv4(&mut self) -> Option { - let mut ephemeral_port = None; - for i in 0..(u16::MAX >> 2) { - let port = self.udp_port_ipv4_ephemeral.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - - if !self.udp_ports_ipv4.contains(&port) { - self.udp_ports_ipv4.insert(port); - self.udp_port_ipv4_ephemeral = port; - ephemeral_port = Some(port); - break; - } - } - - ephemeral_port - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn set_port_in_use_udp_ipv4(&mut self, port: u16) { - self.udp_ports_ipv4.insert(port); - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn is_port_in_use_udp_ipv4(&mut self, port: u16) -> bool { - self.udp_ports_ipv4.contains(&port) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn free_port_udp_ipv4(&mut self, port: u16) { - self.udp_ports_ipv4.remove(&port); - } - - #[cfg(not(feature = "std"))] - fn get_ephemeral_port_udp_ipv6(&mut self) -> Option { - let mut ephemeral_port = None; - for i in 0..(u16::MAX >> 2) { - let port = self.udp_port_ipv6_ephemeral.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - - if !self.udp_ports_ipv6.contains(&port) { - self.udp_ports_ipv6.insert(port); - self.udp_port_ipv4_ephemeral = port; - ephemeral_port = Some(port); - break; - } - } - - ephemeral_port - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn set_port_in_use_udp_ipv6(&mut self, port: u16) { - self.udp_ports_ipv6.insert(port); - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn is_port_in_use_udp_ipv6(&mut self, port: u16) -> bool { - self.udp_ports_ipv6.contains(&port) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn free_port_udp_ipv6(&mut self, port: u16) { - self.udp_ports_ipv6.remove(&port); - } - - #[cfg(not(feature = "std"))] - fn get_ephemeral_port_tcp_ipv4(&mut self) -> Option { - let mut ephemeral_port = None; - for i in 0..(u16::MAX >> 2) { - let port = self.tcp_port_ipv4_ephemeral.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - - let entry = self.tcp_ports_ipv4.entry(i); - - match entry { - Entry::Occupied(_) => (), - Entry::Vacant(e) => { - e.insert(1); - ephemeral_port = Some(TcpPort::new(port, true)); - self.tcp_port_ipv4_ephemeral = port; - break; - } - } - } - - ephemeral_port - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn is_port_in_use_tcp_ipv4(&mut self, port: u16) -> bool { - self.tcp_ports_ipv4.contains_key(&port) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn port_in_use_tcp_ipv4(&mut self, port: u16) -> TcpPort { - if let Some(e) = self.tcp_ports_ipv4.get_mut(&port) { - *e += 1; - } else { - self.tcp_ports_ipv4.insert(port, 1); - } - - TcpPort::new(port, true) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn decrement_port_in_use_tcp_ipv4(&mut self, port: u16) { - if let Some(e) = self.tcp_ports_ipv4.get_mut(&port) { - *e -= 1; - if *e == 0 { - self.tcp_ports_ipv4.remove(&port); - } - } - } - - #[cfg(not(feature = "std"))] - fn get_ephemeral_port_tcp_ipv6(&mut self) -> Option { - let mut ephemeral_port = None; - for i in 0..(u16::MAX >> 2) { - let port = self.tcp_port_ipv6_ephemeral.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - - let entry = self.tcp_ports_ipv6.entry(i); - - match entry { - Entry::Occupied(_) => (), - Entry::Vacant(e) => { - e.insert(1); - ephemeral_port = Some(TcpPort::new(port, false)); - self.tcp_port_ipv6_ephemeral = port; - break; - } - } - } - - ephemeral_port - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn is_port_in_use_tcp_ipv6(&mut self, port: u16) -> bool { - self.tcp_ports_ipv6.contains_key(&port) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn port_in_use_tcp_ipv6(&mut self, port: u16) -> TcpPort { - if let Some(e) = self.tcp_ports_ipv6.get_mut(&port) { - *e += 1; - } else { - self.tcp_ports_ipv6.insert(port, 1); - } - - TcpPort::new(port, true) - } - - #[cfg(not(feature = "std"))] - #[inline(always)] - fn decrement_port_in_use_tcp_ipv6(&mut self, port: u16) { - if let Some(e) = self.tcp_ports_ipv6.get_mut(&port) { - *e -= 1; - if *e == 0 { - self.tcp_ports_ipv6.remove(&port); - } - } - } } pub fn get_interface(interface_id: u64) -> Result { diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs new file mode 100644 index 000000000..8e1612b4b --- /dev/null +++ b/awkernel_lib/src/net/port_alloc.rs @@ -0,0 +1,237 @@ +#[cfg(not(feature = "std"))] +use alloc::collections::{ + btree_map::Entry, + BTreeMap, BTreeSet, +}; +#[cfg(not(feature = "std"))] +use core::sync::atomic::{AtomicU16, Ordering}; + +#[cfg(not(feature = "std"))] +use crate::sync::{mcs::MCSNode, mutex::Mutex}; + +#[cfg(not(feature = "std"))] +use super::tcp::TcpPort; + +#[allow(dead_code)] +pub(super) struct PortAllocator { + #[cfg(not(feature = "std"))] + tcp_ipv4: Mutex>, + #[cfg(not(feature = "std"))] + tcp_ipv4_ephemeral: AtomicU16, + #[cfg(not(feature = "std"))] + tcp_ipv6: Mutex>, + #[cfg(not(feature = "std"))] + tcp_ipv6_ephemeral: AtomicU16, + #[cfg(not(feature = "std"))] + udp_ipv4: Mutex>, + #[cfg(not(feature = "std"))] + udp_ipv4_ephemeral: AtomicU16, + #[cfg(not(feature = "std"))] + udp_ipv6: Mutex>, + #[cfg(not(feature = "std"))] + udp_ipv6_ephemeral: AtomicU16, +} + +#[allow(dead_code)] +pub(super) static PORT_ALLOC: PortAllocator = PortAllocator::new(); + +impl PortAllocator { + pub(super) const fn new() -> Self { + Self { + #[cfg(not(feature = "std"))] + tcp_ipv4: Mutex::new(BTreeMap::new()), + #[cfg(not(feature = "std"))] + tcp_ipv4_ephemeral: AtomicU16::new(u16::MAX >> 2), + #[cfg(not(feature = "std"))] + tcp_ipv6: Mutex::new(BTreeMap::new()), + #[cfg(not(feature = "std"))] + tcp_ipv6_ephemeral: AtomicU16::new(u16::MAX >> 2), + #[cfg(not(feature = "std"))] + udp_ipv4: Mutex::new(BTreeSet::new()), + #[cfg(not(feature = "std"))] + udp_ipv4_ephemeral: AtomicU16::new(u16::MAX >> 2), + #[cfg(not(feature = "std"))] + udp_ipv6: Mutex::new(BTreeSet::new()), + #[cfg(not(feature = "std"))] + udp_ipv6_ephemeral: AtomicU16::new(u16::MAX >> 2), + } + } + + /// Allocate an ephemeral TCP IPv4 port. + #[cfg(not(feature = "std"))] + pub(super) fn get_ephemeral_tcp_ipv4(&self) -> Option { + let cursor = self.tcp_ipv4_ephemeral.load(Ordering::Relaxed); + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv4.lock(&mut node); + for i in 0..(u16::MAX >> 2) { + let port = cursor.wrapping_add(i); + let port = if port == 0 { u16::MAX >> 2 } else { port }; + if let Entry::Vacant(e) = map.entry(port) { + e.insert(1); + self.tcp_ipv4_ephemeral.store(port, Ordering::Relaxed); + return Some(TcpPort::new(port, true)); + } + } + None + } + + /// Claim a specific TCP IPv4 port. Returns `None` if the port is already in use. + #[cfg(not(feature = "std"))] + pub(super) fn try_claim_tcp_ipv4(&self, port: u16) -> Option { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv4.lock(&mut node); + if map.contains_key(&port) { + None + } else { + map.insert(port, 1); + Some(TcpPort::new(port, true)) + } + } + + /// Increment the reference count for a TCP IPv4 port (used by `TcpListener::accept`). + #[cfg(not(feature = "std"))] + pub(super) fn increment_ref_tcp_ipv4(&self, port: u16) -> TcpPort { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv4.lock(&mut node); + if let Some(e) = map.get_mut(&port) { + *e += 1; + } else { + map.insert(port, 1); + } + TcpPort::new(port, true) + } + + /// Decrement the reference count for a TCP IPv4 port, freeing it when it reaches zero. + #[cfg(not(feature = "std"))] + pub(super) fn decrement_ref_tcp_ipv4(&self, port: u16) { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv4.lock(&mut node); + if let Some(e) = map.get_mut(&port) { + *e -= 1; + if *e == 0 { + map.remove(&port); + } + } + } + + /// Allocate an ephemeral TCP IPv6 port. + #[cfg(not(feature = "std"))] + pub(super) fn get_ephemeral_tcp_ipv6(&self) -> Option { + let cursor = self.tcp_ipv6_ephemeral.load(Ordering::Relaxed); + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv6.lock(&mut node); + for i in 0..(u16::MAX >> 2) { + let port = cursor.wrapping_add(i); + let port = if port == 0 { u16::MAX >> 2 } else { port }; + if let Entry::Vacant(e) = map.entry(port) { + e.insert(1); + self.tcp_ipv6_ephemeral.store(port, Ordering::Relaxed); + return Some(TcpPort::new(port, false)); + } + } + None + } + + /// Claim a specific TCP IPv6 port. Returns `None` if the port is already in use. + #[cfg(not(feature = "std"))] + pub(super) fn try_claim_tcp_ipv6(&self, port: u16) -> Option { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv6.lock(&mut node); + if map.contains_key(&port) { + None + } else { + map.insert(port, 1); + Some(TcpPort::new(port, false)) + } + } + + /// Increment the reference count for a TCP IPv6 port. + #[cfg(not(feature = "std"))] + pub(super) fn increment_ref_tcp_ipv6(&self, port: u16) -> TcpPort { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv6.lock(&mut node); + if let Some(e) = map.get_mut(&port) { + *e += 1; + } else { + map.insert(port, 1); + } + TcpPort::new(port, false) + } + + /// Decrement the reference count for a TCP IPv6 port, freeing it when it reaches zero. + #[cfg(not(feature = "std"))] + pub(super) fn decrement_ref_tcp_ipv6(&self, port: u16) { + let mut node = MCSNode::new(); + let mut map = self.tcp_ipv6.lock(&mut node); + if let Some(e) = map.get_mut(&port) { + *e -= 1; + if *e == 0 { + map.remove(&port); + } + } + } + + /// Allocate an ephemeral UDP IPv4 port. + #[cfg(not(feature = "std"))] + pub(super) fn get_ephemeral_udp_ipv4(&self) -> Option { + let cursor = self.udp_ipv4_ephemeral.load(Ordering::Relaxed); + let mut node = MCSNode::new(); + let mut set = self.udp_ipv4.lock(&mut node); + for i in 0..(u16::MAX >> 2) { + let port = cursor.wrapping_add(i); + let port = if port == 0 { u16::MAX >> 2 } else { port }; + if set.insert(port) { + self.udp_ipv4_ephemeral.store(port, Ordering::Relaxed); + return Some(port); + } + } + None + } + + /// Claim a specific UDP IPv4 port. Returns `false` if the port is already in use. + #[cfg(not(feature = "std"))] + pub(super) fn try_claim_udp_ipv4(&self, port: u16) -> bool { + let mut node = MCSNode::new(); + let result = self.udp_ipv4.lock(&mut node).insert(port); + result + } + + /// Free a UDP IPv4 port. + #[cfg(not(feature = "std"))] + pub(super) fn free_udp_ipv4(&self, port: u16) { + let mut node = MCSNode::new(); + self.udp_ipv4.lock(&mut node).remove(&port); + } + + /// Allocate an ephemeral UDP IPv6 port. + #[cfg(not(feature = "std"))] + pub(super) fn get_ephemeral_udp_ipv6(&self) -> Option { + let cursor = self.udp_ipv6_ephemeral.load(Ordering::Relaxed); + let mut node = MCSNode::new(); + let mut set = self.udp_ipv6.lock(&mut node); + for i in 0..(u16::MAX >> 2) { + let port = cursor.wrapping_add(i); + let port = if port == 0 { u16::MAX >> 2 } else { port }; + if set.insert(port) { + self.udp_ipv6_ephemeral.store(port, Ordering::Relaxed); + return Some(port); + } + } + None + } + + /// Claim a specific UDP IPv6 port. Returns `false` if the port is already in use. + #[cfg(not(feature = "std"))] + pub(super) fn try_claim_udp_ipv6(&self, port: u16) -> bool { + let mut node = MCSNode::new(); + let result = self.udp_ipv6.lock(&mut node).insert(port); + result + } + + /// Free a UDP IPv6 port. + #[cfg(not(feature = "std"))] + pub(super) fn free_udp_ipv6(&self, port: u16) { + let mut node = MCSNode::new(); + self.udp_ipv6.lock(&mut node).remove(&port); + } +} diff --git a/awkernel_lib/src/net/tcp.rs b/awkernel_lib/src/net/tcp.rs index 0fc0f5cff..fe1079913 100644 --- a/awkernel_lib/src/net/tcp.rs +++ b/awkernel_lib/src/net/tcp.rs @@ -41,11 +41,10 @@ impl Drop for TcpPort { fn drop(&mut self) { #[cfg(not(feature = "std"))] { - let mut net_manager = super::NET_MANAGER.write(); if self.is_ipv4 { - net_manager.decrement_port_in_use_tcp_ipv4(self.port); + super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv4(self.port); } else { - net_manager.decrement_port_in_use_tcp_ipv6(self.port); + super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv6(self.port); } } } diff --git a/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs b/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs index 8d548e368..f840e7c5a 100644 --- a/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs +++ b/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs @@ -6,7 +6,8 @@ use crate::sync::mcs::MCSNode; use alloc::{vec, vec::Vec}; use crate::net::{ - ip_addr::IpAddr, tcp::TcpPort, tcp_stream::TcpStream, NetManagerError, NET_MANAGER, + ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, tcp_stream::TcpStream, + NetManagerError, NET_MANAGER, }; use super::SockTcpListener; @@ -30,14 +31,15 @@ impl SockTcpListener for TcpListener { tx_buffer_size: usize, backlogs: usize, ) -> Result { - let mut net_manager = NET_MANAGER.write(); - // Find the interface that has the specified address. - let if_net = net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)? - .clone(); + let if_net = { + let net_manager = NET_MANAGER.read(); + net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)? + .clone() + }; let port = if let Some(port) = port { if port == 0 { @@ -45,34 +47,26 @@ impl SockTcpListener for TcpListener { } if addr.is_ipv4() { - // Check if the specified port is available. - if net_manager.is_port_in_use_tcp_ipv4(port) { - return Err(NetManagerError::PortInUse); - } - - net_manager.port_in_use_tcp_ipv4(port) + PORT_ALLOC + .try_claim_tcp_ipv4(port) + .ok_or(NetManagerError::PortInUse)? } else { - // Check if the specified port is available. - if net_manager.is_port_in_use_tcp_ipv6(port) { - return Err(NetManagerError::PortInUse); - } - - net_manager.port_in_use_tcp_ipv6(port) + PORT_ALLOC + .try_claim_tcp_ipv6(port) + .ok_or(NetManagerError::PortInUse)? } } else if addr.is_ipv4() { // Find an ephemeral port. - net_manager - .get_ephemeral_port_tcp_ipv4() + PORT_ALLOC + .get_ephemeral_tcp_ipv4() .ok_or(NetManagerError::NoAvailablePort)? } else { // Find an ephemeral port. - net_manager - .get_ephemeral_port_tcp_ipv6() + PORT_ALLOC + .get_ephemeral_tcp_ipv6() .ok_or(NetManagerError::NoAvailablePort)? }; - drop(net_manager); - let mut handles = Vec::new(); for _ in 0..backlogs { @@ -98,13 +92,10 @@ impl SockTcpListener for TcpListener { fn accept(&mut self, waker: &core::task::Waker) -> Result, NetManagerError> { // If there is a connected socket, return it. if let Some(handle) = self.connected_sockets.pop_front() { - let port = { - let mut net_manager = NET_MANAGER.write(); - if self.addr.is_ipv4() { - net_manager.port_in_use_tcp_ipv4(self.port.port()) - } else { - net_manager.port_in_use_tcp_ipv6(self.port.port()) - } + let port = if self.addr.is_ipv4() { + PORT_ALLOC.increment_ref_tcp_ipv4(self.port.port()) + } else { + PORT_ALLOC.increment_ref_tcp_ipv6(self.port.port()) }; return Ok(Some(TcpStream { handle, @@ -171,13 +162,10 @@ impl SockTcpListener for TcpListener { // If there is a connected socket, return it. if let Some(handle) = self.connected_sockets.pop_front() { - let port = { - let mut net_manager = NET_MANAGER.write(); - if self.addr.is_ipv4() { - net_manager.port_in_use_tcp_ipv4(self.port.port()) - } else { - net_manager.port_in_use_tcp_ipv6(self.port.port()) - } + let port = if self.addr.is_ipv4() { + PORT_ALLOC.increment_ref_tcp_ipv4(self.port.port()) + } else { + PORT_ALLOC.increment_ref_tcp_ipv6(self.port.port()) }; if_net.poll_tx_only(crate::cpu::raw_cpu_id() & (if_net.net_device.num_queues() - 1)); diff --git a/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs b/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs index f0e1dd6d3..09d04afc4 100644 --- a/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs +++ b/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs @@ -1,4 +1,4 @@ -use crate::net::{ip_addr::IpAddr, tcp::TcpPort, NetManagerError, NET_MANAGER}; +use crate::net::{ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, NetManagerError, NET_MANAGER}; use super::{SockTcpStream, TcpResult}; @@ -90,26 +90,25 @@ impl SockTcpStream for TcpStream { tx_buffer_size: usize, waker: &core::task::Waker, ) -> Result { - let mut net_manager = NET_MANAGER.write(); - - let if_net = net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)?; - let if_net = if_net.clone(); + let if_net = { + let net_manager = NET_MANAGER.read(); + net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)? + .clone() + }; let local_port = if remote_addr.is_ipv4() { - net_manager - .get_ephemeral_port_tcp_ipv4() + PORT_ALLOC + .get_ephemeral_tcp_ipv4() .ok_or(NetManagerError::NoAvailablePort)? } else { - net_manager - .get_ephemeral_port_tcp_ipv6() + PORT_ALLOC + .get_ephemeral_tcp_ipv6() .ok_or(NetManagerError::NoAvailablePort)? }; - drop(net_manager); - let rx_buffer = smoltcp::socket::tcp::SocketBuffer::new(vec![0; rx_buffer_size]); let tx_buffer = smoltcp::socket::tcp::SocketBuffer::new(vec![0; tx_buffer_size]); diff --git a/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs b/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs index bf332ac7d..00cc8ccc0 100644 --- a/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs +++ b/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs @@ -1,6 +1,6 @@ use core::net::Ipv4Addr; -use crate::net::{ip_addr::IpAddr, NET_MANAGER}; +use crate::net::{ip_addr::IpAddr, port_alloc::PORT_ALLOC, NET_MANAGER}; use awkernel_sync::{mcs::MCSNode, mutex::Mutex}; use super::{NetManagerError, SockUdp}; @@ -32,7 +32,15 @@ impl super::SockUdp for UdpSocket { rx_buffer_size: usize, tx_buffer_size: usize, ) -> Result { - let mut net_manager = NET_MANAGER.write(); + // Find the interface that has the specified address. + let if_net = { + let net_manager = NET_MANAGER.read(); + net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)? + .clone() + }; let is_ipv4; let port = if let Some(port) = port { @@ -40,48 +48,35 @@ impl super::SockUdp for UdpSocket { return Err(NetManagerError::InvalidPort); } - // Check if the specified port is available. + // Check if the specified port is available and claim it atomically. if addr.is_ipv4() { - if net_manager.is_port_in_use_udp_ipv4(port) { + if !PORT_ALLOC.try_claim_udp_ipv4(port) { return Err(NetManagerError::PortInUse); } - is_ipv4 = true; - net_manager.set_port_in_use_udp_ipv4(port); port } else { - if net_manager.is_port_in_use_udp_ipv6(port) { + if !PORT_ALLOC.try_claim_udp_ipv6(port) { return Err(NetManagerError::PortInUse); } - is_ipv4 = false; - net_manager.set_port_in_use_udp_ipv6(port); port } } else { // Find an ephemeral port. if addr.is_ipv4() { is_ipv4 = true; - net_manager - .get_ephemeral_port_udp_ipv4() + PORT_ALLOC + .get_ephemeral_udp_ipv4() .ok_or(NetManagerError::PortInUse)? } else { is_ipv4 = false; - net_manager - .get_ephemeral_port_udp_ipv6() + PORT_ALLOC + .get_ephemeral_udp_ipv6() .ok_or(NetManagerError::PortInUse)? } }; - // Find the interface that has the specified address. - let if_net = net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)? - .clone(); - - drop(net_manager); - // Create a UDP socket. use smoltcp::socket::udp; let udp_rx_buffer = udp::PacketBuffer::new( @@ -331,14 +326,13 @@ impl Drop for UdpSocket { } } - let mut net_manager = NET_MANAGER.write(); - if self.is_ipv4 { - net_manager.free_port_udp_ipv4(self.port); + PORT_ALLOC.free_udp_ipv4(self.port); } else { - net_manager.free_port_udp_ipv6(self.port); + PORT_ALLOC.free_udp_ipv6(self.port); } + let net_manager = NET_MANAGER.read(); if let Some(if_net) = net_manager.interfaces.get(&self.interface_id) { if_net.socket_set.write().remove(self.handle); } diff --git a/improve_nw.md b/improve_nw.md index b62a8431a..983f2b507 100644 --- a/improve_nw.md +++ b/improve_nw.md @@ -810,3 +810,98 @@ wait | SPIN モデル検証 (`make run` in specification/) | ポーリングアルゴリズムの正しさ | Phase 4.2 実装前後 | | `make qemu-x86_64-net` + `scripts/udp.py` | 基本 UDP/TCP 疎通 | 各フェーズの動作確認 | | VM テスト (本セクション) | 多数コネクション負荷・スループット計測 | フェーズ前後のベースライン比較 | + +--- + +## 9. 進捗状況 + +### 凡例 + +| 記号 | 意味 | +|---|---| +| ✅ | 実装・検証済み | +| 🔲 | 未着手 | + +### フェーズ別完了状態 + +| フェーズ | 状態 | 完了日 | +|---|---|---| +| **Pre-Phase** バグ修正 | ✅ | 2026-04-21 | +| **Phase 1.1** PortAllocator 分離 | ✅ | 2026-04-21 | +| **Phase 1.2** NET_MANAGER 読み取り専用化確認 | 🔲 | — | +| **Phase 2.1** IfNetInner 分割(IfNetCore + IfNetMulticast) | 🔲 | — | +| **Phase 3.1** connect() 二重ロック解消 | 🔲 | — | +| **Phase 3.2** Drop キュー導入 | 🔲 | — | +| **Phase 4.1** per-iface PortAllocator | 🔲 | — | +| **Phase 4.2** 2 フェーズポーリング | 🔲 | — | + +--- + +### Pre-Phase — 実施内容(✅ 完了) + +**計画:** +- `get_ephemeral_port_tcp_ipv4/v6` の `entry(i)` → `entry(port)` バグ修正 + +**実際に行ったこと:** +- `get_ephemeral_port_tcp_ipv4` / `get_ephemeral_port_tcp_ipv6` の `entry(i)` → `entry(port)` 修正 +- 追加発見バグ: `get_ephemeral_port_udp_ipv6` で `self.udp_port_ipv4_ephemeral = port` と + なっていた(IPv6 カーソルではなく IPv4 カーソルを更新していた)。これも同時に修正 + +**計画との差異:** なし(1 箇所追加修正あり) + +--- + +### Phase 1.1 — 実施内容(✅ 完了) + +**計画:** +- `awkernel_lib/src/net/port_alloc.rs` を新規作成 +- `PortAllocator` 構造体(4 プロトコル × 独立 Mutex + AtomicU16 カーソル) +- 全呼び出し元を `PORT_ALLOC` 経由に変更 + +**実際に行ったこと:** + +新規作成: +- `awkernel_lib/src/net/port_alloc.rs` + - `PortAllocator` 構造体: TCP IPv4/IPv6 は `Mutex>`、 + UDP IPv4/IPv6 は `Mutex>`、各エフェメラルカーソルは `AtomicU16` + - 公開 API: `get_ephemeral_tcp_ipv4/v6`、`try_claim_tcp_ipv4/v6`、 + `increment_ref_tcp_ipv4/v6`、`decrement_ref_tcp_ipv4/v6`、 + `get_ephemeral_udp_ipv4/v6`、`try_claim_udp_ipv4/v6`、`free_udp_ipv4/v6` + - `static PORT_ALLOC: PortAllocator = PortAllocator::new()` を定義 + +変更ファイル: +- `awkernel_lib/src/net.rs`: `NetManager` からポートフィールド 8 個・ポートメソッド 16 個を削除。 + `mod port_alloc;` 追加。`#[cfg(not(feature = "std"))]` の `TcpPort` / `BTreeSet` インポート削除 +- `awkernel_lib/src/net/tcp.rs`: `TcpPort::drop` の `NET_MANAGER.write()` → `PORT_ALLOC` +- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs`: + `connect()` の `NET_MANAGER.write()` → `PORT_ALLOC` + `NET_MANAGER.read()` +- `awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs`: + `bind_on_interface()` + `accept()` 2 箇所の `NET_MANAGER.write()` → `PORT_ALLOC` +- `awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs`: + `bind_on_interface()` + `Drop` の `NET_MANAGER.write()` → `PORT_ALLOC` + +ビルド・テスト確認: +- `make x86_64 RELEASE=1` 成功 +- `make aarch64 BSP=aarch64_virt RELEASE=1` 成功(コンパイル部分) +- `make test` 全テスト通過(371 テスト、0 失敗) + +**計画との差異:** + +| 項目 | 計画 | 実際 | 理由 | +|---|---|---|---| +| UDP ポートチェック API | `is_in_use_udp_*` + `set_in_use_udp_*` を別メソッドで提供 | `try_claim_udp_*` に統合(1 メソッド) | check-and-insert を Mutex 内でアトミックに行い TOCTOU を排除するため | +| `udp_socket::bind_on_interface` のロック順序 | 計画に記載なし | インターフェース参照取得(`NET_MANAGER.read()`)を port 操作の **前** に変更 | 元コードではポート確保後にインターフェース検索失敗した場合にポートがリークする設計だったため修正 | + +--- + +### 次フェーズ + +**Phase 1.2**(NET_MANAGER 読み取り専用化確認)が次の着手対象。 +`NET_MANAGER.write()` の残存箇所を `grep` で確認し、 +`add_interface`(起動時初期化パス)以外に書き込みロックがないことを検証する。 + +```bash +grep -n 'NET_MANAGER\.write' awkernel_lib/src/net/*.rs awkernel_lib/src/net/**/*.rs +``` + +確認後、Phase 2.1(`IfNetInner` 分割)に進む。 From c7189e55fbd390e329a9de2d516e25c37360d739 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 18:45:54 +0900 Subject: [PATCH 4/9] fix(net): embed ephemeral cursor inside Mutex and use advancing cursor pattern (Phase 1.1 follow-up) - Replace AtomicU16 ephemeral cursors with u16 fields inside TcpPortsInner/UdpPortsInner structs, eliminating the load-outside/store-inside asymmetry that could produce stale cursor reads on aarch64's weak memory model - Change search loop from cursor.wrapping_add(i) (i=0) to advancing cursor (cursor+=1 before each check), avoiding the wasted first iteration that always retried the last-allocated port Co-Authored-By: Claude Sonnet 4.6 --- awkernel_lib/src/net/port_alloc.rs | 156 ++++++++++++++--------------- improve_nw.md | 10 +- 2 files changed, 85 insertions(+), 81 deletions(-) diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs index 8e1612b4b..22650fc6a 100644 --- a/awkernel_lib/src/net/port_alloc.rs +++ b/awkernel_lib/src/net/port_alloc.rs @@ -3,8 +3,6 @@ use alloc::collections::{ btree_map::Entry, BTreeMap, BTreeSet, }; -#[cfg(not(feature = "std"))] -use core::sync::atomic::{AtomicU16, Ordering}; #[cfg(not(feature = "std"))] use crate::sync::{mcs::MCSNode, mutex::Mutex}; @@ -12,24 +10,28 @@ use crate::sync::{mcs::MCSNode, mutex::Mutex}; #[cfg(not(feature = "std"))] use super::tcp::TcpPort; +#[cfg(not(feature = "std"))] +struct TcpPortsInner { + map: BTreeMap, + cursor: u16, +} + +#[cfg(not(feature = "std"))] +struct UdpPortsInner { + set: BTreeSet, + cursor: u16, +} + #[allow(dead_code)] pub(super) struct PortAllocator { #[cfg(not(feature = "std"))] - tcp_ipv4: Mutex>, - #[cfg(not(feature = "std"))] - tcp_ipv4_ephemeral: AtomicU16, + tcp_ipv4: Mutex, #[cfg(not(feature = "std"))] - tcp_ipv6: Mutex>, + tcp_ipv6: Mutex, #[cfg(not(feature = "std"))] - tcp_ipv6_ephemeral: AtomicU16, + udp_ipv4: Mutex, #[cfg(not(feature = "std"))] - udp_ipv4: Mutex>, - #[cfg(not(feature = "std"))] - udp_ipv4_ephemeral: AtomicU16, - #[cfg(not(feature = "std"))] - udp_ipv6: Mutex>, - #[cfg(not(feature = "std"))] - udp_ipv6_ephemeral: AtomicU16, + udp_ipv6: Mutex, } #[allow(dead_code)] @@ -39,36 +41,38 @@ impl PortAllocator { pub(super) const fn new() -> Self { Self { #[cfg(not(feature = "std"))] - tcp_ipv4: Mutex::new(BTreeMap::new()), - #[cfg(not(feature = "std"))] - tcp_ipv4_ephemeral: AtomicU16::new(u16::MAX >> 2), - #[cfg(not(feature = "std"))] - tcp_ipv6: Mutex::new(BTreeMap::new()), - #[cfg(not(feature = "std"))] - tcp_ipv6_ephemeral: AtomicU16::new(u16::MAX >> 2), - #[cfg(not(feature = "std"))] - udp_ipv4: Mutex::new(BTreeSet::new()), + tcp_ipv4: Mutex::new(TcpPortsInner { + map: BTreeMap::new(), + cursor: u16::MAX >> 2, + }), #[cfg(not(feature = "std"))] - udp_ipv4_ephemeral: AtomicU16::new(u16::MAX >> 2), + tcp_ipv6: Mutex::new(TcpPortsInner { + map: BTreeMap::new(), + cursor: u16::MAX >> 2, + }), #[cfg(not(feature = "std"))] - udp_ipv6: Mutex::new(BTreeSet::new()), + udp_ipv4: Mutex::new(UdpPortsInner { + set: BTreeSet::new(), + cursor: u16::MAX >> 2, + }), #[cfg(not(feature = "std"))] - udp_ipv6_ephemeral: AtomicU16::new(u16::MAX >> 2), + udp_ipv6: Mutex::new(UdpPortsInner { + set: BTreeSet::new(), + cursor: u16::MAX >> 2, + }), } } /// Allocate an ephemeral TCP IPv4 port. #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_tcp_ipv4(&self) -> Option { - let cursor = self.tcp_ipv4_ephemeral.load(Ordering::Relaxed); let mut node = MCSNode::new(); - let mut map = self.tcp_ipv4.lock(&mut node); - for i in 0..(u16::MAX >> 2) { - let port = cursor.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - if let Entry::Vacant(e) = map.entry(port) { + let mut ports = self.tcp_ipv4.lock(&mut node); + for _ in 0..(u16::MAX >> 2) { + ports.cursor = ports.cursor.wrapping_add(1); + let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + if let Entry::Vacant(e) = ports.map.entry(port) { e.insert(1); - self.tcp_ipv4_ephemeral.store(port, Ordering::Relaxed); return Some(TcpPort::new(port, true)); } } @@ -79,11 +83,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn try_claim_tcp_ipv4(&self, port: u16) -> Option { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv4.lock(&mut node); - if map.contains_key(&port) { + let mut ports = self.tcp_ipv4.lock(&mut node); + if ports.map.contains_key(&port) { None } else { - map.insert(port, 1); + ports.map.insert(port, 1); Some(TcpPort::new(port, true)) } } @@ -92,11 +96,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn increment_ref_tcp_ipv4(&self, port: u16) -> TcpPort { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv4.lock(&mut node); - if let Some(e) = map.get_mut(&port) { + let mut ports = self.tcp_ipv4.lock(&mut node); + if let Some(e) = ports.map.get_mut(&port) { *e += 1; } else { - map.insert(port, 1); + ports.map.insert(port, 1); } TcpPort::new(port, true) } @@ -105,11 +109,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn decrement_ref_tcp_ipv4(&self, port: u16) { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv4.lock(&mut node); - if let Some(e) = map.get_mut(&port) { + let mut ports = self.tcp_ipv4.lock(&mut node); + if let Some(e) = ports.map.get_mut(&port) { *e -= 1; if *e == 0 { - map.remove(&port); + ports.map.remove(&port); } } } @@ -117,15 +121,13 @@ impl PortAllocator { /// Allocate an ephemeral TCP IPv6 port. #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_tcp_ipv6(&self) -> Option { - let cursor = self.tcp_ipv6_ephemeral.load(Ordering::Relaxed); let mut node = MCSNode::new(); - let mut map = self.tcp_ipv6.lock(&mut node); - for i in 0..(u16::MAX >> 2) { - let port = cursor.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - if let Entry::Vacant(e) = map.entry(port) { + let mut ports = self.tcp_ipv6.lock(&mut node); + for _ in 0..(u16::MAX >> 2) { + ports.cursor = ports.cursor.wrapping_add(1); + let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + if let Entry::Vacant(e) = ports.map.entry(port) { e.insert(1); - self.tcp_ipv6_ephemeral.store(port, Ordering::Relaxed); return Some(TcpPort::new(port, false)); } } @@ -136,11 +138,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn try_claim_tcp_ipv6(&self, port: u16) -> Option { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv6.lock(&mut node); - if map.contains_key(&port) { + let mut ports = self.tcp_ipv6.lock(&mut node); + if ports.map.contains_key(&port) { None } else { - map.insert(port, 1); + ports.map.insert(port, 1); Some(TcpPort::new(port, false)) } } @@ -149,11 +151,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn increment_ref_tcp_ipv6(&self, port: u16) -> TcpPort { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv6.lock(&mut node); - if let Some(e) = map.get_mut(&port) { + let mut ports = self.tcp_ipv6.lock(&mut node); + if let Some(e) = ports.map.get_mut(&port) { *e += 1; } else { - map.insert(port, 1); + ports.map.insert(port, 1); } TcpPort::new(port, false) } @@ -162,11 +164,11 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn decrement_ref_tcp_ipv6(&self, port: u16) { let mut node = MCSNode::new(); - let mut map = self.tcp_ipv6.lock(&mut node); - if let Some(e) = map.get_mut(&port) { + let mut ports = self.tcp_ipv6.lock(&mut node); + if let Some(e) = ports.map.get_mut(&port) { *e -= 1; if *e == 0 { - map.remove(&port); + ports.map.remove(&port); } } } @@ -174,14 +176,12 @@ impl PortAllocator { /// Allocate an ephemeral UDP IPv4 port. #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_udp_ipv4(&self) -> Option { - let cursor = self.udp_ipv4_ephemeral.load(Ordering::Relaxed); let mut node = MCSNode::new(); - let mut set = self.udp_ipv4.lock(&mut node); - for i in 0..(u16::MAX >> 2) { - let port = cursor.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - if set.insert(port) { - self.udp_ipv4_ephemeral.store(port, Ordering::Relaxed); + let mut ports = self.udp_ipv4.lock(&mut node); + for _ in 0..(u16::MAX >> 2) { + ports.cursor = ports.cursor.wrapping_add(1); + let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + if ports.set.insert(port) { return Some(port); } } @@ -192,28 +192,27 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn try_claim_udp_ipv4(&self, port: u16) -> bool { let mut node = MCSNode::new(); - let result = self.udp_ipv4.lock(&mut node).insert(port); - result + let mut ports = self.udp_ipv4.lock(&mut node); + ports.set.insert(port) } /// Free a UDP IPv4 port. #[cfg(not(feature = "std"))] pub(super) fn free_udp_ipv4(&self, port: u16) { let mut node = MCSNode::new(); - self.udp_ipv4.lock(&mut node).remove(&port); + let mut ports = self.udp_ipv4.lock(&mut node); + ports.set.remove(&port); } /// Allocate an ephemeral UDP IPv6 port. #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_udp_ipv6(&self) -> Option { - let cursor = self.udp_ipv6_ephemeral.load(Ordering::Relaxed); let mut node = MCSNode::new(); - let mut set = self.udp_ipv6.lock(&mut node); - for i in 0..(u16::MAX >> 2) { - let port = cursor.wrapping_add(i); - let port = if port == 0 { u16::MAX >> 2 } else { port }; - if set.insert(port) { - self.udp_ipv6_ephemeral.store(port, Ordering::Relaxed); + let mut ports = self.udp_ipv6.lock(&mut node); + for _ in 0..(u16::MAX >> 2) { + ports.cursor = ports.cursor.wrapping_add(1); + let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + if ports.set.insert(port) { return Some(port); } } @@ -224,14 +223,15 @@ impl PortAllocator { #[cfg(not(feature = "std"))] pub(super) fn try_claim_udp_ipv6(&self, port: u16) -> bool { let mut node = MCSNode::new(); - let result = self.udp_ipv6.lock(&mut node).insert(port); - result + let mut ports = self.udp_ipv6.lock(&mut node); + ports.set.insert(port) } /// Free a UDP IPv6 port. #[cfg(not(feature = "std"))] pub(super) fn free_udp_ipv6(&self, port: u16) { let mut node = MCSNode::new(); - self.udp_ipv6.lock(&mut node).remove(&port); + let mut ports = self.udp_ipv6.lock(&mut node); + ports.set.remove(&port); } } diff --git a/improve_nw.md b/improve_nw.md index 983f2b507..2e06ec094 100644 --- a/improve_nw.md +++ b/improve_nw.md @@ -862,8 +862,10 @@ wait 新規作成: - `awkernel_lib/src/net/port_alloc.rs` - - `PortAllocator` 構造体: TCP IPv4/IPv6 は `Mutex>`、 - UDP IPv4/IPv6 は `Mutex>`、各エフェメラルカーソルは `AtomicU16` + - `TcpPortsInner` 構造体(`map: BTreeMap` + `cursor: u16`) + - `UdpPortsInner` 構造体(`set: BTreeSet` + `cursor: u16`) + - `PortAllocator` 構造体: TCP IPv4/IPv6 は `Mutex`、 + UDP IPv4/IPv6 は `Mutex`。カーソルは各 Inner 内の `u16`(AtomicU16 ではない) - 公開 API: `get_ephemeral_tcp_ipv4/v6`、`try_claim_tcp_ipv4/v6`、 `increment_ref_tcp_ipv4/v6`、`decrement_ref_tcp_ipv4/v6`、 `get_ephemeral_udp_ipv4/v6`、`try_claim_udp_ipv4/v6`、`free_udp_ipv4/v6` @@ -883,7 +885,7 @@ wait ビルド・テスト確認: - `make x86_64 RELEASE=1` 成功 - `make aarch64 BSP=aarch64_virt RELEASE=1` 成功(コンパイル部分) -- `make test` 全テスト通過(371 テスト、0 失敗) +- `make test` 全テスト通過(367 テスト、0 失敗) **計画との差異:** @@ -891,6 +893,8 @@ wait |---|---|---|---| | UDP ポートチェック API | `is_in_use_udp_*` + `set_in_use_udp_*` を別メソッドで提供 | `try_claim_udp_*` に統合(1 メソッド) | check-and-insert を Mutex 内でアトミックに行い TOCTOU を排除するため | | `udp_socket::bind_on_interface` のロック順序 | 計画に記載なし | インターフェース参照取得(`NET_MANAGER.read()`)を port 操作の **前** に変更 | 元コードではポート確保後にインターフェース検索失敗した場合にポートがリークする設計だったため修正 | +| エフェメラルカーソルの型 | `AtomicU16`(計画: `fetch_add` でロックフリー更新) | `u16`(Mutex 内の `Inner` 構造体に埋め込み) | `AtomicU16` をロック外 load・ロック内 store するパターンは aarch64 弱順序モデルでカーソルが陳腐化する可能性があり、かつ i=0 始まりの `wrapping_add(i)` により毎回 1 イテレーション無駄になるバグがあったため修正 | +| 探索アルゴリズム | `cursor.wrapping_add(i)`(i=0 始まり) | advancing cursor(ループ先頭で `cursor += 1`) | i=0 始まりは直前に割り当てたポートを毎回最初に試し、必ず 1 イテレーション空振りするため | --- From b9e7a904e6bb4fda8990573b036523d96eeb0ed9 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 19:03:41 +0900 Subject: [PATCH 5/9] refactor(net): replace #[allow(dead_code)] with #[cfg(not(feature = "std"))] (Phase 1.1 follow-up) - port_alloc.rs: add #![cfg(not(feature = "std"))] inner attribute to gate the entire module; remove all per-item cfg annotations and both #[allow(dead_code)] suppressions - tcp.rs: gate TcpPort struct and its impl/Drop blocks with #[cfg(not(feature = "std"))]; remove redundant inner cfg block in Drop Co-Authored-By: Claude Sonnet 4.6 --- awkernel_lib/src/net/port_alloc.rs | 31 ++---------------------------- awkernel_lib/src/net/tcp.rs | 15 +++++++-------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs index 22650fc6a..dd2354403 100644 --- a/awkernel_lib/src/net/port_alloc.rs +++ b/awkernel_lib/src/net/port_alloc.rs @@ -1,61 +1,48 @@ -#[cfg(not(feature = "std"))] +#![cfg(not(feature = "std"))] + use alloc::collections::{ btree_map::Entry, BTreeMap, BTreeSet, }; -#[cfg(not(feature = "std"))] use crate::sync::{mcs::MCSNode, mutex::Mutex}; -#[cfg(not(feature = "std"))] use super::tcp::TcpPort; -#[cfg(not(feature = "std"))] struct TcpPortsInner { map: BTreeMap, cursor: u16, } -#[cfg(not(feature = "std"))] struct UdpPortsInner { set: BTreeSet, cursor: u16, } -#[allow(dead_code)] pub(super) struct PortAllocator { - #[cfg(not(feature = "std"))] tcp_ipv4: Mutex, - #[cfg(not(feature = "std"))] tcp_ipv6: Mutex, - #[cfg(not(feature = "std"))] udp_ipv4: Mutex, - #[cfg(not(feature = "std"))] udp_ipv6: Mutex, } -#[allow(dead_code)] pub(super) static PORT_ALLOC: PortAllocator = PortAllocator::new(); impl PortAllocator { pub(super) const fn new() -> Self { Self { - #[cfg(not(feature = "std"))] tcp_ipv4: Mutex::new(TcpPortsInner { map: BTreeMap::new(), cursor: u16::MAX >> 2, }), - #[cfg(not(feature = "std"))] tcp_ipv6: Mutex::new(TcpPortsInner { map: BTreeMap::new(), cursor: u16::MAX >> 2, }), - #[cfg(not(feature = "std"))] udp_ipv4: Mutex::new(UdpPortsInner { set: BTreeSet::new(), cursor: u16::MAX >> 2, }), - #[cfg(not(feature = "std"))] udp_ipv6: Mutex::new(UdpPortsInner { set: BTreeSet::new(), cursor: u16::MAX >> 2, @@ -64,7 +51,6 @@ impl PortAllocator { } /// Allocate an ephemeral TCP IPv4 port. - #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_tcp_ipv4(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv4.lock(&mut node); @@ -80,7 +66,6 @@ impl PortAllocator { } /// Claim a specific TCP IPv4 port. Returns `None` if the port is already in use. - #[cfg(not(feature = "std"))] pub(super) fn try_claim_tcp_ipv4(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv4.lock(&mut node); @@ -93,7 +78,6 @@ impl PortAllocator { } /// Increment the reference count for a TCP IPv4 port (used by `TcpListener::accept`). - #[cfg(not(feature = "std"))] pub(super) fn increment_ref_tcp_ipv4(&self, port: u16) -> TcpPort { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv4.lock(&mut node); @@ -106,7 +90,6 @@ impl PortAllocator { } /// Decrement the reference count for a TCP IPv4 port, freeing it when it reaches zero. - #[cfg(not(feature = "std"))] pub(super) fn decrement_ref_tcp_ipv4(&self, port: u16) { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv4.lock(&mut node); @@ -119,7 +102,6 @@ impl PortAllocator { } /// Allocate an ephemeral TCP IPv6 port. - #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_tcp_ipv6(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv6.lock(&mut node); @@ -135,7 +117,6 @@ impl PortAllocator { } /// Claim a specific TCP IPv6 port. Returns `None` if the port is already in use. - #[cfg(not(feature = "std"))] pub(super) fn try_claim_tcp_ipv6(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv6.lock(&mut node); @@ -148,7 +129,6 @@ impl PortAllocator { } /// Increment the reference count for a TCP IPv6 port. - #[cfg(not(feature = "std"))] pub(super) fn increment_ref_tcp_ipv6(&self, port: u16) -> TcpPort { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv6.lock(&mut node); @@ -161,7 +141,6 @@ impl PortAllocator { } /// Decrement the reference count for a TCP IPv6 port, freeing it when it reaches zero. - #[cfg(not(feature = "std"))] pub(super) fn decrement_ref_tcp_ipv6(&self, port: u16) { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv6.lock(&mut node); @@ -174,7 +153,6 @@ impl PortAllocator { } /// Allocate an ephemeral UDP IPv4 port. - #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_udp_ipv4(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv4.lock(&mut node); @@ -189,7 +167,6 @@ impl PortAllocator { } /// Claim a specific UDP IPv4 port. Returns `false` if the port is already in use. - #[cfg(not(feature = "std"))] pub(super) fn try_claim_udp_ipv4(&self, port: u16) -> bool { let mut node = MCSNode::new(); let mut ports = self.udp_ipv4.lock(&mut node); @@ -197,7 +174,6 @@ impl PortAllocator { } /// Free a UDP IPv4 port. - #[cfg(not(feature = "std"))] pub(super) fn free_udp_ipv4(&self, port: u16) { let mut node = MCSNode::new(); let mut ports = self.udp_ipv4.lock(&mut node); @@ -205,7 +181,6 @@ impl PortAllocator { } /// Allocate an ephemeral UDP IPv6 port. - #[cfg(not(feature = "std"))] pub(super) fn get_ephemeral_udp_ipv6(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv6.lock(&mut node); @@ -220,7 +195,6 @@ impl PortAllocator { } /// Claim a specific UDP IPv6 port. Returns `false` if the port is already in use. - #[cfg(not(feature = "std"))] pub(super) fn try_claim_udp_ipv6(&self, port: u16) -> bool { let mut node = MCSNode::new(); let mut ports = self.udp_ipv6.lock(&mut node); @@ -228,7 +202,6 @@ impl PortAllocator { } /// Free a UDP IPv6 port. - #[cfg(not(feature = "std"))] pub(super) fn free_udp_ipv6(&self, port: u16) { let mut node = MCSNode::new(); let mut ports = self.udp_ipv6.lock(&mut node); diff --git a/awkernel_lib/src/net/tcp.rs b/awkernel_lib/src/net/tcp.rs index fe1079913..2aed89cd4 100644 --- a/awkernel_lib/src/net/tcp.rs +++ b/awkernel_lib/src/net/tcp.rs @@ -19,13 +19,14 @@ impl TCPHdr { } } -#[allow(dead_code)] +#[cfg(not(feature = "std"))] #[derive(Debug)] pub struct TcpPort { port: u16, is_ipv4: bool, } +#[cfg(not(feature = "std"))] impl TcpPort { pub fn new(port: u16, is_ipv4: bool) -> Self { Self { port, is_ipv4 } @@ -37,15 +38,13 @@ impl TcpPort { } } +#[cfg(not(feature = "std"))] impl Drop for TcpPort { fn drop(&mut self) { - #[cfg(not(feature = "std"))] - { - if self.is_ipv4 { - super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv4(self.port); - } else { - super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv6(self.port); - } + if self.is_ipv4 { + super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv4(self.port); + } else { + super::port_alloc::PORT_ALLOC.decrement_ref_tcp_ipv6(self.port); } } } From b3527b622e530d14d6474c83faf50ca4d201a00c Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 19:11:50 +0900 Subject: [PATCH 6/9] remove improve_nw.md Signed-off-by: Yuuki Takano --- improve_nw.md | 911 -------------------------------------------------- 1 file changed, 911 deletions(-) delete mode 100644 improve_nw.md diff --git a/improve_nw.md b/improve_nw.md deleted file mode 100644 index 2e06ec094..000000000 --- a/improve_nw.md +++ /dev/null @@ -1,911 +0,0 @@ -# ネットワークスタック改善ロードマップ - -## 1. 概要 - -awkernel の TCP/UDP ネットワークスタックは、多数のコネクションを同時処理する際に -グローバルな `RwLock` 書き込みロックがボトルネックとなる。 - -ソケット生成・ポート割り当て・ソケット破棄のたびに `NET_MANAGER` グローバルロックを -取得するため、並列度が上がるほどスループットが頭打ちになる。本文書では問題箇所を -優先度順に整理し、段階的な改善フェーズを定義する。 - ---- - -## 2. 現状のロック構造 - -### 2.1 グローバル静的変数 - -``` -net.rs -├── NET_MANAGER: RwLock -│ ├── interfaces: BTreeMap> -│ ├── interface_id: u64 -│ ├── tcp_ports_ipv4: BTreeMap (port → 参照カウント) -│ ├── tcp_port_ipv4_ephemeral: u16 -│ ├── udp_ports_ipv4: BTreeSet -│ ├── udp_port_ipv4_ephemeral: u16 -│ └── ... (IPv6 も同様) -├── IRQ_WAKERS: Mutex> -└── POLL_WAKERS: Mutex> - -tcp_stream_no_std.rs -└── CLOSED_CONNECTIONS: Mutex>> - -udp_socket_no_std.rs -└── NUM_MULTICAST_JOIN_IPV4: Mutex> -``` - -### 2.2 インターフェースごとの変数 (if_net.rs) - -``` -IfNet -├── inner: Mutex ← LOCK #3 -│ ├── interface: Interface (smoltcp 本体) -│ ├── default_gateway_ipv4 -│ ├── multicast_addr_ipv4: BTreeSet -│ └── multicast_addr_mac: BTreeMap -├── socket_set: RwLock -├── tx_only_ringq: Vec> ← LOCK #2 -└── rx_irq_to_driver: BTreeMap - └── NetDriver::rx_ringq: Mutex ← LOCK #1 -``` - -### 2.3 ドキュメント化されたロック順序 - -``` -1. NetDriver::rx_ringq -2. IfNet::tx_ringq (tx_only_ringq[i]) -3. IfNet::inner -``` - -### 2.4 主要パスのロック取得チェーン - -**TCP 接続確立 (connect)** -``` -NET_MANAGER.write() ← グローバル書き込みロック (ポート確保) - IfNet::inner.lock() ← interface.poll 用 - IfNet::socket_set.write() ← ソケット追加 -``` - -**TCP/UDP データ送受信** -``` -NET_MANAGER.read() - socket_set.read() - socket.lock() (MCS ロック) -``` - -**ポーリング (poll_rx_tx)** -``` -rx_ringq.lock() ← パケット受信バッファ - tx_ringq.lock() ← パケット送信バッファ - inner.lock() ← interface.poll() 実行 -``` - ---- - -## 3. 特定されたボトルネック - -### 優先度一覧 - -| 優先度 | 場所 | ロック種別 | 問題 | -|---|---|---|---| -| CRITICAL | `net.rs:NET_MANAGER` | `RwLock::write()` | 全 TCP/UDP 接続確立がグローバル書き込みロックを取得。エフェメラルポート探索は最悪 O(16384) 反復しながらロック保持 | -| CRITICAL | `net.rs:get_ephemeral_port_tcp_ipv4` | 同上 | `entry(i)` (ループインデックス) で検索するバグ。正しくは `entry(port)` | -| HIGH | `if_net.rs:IfNet::inner` | `Mutex::lock()` | `interface.poll()` 呼び出し全体でロック保持。複数コアが同一インターフェースをポーリングするとシリアライズ | -| HIGH | `if_net.rs:IfNet::socket_set` | `RwLock::write()` | ソケット追加・削除で書き込みロック。接続レートが高いと write lock 競合が多発 | -| MEDIUM | `tcp_stream_no_std.rs:CLOSED_CONNECTIONS` | `Mutex::lock()` | グローバル Mutex。Drop ハンドラで毎回取得 | -| MEDIUM | `tcp_stream_no_std.rs:connect()` | 二重ロック | `inner.lock()` 保持中に `socket_set.write()` を取得。ドキュメントのロック順序に反する | - -### 3.1 NET_MANAGER グローバル書き込みロック (CRITICAL) - -`TcpStream::connect`、`TcpListener::bind_on_interface`、`TcpListener::accept`(2 回)、 -`UdpSocket::bind_on_interface`、`UdpSocket::drop`、`TcpPort::drop` がすべて -`NET_MANAGER.write()` を取得する。 - -多数のコネクションが並行して確立・破棄されると、全スレッドがこの単一ロックで -逐次化される。 - -### 3.2 エフェメラルポート探索バグ (CRITICAL) - -`net.rs` 内の `get_ephemeral_port_tcp_ipv4`(および IPv6 版)は次のような実装になっている。 - -```rust -for i in 0..(u16::MAX >> 2) { - let port = self.tcp_port_ipv4_ephemeral.wrapping_add(i); - let entry = self.tcp_ports_ipv4.entry(i); // ← バグ: entry(port) が正しい - ... -} -``` - -ループインデックス `i` でキーを検索しているため、ポートの空き判定が正しく動作しない。 -`NET_MANAGER.write()` を保持したまま最悪 16384 回反復するため、パフォーマンス上も問題。 - -### 3.3 IfNet::inner のポーリング中ロック保持 (HIGH) - -`poll_rx_tx` / `poll_tx_only` はいずれも `inner.lock()` を保持したまま -`interface.poll()` を呼ぶ。smoltcp の `Interface::poll` はパケット処理・TCP 状態機械 -更新など相当量の作業を行うため、同一インターフェースの別ポーリングスレッドが -ロック待ちとなる。 - -さらに `IfNetInner` はポーリングに不要なマルチキャスト状態 -(`multicast_addr_ipv4`、`multicast_addr_mac`)も同一 Mutex に含めているため、 -マルチキャスト操作がポーリングを遅延させる。 - -### 3.4 socket_set 書き込みロックの競合 (HIGH) - -ソケット追加(connect/bind/accept)・削除(drop)は `socket_set.write()` を必要とする。 -これは全既存ソケットの `socket_set.read()` を保持しているスレッドをブロックする。 -高接続レートではデータ転送が接続確立によって阻害される。 - -### 3.5 CLOSED_CONNECTIONS グローバル Mutex (MEDIUM) - -`TcpStream::Drop` が毎回 `CLOSED_CONNECTIONS.lock()` を取得する。 -接続破棄が集中するとグローバル Mutex で逐次化される。 - -### 3.6 connect() の二重ロック (MEDIUM) - -`TcpStream::connect` では `inner.lock()` を保持したまま `socket_set.write()` を取得する。 -`socket.connect()` に `interface.context()` を渡す必要があるためだが、 -これによりロック順序(inner → socket_set)がドキュメントと逆になっている。 - ---- - -## 4. ロードマップ - -### Pre-Phase: バグ修正(即日対応) - -**変更内容:** `entry(i)` → `entry(port)` の修正 - -**対象ファイル:** -- `awkernel_lib/src/net.rs` - - `get_ephemeral_port_tcp_ipv4` 内の `self.tcp_ports_ipv4.entry(i)` → `entry(port)` - - `get_ephemeral_port_tcp_ipv6` 内の同様の箇所 - -**効果:** 正確なポート空き判定が可能になる。ポート探索が O(1) に近づく( -既使用ポートが少ない通常ケース)。 - -**リスク:** 低。1 行修正。 - ---- - -### Phase 1: PortAllocator 分離 — NET_MANAGER 書き込みロック除去 - -**目的:** ポート割り当て・解放操作を `NET_MANAGER` から分離し、 -グローバル書き込みロックをホットパスから除去する。 - -#### 1.1 PortAllocator 新設 - -`NetManager` からポート関連フィールドをすべて抽出し、独立した構造体に移動する。 - -```rust -// 新設: awkernel_lib/src/net/port_alloc.rs -pub struct PortAllocator { - tcp_ipv4: Mutex>, - tcp_ipv4_ephemeral: AtomicU16, - tcp_ipv6: Mutex>, - tcp_ipv6_ephemeral: AtomicU16, - udp_ipv4: Mutex>, - udp_ipv4_ephemeral: AtomicU16, - udp_ipv6: Mutex>, - udp_ipv6_ephemeral: AtomicU16, -} - -static PORT_ALLOC: PortAllocator = PortAllocator::new(); -``` - -エフェメラルカーソルは `AtomicU16::fetch_add` で前進させる(ロック不要)。 -実際のポート占有確認・挿入は各プロトコルの個別 Mutex だけを取得する。 - -これにより 4 つのプロトコル名前空間が独立し、TCP IPv4 の割り当てが -TCP IPv6 や UDP をブロックしない。 - -**変更対象ファイル:** -- `awkernel_lib/src/net.rs` — `NetManager` からポートフィールドを削除 -- `awkernel_lib/src/net/port_alloc.rs` — 新規作成 -- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` — `NET_MANAGER.write()` → `PORT_ALLOC` 呼び出し -- `awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs` — 同様(accept 内の 2 箇所含む) -- `awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs` — 同様 -- `awkernel_lib/src/net/tcp.rs` — `TcpPort::drop` 内の `NET_MANAGER.write()` → `PORT_ALLOC` - -**期待効果:** -- N スレッドが並行して接続確立する際、グローバル書き込みロック待ちがなくなる -- `NET_MANAGER` はインターフェース参照取得の読み込みロックのみになる -- 接続確立スループットがコア数に対しほぼ線形スケール - -**リスク:** 中。`TcpPort::drop` のポート参照解放先変更に注意。 - -#### 1.2 NET_MANAGER の読み取り専用化確認 - -Phase 1.1 完了後、`NET_MANAGER.write()` の残存箇所を監査し、 -`add_interface`(初期化パス)以外に書き込みロックがないことを確認する。 - -**変更対象ファイル:** `net.rs`(コード変更なし、確認のみ) - ---- - -### Phase 2: IfNetInner 分割 — ポーリングとマルチキャストの分離 - -**目的:** `IfNet::inner` ロックの保持時間を短縮し、マルチキャスト操作が -ポーリングを遅延させないようにする。 - -#### 2.1 IfNetInner を IfNetCore と IfNetMulticast に分割 - -```rust -// 変更後 -pub(super) struct IfNet { - inner: Mutex, // smoltcp Interface + ゲートウェイ - multicast: Mutex, // マルチキャスト管理(独立) - socket_set: RwLock, - ... -} - -struct IfNetCore { - interface: Interface, - default_gateway_ipv4: Option, -} - -struct IfNetMulticast { - multicast_addr_ipv4: BTreeSet, - multicast_addr_mac: BTreeMap<[u8; 6], u32>, -} -``` - -`poll_rx_tx` / `poll_tx_only` は `inner.lock()` のみ取得し、 -`multicast` は触れない。`join_multicast_v4` / `leave_multicast_v4` は -`multicast.lock()` のみ取得する。 - -**ロック順序の更新(ドキュメント要更新):** -``` -1. NetDriver::rx_ringq -2. IfNet::tx_ringq -3. IfNet::inner (IfNetCore) - ※ multicast は inner と同時に保持しない -``` - -**変更対象ファイル:** -- `awkernel_lib/src/net/if_net.rs` — 構造体分割、各メソッドの lock 先変更 -- `awkernel_lib/src/net.rs` — `if_net.inner` でマルチキャスト参照している箇所を `if_net.multicast` に変更 - -**期待効果:** -- マルチキャスト操作中にポーリングが止まらなくなる -- `join_multicast_v4` / `leave_multicast_v4` がポーリングパスを阻害しない - -**リスク:** 低〜中。smoltcp の `Interface::join_multicast_group()` は -`IfNetCore` 内の `interface` にアクセスするため、マルチキャストの smoltcp 側操作は -`inner.lock()` で、awkernel 側のブックキーピングは `multicast.lock()` で行う必要がある。 -両方を同時保持しないよう注意(ロック順序ドキュメントを更新すること)。 - -**前提条件:** Phase 1 - ---- - -### Phase 3: 二重ロック解消・Drop キュー導入 - -#### 3.1 connect() の二重ロック修正 - -**現状:** -```rust -inner.lock() { - socket_set.write() { // ← inner を保持したまま write - socket.connect(interface.context(), ...) - } -} -``` - -`socket.connect()` が `interface.context()` を必要とするため `inner` を保持するが、 -`socket_set.write()` は進行中の送受信(`socket_set.read()`)をブロックする。 - -**変更方針:** -`inner.lock()` を短命な読み取りに変えて、接続に必要な情報(ローカル IP アドレス、 -現在時刻)だけを取り出してロック解放後に `socket_set.write()` を取得する。 - -```rust -// inner を短時間だけロックして必要情報を取り出す -let ctx = { inner.lock().extract_connect_context() }; -// inner 解放後に socket_set を書き込みロック -socket_set.write().add(...); -socket.connect(ctx, ...); -``` - -smoltcp のフォーク版に `Interface::extract_connect_context()` または同等の -軽量アクセサを追加する。 - -**変更対象ファイル:** -- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` -- `awkernel_lib/smoltcp/src/iface/interface/mod.rs` — コンテキスト取り出しアクセサ追加 - -**期待効果:** -- connect 中に進行中の送受信がブロックされなくなる -- ロック順序がドキュメントと一致する - -**リスク:** 中。smoltcp フォークへの変更を伴う。 - -#### 3.2 Drop キューによる socket_set.write() 遅延解放 - -**現状:** `TcpStream::Drop` が `socket_set.write()` を取得してソケットを即座に削除。 -接続破棄が集中すると write lock がデータ転送をブロックする。 - -**変更方針:** `IfNet` にインターフェースごとの非同期削除キューを追加する。 - -```rust -pub(super) struct IfNet { - ... - drop_queue: Mutex>, // 新規追加 -} -``` - -`TcpStream::Drop` では `socket_set.write()` の代わりに `drop_queue` に -ハンドルを積む(`Mutex` のロック取得のみ)。 -既存の `tcp_garbage_collector`(100ms 周期)がキューを消費して -`socket_set.write()` でソケットを削除する。 - -`CLOSED_CONNECTIONS` グローバル Mutex の代替にもなる。 -インターフェースごとに独立したキューなので、複数インターフェース間の -競合が生じない。 - -**変更対象ファイル:** -- `awkernel_lib/src/net/if_net.rs` — `drop_queue` フィールド追加、drain 処理追加 -- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs` — Drop を drop_queue push に変更 -- `awkernel_services/src/network_service.rs` — `tcp_garbage_collector` を更新 - -**期待効果:** -- 接続破棄が集中しても `socket_set.write()` 競合が発生しない(GC レートに平滑化) -- `CLOSED_CONNECTIONS` グローバル Mutex のボトルネック解消 - -**リスク:** 低〜中。遅延削除のため、Drop 後 100ms 以内にハンドルが -再利用されないことを保証する必要がある(smoltcp の SocketSet はスロット再利用を -GC 後にのみ行うため問題なし)。 - -**前提条件:** Phase 2 - ---- - -### Phase 4: インターフェースごとのスケールアウト - -#### 4.1 PortAllocator のインターフェースごとへの移動 - -Phase 1 で作成したグローバル `PORT_ALLOC` を `IfNet` 内に移動する。 -TCP/UDP ソケットはすでに単一インターフェースに紐付いているため、 -ポート名前空間をインターフェースごとに独立させることができる。 - -```rust -pub(super) struct IfNet { - ... - port_alloc: PortAllocator, // グローバル PORT_ALLOC を廃止 -} -``` - -`TcpPort::drop` はポート返却先インターフェースへの参照(`Weak`)を持つ必要がある。 - -**変更対象ファイル:** -- `awkernel_lib/src/net/if_net.rs` — `port_alloc` フィールド追加 -- `awkernel_lib/src/net/tcp.rs` — `TcpPort` に `Weak` を追加 -- `awkernel_lib/src/net/port_alloc.rs` — グローバル静的変数を削除 - -**期待効果:** -- 異なるインターフェース間でポート割り当て競合がゼロになる -- 複数 NIC 構成でポート確保スループットが NIC 数に比例してスケール - -**リスク:** 中。`TcpPort` に `Weak` を持たせる設計変更。 -インターフェース削除前にポートが解放される通常ケースでは問題ない。 - -**前提条件:** Phase 1、Phase 2 - -#### 4.2 マルチキュー RX のフェーズ分離ポーリング - -複数の独立した RX キューを持つ NIC では、パケット受信(RX ドレイン)を -コアごとに並列実行し、smoltcp の `interface.poll()` のみ逐次化する -2 フェーズポーリングを導入する。 - -``` -Phase A (並列, per-queue): hardware → rx_ringq (rx_ringq.lock() のみ) -Phase B (逐次, per-iface): rx_ringq → interface.poll() (inner.lock() が必要) -``` - -現在の `will_poll` AtomicUsize による協調排他を拡張し、Phase A と Phase B を -明示的に分離する。 - -**変更対象ファイル:** -- `awkernel_lib/src/net/if_net.rs` — `poll_rx_tx` のフェーズ分離リファクタリング - -**注意:** SPIN モデル(`specification/awkernel_lib/src/net/if_net/if_net.pml`)の -LTL プロパティ(全パケットが最終的に送信される)をこのフェーズ変更に対して -再検証すること。実装前にモデルを更新し、`spin -a` で再チェックすること。 - -**期待効果:** -- 4 キュー NIC を 4 コアでポーリングする場合、RX 受信スループットが 4 倍近く向上 -- `interface.poll()` のシリアライズ区間のみに競合を限定 - -**リスク:** 高。SPIN モデルの再検証が必要。Phase A/B の境界で -パケットの可視性保証を維持する必要がある。 - -**前提条件:** Phase 2(`IfNetCore` が分離済み)、Phase 3.1(二重ロック解消済み) - ---- - -## 5. フェーズ依存関係 - -``` -Pre-Phase (エフェメラルポートバグ修正) - │ - ▼ -Phase 1.1 (PortAllocator 分離) - │ - ▼ -Phase 1.2 (NET_MANAGER 読み取り専用化確認) - │ - ▼ -Phase 2.1 (IfNetInner → IfNetCore + IfNetMulticast) - │ - ├──────────────────────┐ - ▼ ▼ -Phase 3.1 Phase 3.2 -(connect 二重ロック解消) (Drop キュー導入) - │ │ - └──────────┬───────────┘ - ▼ - Phase 4.1 Phase 4.2 - (per-iface PortAllocator) (2 フェーズポーリング) - │ │ - └───────────┬───────────┘ - ▼ - (完全実装) -``` - ---- - -## 6. 各フェーズの期待効果 - -| フェーズ | 解消するボトルネック | 機構 | 期待効果 | -|---|---|---|---| -| Pre | ポート探索バグ | 1 行修正 | 正確性回復、探索 O(1) 化 | -| 1.1 | NET_MANAGER.write() 全接続逐次化 | AtomicU16 + 4 独立 Mutex | 並行接続確立がほぼ線形スケール | -| 1.2 | 残存 write lock 確認 | 監査のみ | 安全性確認 | -| 2.1 | inner lock がマルチキャストとポーリングを混在 | 構造体分割 | マルチキャスト操作がポーリングを阻害しない | -| 3.1 | connect の二重ロック | コンテキスト事前取り出し | connect 中も送受信が継続 | -| 3.2 | Drop 時の socket_set.write() 競合と CLOSED_CONNECTIONS | per-iface 遅延削除キュー | 接続破棄集中時も送受信が継続 | -| 4.1 | 複数 iface 間のポート割り当て競合 | per-iface PortAllocator | 複数 NIC 構成でポート確保が完全独立 | -| 4.2 | マルチキュー RX がシリアライズ | 2 フェーズポーリング | N キュー NIC で受信スループット N 倍近く改善 | - ---- - -## 7. 変更しない不変条件 - -- **ロック順序:** rx_ringq → tx_ringq → inner (IfNetCore) の順序を全フェーズで維持する。 - `multicast` および `drop_queue` は葉ロックとし、`inner` と同時保持しない。 - 各フェーズでロック順序コメント(`if_net.rs` 冒頭)を更新すること。 - -- **公開 API:** `TcpStream`、`UdpSocket`、`TcpListener`、`SockTcpStream`、 - `SockTcpListener`、`SockUdp` トレイトの型シグネチャを変更しない。 - -- **smoltcp フォーク変更の最小化:** Phase 3.1 のアクセサ追加、Phase 4.2 の - ポーリング構造リファクタリングに限定する。それ以外のフェーズは awkernel 側のみ変更。 - -- **SPIN モデル:** Phase 4.2 の実装前に - `specification/awkernel_lib/src/net/if_net/if_net.pml` の LTL プロパティを - 2 フェーズポーリングモデルに合わせて更新し、`spin -a` で再検証すること。 - -- **no_std 制約:** 全フェーズで `std` API に依存しない。追加する構造体は - `Mutex`、`AtomicU16`、`BTreeMap`、`VecDeque` など no_std 互換の型のみ使用する。 - ---- - -## 8. テスト手法 - -### 8.1 テスト環境の構成 - -現状のプロジェクトは `make qemu-x86_64-net` で QEMU を直接起動し、 -ホスト↔ゲスト間の通信のみをテストしている。ここでは **VM 間通信** と -**多数コネクション負荷テスト** を可能にするため、libvirt/KVM を用いた -2-VM 構成を追加する。 - -``` -Host Machine (libvirt / KVM) -├── virbr-awk (NAT bridge, 192.168.100.0/24) ← virsh net-define -├── VM: awkernel (awkernel カーネル, QEMU/KVM, OVMF) -│ ├── vnet0: e1000e, 192.168.100.10 -│ └── vnet1: virtio, 192.168.100.11 -└── VM: counterpart (Fedora/Ubuntu, iperf3 / netperf サーバ) - └── vnet0: 192.168.100.2 -``` - -`awkernel` 側のネットワーク設定は既存の QEMU 引数と同じ -(e1000e + virtio-net-pci、MAC アドレスは Makefile の値を維持)。 - ---- - -### 8.2 環境セットアップ - -#### 8.2.1 仮想ネットワーク定義 - -```xml - - - virbr-awk - - - - - - - - -``` - -```bash -virsh net-define awkernel-net.xml -virsh net-start virbr-awk -virsh net-autostart virbr-awk -virsh net-list --all # 起動確認 -``` - -#### 8.2.2 カウンタパート VM の作成 - -```bash -# Fedora/Ubuntu など標準 Linux を 1 台用意する -virt-install \ - --name counterpart \ - --memory 2048 --vcpus 2 \ - --os-variant fedora40 \ - --network network=virbr-awk,model=virtio \ - --disk path=/var/lib/libvirt/images/counterpart.qcow2,size=20,format=qcow2 \ - --location /path/to/fedora.iso \ - --extra-args 'console=ttyS0' \ - --nographics --noautoconsole - -virsh start counterpart -virsh console counterpart # インストール後にログイン -# インストール完了後 -sudo dnf install -y iperf3 netperf nc # または apt install -``` - -#### 8.2.3 awkernel VM の定義 - -Makefile の QEMU 引数を virsh の `` XML に変換して登録する。 -NIC を既存の QEMU usermode ネットワークから `virbr-awk` ブリッジに切り替える。 - -```xml - - - awkernel - 4 - 16 - - hvm - /usr/share/OVMF/OVMF_CODE.fd - /var/lib/libvirt/qemu/nvram/awkernel_VARS.fd - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -```bash -virsh define awkernel-vm.xml -virsh start awkernel -virsh console awkernel # シリアルコンソールでカーネルログ確認 -``` - -#### 8.2.4 VM の停止・削除 - -```bash -virsh destroy awkernel # 強制停止 -virsh destroy counterpart -virsh undefine awkernel # 定義削除 -virsh undefine counterpart -virsh net-destroy virbr-awk -virsh net-undefine virbr-awk -``` - ---- - -### 8.3 テスト項目 - -| # | テスト名 | 目的 | ツール | 計測値 | -|---|---|---|---|---| -| T1 | 基本疎通確認 | TCP/UDP が到達すること | nc, ping | 成功/失敗 | -| T2 | UDP スループット | NET_MANAGER 改善前後の比較 | iperf3 -u | Gbps | -| T3 | TCP スループット | socket_set 競合改善の確認 | iperf3 | Gbps | -| T4 | 並列 TCP 接続 | 多数コネクション時スケーリング | iperf3 -P N | Gbps, CPU% | -| T5 | 接続確立レート | Phase 1 前後の比較 | netperf TCP_CRR | conn/s | -| T6 | リクエスト/レスポンス RTT | Phase 3 前後の比較 | netperf TCP_RR | μs | -| T7 | パケットキャプチャ解析 | ロスト・再送の確認 | tcpdump, tshark | ロス率% | -| T8 | PCAP ベースの手動確認 | awkernel 側パケット検査 | 既存 filter-dump | - | - ---- - -### 8.4 テストコマンド詳細 - -#### 基本疎通 (T1) - -```bash -# counterpart VM 上 -nc -l -p 9000 # TCP リスナー -nc -u -l -p 9001 # UDP リスナー - -# ホストから awkernel を経由した疎通確認(awkernel がエコーを返す場合) -nc 192.168.100.10 26099 -echo "hello" | nc -u 192.168.100.10 26099 -``` - -#### UDP スループット (T2) - -```bash -# counterpart VM でサーバ起動 -iperf3 -s - -# ホストまたは counterpart から awkernel 向けに送信 -iperf3 -c 192.168.100.10 -u -b 0 -t 30 -l 1400 # 帯域無制限, 30 秒 -iperf3 -c 192.168.100.10 -u -b 0 -t 30 -P 32 # 32 並列ストリーム -``` - -#### TCP スループット・並列接続 (T3, T4) - -```bash -# counterpart VM でサーバ起動 -iperf3 -s -p 5201 - -# N 並列 TCP ストリーム -for N in 1 4 8 16 32 64 128; do - echo "=== -P $N ===" - iperf3 -c 192.168.100.10 -p 5201 -P $N -t 30 -J \ - | tee result_tcp_P${N}.json -done -``` - -#### 接続確立レート (T5) — Phase 1 比較に重要 - -```bash -# counterpart VM で netperf サーバ起動 -netserver -p 12865 - -# TCP 接続生成レートの計測(30 秒間) -netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 -``` - -期待値: Phase 1 適用後は Phase 0 比 **2〜4 倍**の接続/秒を記録する。 - -#### RTT レイテンシ (T6) — Phase 3 比較に重要 - -```bash -netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 -``` - -#### パケットキャプチャ (T7, T8) - -```bash -# awkernel VM は QEMU filter-dump で自動キャプチャ済み -# counterpart 側で補足する場合 -virsh domifstat counterpart vnet0 # パケット統計 -virsh qemu-monitor-command awkernel --hmp \ - 'info network' # QEMU ネットワーク状態確認 - -# キャプチャ解析 -tcpdump -vvv -XXnr packets_net0.pcap | head -100 -tshark -r packets_net0.pcap -z io,stat,1 "tcp" # 1 秒ごとの TCP 統計 -``` - ---- - -### 8.5 ベースライン取得手順 - -各フェーズの実装前に必ずベースラインを記録する。 - -```bash -#!/bin/bash -# baseline.sh — 変更前に実行してベースライン保存 -DATE=$(date +%Y%m%d_%H%M%S) -DIR="bench_baseline_${DATE}" -mkdir -p $DIR - -# T3: TCP スループット -iperf3 -c 192.168.100.10 -p 5201 -t 30 -J > $DIR/tcp_single.json - -# T4: 並列 TCP -for N in 4 16 64; do - iperf3 -c 192.168.100.10 -p 5201 -P $N -t 30 -J > $DIR/tcp_P${N}.json -done - -# T5: 接続確立レート -netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 \ - > $DIR/tcp_crr.txt - -# T6: RTT -netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 \ - > $DIR/tcp_rr.txt - -echo "Baseline saved to $DIR" -``` - -フェーズ実装後に同スクリプトを `bench_after_phaseN_${DATE}` として再実行し、 -数値を比較する。 - ---- - -### 8.6 フェーズ別テスト手順 - -#### Pre-Phase(バグ修正)後 - -```bash -# ポート割り当てが正しく動作することを確認 -# 1. 64 並列 TCP 接続を確立し、全て成功することを確認 -iperf3 -c 192.168.100.10 -p 5201 -P 64 -t 10 -# 2. エフェメラルポート番号が想定範囲内かシリアルコンソールで確認 -virsh console awkernel -``` - -#### Phase 1(PortAllocator 分離)後 - -```bash -# T5: 接続確立レートがベースライン比 2 倍以上であることを確認 -netperf -H 192.168.100.10 -p 12865 -t TCP_CRR -l 30 -- -r 1,1 -# T4: 64 並列接続でのスループットがベースライン比 1.5 倍以上 -iperf3 -c 192.168.100.10 -p 5201 -P 64 -t 30 -J -``` - -#### Phase 2(IfNetInner 分割)後 - -```bash -# マルチキャスト join/leave 中にデータ転送が止まらないことを確認 -# 別ターミナルで転送継続 -iperf3 -c 192.168.100.10 -p 5201 -t 60 & -# awkernel コンソールでマルチキャスト join/leave を繰り返す -virsh console awkernel -# iperf3 のスループットが join/leave 中も維持されていることを確認 -``` - -#### Phase 3(二重ロック解消・Drop キュー)後 - -```bash -# T6: RTT がベースライン比 改善していることを確認 -netperf -H 192.168.100.10 -p 12865 -t TCP_RR -l 30 -- -r 64,64 -# 接続破棄集中テスト: 短命コネクションを高レートで生成 -for i in $(seq 1 1000); do - nc -z 192.168.100.10 26099 & -done -wait -# iperf3 の転送が阻害されていないことを並行確認 -``` - -#### Phase 4(per-iface スケールアウト)後 - -```bash -# 複数インターフェース経由の並列転送 -iperf3 -c 192.168.100.10 -p 5201 -P 32 -t 30 -J & # net0 経由 -iperf3 -c 192.168.100.11 -p 5201 -P 32 -t 30 -J & # net1 経由 -wait -# 合算スループットが単独 NIC の 2 倍近いことを確認 -``` - ---- - -### 8.7 既存テストとの併用 - -上記 VM テストは既存のテスト体系を置き換えるものではなく、補完する。 - -| テスト種別 | 用途 | 実行タイミング | -|---|---|---| -| `make test` | 単体テスト(ロック・データ構造) | 毎コミット (CI) | -| SPIN モデル検証 (`make run` in specification/) | ポーリングアルゴリズムの正しさ | Phase 4.2 実装前後 | -| `make qemu-x86_64-net` + `scripts/udp.py` | 基本 UDP/TCP 疎通 | 各フェーズの動作確認 | -| VM テスト (本セクション) | 多数コネクション負荷・スループット計測 | フェーズ前後のベースライン比較 | - ---- - -## 9. 進捗状況 - -### 凡例 - -| 記号 | 意味 | -|---|---| -| ✅ | 実装・検証済み | -| 🔲 | 未着手 | - -### フェーズ別完了状態 - -| フェーズ | 状態 | 完了日 | -|---|---|---| -| **Pre-Phase** バグ修正 | ✅ | 2026-04-21 | -| **Phase 1.1** PortAllocator 分離 | ✅ | 2026-04-21 | -| **Phase 1.2** NET_MANAGER 読み取り専用化確認 | 🔲 | — | -| **Phase 2.1** IfNetInner 分割(IfNetCore + IfNetMulticast) | 🔲 | — | -| **Phase 3.1** connect() 二重ロック解消 | 🔲 | — | -| **Phase 3.2** Drop キュー導入 | 🔲 | — | -| **Phase 4.1** per-iface PortAllocator | 🔲 | — | -| **Phase 4.2** 2 フェーズポーリング | 🔲 | — | - ---- - -### Pre-Phase — 実施内容(✅ 完了) - -**計画:** -- `get_ephemeral_port_tcp_ipv4/v6` の `entry(i)` → `entry(port)` バグ修正 - -**実際に行ったこと:** -- `get_ephemeral_port_tcp_ipv4` / `get_ephemeral_port_tcp_ipv6` の `entry(i)` → `entry(port)` 修正 -- 追加発見バグ: `get_ephemeral_port_udp_ipv6` で `self.udp_port_ipv4_ephemeral = port` と - なっていた(IPv6 カーソルではなく IPv4 カーソルを更新していた)。これも同時に修正 - -**計画との差異:** なし(1 箇所追加修正あり) - ---- - -### Phase 1.1 — 実施内容(✅ 完了) - -**計画:** -- `awkernel_lib/src/net/port_alloc.rs` を新規作成 -- `PortAllocator` 構造体(4 プロトコル × 独立 Mutex + AtomicU16 カーソル) -- 全呼び出し元を `PORT_ALLOC` 経由に変更 - -**実際に行ったこと:** - -新規作成: -- `awkernel_lib/src/net/port_alloc.rs` - - `TcpPortsInner` 構造体(`map: BTreeMap` + `cursor: u16`) - - `UdpPortsInner` 構造体(`set: BTreeSet` + `cursor: u16`) - - `PortAllocator` 構造体: TCP IPv4/IPv6 は `Mutex`、 - UDP IPv4/IPv6 は `Mutex`。カーソルは各 Inner 内の `u16`(AtomicU16 ではない) - - 公開 API: `get_ephemeral_tcp_ipv4/v6`、`try_claim_tcp_ipv4/v6`、 - `increment_ref_tcp_ipv4/v6`、`decrement_ref_tcp_ipv4/v6`、 - `get_ephemeral_udp_ipv4/v6`、`try_claim_udp_ipv4/v6`、`free_udp_ipv4/v6` - - `static PORT_ALLOC: PortAllocator = PortAllocator::new()` を定義 - -変更ファイル: -- `awkernel_lib/src/net.rs`: `NetManager` からポートフィールド 8 個・ポートメソッド 16 個を削除。 - `mod port_alloc;` 追加。`#[cfg(not(feature = "std"))]` の `TcpPort` / `BTreeSet` インポート削除 -- `awkernel_lib/src/net/tcp.rs`: `TcpPort::drop` の `NET_MANAGER.write()` → `PORT_ALLOC` -- `awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs`: - `connect()` の `NET_MANAGER.write()` → `PORT_ALLOC` + `NET_MANAGER.read()` -- `awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs`: - `bind_on_interface()` + `accept()` 2 箇所の `NET_MANAGER.write()` → `PORT_ALLOC` -- `awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs`: - `bind_on_interface()` + `Drop` の `NET_MANAGER.write()` → `PORT_ALLOC` - -ビルド・テスト確認: -- `make x86_64 RELEASE=1` 成功 -- `make aarch64 BSP=aarch64_virt RELEASE=1` 成功(コンパイル部分) -- `make test` 全テスト通過(367 テスト、0 失敗) - -**計画との差異:** - -| 項目 | 計画 | 実際 | 理由 | -|---|---|---|---| -| UDP ポートチェック API | `is_in_use_udp_*` + `set_in_use_udp_*` を別メソッドで提供 | `try_claim_udp_*` に統合(1 メソッド) | check-and-insert を Mutex 内でアトミックに行い TOCTOU を排除するため | -| `udp_socket::bind_on_interface` のロック順序 | 計画に記載なし | インターフェース参照取得(`NET_MANAGER.read()`)を port 操作の **前** に変更 | 元コードではポート確保後にインターフェース検索失敗した場合にポートがリークする設計だったため修正 | -| エフェメラルカーソルの型 | `AtomicU16`(計画: `fetch_add` でロックフリー更新) | `u16`(Mutex 内の `Inner` 構造体に埋め込み) | `AtomicU16` をロック外 load・ロック内 store するパターンは aarch64 弱順序モデルでカーソルが陳腐化する可能性があり、かつ i=0 始まりの `wrapping_add(i)` により毎回 1 イテレーション無駄になるバグがあったため修正 | -| 探索アルゴリズム | `cursor.wrapping_add(i)`(i=0 始まり) | advancing cursor(ループ先頭で `cursor += 1`) | i=0 始まりは直前に割り当てたポートを毎回最初に試し、必ず 1 イテレーション空振りするため | - ---- - -### 次フェーズ - -**Phase 1.2**(NET_MANAGER 読み取り専用化確認)が次の着手対象。 -`NET_MANAGER.write()` の残存箇所を `grep` で確認し、 -`add_interface`(起動時初期化パス)以外に書き込みロックがないことを検証する。 - -```bash -grep -n 'NET_MANAGER\.write' awkernel_lib/src/net/*.rs awkernel_lib/src/net/**/*.rs -``` - -確認後、Phase 2.1(`IfNetInner` 分割)に進む。 From 0fe06499d6d75e5fe981514a7c244088c698c4b9 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 19:26:15 +0900 Subject: [PATCH 7/9] fmt Signed-off-by: Yuuki Takano --- awkernel_lib/src/net/port_alloc.rs | 29 ++++++++++++++----- .../net/tcp_listener/tcp_listener_no_std.rs | 4 +-- .../src/net/tcp_stream/tcp_stream_no_std.rs | 4 ++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs index dd2354403..8cc20c272 100644 --- a/awkernel_lib/src/net/port_alloc.rs +++ b/awkernel_lib/src/net/port_alloc.rs @@ -1,9 +1,6 @@ #![cfg(not(feature = "std"))] -use alloc::collections::{ - btree_map::Entry, - BTreeMap, BTreeSet, -}; +use alloc::collections::{btree_map::Entry, BTreeMap, BTreeSet}; use crate::sync::{mcs::MCSNode, mutex::Mutex}; @@ -56,7 +53,11 @@ impl PortAllocator { let mut ports = self.tcp_ipv4.lock(&mut node); for _ in 0..(u16::MAX >> 2) { ports.cursor = ports.cursor.wrapping_add(1); - let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + let port = if ports.cursor == 0 { + u16::MAX >> 2 + } else { + ports.cursor + }; if let Entry::Vacant(e) = ports.map.entry(port) { e.insert(1); return Some(TcpPort::new(port, true)); @@ -107,7 +108,11 @@ impl PortAllocator { let mut ports = self.tcp_ipv6.lock(&mut node); for _ in 0..(u16::MAX >> 2) { ports.cursor = ports.cursor.wrapping_add(1); - let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + let port = if ports.cursor == 0 { + u16::MAX >> 2 + } else { + ports.cursor + }; if let Entry::Vacant(e) = ports.map.entry(port) { e.insert(1); return Some(TcpPort::new(port, false)); @@ -158,7 +163,11 @@ impl PortAllocator { let mut ports = self.udp_ipv4.lock(&mut node); for _ in 0..(u16::MAX >> 2) { ports.cursor = ports.cursor.wrapping_add(1); - let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + let port = if ports.cursor == 0 { + u16::MAX >> 2 + } else { + ports.cursor + }; if ports.set.insert(port) { return Some(port); } @@ -186,7 +195,11 @@ impl PortAllocator { let mut ports = self.udp_ipv6.lock(&mut node); for _ in 0..(u16::MAX >> 2) { ports.cursor = ports.cursor.wrapping_add(1); - let port = if ports.cursor == 0 { u16::MAX >> 2 } else { ports.cursor }; + let port = if ports.cursor == 0 { + u16::MAX >> 2 + } else { + ports.cursor + }; if ports.set.insert(port) { return Some(port); } diff --git a/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs b/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs index f840e7c5a..030c7a6b5 100644 --- a/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs +++ b/awkernel_lib/src/net/tcp_listener/tcp_listener_no_std.rs @@ -6,8 +6,8 @@ use crate::sync::mcs::MCSNode; use alloc::{vec, vec::Vec}; use crate::net::{ - ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, tcp_stream::TcpStream, - NetManagerError, NET_MANAGER, + ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, tcp_stream::TcpStream, NetManagerError, + NET_MANAGER, }; use super::SockTcpListener; diff --git a/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs b/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs index 09d04afc4..3011f3099 100644 --- a/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs +++ b/awkernel_lib/src/net/tcp_stream/tcp_stream_no_std.rs @@ -1,4 +1,6 @@ -use crate::net::{ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, NetManagerError, NET_MANAGER}; +use crate::net::{ + ip_addr::IpAddr, port_alloc::PORT_ALLOC, tcp::TcpPort, NetManagerError, NET_MANAGER, +}; use super::{SockTcpStream, TcpResult}; From 322f5e0ec07ccc41e2aeef17575a78d27e7cc202 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 19:33:10 +0900 Subject: [PATCH 8/9] fix(net): resolve clippy warnings (needless_pub_self, map_entry) Co-Authored-By: Claude Sonnet 4.6 --- awkernel_lib/src/net.rs | 2 +- awkernel_lib/src/net/port_alloc.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/awkernel_lib/src/net.rs b/awkernel_lib/src/net.rs index 59af53ed1..e8eef3453 100644 --- a/awkernel_lib/src/net.rs +++ b/awkernel_lib/src/net.rs @@ -28,7 +28,7 @@ pub mod ip_addr; pub mod ipv6; pub mod multicast; pub mod net_device; -pub(self) mod port_alloc; +mod port_alloc; pub mod tcp; pub mod tcp_listener; pub mod tcp_stream; diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs index 8cc20c272..57d427bde 100644 --- a/awkernel_lib/src/net/port_alloc.rs +++ b/awkernel_lib/src/net/port_alloc.rs @@ -70,11 +70,11 @@ impl PortAllocator { pub(super) fn try_claim_tcp_ipv4(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv4.lock(&mut node); - if ports.map.contains_key(&port) { - None - } else { - ports.map.insert(port, 1); + if let Entry::Vacant(e) = ports.map.entry(port) { + e.insert(1); Some(TcpPort::new(port, true)) + } else { + None } } @@ -125,11 +125,11 @@ impl PortAllocator { pub(super) fn try_claim_tcp_ipv6(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.tcp_ipv6.lock(&mut node); - if ports.map.contains_key(&port) { - None - } else { - ports.map.insert(port, 1); + if let Entry::Vacant(e) = ports.map.entry(port) { + e.insert(1); Some(TcpPort::new(port, false)) + } else { + None } } From 7c49ce5fa03df9b7c8a683afa81d42e16a003d11 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Wed, 22 Apr 2026 14:15:43 +0900 Subject: [PATCH 9/9] reflect the review Signed-off-by: Yuuki Takano --- awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs b/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs index 00cc8ccc0..b2fca5937 100644 --- a/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs +++ b/awkernel_lib/src/net/udp_socket/udp_socket_no_std.rs @@ -326,15 +326,17 @@ impl Drop for UdpSocket { } } + { + let net_manager = NET_MANAGER.read(); + if let Some(if_net) = net_manager.interfaces.get(&self.interface_id) { + if_net.socket_set.write().remove(self.handle); + } + } + if self.is_ipv4 { PORT_ALLOC.free_udp_ipv4(self.port); } else { PORT_ALLOC.free_udp_ipv6(self.port); } - - let net_manager = NET_MANAGER.read(); - if let Some(if_net) = net_manager.interfaces.get(&self.interface_id) { - if_net.socket_set.write().remove(self.handle); - } } }