afpacket

package
v0.1.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

afpacket - the one that always works

No hardware requirements, no build tag, no cgo, no setup. If the machine runs Linux, this backend runs: a laptop, a VM, a container, a veth pair in CI.

It is also by far the slowest - roughly 1-2 Mpps, against 66 for Direct Verbs. It is here so that code written against this library runs anywhere, and as the floor the other backends are measured against.

Receive is a TPACKET_V3 mmap ring; transmit is batched sendmmsg. A transmit ring was tried and measured slower, so it is deliberately absent.

What you need

CAP_NET_RAW. That is the whole list.

d, err := afpacket.Open("eth0")
if err != nil {
        log.Fatal(err)
}
defer d.Close()

Unlike mlx5 and dpdk, a bare Open here gives you one queue in each direction, so both examples in the main README run as written.

Options

WithQueues(n) / WithTxQueues(n) / WithRxQueues(n)
WithFrames(n) / WithFrameSize(n)
WithSocketBuffer(bytes)
WithPromiscuous()
WithGSO()                 segmentation and checksum metadata per frame
WithMultiBuffer()         a received packet may span several frames

There is no WithSteering and no affinity option, and both absences are deliberate - see below.

Timestamps

The kernel stamps every frame as it goes into the ring, and the backend hands that time to anyone who asks:

rx, _ := d.RxQueue(0).(packetio.TimestampReceiver)
descs, ts := rx.ReceiveTimestamps(256)

It costs nothing to offer, because the kernel writes the field whether or not it is read. Note that with receive offloads on, the kernel coalesces segments into one super-frame and stamps that once, when it coalesced: a measurement over such a link sees a fraction of the packets it thinks it does. The clock is CLOCK_REALTIME as the frame went into the ring, not a NIC reading off the wire, so it includes the trip up through the kernel and it can step when the wall clock is adjusted. For a time taken on the wire use a card that stamps, such as mlx5.

Offload metadata does not come back on this path: a device opened WithGSO that needs both should use ReceiveOffload. examples/timestamps is a jitter meter built on this and runs anywhere.

Sending from your own memory

A packet does not have to live in this backend's region to be sent:

if g, ok := d.TxQueue(0).(packetio.GatherTransmitter); ok {
        n, err := g.TransmitGather(segs, counts, nil)   // segs are yours
}

sendmmsg copies into the kernel before it returns, so there is nothing to wait for and nothing to hand back: no frame is taken from the pool, no packet pends, and Complete has nothing of this to give. segs holds every packet's slices back to back and counts[i] is how many belong to packet i, so one packet may be as many pieces as you have.

It is for a forwarder whose packets already sit in its own buffers. Copying them into the region first protects against a device reading the memory after the call, and here none does, so it is a copy of every byte for nothing. No backend with a NIC doing its own DMA can offer this: Capabilities().GatherTx says who can.

Receiving a packet too big for a frame

WithGSO makes the kernel hand you TCP super-frames of up to 64 KB whole. The straightforward way to hold one is a 64 KB frame, and that is what WithGSO sets up on its own: 256 of them, 16 MiB. It works, and for a forwarder it is a lot of memory to move a packet through.

WithMultiBuffer is the other answer. Keep small frames and let a big packet lie across as many as it takes:

d, _ := afpacket.Open("eth0", afpacket.WithGSO(),
        afpacket.WithFrameSize(2048), afpacket.WithMultiBuffer())

for _, desc := range d.RxQueue(0).Receive(64) {
        if desc.Options&packetio.OptContinued != 0 {
                // more of this packet follows in the next descriptor
        }
}

