From f53ad3f21af36b135c8e03025b799b53e5ec6a5b Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 21 Apr 2026 14:47:46 +0900 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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); - } } } From f59a5cbd2083ed9b98e5ff9af186dc898d32e0a1 Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Thu, 18 Jun 2026 17:50:47 +0900 Subject: [PATCH 10/10] fix(net): make socket add race-safe and free UDP ports via RAII Guard against a future dynamic interface removal: add the socket to the interface while holding NET_MANAGER.read() (re-looking up the interface), so a concurrent removal taking the write lock cannot orphan the socket. Introduce a UdpPort RAII guard that frees the UDP port on drop, mirroring TcpPort, so the claimed port is released on any error path between claiming it and constructing the owning socket. Co-Authored-By: Claude Opus 4.8 (1M context) --- awkernel_lib/src/net/port_alloc.rs | 64 ++++++++++++++--- .../net/tcp_listener/tcp_listener_no_std.rs | 43 +++++++----- .../src/net/tcp_stream/tcp_stream_no_std.rs | 34 ++++++--- .../src/net/udp_socket/udp_socket_no_std.rs | 70 ++++++++++--------- 4 files changed, 138 insertions(+), 73 deletions(-) diff --git a/awkernel_lib/src/net/port_alloc.rs b/awkernel_lib/src/net/port_alloc.rs index 57d427bde..c579a75c1 100644 --- a/awkernel_lib/src/net/port_alloc.rs +++ b/awkernel_lib/src/net/port_alloc.rs @@ -6,6 +6,30 @@ use crate::sync::{mcs::MCSNode, mutex::Mutex}; use super::tcp::TcpPort; +/// RAII handle for a claimed UDP port. Frees the port from [`PORT_ALLOC`] on drop, +/// so the port is released on any error path between claiming it and constructing +/// the owning socket. +pub(super) struct UdpPort { + port: u16, + is_ipv4: bool, +} + +impl UdpPort { + pub(super) fn port(&self) -> u16 { + self.port + } +} + +impl Drop for UdpPort { + fn drop(&mut self) { + if self.is_ipv4 { + PORT_ALLOC.free_udp_ipv4(self.port); + } else { + PORT_ALLOC.free_udp_ipv6(self.port); + } + } +} + struct TcpPortsInner { map: BTreeMap, cursor: u16, @@ -158,7 +182,7 @@ impl PortAllocator { } /// Allocate an ephemeral UDP IPv4 port. - pub(super) fn get_ephemeral_udp_ipv4(&self) -> Option { + pub(super) fn get_ephemeral_udp_ipv4(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv4.lock(&mut node); for _ in 0..(u16::MAX >> 2) { @@ -169,17 +193,27 @@ impl PortAllocator { ports.cursor }; if ports.set.insert(port) { - return Some(port); + return Some(UdpPort { + port, + is_ipv4: true, + }); } } None } - /// Claim a specific UDP IPv4 port. Returns `false` if the port is already in use. - pub(super) fn try_claim_udp_ipv4(&self, port: u16) -> bool { + /// Claim a specific UDP IPv4 port. Returns `None` if the port is already in use. + pub(super) fn try_claim_udp_ipv4(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv4.lock(&mut node); - ports.set.insert(port) + if ports.set.insert(port) { + Some(UdpPort { + port, + is_ipv4: true, + }) + } else { + None + } } /// Free a UDP IPv4 port. @@ -190,7 +224,7 @@ impl PortAllocator { } /// Allocate an ephemeral UDP IPv6 port. - pub(super) fn get_ephemeral_udp_ipv6(&self) -> Option { + pub(super) fn get_ephemeral_udp_ipv6(&self) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv6.lock(&mut node); for _ in 0..(u16::MAX >> 2) { @@ -201,17 +235,27 @@ impl PortAllocator { ports.cursor }; if ports.set.insert(port) { - return Some(port); + return Some(UdpPort { + port, + is_ipv4: false, + }); } } None } - /// Claim a specific UDP IPv6 port. Returns `false` if the port is already in use. - pub(super) fn try_claim_udp_ipv6(&self, port: u16) -> bool { + /// Claim a specific UDP IPv6 port. Returns `None` if the port is already in use. + pub(super) fn try_claim_udp_ipv6(&self, port: u16) -> Option { let mut node = MCSNode::new(); let mut ports = self.udp_ipv6.lock(&mut node); - ports.set.insert(port) + if ports.set.insert(port) { + Some(UdpPort { + port, + is_ipv4: false, + }) + } else { + None + } } /// Free a UDP IPv6 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 030c7a6b5..222f415be 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 @@ -31,15 +31,13 @@ impl SockTcpListener for TcpListener { tx_buffer_size: usize, backlogs: usize, ) -> Result { - // Find the interface that has the specified address. - let if_net = { + // Validate the interface exists before claiming a port. + { let net_manager = NET_MANAGER.read(); - net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)? - .clone() - }; + if !net_manager.interfaces.contains_key(&interface_id) { + return Err(NetManagerError::InvalidInterfaceID); + } + } let port = if let Some(port) = port { if port == 0 { @@ -67,16 +65,25 @@ impl SockTcpListener for TcpListener { .ok_or(NetManagerError::NoAvailablePort)? }; - let mut handles = Vec::new(); - - for _ in 0..backlogs { - // Create a TCP socket. - let socket = create_listen_socket(addr, port.port(), rx_buffer_size, tx_buffer_size); - - let handle = if_net.socket_set.write().add(socket); - - handles.push(handle); - } + // Add the listening sockets while holding the read lock, so that a concurrent + // interface removal (which takes the write lock) cannot orphan them. + // If the interface is gone, `port` (TcpPort) frees the port on the early return. + let handles = { + let net_manager = NET_MANAGER.read(); + let if_net = net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)?; + + let mut handles = Vec::new(); + for _ in 0..backlogs { + // Create a TCP socket. + let socket = + create_listen_socket(addr, port.port(), rx_buffer_size, tx_buffer_size); + handles.push(if_net.socket_set.write().add(socket)); + } + handles + }; Ok(TcpListener { handles, 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 3011f3099..bb301ff8a 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 @@ -92,14 +92,13 @@ impl SockTcpStream for TcpStream { tx_buffer_size: usize, waker: &core::task::Waker, ) -> Result { - let if_net = { + // Validate the interface exists before claiming a port or allocating buffers. + { let net_manager = NET_MANAGER.read(); - net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)? - .clone() - }; + if !net_manager.interfaces.contains_key(&interface_id) { + return Err(NetManagerError::InvalidInterfaceID); + } + } let local_port = if remote_addr.is_ipv4() { PORT_ALLOC @@ -116,15 +115,24 @@ impl SockTcpStream for TcpStream { let socket = smoltcp::socket::tcp::Socket::new(rx_buffer, tx_buffer); - let handle; - { + // Add the socket and connect it while holding the read lock, so that a concurrent + // interface removal (which takes the write lock) cannot orphan it. If the interface + // is gone, `local_port` (TcpPort) frees the port on the early return. + let (handle, if_net) = { + let net_manager = NET_MANAGER.read(); + let if_net = net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)? + .clone(); + let mut node = MCSNode::new(); let mut inner = if_net.inner.lock(&mut node); let interface = inner.get_interface(); let mut socket_set = if_net.socket_set.write(); - handle = socket_set.add(socket); + let handle = socket_set.add(socket); let connect_is_err = { let mut node: MCSNode = MCSNode::new(); @@ -147,7 +155,11 @@ impl SockTcpStream for TcpStream { socket_set.remove(handle); return Err(NetManagerError::InvalidState); } - } + + drop(socket_set); + drop(inner); + (handle, if_net) + }; let que_id = crate::cpu::raw_cpu_id() & (if_net.net_device.num_queues() - 1); if_net.poll_tx_only(que_id); 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 b2fca5937..06e2f6ea4 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,10 @@ use core::net::Ipv4Addr; -use crate::net::{ip_addr::IpAddr, port_alloc::PORT_ALLOC, NET_MANAGER}; +use crate::net::{ + ip_addr::IpAddr, + port_alloc::{UdpPort, PORT_ALLOC}, + NET_MANAGER, +}; use awkernel_sync::{mcs::MCSNode, mutex::Mutex}; use super::{NetManagerError, SockUdp}; @@ -19,8 +23,9 @@ pub struct UdpSocket { handle: smoltcp::iface::SocketHandle, interface_id: u64, addr: IpAddr, - port: u16, - is_ipv4: bool, + // Held only to free the port via its `Drop` impl when the socket is dropped. + #[allow(dead_code)] + udp_port: UdpPort, joined_multicast_addr_v4: BTreeSet, } @@ -32,50 +37,43 @@ impl super::SockUdp for UdpSocket { rx_buffer_size: usize, tx_buffer_size: usize, ) -> Result { - // Find the interface that has the specified address. - let if_net = { + // Validate the interface exists before claiming a port or allocating buffers. + { let net_manager = NET_MANAGER.read(); - net_manager - .interfaces - .get(&interface_id) - .ok_or(NetManagerError::InvalidInterfaceID)? - .clone() - }; + if !net_manager.interfaces.contains_key(&interface_id) { + return Err(NetManagerError::InvalidInterfaceID); + } + } - let is_ipv4; - let port = if let Some(port) = port { + // Claim the port. `udp_port` is RAII: it frees the port on any early return below. + let udp_port = if let Some(port) = port { if port == 0 { return Err(NetManagerError::InvalidPort); } // Check if the specified port is available and claim it atomically. if addr.is_ipv4() { - if !PORT_ALLOC.try_claim_udp_ipv4(port) { - return Err(NetManagerError::PortInUse); - } - is_ipv4 = true; - port + PORT_ALLOC + .try_claim_udp_ipv4(port) + .ok_or(NetManagerError::PortInUse)? } else { - if !PORT_ALLOC.try_claim_udp_ipv6(port) { - return Err(NetManagerError::PortInUse); - } - is_ipv4 = false; - port + PORT_ALLOC + .try_claim_udp_ipv6(port) + .ok_or(NetManagerError::PortInUse)? } } else { // Find an ephemeral port. if addr.is_ipv4() { - is_ipv4 = true; PORT_ALLOC .get_ephemeral_udp_ipv4() .ok_or(NetManagerError::PortInUse)? } else { - is_ipv4 = false; PORT_ALLOC .get_ephemeral_udp_ipv6() .ok_or(NetManagerError::PortInUse)? } }; + let port = udp_port.port(); // Create a UDP socket. use smoltcp::socket::udp; @@ -110,15 +108,23 @@ impl super::SockUdp for UdpSocket { } } - // Add the socket to the interface. - let handle = if_net.socket_set.write().add(socket); + // Add the socket to the interface while holding the read lock, so that a + // concurrent interface removal (which takes the write lock) cannot orphan it. + let handle = { + let net_manager = NET_MANAGER.read(); + let if_net = net_manager + .interfaces + .get(&interface_id) + .ok_or(NetManagerError::InvalidInterfaceID)?; + let handle = if_net.socket_set.write().add(socket); + handle + }; Ok(UdpSocket { handle, interface_id, addr: addr.clone(), - port, - is_ipv4, + udp_port, joined_multicast_addr_v4: Default::default(), }) } @@ -333,10 +339,6 @@ impl Drop for UdpSocket { } } - if self.is_ipv4 { - PORT_ALLOC.free_udp_ipv4(self.port); - } else { - PORT_ALLOC.free_udp_ipv6(self.port); - } + // `self.udp_port` (UdpPort) frees the port via its Drop impl. } }