Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions drivers/net/e1000/src/e1000.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2072,7 +2072,7 @@ pub fn probe(device: BusDevice, cap: Cap<BusDeviceCap, Write>) -> Result<(), nar
// gigabit packet rates. The coalescer excludes loopback and preserves
// control/options boundaries; the RX pump flushes it whenever the ring is
// drained to empty.
narf_net::tcp_stack::set_gro(true);
let _ = narf_net::iface::set_software_gro(e1000_ifname(), true);
// Opt into zero-copy TX (P-F): the stack builds egress frames straight into
// a pooled DMA buffer and hands it to `e1000_send_frame_zc`, which DMAs it
// without the per-frame slice copy `e1000_send_frame` does. The 64 KiB
Expand Down Expand Up @@ -2136,7 +2136,7 @@ async fn e1000_rx_pump(device: Arc<E1000>) {
}

// Empty ring is the NAPI-complete point for software GRO. This is a
// no-op while GRO remains disabled.
// no-op when this CPU holds no GRO flows.
narf_net::tcp_stack::gro_flush();
let missed = device.read_mpc();
if missed > 0 {
Expand Down
289 changes: 219 additions & 70 deletions drivers/net/igc/src/igc.rs

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions drivers/net/specification/high-throughput-datapath.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ progress toward line rate.

| Driver | Speed | Today | Target work |
|---------|-------|-------|-------------|
| e1000 | 1 G | 256-entry pipelined zero-copy TX; 16 KiB IPv4 TSO; unified RX dispatch | multi-queue/GRO if justified; reference-simple, not a high-speed target |
| igc | 2.5 G | basic | batched datapath |
| e1000 | 1 G | 256-entry pipelined zero-copy TX; 64 KiB IPv4 TSO; batched RX + software GRO | reference-simple, not a high-speed target |
| igc | 2.5 G | 256-entry, IRQ-driven, 64-frame batched RX | async/batched TX; multi-queue/RSS |
| ixgbe | 10 G | TSO + csum descriptors present, unused by stack | drive TSO/csum from stack; mq/RSS; NAPI |
| i40e | 40 G | caps advertise TSO/csum | same + deep rings |
| mlx5 | 100 G | RSS RQT present | full mq/RSS + TSO/GRO + moderation; the 100G reference |
| virtio-net | host-bound | mq, NAPI, RSS, mergeable bufs | batched drain + GRO; the in-VM high-throughput path |
| virtio-net | host-bound | mq, NAPI, RSS, mergeable bufs, per-interface GRO | the in-VM high-throughput path |

## 4. Phased implementation (each phase lands and is testable on its own)

Expand All @@ -144,7 +144,9 @@ progress toward line rate.
extensions (P-B). _Landed:_ the `net::gro` engine plus live wiring into
`tcp_stack::rx_handler` — per-CPU, iface-tagged, sitting after XDP/AF_PACKET
and before L3, flushed at NAPI-complete (`drain_pump` idle, virtio park).
Opt-in (`set_gro`) and loopback-bypassed, so the default path is unchanged.
Opt-in per interface (`iface::set_software_gro`) and loopback-bypassed, so
enabling one driver's poll loop cannot change another interface's receive
semantics.
- **P4 — Multi-queue + RSS + NAPI/moderation.** Per-queue contract, per-core
pollers, RSS steering, shared NAPI helper, ITR (P-D, P-E). _Landed:_ the
first-class contract (`Interface`/`HwNic` `num_rx_queues`/`num_tx_queues`/
Expand Down
16 changes: 14 additions & 2 deletions drivers/net/specification/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ batch. It retains a bounded polling fallback for lost or unavailable
interrupts. The shared completion vector starts with the legacy e1000 Linux
policy of 20,000 interrupts/second (ITR/EITR0 interval 195, about 50
microseconds) instead of generating an interrupt for every received frame.
Registering an e1000 enables the stack's bounded software GRO path; the driver
flushes held flows at RX-ring quiescence.
Registering an e1000 opts that interface into the stack's bounded software GRO
path; the driver flushes held flows at RX-ring quiescence. GRO selection is
per-interface, so another NIC sharing the CPU bypasses GRO unless its own RX
poll loop also opts in and supplies a completion flush.

For Ethernet/IPv4/TCP, e1000 advertises a bounded 64 KiB TSO frame. The public
`TxContextDesc::ipv4_tso` constructor encodes the 8254x context descriptor;
Expand All @@ -81,6 +83,16 @@ feature assigns the first registered NIC QEMU SLIRP's `10.0.2.15/24` lease and
`10.0.2.2` gateway at `Stage::Late`; production networking leaves address
selection to userspace.

IGC uses the I225/I226 extended interrupt block (`ICR` 0x1500, `EIMS`/`EIMC`,
`EIAC`/`EIAM`, `EITR0`, `IVAR0` and `IVAR_MISC`) rather than the legacy e1000
interrupt aliases. Its current single queue has 256 RX descriptors. Queue-0
RX, TX and other causes share MSI-X entry 0; the initial EITR interval is 648
256-ns units (Linux's approximately 6,000 interrupts/second starting point).
The RX worker snapshots its IRQ waiter before checking descriptors, drains at
most 64 frames per round, rearms all consumed descriptors with one RDT write,
then reenables and rechecks the vector before parking. A 10-ms lost-interrupt
deadline and 1-ms no-vector polling fallback preserve forward progress.

### 3.1 i40e runtime profile

Each X710/XL710/XXV710/X722 PF negotiates 1–8 64-entry TX/RX pairs.
Expand Down
4 changes: 4 additions & 0 deletions drivers/virtio/specification/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ blk/net/console/rng.
native parallelism the moment we touch real hardware. `class_blk`
and `class_net` modules consume the per-queue rings and present
the unified block/net subsystem interfaces.
- The primary virtio-net interface opts into the kernel stack's
per-interface software GRO stage. Every RX forwarder flushes the current
CPU's held flows after its queue drains and before it parks; secondary
frame-ring-only interfaces do not alter the kernel-stack setting.
- virtio-gpu exposes `offered_features()`, `host_offers_virgl()`, and
`virgl_enabled()`. The first two report the pre-negotiation host offer;
`virgl_enabled()` is true only after immutable negotiation accepts
Expand Down
7 changes: 6 additions & 1 deletion drivers/virtio/src/net_pci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,10 @@ fn register_net_interface(idx: usize, bus_info: alloc::string::String) {
let name = narf_net::iface::register_static("eth%d", mac, vnet0_send_fn);
// `virtnet_get_drvinfo`: driver "virtio_net", VIRTNET_DRIVER_VERSION.
narf_net::iface::set_driver_info(name, "virtio_net", Some("1.0.0"), bus_info);
// The primary RX forwarder drains complete bursts and calls
// `gro_flush` before it parks, so this interface can safely opt into
// the stack's per-interface software GRO path.
let _ = narf_net::iface::set_software_gro(name, true);
*PRIMARY_IFNAME.lock() = Some(name);
// Opt into zero-copy TX (P-F), sharing this controller's own TX pool so
// the stack's `tx_acquire` and the TX reaper use one free-list. 12-byte
Expand Down Expand Up @@ -1803,7 +1807,8 @@ fn register_net_interface(idx: usize, bus_info: alloc::string::String) {
// delivered. Only pair/idx 0 feeds GRO (it owns the synchronous
// tap into the stack), so only it needs draining. This runs on
// the forwarder's own CPU — the same CPU the offers ran on — so
// it drains that CPU's per-CPU coalescer. No-op when GRO is off.
// it drains that CPU's per-CPU coalescer. No-op when that CPU
// holds no GRO flows.
if idx == 0 {
narf_net::tcp_stack::gro_flush();
}
Expand Down
11 changes: 11 additions & 0 deletions net/specification/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ when a device advertises the generic limit.
is false. These fields describe the in-kernel frame-ring contract; they do not
change the stack-daemon attach wire ABI.

Software GRO is an explicit per-interface receive property:

```rust
pub fn iface::set_software_gro(name: &str, enabled: bool) -> bool;
```

New interfaces default to disabled. A driver opts in only when every RX path
feeding that interface flushes the current CPU's GRO table at poll completion.
Enabling one interface never changes receive semantics for another interface;
`NetIfaceSnapshot::software_gro` records the selected state.

### 3.3 Control-plane operations

```rust
Expand Down
45 changes: 44 additions & 1 deletion net/src/iface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ pub struct NetIfaceEntry {
/// checksum/TSO offload via [`crate::TxMeta`] instead of doing the work
/// in software.
pub offloads: crate::OffloadCapabilities,
/// Whether this interface opts into the kernel stack's software GRO stage.
/// This is deliberately per-interface: one GRO-capable driver must not
/// change receive semantics for another driver's poll loop.
pub software_gro: bool,
/// Zero-copy TX hook (P-F): `Some` once the driver opts in via
/// [`enable_zero_copy_tx`]. When set, the stack builds egress frames
/// directly into a buffer from `tx_pool` and hands ownership here instead
Expand Down Expand Up @@ -187,6 +191,22 @@ pub fn set_offloads(name: &str, offloads: crate::OffloadCapabilities) {
}
}

/// Enable or disable software GRO for one registered interface.
///
/// Drivers opt in only when their RX path flushes GRO at poll completion.
/// Returns `false` when `name` is not registered.
pub fn set_software_gro(name: &str, enabled: bool) -> bool {
let mut g = IFACES.lock();
let Some(entry) = g
.as_mut()
.and_then(|ifaces| ifaces.iter_mut().find(|entry| entry.name == name))
else {
return false;
};
entry.software_gro = enabled;
true
}

/// Opt an interface into zero-copy TX (P-F) with an iface-owned recycling pool
/// of `cap` buffers of `buf_len` bytes and no head-room — the shape for a
/// driver (e1000) whose TX is synchronous and recycles via [`tx_release`]. The
Expand Down Expand Up @@ -562,6 +582,7 @@ fn register_inner(
offloads: old
.as_ref()
.map_or_else(Default::default, |old| old.offloads),
software_gro: old.as_ref().is_some_and(|old| old.software_gro),
// A re-probe preserves any zero-copy TX opt-in (hook + pool + headroom)
// so the stack keeps the same recycling pool across a driver reset.
send_frame: old.as_ref().and_then(|old| old.send_frame),
Expand Down Expand Up @@ -1014,6 +1035,8 @@ pub struct NetIfaceSnapshot {
/// Offloads this interface's send path honors; the stack requests them
/// via [`crate::TxMeta`] rather than computing in software.
pub offloads: crate::OffloadCapabilities,
/// Whether ingress on this interface uses software GRO.
pub software_gro: bool,
/// Zero-copy TX hook, mirrored from the entry. `Some` ⇒ the stack may hand
/// an owned [`crate::Frame`] to [`Self::xmit_frame`]; private so every
/// transmit still passes through the snapshot's tap-aware methods.
Expand Down Expand Up @@ -1136,6 +1159,7 @@ fn snapshot(entry: &NetIfaceEntry) -> NetIfaceSnapshot {
duplex: entry.duplex,
stats: entry.stats.clone(),
offloads: entry.offloads,
software_gro: entry.software_gro,
send_frame: entry.send_frame,
}
}
Expand Down Expand Up @@ -1628,7 +1652,7 @@ pub fn drain_pump() -> bool {
// A round that drained nothing is the end of a receive burst (the
// NAPI-complete point): flush any TCP segments GRO is still holding so
// coalesced data is never stranded waiting for a next segment that may not
// come. A no-op when GRO is disabled or this CPU holds nothing.
// come. A no-op when this CPU holds no GRO flows.
if !any {
crate::tcp_stack::gro_flush();
}
Expand Down Expand Up @@ -1680,4 +1704,23 @@ mod zc_tx_tests {
TestResult::Pass
}
kernel_test_in!("net/iface", smoke_iface_zero_copy_tx_acquire);

fn smoke_iface_software_gro_is_per_device() -> TestResult {
let enabled = register("gro-enabled-selftest", [2, 0, 0, 0, 0, 2], dummy_send);
let disabled = register("gro-disabled-selftest", [2, 0, 0, 0, 0, 3], dummy_send);
if !set_software_gro(&enabled, true) {
return TestResult::Fail("software GRO opt-in rejected a registered interface");
}
if lookup(&enabled).is_none_or(|iface| !iface.software_gro) {
return TestResult::Fail("software GRO opt-in was not recorded");
}
if lookup(&disabled).is_none_or(|iface| iface.software_gro) {
return TestResult::Fail("software GRO leaked to another interface");
}
if set_software_gro("missing-gro-selftest", true) {
return TestResult::Fail("software GRO accepted an unknown interface");
}
TestResult::Pass
}
kernel_test_in!("net/iface", smoke_iface_software_gro_is_per_device);
}
25 changes: 2 additions & 23 deletions net/src/tcp_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,10 @@ pub fn rx_handler(iface_name: &str, frame: &mut [u8]) {
// state across frames, so two paths bypass it and dispatch inline — a
// byte-for-byte match for the pre-GRO code:
// * loopback, which has no poll loop to flush held flows; and
// * the disabled default (GRO stays opt-in until exercised on real
// multi-queue hardware).
// * interfaces that have not opted in (GRO stays disabled by default).
// Linux ref: `napi_gro_receive` sits exactly here — after the `packet_rcv`
// AF_PACKET taps and before the protocol handlers.
if gro_rx::enabled() && iface_name != "lo" {
if ingress.as_ref().is_some_and(|dev| dev.software_gro) && iface_name != "lo" {
gro_rx::offer(iface_name, net_ns_id, frame);
} else {
dispatch_l3(_iface_name, net_ns_id, frame);
Expand Down Expand Up @@ -323,25 +322,15 @@ fn dispatch_l3(iface_name: Option<&str>, net_ns_id: u64, frame: &[u8]) {
mod gro_rx {
use super::{dispatch_l3, iface, IrqSafeSpinLock};
use crate::gro::Gro;
use core::sync::atomic::{AtomicBool, Ordering};

const MAX_CPUS: usize = narf_lib::percpu::MAX_CPUS;
static TABLES: [IrqSafeSpinLock<Gro>; MAX_CPUS] =
[const { IrqSafeSpinLock::new(Gro::new()) }; MAX_CPUS];
static ENABLED: AtomicBool = AtomicBool::new(false);

fn cpu() -> usize {
narf_lib::percpu::current_cpu().min(MAX_CPUS - 1)
}

pub fn set_enabled(on: bool) {
ENABLED.store(on, Ordering::Relaxed);
}

pub fn enabled() -> bool {
ENABLED.load(Ordering::Relaxed)
}

/// Offer one received frame to this CPU's coalescer and dispatch whatever
/// it releases now (0 when the frame was merged/held, 1–2 on a flush). The
/// lock is dropped before dispatch so the protocol handlers never run under
Expand All @@ -366,16 +355,6 @@ mod gro_rx {
}
}

/// Enable or disable software GRO (receive coalescing). Off by default.
pub fn set_gro(on: bool) {
gro_rx::set_enabled(on);
}

/// Whether software GRO is currently enabled.
pub fn gro_enabled() -> bool {
gro_rx::enabled()
}

/// Flush this CPU's held GRO flows. Drivers call this at NAPI-complete / when
/// the RX pump parks, so coalesced data is never stranded waiting for the next
/// segment that may not arrive.
Expand Down
Loading