Every descriptor but the last of a packet carries OptContinued, the same convention AF_XDP uses. Three things follow from a packet being several descriptors:

  • The metadata is the packet's, not the frame's. ReceiveOffload puts the Offload on the first descriptor; the rest are zero.
  • A partial checksum is yours to finish. An ordinary frame gets its checksum completed here. A chained one cannot: no single frame holds all the bytes the sum covers, so OffloadNeedsCsum stays set and you finish it after you have put the packet back together. Nothing will tell you if you forget - the packet goes out with an unfinished sum.
  • Size your batch for the traffic. A packet needs ceil(len / MaxFrameSize) slots. If it needs more than the whole max you passed, no call of that size could ever deliver it, so it is dropped and counted rather than retried forever. 64 KB over 2 KB frames means max of at least 33.

Transmit has always been able to do this: hand Transmit a chain marked the same way and the kernel gathers it. Capabilities().MultiBuffer reports both halves.

Why there is no steering

AF_PACKET is a tap, not a diversion. The kernel hands your socket a copy and processes the original anyway. A filter here would only choose which copies you receive; it could never keep traffic from the kernel, which is what steering means on every other backend.

Rather than offer something weaker under the same name, this backend offers nothing: its receive queues take everything and you select in your own loop. Capabilities() reports no steering, so a program can ask rather than assume.

That tap behaviour is also the performance story. Point a line-rate flood at an AF_PACKET receiver and the machine burns ~50 cores of softirq to deliver 1.7 Mpps of useful packets - the rest is the kernel dutifully processing (and dropping) the originals of everything you are reading copies of.

Why there is no affinity option

This backend has no per-queue worker placement to configure - no Pin, no placement machinery. Its ceiling is the kernel stack, not core placement, so adding the option names would be uniformity theatre. mlx5, afxdp and dpdk all place workers automatically and accept WithAffinity.

Offload

WithGSO() turns on PACKET_VNET_HDR, so segmentation and checksum metadata travels with each frame and a 64 KB super-frame crosses the device whole:

d, _ := afpacket.Open("eth0", afpacket.WithGSO())
if r, ok := d.RxQueue(0).(packetio.OffloadReceiver); ok {
        descs, offs := r.ReceiveOffload(64)
}

A received frame whose checksum is only the pseudo-header partial is completed on the way out, and a VLAN tag the kernel stripped is put back with the checksum offsets shifted to match.

Performance

64-byte frames on a ConnectX-6 Dx, one core:

rate cores
transmit, one queue 1.7 Mpps 1
transmit, sixteen queues 17.4 Mpps 16
receive, under a line-rate flood 1.4 Mpps 50
receive, offered 12 Mpps 12.0 Mpps 16 sockets
forwarding, under a line-rate flood 0.01 Mpps 50
forwarding, offered 12 Mpps 7.1 Mpps 16 sockets

Those core counts are not a typo and they are the whole story: under a 148.8 Mpps flood the kernel processes every frame whether or not your program reads a copy, so the machine burns 50 cores to hand you 1.4 Mpps.

Note the two pairs of rows. AF_PACKET does not slow down under overload, it collapses. Offered 12 Mpps it takes all of it; offered 25 it takes 3; offered a full 148.8 it takes 1.4 and forwards essentially nothing. That is receive livelock, and it is the one failure mode here that gets worse the harder you push. Every other backend in this repository holds its number under a full flood. If you are using a packet socket, keep the offered load well under its knee.

Transmit does scale with sockets, at about 1.1 Mpps each: 1.7 / 2.5 / 4.6 / 9.0 / 17.4 on 1 / 2 / 4 / 8 / 16. Receive barely moves, because there the ceiling is the kernel's own processing of every frame on the wire and not how many rings you drain it into.

What it is good for

  • Development and CI. The conformance suite runs against this backend on a veth pair, so the contract in BACKENDS.md is verified on every machine with no hardware at all.
  • Tools and probes where a million packets a second is plenty.
  • Portability insurance. Write against the API, ship everywhere, and switch the import on the machines that have the hardware.

Examples

hello uses this backend, is about a hundred lines, does both directions, and runs on any interface:

sudo go run ./examples/hello -i eth0

Documentation

Overview

Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit.

It is the backend that works everywhere. There is no hardware requirement, no driver requirement, no cgo, and nothing to load: if the interface exists and the process has CAP_NET_RAW, this works. That is the whole reason it is here.

It is also, by a wide margin, the slowest of the three. Every packet is copied by the kernel into the ring and copied again out of it, and transmit costs a system call per batch rather than a doorbell write. Expect a couple of million packets per second where github.com/atoonk/packetio/afxdp does tens of millions and github.com/atoonk/packetio/mlx5 does more still. Use it for correctness, for portability, and as the floor to measure the others against, not to fill a 100G link.

There is no receive filter on this backend, deliberately. AF_PACKET is a tap: the kernel processes every packet whether or not a socket takes a copy, so nothing here could keep traffic away from the kernel or steer it toward this program. A filter would only decide what is copied into the ring, and offering that under the same name the steering backends use invites the wrong expectation. The receive queues take everything the interface sees; select in your receive loop, or use a backend that steers.

Like every packetio backend, a queue belongs to one goroutine. Nothing here is synchronised, and two goroutines sharing a queue will corrupt its pool. Different queues of one Device are independent and may run concurrently.

The receive path is a TPACKET_V3 memory-mapped ring; see tpacket.go for how it works and why it is a ring rather than recvmmsg.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is an open AF_PACKET capture of one interface.

Several receive queues means several sockets joined into a PACKET_FANOUT group, so the kernel spreads received packets across them by flow hash. Transmit queues are independent: any socket can send anything.

func Open

func Open(iface string, opts ...Option) (d *Device, err error)

Open captures iface through AF_PACKET.

func (*Device) Capabilities

func (d *Device) Capabilities() packetio.Capabilities

Capabilities describes what this backend can do, which is not much: the kernel copies every packet in both directions, and the only way to spread receive over several queues is the fanout group's flow hash.

func (*Device) Close

func (d *Device) Close() error

Close releases the queues, the rings, the sockets and the frame memory.

It is safe to call on a half-built device, safe to call twice, and safe to call from another goroutine than the one using the queues: closing a receive queue wakes a goroutine blocked in its Poll, which then returns packetio.ErrClosed. It is still not safe to call while a goroutine is part-way through Receive or Transmit, which read memory this unmaps.

func (*Device) Interface

func (d *Device) Interface() string

Interface returns the captured interface's name, and MTU its MTU as it was when the device was opened.

func (*Device) MTU

func (d *Device) MTU() int

MTU is the interface's MTU as it was when the device was opened.

func (*Device) NumRxQueues

func (d *Device) NumRxQueues() int

NumRxQueues is how many receive queues were opened.

func (*Device) NumTxQueues

func (d *Device) NumTxQueues() int

NumTxQueues is how many transmit queues were opened.

func (*Device) Region

func (d *Device) Region() packetio.Region

Region is the frame memory every queue of this device shares.

func (*Device) Rx

func (d *Device) Rx(i int) *RxQueue

Rx returns the concrete receive queue i, or nil when i is out of range.

func (*Device) RxQueue

func (d *Device) RxQueue(i int) packetio.RxQueue

RxQueue returns receive queue i, or nil when i is out of range.

func (*Device) Tx

func (d *Device) Tx(i int) *TxQueue

Tx and Rx return the concrete queues, for the few things specific to this backend: the kernel drop counters and the last transmit errno.

func (*Device) TxQueue

func (d *Device) TxQueue(i int) packetio.TxQueue

TxQueue and RxQueue return queue i, or nil when i is out of range.

type Option

type Option func(*config)

Option configures a Device.

func WithFrameSize

func WithFrameSize(n int) Option

WithFrameSize sets the size of one frame. A packet starts a little way into its frame, so the largest packet is smaller than this; ask Capabilities().MaxFrameSize rather than assuming. It must be a power of two. Without WithGSO the ceiling is the ring's 64 KiB block; the ring delivers packets up to about 65400 bytes (the block less its headers) and counts anything longer oversize. A 16384-byte frame carries a 9000-byte MTU whole.

func WithFrames

func WithFrames(n int) Option

WithFrames sets how many frames the region holds, shared out evenly between the queues.

func WithGSO

func WithGSO() Option

WithGSO turns on PACKET_VNET_HDR, so segmentation and checksum offload metadata travels with each frame in both directions.

With it on, the kernel delivers a TCP super-frame of up to 64 KB whole rather than dropping or truncating it, and a frame transmitted through TxQueue.TransmitOffload is segmented by the kernel instead of here. That is worth a great deal: one descriptor carries what would otherwise be forty packets. The cost is memory, because every frame must now be big enough to hold a super-frame; use WithFrames to keep the region a sensible size.

It also makes the queues implement packetio.OffloadReceiver and packetio.OffloadTransmitter.

func WithMultiBuffer added in v0.1.2

func WithMultiBuffer() Option

WithMultiBuffer lets a received packet span several frames.

Without it a packet too big for one frame is counted oversize and dropped, because a caller who was handed the first frame of one and told nothing would forward a fragment. With it the packet is laid across as many frames as it takes, every one but the last marked packetio.OptContinued, and [Capabilities.MultiBuffer] says so.

It exists for a forwarder whose buffers are smaller than the packets it carries -- a segmentation-offloaded stream arrives in 64 KB super-frames -- so the ring is copied straight into those buffers instead of into one large frame and out of it again. That copy is free while a core has time to spare and is the whole cost once it does not.

Two things become the caller's business. A packet of n bytes needs ceil(n/[Device.MaxFrameSize]) descriptors, so the max passed to Receive must be at least that or the packet is dropped and counted -- 64 KB over 2 KB frames wants 33. And a partial checksum on a chained packet is left unfinished, because no one frame holds all the bytes it covers; the caller finishes it after reassembly, and nothing detects the omission.

func WithPromiscuous

func WithPromiscuous() Option

WithPromiscuous puts the interface into promiscuous mode for as long as the device is open, so it takes packets not addressed to it. The kernel undoes this when the socket closes, including if the process dies.

func WithQueues

func WithQueues(n int) Option

WithQueues sets both directions at once.

func WithRxQueues

func WithRxQueues(n int) Option

WithRxQueues sets how many receive queues to open. More than one joins the sockets into a PACKET_FANOUT group, so the kernel spreads received packets across them by flow hash. Zero is allowed, for a transmit-only device.

func WithSocketBuffer

func WithSocketBuffer(bytes int) Option

WithSocketBuffer sets SO_RCVBUF and SO_SNDBUF on every socket.

func WithTxQueues

func WithTxQueues(n int) Option

WithTxQueues sets how many transmit queues to open. Zero is allowed, for a receive-only device.

type RxQueue

type RxQueue struct {
	// contains filtered or unexported fields
}

RxQueue receives from one socket's TPACKET_V3 ring.

The ring is the buffer the kernel fills, so unlike the other backends there is nothing to hand it in advance: Fill reports how many frames are free to copy into, and Receive copies out of the ring into them. That copy is not the expensive part of this backend, and removing it would mean handing callers pointers into a block the kernel wants back, which is a much worse trade.

One goroutine per queue. Nothing here is synchronised except the wake channel, which exists so Close can be called from another one.

func (*RxQueue) Close

func (q *RxQueue) Close() error

Close stops the queue, waking any goroutine blocked in Poll, and unmaps its ring. The socket belongs to the Device. It is safe to call twice.

func (*RxQueue) Err

func (q *RxQueue) Err() error

Err reports that the queue is out of service, or nil.

A packet socket has no failure that outlives one call: a ring that fills is backpressure and its drops are counted, not a fault. So this is nil until the queue is closed.

func (*RxQueue) Fd added in v0.1.1

func (q *RxQueue) Fd() int

Fd is the receive socket's file descriptor, for a caller that must wait on several queues at once.

Poll covers the ordinary case: one queue, its own wakeup on close. A caller driving several devices from one goroutine needs them all in a single poll(2) alongside its own wake descriptors, and cannot get that by calling Poll per queue. This is the afpacket twin of the afxdp backend's Socket().

The descriptor belongs to the queue: poll it, do not read, close, or otherwise operate on it, and do not use it after Close.

func (*RxQueue) Fill

func (q *RxQueue) Fill(n int) int

Fill reports how many frames are free to receive into. The kernel owns the ring it fills, so there is nothing to post; this exists so that code written against the other backends works unchanged.

func (*RxQueue) NumFreeFillSlots

func (q *RxQueue) NumFreeFillSlots() int

NumFreeFillSlots is how many frames could be received into now. The kernel owns the ring, so a free frame is a free slot.

func (*RxQueue) NumFreeFrames

func (q *RxQueue) NumFreeFrames() int

NumFreeFrames is how many frames are in the free pool.

func (*RxQueue) NumReceived

func (q *RxQueue) NumReceived() int

NumReceived is at least how many packets Receive would return now: it counts the block the ring is on, and Receive drains across every block that is ready, so a busy queue often has more waiting than this reports.

func (*RxQueue) Poll

func (q *RxQueue) Poll(timeout time.Duration) (int, error)

Poll waits for packets and returns how many are ready.

A negative timeout waits indefinitely, zero returns at once, and a positive one waits that long. It returns packetio.ErrClosed if the queue is closed while it waits.

func (*RxQueue) Receive

func (q *RxQueue) Receive(max int) []packetio.Desc

Receive copies up to max packets out of the ring into free frames and returns descriptors naming them. The returned slice is reused by the next call.

With WithMultiBuffer a packet larger than one frame is laid across several, every one but the last carrying packetio.OptContinued; without it such a packet is counted oversize and dropped.

Either way a packet is delivered whole or not at all: half a packet is not a shorter packet, it is a fragment the caller cannot recognise as one. What happens to a packet that will not fit depends on which room ran out. If the frames are momentarily gone, or this batch is simply full, it stays in the ring and arrives next call. If it needs more slots than max, no call of this size can ever take it, so it is counted oversize and dropped rather than retried forever -- waiting would stop the queue for good. Size the batch for the traffic: a packet of n bytes needs ceil(n/[Device.MaxFrameSize]) slots, so a forwarder carrying 64 KB super-frames over 2 KB frames wants max of at least 33.

func (*RxQueue) ReceiveOffload

func (q *RxQueue) ReceiveOffload(max int) ([]packetio.Desc, []packetio.Offload)

ReceiveOffload is Receive, and also returns the segmentation and checksum metadata the kernel reported for each frame. Without WithGSO every Offload is the zero value, which means an ordinary frame.

An ordinary frame delivered with only a partial checksum is finished here, and its OffloadNeedsCsum is cleared, because after that nobody downstream has to finish anything. A super-frame is left alone: its partial is what the segmenter works from, so the flag stays set and the frame keeps its partial all the way to whoever cuts it up. A packet spread across several frames by WithMultiBuffer is likewise left alone, because no one frame holds all the bytes the sum covers: its OffloadNeedsCsum stays set on the first descriptor and finishing it is the caller's job, after the chain is back together. A frame whose offsets did not fit is delivered untouched with the flag still set, and counted in Stats().Backend["bad_checksum"].

It implements packetio.OffloadReceiver.

func (*RxQueue) ReceiveTimestamps added in v0.1.2

func (q *RxQueue) ReceiveTimestamps(max int) ([]packetio.Desc, []uint64)

ReceiveTimestamps is Receive, and also returns when the kernel stamped each frame, in nanoseconds.

The clock is CLOCK_REALTIME: this backend does not offer PACKET_TIMESTAMP, so it is always the kernel's wall-clock reading as the frame went into the ring, not the wire time a NIC would record. Being the wall clock, it can step: an adjustment moves these stamps with it, so a long measurement should treat a large jump as the clock being set rather than as traffic.

Offload metadata is not carried on this path. A device opened WithGSO whose caller needs both should use ReceiveOffload and read the times from its own clock, or open a second queue.

It implements packetio.TimestampReceiver.

func (*RxQueue) Recycle

func (q *RxQueue) Recycle(descs []packetio.Desc)

Recycle returns received frames to the pool. A frame this pool does not own is refused there and counted, not silently accepted.

func (*RxQueue) Region

func (q *RxQueue) Region() packetio.Region

Region is the frame memory this queue receives into.

func (*RxQueue) Stats

func (q *RxQueue) Stats() (packetio.RxStats, error)

Stats reports what this queue received, plus what the kernel dropped on its behalf. The kernel figure is the important one: it is the only evidence that a receiver fell behind, and it is invisible everywhere else.

Packets counts frames, not wire packets: a packet delivered as a chain of several under WithMultiBuffer counts once per frame, which is what keeps it consistent with the pool arithmetic. Backend["oversize"] is every packet dropped for not fitting, and Backend["batch_too_small"] is the part of that caused by the caller's max rather than its frame size -- the part a bigger batch would have fixed.

type TxQueue

type TxQueue struct {
	// contains filtered or unexported fields
}

TxQueue transmits with batched sendmmsg.

A transmit ring (PACKET_TX_RING) was tried in the code this is ported from and measured slower than sendmmsg, so it is deliberately not here: TPACKET_V2 is the only transmit ring the kernel offers, its frames are fixed size, and the doorbell is still a system call. One sendmmsg per batch with iovecs pointing straight into the region is both faster and much simpler.

sendmmsg is synchronous, so a frame the kernel accepted is finished the moment Transmit returns. It is still not handed back until Complete or Reclaim asks for it, because that is the contract every backend shares. The forwarding cycle in the packetio package doc transmits frames belonging to a receive queue and gets them back through Reclaim; a backend that quietly returned them to its own pool would starve that receive queue and alias its frames into this one. So sent frames wait on a pending list -- this backend's stand-in for a ring -- until they are asked for.

One goroutine per queue, as everywhere in packetio. There is no lock: the pool is unsynchronised by design and the scratch slices are reused.

func (*TxQueue) Alloc

func (q *TxQueue) Alloc(n int) []packetio.Desc

Alloc takes up to n frames from this queue's pool, never more than the pending list has room for. The returned slice is reused by the next call.

func (*TxQueue) Close

func (q *TxQueue) Close() error

Close stops the queue. The socket belongs to the Device and is closed there.

func (*TxQueue) Complete

func (q *TxQueue) Complete(max int) int

Complete returns up to max sent frames to this queue's pool and reports how many. Frames that came from a receive queue must go back through Reclaim instead; this pool refuses them.

func (*TxQueue) Err

func (q *TxQueue) Err() error

Err reports that the queue is out of service, or nil.

A packet socket has no failure that outlives one call: a send that fails fails for that batch, and the queue is as usable afterwards as before. So this is nil until the queue is closed, and that is the honest answer rather than a stub that can never say anything.

func (*TxQueue) Free

func (q *TxQueue) Free(descs []packetio.Desc)

Free returns frames to the pool without transmitting them. A frame this pool does not own is refused there and counted, not silently accepted.

func (*TxQueue) LastErrno

func (q *TxQueue) LastErrno() int32

LastErrno returns the errno that ended the most recent failed batch, or zero. Specific to this backend: it is the only way to tell a full device queue from an interface that has gone away.

func (*TxQueue) NumCompleted

func (q *TxQueue) NumCompleted() int

NumCompleted is how many frames Complete or Reclaim would hand back now.

func (*TxQueue) NumFreeFrames

func (q *TxQueue) NumFreeFrames() int

NumFreeFrames is how many frames are in the free pool.

func (*TxQueue) NumFreeSlots

func (q *TxQueue) NumFreeSlots() int

NumFreeSlots is how many more frames Transmit will accept before the pending list is full.

func (*TxQueue) NumInFlight

func (q *TxQueue) NumInFlight() int

NumInFlight is the same number. sendmmsg is synchronous, so everything the kernel took is already on the wire and merely waiting to be handed back; there is no moment when the NIC owns a frame this queue does not.

func (*TxQueue) Reclaim

func (q *TxQueue) Reclaim(max int, out []packetio.Desc) []packetio.Desc

Reclaim hands up to max sent frames to the caller instead of returning them to this queue's pool, for frames that belong to a receive queue.

func (*TxQueue) Region

func (q *TxQueue) Region() packetio.Region

Region is the frame memory this queue draws on.

func (*TxQueue) SendFunc

func (q *TxQueue) SendFunc(count int, build func(i int, frame []byte) int) (int, error)

SendFunc is the whole transmit cycle in one call, as packetio.TxQueue describes it: reclaim, allocate, build, transmit.

func (*TxQueue) Stats

func (q *TxQueue) Stats() (packetio.TxStats, error)

Stats reports this queue's counters. Backend holds "rejected_descs", the descriptors refused before the syscall, and "pool_rejected", frames the pool refused as foreign or surplus; either being non-zero is a bug in the caller.

Packets counts frames, not wire packets: a packet sent as a chain counts once per frame, and a TransmitGather packet once per segment. That is the unit the pool and Completed are in, and mixing the two in one counter would make any rate computed from it move with the traffic mix.

func (*TxQueue) Transmit

func (q *TxQueue) Transmit(descs []packetio.Desc) int

Transmit sends descs and returns how many the kernel accepted, always a prefix. Accepted frames wait for Complete or Reclaim; the unaccepted suffix still belongs to the caller. A packet may be given as several frames, each but the last marked OptContinued: the kernel then gathers them straight out of the region and nothing is copied to make the packet contiguous first. Such a packet is taken whole or not at all, so the prefix this returns never stops inside one. Chained transmit needs no option: a batch is chained if its descriptors say so. Capabilities.MultiBuffer describes the receive side, which does need one -- see WithMultiBuffer.

func (*TxQueue) TransmitGather added in v0.1.2

func (q *TxQueue) TransmitGather(segs [][]byte, counts []int, offs []packetio.Offload) (int, error)

TransmitGather sends packets whose bytes are the caller's, not this queue's.

segs holds every packet's slices back to back and counts[i] says how many belong to packet i; offs, when not nil, is one Offload per packet, zero for an ordinary one. It returns how many packets were accepted, always a prefix, and a packet goes whole or not at all.

There is no ownership to hand over and nothing to reclaim, which is why this can exist at all: sendmmsg copies into the kernel before it returns, so the bytes are the caller's again the moment the call does. Nothing is taken from the pool, nothing waits on the pending list, and Complete has nothing of this to give back.

It exists for a forwarder whose packets already sit in its own buffers. Copying them into this queue's region first would protect nothing -- the region buys safety where a NIC reads memory after the call, and here none does -- while costing a copy of every byte, which is the whole of the difference between this backend and a native one on offloaded traffic.

It implements packetio.GatherTransmitter, and only a backend whose hardware reads nothing after the call can: [Capabilities.GatherTx] says which. mlx5 and dpdk cannot, because their devices read caller memory long after, and it must be registered with them first.

func (*TxQueue) TransmitOffload

func (q *TxQueue) TransmitOffload(descs []packetio.Desc, offs []packetio.Offload) (int, error)

TransmitOffload is Transmit with segmentation and checksum metadata for each frame, so a super-frame is cut up by the kernel rather than here. A zero Offload sends an ordinary frame.

It implements packetio.OffloadTransmitter: it returns the accepted prefix, and an error when a descriptor or its Offload was refused, or when this queue was not opened with WithGSO.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL