Documentation
¶
Overview ¶
Package packetio is a small, backend-neutral Ethernet packet I/O API.
The library moves frames between an application and a NIC with no per-packet allocation, and with no copies on every backend whose mechanism allows it -- of the four, only AF_PACKET copies, because a packet socket is a copy. It is deliberately not a networking framework: it owns queues and buffers, and nothing above that.
Backends ¶
A backend implements Device, TxQueue and RxQueue over some kernel or hardware mechanism:
mlx5 NVIDIA/Mellanox ConnectX and BlueField via mlx5 Direct Verbs.
Queue memory and doorbells are mapped into the process, so the
packet path is ordinary loads and stores plus one MMIO write per
batch: no syscall, no library call, no cgo call per packet.
afxdp Linux AF_XDP, an adapter over github.com/atoonk/go-afxdp. Works
on any driver with XDP support, and keeps the interface usable.
dpdk Any NIC DPDK has a poll-mode driver for -- Intel, Broadcom,
virtio, and the ConnectX too. A custom mempool keeps packetio's
frame-ownership model intact under the driver; the packet path
is one cgo crossing per burst, never one per packet. x86-64
only, behind the dpdk build tag.
afpacket Linux AF_PACKET: a TPACKET_V3 mmap ring on receive and batched
sendmmsg on transmit. Needs no hardware support and no cgo, and
is far slower than the others. It is the fallback that works
anywhere, and the floor the others are measured against.
The model ¶
Every backend exposes the same shape, borrowed from AF_XDP because it maps cleanly onto hardware descriptor rings as well:
- A Region is one contiguous chunk of frame memory the NIC can DMA to and from. It is allocated and registered once, at open.
- A Desc names one frame inside that Region: an offset and a length. It is a value, not a pointer, and it is the unit of ownership.
- Transmit is Alloc, fill, Transmit, Complete. Receive is Fill, Poll, Receive, Recycle. In both directions a frame is owned by exactly one of the pool, the application, or the NIC, and the verbs are the transitions.
What is deliberately absent ¶
The common API describes packet and buffer movement, not the mechanics of a particular kernel interface. AF_XDP's need-wakeup kicks and mlx5's completion queue arming are not methods on TxQueue or RxQueue; a backend performs whatever its hardware or kernel requires inside Transmit, Poll and friends. Where an application genuinely needs backend-specific control, it type asserts for an optional interface such as Offload metadata.
Concurrency ¶
A queue is owned by exactly one goroutine. Two goroutines may drive a TxQueue and an RxQueue of the same Device concurrently; two goroutines may not drive the same queue. This is what keeps the frame pools free of locks.
Index ¶
- Constants
- Variables
- type Capabilities
- type Desc
- type Device
- type GatherTransmitter
- type Match
- func MatchDstIP(p netip.Prefix) Match
- func MatchDstMAC(mac [6]byte) Match
- func MatchDstPort(proto uint8, port uint16) []Match
- func MatchEtherType(t uint16) Match
- func MatchIPProto(p uint8) Match
- func MatchSrcIP(p netip.Prefix) Match
- func MatchSrcPort(proto uint8, port uint16) []Match
- func MatchVLAN(id uint16) Match
- type MatchKind
- type Offload
- type OffloadReceiver
- type OffloadTransmitter
- type Region
- type Rule
- type RxQueue
- type RxStats
- type SteeringFilter
- type TimestampReceiver
- type TxQueue
- type TxStats
Constants ¶
const ( // OptContinued marks a frame that is one fragment of a larger packet, with // at least one more fragment following. The last fragment does not have it. OptContinued uint32 = 1 << 0 // OptChecksumOK reports that the NIC verified the L3 and L4 checksums of a // received frame and found them correct. OptChecksumOK uint32 = 1 << 1 // OptL3ChecksumOK reports that the NIC verified the received packet's IP // header checksum and found it correct. A forwarder that would otherwise // verify the header itself can trust this and skip the work; it says // nothing about the payload. OptL3ChecksumOK uint32 = 1 << 2 // OptionsBackendShift is the first Options bit available to backends. OptionsBackendShift = 16 )
Option bits defined by this package. A backend that cannot express one simply never sets it.
const ( IPProtoTCP uint8 = 6 IPProtoUDP uint8 = 17 )
IP protocol numbers, for MatchIPProto and the port matches.
const ( OffloadNeedsCsum uint8 = 1 // VIRTIO_NET_HDR_F_NEEDS_CSUM OffloadGSONone uint8 = 0 // VIRTIO_NET_HDR_GSO_NONE OffloadGSOTCPv4 uint8 = 1 // VIRTIO_NET_HDR_GSO_TCPV4 OffloadGSOUDP uint8 = 3 // VIRTIO_NET_HDR_GSO_UDP OffloadGSOTCPv6 uint8 = 4 // VIRTIO_NET_HDR_GSO_TCPV6 OffloadGSOECN uint8 = 0x80 // VIRTIO_NET_HDR_GSO_ECN, or'd into GSOType )
Virtio-net header flags and segmentation types (linux/virtio_net.h). These are the values that go on the wire, so they are fixed, not ours to choose.
const MaxRules = 16
MaxRules bounds how many rules one SteeringFilter may expand to. A filter needing more is better expressed as a wider match than as a long list, and every backend has some limit on what it will install.
const MaxVLAN = 4095
MaxVLAN is the largest 802.1Q tag identifier: the field is twelve bits.
const OffloadHdrLen = 10
OffloadHdrLen is the size of the virtio-net header as it appears on the wire in front of a frame, without the mergeable-receive-buffer field.
Variables ¶
var ( // ErrClosed is returned by a method on a queue or device that has been // closed. ErrClosed = errors.New("packetio: closed") // ErrQueueFailed is returned when the hardware reported an error that put // the queue out of service. The queue accepts no further work; close it and // open a new one. ErrQueueFailed = errors.New("packetio: queue failed") // ErrBadLength is returned when a build callback reports a length outside // the frame it was given. Nothing is transmitted. ErrBadLength = errors.New("packetio: build returned invalid length") // ErrUnsupported is returned for an option or operation this backend or // this NIC cannot provide. ErrUnsupported = errors.New("packetio: unsupported") )
Functions ¶
This section is empty.
Types ¶
type Capabilities ¶
type Capabilities struct {
// Backend names the implementation: "mlx5", "afxdp", "afpacket", "dpdk".
Backend string
// ZeroCopy is true when the NIC DMAs directly to and from the Region, with
// no copy in the kernel.
ZeroCopy bool
// KernelCoexistence is true when the kernel keeps its own interface to
// this device while it is open: packets the steering filter does not
// match still reach Linux, so SSH, ARP and monitoring carry on. It is
// false when opening the device took the port away from the kernel
// entirely -- then nothing but these queues sees the wire, whatever the
// filter says. A property of the opened device, not the backend: dpdk
// answers true on a bifurcated ConnectX and false on an Intel card bound
// to vfio-pci. A program must not open a device it also manages itself
// over -- its management NIC -- unless this is true.
KernelCoexistence bool
// MultiBuffer is true when a packet may span several frames, marked with
// OptContinued.
MultiBuffer bool
// RSS is true when receive traffic can be spread over several queues by a
// per-flow hash -- the NIC's RSS, or the kernel's fanout hash where the
// backend is a packet socket. What a caller may rely on is the spreading,
// and that one flow stays on one queue; not where the hash is computed.
RSS bool
// TxChecksumOffload is true when the NIC can compute L3 and L4 checksums on
// transmit.
TxChecksumOffload bool
// Offload is true when the queues implement [OffloadReceiver] and
// [OffloadTransmitter], so segmentation and checksum metadata travels with
// each frame and a super-frame can cross this device whole.
Offload bool
// RxChecksumFlags is true when received descriptors carry OptChecksumOK.
RxChecksumFlags bool
// GatherTx is true when the transmit queues implement
// [GatherTransmitter], so a packet may be sent straight out of the
// caller's memory without being copied into a frame first.
//
// It is false wherever the hardware reads the bytes after the call
// returns, which is every device that does its own DMA: there the memory
// has to be registered first, and a Region is what registered memory
// looks like here.
GatherTx bool
// RxTimestamps is true when the receive queues implement
// [TimestampReceiver], so every packet arrives with the time the device
// recorded for it.
RxTimestamps bool
// BlockingPoll is true when RxQueue.Poll can sleep rather than spin.
BlockingPoll bool
// move between queues without a copy.
SharedRegion bool
// HandsBackFrames is true when TxQueue.Reclaim can name the frames it
// reclaimed and hand them to the caller.
//
// Where it is false the backend cannot see which frames came back -- AF_XDP
// drains its completion ring straight into a pool -- so Reclaim completes
// and returns nothing, and a forwarder must arrange for the frames to reach
// the receive side another way. A forwarder written to the interface should
// check this rather than discover it as a queue that slowly stops
// receiving.
HandsBackFrames bool
// MaxFrameSize is the largest packet one frame carries -- the frame less
// whatever headroom the backend or the kernel keeps in front of it -- and
// MaxQueues the most queues of either direction that can be opened: the
// backend's ceiling, lowered to the device's own limit where the backend
// can see it.
MaxFrameSize int
MaxQueues int
}
Capabilities reports what a Device supports. A field that is false or zero means "not available here", never "unknown".
type Desc ¶
type Desc struct {
// Addr is the byte offset of the frame within the Region. For a received
// frame it points at the first byte of the Ethernet header, which need not
// be the start of the frame if the backend reserved headroom.
Addr uint64
// Len is the number of valid bytes at Addr: the Ethernet frame length,
// excluding the FCS the NIC appends on transmit and strips on receive.
Len uint32
// Options carries backend-defined flags. Bits below OptionsBackendShift are
// reserved for this package; the rest belong to the backend that produced
// the descriptor. Transmit ignores these bits, so a forwarder may pass a
// received descriptor straight back without clearing them.
Options uint32
}
Desc identifies one frame within a Region: where it starts and how many bytes of it are meaningful.
The layout matches AF_XDP's xdp_desc field for field, which keeps the AF_XDP adapter's conversion trivial. It is a field-by-field copy, not a cast: nothing here depends on the two structs having the same memory layout, and nothing should start to without a compile-time assertion.
type Device ¶
type Device interface {
// Capabilities describes what this backend and this NIC can do.
Capabilities() Capabilities
// NumTxQueues and NumRxQueues are how many queues were opened.
NumTxQueues() int
NumRxQueues() int
// TxQueue and RxQueue return queue i, or nil if i is out of range.
TxQueue(i int) TxQueue
RxQueue(i int) RxQueue
// Close shuts down every queue and releases the Region. No queue method may
// be running in another goroutine.
Close() error
}
Device is a NIC opened for packet I/O: a set of queues sharing one Region.
type GatherTransmitter ¶ added in v0.1.2
type GatherTransmitter interface {
TxQueue
// TransmitGather sends packets whose bytes are the caller's. segs holds
// every packet's slices back to back, counts[i] is how many belong to
// packet i, and offs, when not nil, is one Offload per packet.
//
// It returns how many packets were accepted, always a prefix, and takes a
// packet whole or not at all. There is nothing to complete or reclaim:
// the memory is the caller's again as soon as this returns.
TransmitGather(segs [][]byte, counts []int, offs []Offload) (int, error)
}
GatherTransmitter is implemented by a transmit queue that can send a packet out of the caller's own memory, rather than out of frames taken from its pool. Use it through a type assertion, and only where Capabilities.GatherTx is true:
if g, ok := tq.(packetio.GatherTransmitter); ok {
n, err := g.TransmitGather(segs, counts, offs)
}
It exists for a caller whose packets already sit in its own buffers -- a forwarder carrying one packet as several -- for which copying them into the queue's region first is a copy of every byte that buys nothing.
Only a backend whose hardware has finished with the memory by the time the call returns can offer it. AF_PACKET can, because the kernel copies into an skb before sendmmsg returns. A NIC that reads the bytes by DMA long afterwards cannot: its memory has to be registered with the device first, which is what a Region is.
type Match ¶
type Match struct {
Kind MatchKind
// MAC is set for MatchDstMAC.
MAC [6]byte
// VLAN is set for MatchVLAN, EtherType for MatchEtherType, IPProto for
// MatchIPProto, and Port for the port matches.
VLAN uint16
EtherType uint16
IPProto uint8
Port uint16
// Prefix is set for MatchSrcIP and MatchDstIP. A single address is a
// prefix with a full-length mask.
Prefix netip.Prefix
}
A Match is one condition on a packet. Use the Match* constructors; the fields are exported so backends can compile them, not so callers can build them by hand.
func MatchDstIP ¶
MatchDstIP matches packets to an address or a prefix.
func MatchDstMAC ¶
MatchDstMAC matches packets addressed to one Ethernet address.
func MatchDstPort ¶
MatchDstPort matches one destination port of the given protocol; see MatchSrcPort for why it emits the protocol match too.
func MatchEtherType ¶
MatchEtherType matches one EtherType, for example 0x0800 for IPv4.
func MatchIPProto ¶
MatchIPProto matches one IP protocol number, for example IPProtoUDP.
func MatchSrcIP ¶
MatchSrcIP matches packets from an address or a prefix.
func MatchSrcPort ¶
MatchSrcPort and MatchDstPort match one L4 port of the given protocol.
The protocol is part of the match because that is how the hardware works: a card looks for a port at a fixed offset inside a protocol it was told to expect, so a port without a protocol would match the same offset in something else. Both matches are emitted, and a SteeringFilter carrying a port match of one protocol and MatchIPProto of another is refused.
func MatchVLAN ¶
MatchVLAN matches one 802.1Q tag identifier. Only the identifier is matched, so a packet's priority bits do not decide whether it arrives.
An identifier outside the twelve bits of a tag is refused by Validate rather than masked, because masking turns a typed digit into a filter for a VLAN the caller never named.
type Offload ¶
type Offload struct {
// Flags is OffloadNeedsCsum when the L4 checksum is only the pseudo-header
// partial and somebody downstream must finish it.
Flags uint8
// GSOType is OffloadGSONone for an ordinary frame, or one of the
// OffloadGSO* values for a super-frame to be segmented.
GSOType uint8
// HdrLen is how many bytes of headers (L2, L3 and L4 together) are
// replicated into every segment.
HdrLen uint16
// GSOSize is the MSS: payload bytes per segment.
GSOSize uint16
// CsumStart is the offset from the start of the frame to the L4 header,
// and CsumOff the offset from there to the two-byte checksum field.
CsumStart uint16
CsumOff uint16
}
Offload describes segmentation and checksum work that something other than this program will do: the kernel, a NIC, or a virtio peer.
The layout is deliberately virtio_net_hdr, because that is the common currency of every path this library is likely to grow. PACKET_VNET_HDR hands it to an AF_PACKET socket, vhost-user puts it in front of every buffer between guest and device, and tap and memif use the same fields. A backend that speaks any of them can fill this in without translating, and code above packetio does not have to know which one it got.
The point of carrying it at all is throughput. A GSO "super-frame" is one buffer of up to 64 KB that the receiver segments to MTU, so one descriptor does the work of forty. Dropping the metadata means dropping to one packet per MSS, which is most of the difference between line rate on a core and not.
func UnmarshalOffload ¶
UnmarshalOffload reads a virtio-net header from src, which must be at least OffloadHdrLen bytes.
Every field is written by something outside this program -- the kernel, or a guest -- so nothing here is trusted. Callers must bounds-check CsumStart and CsumOff against the frame before using them.
func (Offload) Marshal ¶
Marshal writes the header little-endian into dst, which must be at least OffloadHdrLen bytes.
type OffloadReceiver ¶
type OffloadReceiver interface {
RxQueue
// ReceiveOffload is Receive, and additionally returns one Offload per
// descriptor. Both slices have the same length and are reused by the next
// call, exactly as Receive's is.
ReceiveOffload(max int) ([]Desc, []Offload)
}
OffloadReceiver is implemented by a receive queue that can report per-packet offload metadata. Use it through a type assertion:
if r, ok := rq.(packetio.OffloadReceiver); ok {
descs, offs := r.ReceiveOffload(64)
}
A queue that does not implement it delivers ordinary frames, already segmented by whatever was in front of it.
type OffloadTransmitter ¶
type OffloadTransmitter interface {
TxQueue
// TransmitOffload is Transmit with one Offload per descriptor; a zero
// Offload sends an ordinary frame. It returns how many were accepted,
// always a prefix, and an error when a descriptor or its Offload was
// refused -- an offset past the frame, a segmented frame with no segment
// size, or offs not matching descs in length -- or ErrUnsupported when
// this queue was not opened with offload enabled. A full ring is not an
// error: it is a short return with a nil error, exactly as for Transmit.
TransmitOffload(descs []Desc, offs []Offload) (int, error)
}
OffloadTransmitter is implemented by a transmit queue that can carry offload metadata alongside each frame, so a super-frame is segmented by the kernel or the peer rather than here.
type Region ¶
type Region interface {
// Bytes returns the whole region. The slice aliases the mapping; it is not
// a copy, and writing outside a frame the caller owns corrupts traffic.
Bytes() []byte
// Frame returns the bytes a descriptor names: Bytes()[d.Addr:d.Addr+d.Len].
Frame(d Desc) []byte
// Writable returns the whole of the frame containing d, from d.Addr to the
// end of that frame. Use it to build a packet whose final length is not
// known yet, then set Desc.Len before transmitting.
Writable(d Desc) []byte
// FrameSize is the size of one frame in bytes, and so the largest single
// frame a packet can occupy.
FrameSize() int
// NumFrames is how many frames the region holds.
NumFrames() int
}
Region is a contiguous run of frame memory shared with the NIC. It is allocated and registered once when a Device is opened, which is what pins it, and it stays valid until the Device is closed.
Frames are fixed size and laid out end to end: frame i occupies [i*FrameSize(), (i+1)*FrameSize()). Backends hand out descriptors whose Addr falls inside a frame, not necessarily at its start.
type Rule ¶
type Rule struct {
MAC [6]byte
MACSet bool
VLAN uint16
VLANSet bool
EtherType uint16 // 0 means not matched
IPProto uint8
IPProtoSet bool
SrcPrefix, DstPrefix netip.Prefix // invalid means not matched
SrcPort, DstPort uint16
SrcPortSet, DstPortSet bool
Promiscuous bool
}
A Rule is one conjunction a backend installs: every set field must hold for a packet to match. A SteeringFilter becomes one or more Rules through Rules, and a packet matching any of them is delivered.
Backends compile Rules, not Filters, so the expansion from "these ports on this VLAN" into one rule per port is done once, here, and means the same thing on a card, in an XDP program, and in a classic BPF program.
type RxQueue ¶
type RxQueue interface {
// Region is the frame memory this queue receives into.
Region() Region
// Fill posts up to n frames from the pool for the NIC to receive into and
// returns how many it posted.
Fill(n int) int
// Poll waits until at least one packet has arrived or timeout elapses, and
// returns how many packets are ready.
//
// A zero timeout polls without blocking. A negative timeout waits
// indefinitely, and is only useful on a backend whose Close can wake it --
// Capabilities.BlockingPoll says which. Backends that have no way to block
// spin for up to timeout.
//
// Poll returns ErrClosed if the queue is closed while it waits.
Poll(timeout time.Duration) (int, error)
// Receive takes up to max received packets and returns their descriptors.
// The returned slice is owned by the queue and is reused by the next call.
//
// The frames belong to the caller until Recycle, and belong to it alone:
// no later Receive, no Fill, nothing the queue does in between writes to
// one, hands it out again, or moves it, and Region.Bytes is one mapping
// for the life of the device. So a forwarder may carry received frames
// through whatever it does next rather than copying them out first.
Receive(max int) []Desc
// Err reports that the queue is out of service, or nil while it is
// healthy. It wraps ErrQueueFailed, and is safe to call from any
// goroutine.
//
// A receive queue that has stopped looks exactly like a quiet link:
// Receive returns nothing either way, and the two want opposite
// responses. A receiver that has seen no packets for a while should ask.
Err() error
// Recycle returns received frames to the pool.
Recycle(descs []Desc)
// NumFreeFillSlots is how many more frames the receive ring can hold.
NumFreeFillSlots() int
// NumReceived is how many packets are ready for Receive right now.
NumReceived() int
// NumFreeFrames is how many frames are in the free pool.
NumFreeFrames() int
// Stats reports counters for this queue. Like Err, it is safe to call
// from another goroutine while the queue's own goroutine drives it.
Stats() (RxStats, error)
// Close releases the queue.
Close() error
}
RxQueue is one hardware receive queue.
An RxQueue is owned by one goroutine. The receive cycle is:
q.Fill(q.NumFreeFillSlots()) // give the NIC frames to receive into n, err := q.Poll(timeout) // wait for packets descs := q.Receive(n) // take them ... use descs ... q.Recycle(descs) // give the frames back
A backend may need frames posted before it can receive anything, so Fill comes first, and a receiver that never recycles will starve itself.
type RxStats ¶
type RxStats struct {
// Packets and Bytes are what was handed to the application by Receive.
Packets uint64
Bytes uint64
// Filled is how many frames were posted for the NIC to receive into.
Filled uint64
// Batches is how many times Receive asked the hardware and was given
// something: one call that returned packets. Packets divided by Batches is
// the effective receive batch size, and it is worth watching.
//
// A receive loop that looks busy while taking a fraction of the offered
// load is usually taking it a handful of packets at a time, paying a
// call's fixed cost over too few of them and leaving the card short of
// posted buffers between bursts. Nothing else in this struct shows that:
// packets, bytes and drops all look the same whether they arrived sixty
// at a time or five. A backend once lost half its receive rate to exactly
// that, invisibly, because this counter was not published.
Batches uint64
// Polls is how many times Poll was called, and Completions how many
// completion events were consumed.
Polls uint64
Completions uint64
// PoolEmpty counts refills that posted nothing because no frame was free,
// which is the application holding on to received frames too long.
PoolEmpty uint64
// Errors counts error completions and malformed descriptors.
Errors uint64
// Dropped is what the NIC or kernel discarded before the application saw
// it, when the backend can report it. It is not always attributable to one
// queue; a backend that cannot tell leaves it zero and reports the device
// wide figure in Backend.
Dropped uint64
// Backend holds counters only this backend has.
Backend map[string]uint64
}
RxStats counts what one receive queue has done.
type SteeringFilter ¶
type SteeringFilter struct {
// Match is the set of conditions a packet must satisfy.
//
// Matches of different kinds are ANDed, and repeated matches of one kind
// are alternatives. Two MatchDstPort and one MatchVLAN means "either port,
// on that VLAN" -- two rules. A filter that needs more than MaxRules of
// them is refused rather than narrowed.
Match []Match
// Promiscuous takes every packet the port sees, whoever it is addressed
// to, and ignores Match entirely.
Promiscuous bool
}
A SteeringFilter says which packets are steered to a device: taken away from the kernel and delivered to this program's receive queues. Packets it does not match are left to the kernel, so the interface keeps working -- your SSH session, ARP, and everything else carry on while you take the traffic you asked for.
That second half is a property of the device, not of the filter: Capabilities.KernelCoexistence says whether the kernel still has the interface at all. Where it is false -- DPDK on a device bound to vfio-pci -- the filter still selects what these queues see, but there is no kernel for the rest to carry on to.
Steering is a property of the backend. mlx5 compiles it to hardware flow rules the card matches at no cost per packet; AF_XDP to an eBPF program. AF_PACKET has no steering at all: it is a tap, the kernel sees every packet whatever a socket takes, and so it offers no SteeringFilter rather than one that would mean something weaker under the same name. A backend that cannot express a match refuses it at Open with ErrUnsupported rather than installing a wider one -- a filter that silently delivers more than it was asked for is worse than none, because nothing downstream can tell.
The zero SteeringFilter means "use the backend's default", which is packets addressed to this interface.
func (SteeringFilter) Rules ¶
func (f SteeringFilter) Rules() ([]Rule, error)
Rules expands a filter into the conjunctions a backend installs.
Within a SteeringFilter, repeated matches of one kind are alternatives and different kinds are ANDed: two MatchDstPort and one MatchVLAN is "either port, on that VLAN", and becomes two rules. It validates first, so a contradictory filter is refused before any backend sees it.
func (SteeringFilter) String ¶
func (f SteeringFilter) String() string
String renders a filter for the startup line.
func (SteeringFilter) Validate ¶
func (f SteeringFilter) Validate() error
Validate reports whether a filter is self-consistent, independent of any backend. It catches the contradictions that would otherwise become a rule meaning something other than what was asked.
type TimestampReceiver ¶ added in v0.1.2
type TimestampReceiver interface {
RxQueue
// ReceiveTimestamps is Receive, and additionally returns the arrival time
// of each packet in nanoseconds. Both slices have the same length and are
// reused by the next call, exactly as Receive's is.
//
// The clock is the device's, not the wall's: it advances with an epoch
// nobody promises, so two timestamps from one queue may be subtracted and
// a timestamp on its own means nothing. Comparing across devices needs
// them disciplined to a common source, which this package does not do,
// and neither does anything here tell you when a packet was SENT: no such
// time travels in a packet. What can be measured is time on this machine
// -- how long a packet was held before it went back out, how long it sat
// between the device and this code, and how evenly traffic arrived.
//
// Both slices belong to the queue and are overwritten by the next receive
// call on it, including a plain Receive. Copy what must outlive that.
//
// A timestamp says when a packet ARRIVED, which is not the same as the
// order it was delivered in. A NIC stamps at the port and places into the
// queue afterwards, and when it is dropping -- offered more than it can
// place -- the two orders come apart: measured on a ConnectX-6 Dx, stamps
// rise packet by packet at rates the card keeps up with, and about a third
// of them arrive out of order once it is discarding two thirds of the
// wire. That is the hardware answering honestly about arrival, so code
// that needs ordering must sort, and code measuring the wire should prefer
// the stamps to the order they came in.
ReceiveTimestamps(max int) ([]Desc, []uint64)
}
TimestampReceiver is implemented by a receive queue whose device records when each packet arrived. Use it through a type assertion:
if r, ok := rq.(packetio.TimestampReceiver); ok {
descs, ts := r.ReceiveTimestamps(64)
}
The point of a timestamp taken by the device is that it is not a measurement of this program. A NIC stamps a frame as it arrives at the port, before the DMA, before the completion, before any of this code runs; the interval between two of them is what happened on the wire, and it stays true however long a receive loop was busy elsewhere. Reading the clock in the receive loop instead measures the loop.
Capabilities.RxTimestamps is the authority on whether a device really stamps, and is worth asking first: a queue may carry the method while its device has no clock, and it then returns nothing rather than inventing zeroes.
type TxQueue ¶
type TxQueue interface {
// Region is the frame memory this queue draws from.
//
// Whether it is the same Region as another queue's is
// Capabilities.SharedRegion. Where it is, a frame received on one queue
// may be transmitted on another without a copy; where it is not -- AF_XDP
// maps one region per socket -- moving a frame between queues means
// copying it. A queue refuses a foreign descriptor where it can tell --
// the address is outside its region -- but two regions of one size look
// alike to a bounds check, so keeping descriptors with the device they
// came from is the caller's half of the contract.
Region() Region
// Alloc takes up to n frames from the queue's free pool and returns
// descriptors for them, with Len set to zero. It returns fewer than n, or
// none at all, when the pool or the transmit ring is short.
//
// The returned slice is owned by the queue and is reused by the next call
// to Alloc; copy it if it must outlive that.
Alloc(n int) []Desc
// Transmit hands descriptors to the NIC and returns how many it accepted,
// always a prefix of descs -- and, where a backend carries a packet as
// several frames chained with OptContinued, a prefix of whole packets:
// half a packet accepted would leave the caller with a tail nothing can
// interpret. Frames in the accepted prefix now belong to
// the NIC and must not be touched until Complete or Reclaim returns them.
// Frames in the unaccepted suffix still belong to the caller, who must
// transmit them later or return them with Free. No backend returns an
// accepted frame to its pool on its own.
//
// Every descriptor must name at least one byte inside one frame of the
// Region. A backend that checks refuses the first descriptor that does
// not, returning the prefix before it, so a short return with a free ring
// points at the offending descriptor.
//
// Transmit publishes the batch to the hardware before it returns; there is
// no separate kick or flush step.
//
// A short return is backpressure, not an error: the ring is full, or a
// descriptor was refused. Ask Err whether the queue is still alive.
Transmit(descs []Desc) int
// Err reports that the queue is out of service, or nil while it is
// healthy. It wraps ErrQueueFailed, and is safe to call from any
// goroutine.
//
// A dead queue and a full one both make Transmit return zero, so without
// this a caller cannot tell "try again in a moment" from "this will never
// work again" -- and the second one looks exactly like a slow link
// forever. A caller that loops on Transmit should ask after a run of
// zeroes. Close it and open a new device; nothing revives a failed queue.
Err() error
// Complete reclaims frames whose transmission has finished, returning them
// to the pool, and reports how many it reclaimed.
//
// max bounds the work rather than the result: a backend stops looking once
// it has that many, but it will not split what one completion covers, and
// on a backend that reports one completion per batch that means a whole
// batch comes back at once. Pass the largest number that is useful and
// treat the result as the answer.
Complete(max int) int
// Reclaim is Complete for frames that belong somewhere else: it takes up
// to max frames whose transmission has finished and appends them to out
// instead of returning them to this queue's pool. The caller owns them
// and must Recycle them to the receive queue they came from, or Free them
// here. max bounds the work the same way it does for Complete.
Reclaim(max int, out []Desc) []Desc
// Free returns frames to the pool without transmitting them. It is for
// descriptors Alloc handed out that will not be sent, and for the suffix
// Transmit did not accept.
Free(descs []Desc)
// NumCompleted is how many frames Complete would reclaim right now.
NumCompleted() int
// NumInFlight is how many frames the NIC currently owns.
NumInFlight() int
// NumFreeSlots is how many more frames the transmit ring can accept.
NumFreeSlots() int
// NumFreeFrames is how many frames are in the free pool.
NumFreeFrames() int
// SendFunc is the whole transmit cycle in one call: it reclaims
// completions, takes up to count frames, calls build for each, and
// transmits them. build writes into frame and returns the packet length.
// Zero means there is nothing more to send: the batch ends there and what
// was built is transmitted. A length below zero or past len(frame) is
// ErrBadLength: the entire batch is abandoned, every frame returns to the
// pool, and nothing is transmitted. SendFunc reports how many packets
// reached the NIC.
SendFunc(count int, build func(i int, frame []byte) int) (int, error)
// Stats reports counters for this queue. Like Err, it is safe to call
// from another goroutine while the queue's own goroutine drives it --
// that is what it is for, and every example does it.
Stats() (TxStats, error)
// Close releases the queue. It is not safe to call while another goroutine
// is inside any other method of this queue.
Close() error
}
TxQueue is one hardware transmit queue.
A TxQueue is owned by one goroutine. The transmit cycle is:
q.Complete(q.NumCompleted()) // reclaim frames the NIC is done with
descs := q.Alloc(n) // take frames from the pool
for i := range descs { // fill them
b := q.Region().Writable(descs[i])
descs[i].Len = uint32(build(b))
}
sent := q.Transmit(descs) // hand them to the NIC
Alloc never returns more frames than the following Transmit can accept, so a caller that transmits exactly what it allocated can never leak a frame.
A forwarder sends frames it received rather than frames it allocated. Every queue of a Device shares one Region, so that needs no copy, but the frames belong to the receive queue's pool and must find their way back there:
descs := rx.Receive(n) // frames from the receive pool ... rewrite them in place, set Len ... sent := tx.Transmit(descs) // the NIC owns them now back = tx.Reclaim(max, back[:0]) rx.Recycle(back) // home again rx.Fill(rx.NumFreeFillSlots())
Complete would put them on the transmit pool instead, where the receive queue can never find them again. A receive queue and every transmit queue that carries its frames must be driven by the same goroutine, because the pools are not synchronised.
type TxStats ¶
type TxStats struct {
// Packets and Bytes are what reached the NIC: frames accepted by Transmit,
// and the sum of their lengths.
Packets uint64
Bytes uint64
// Completed is how many frames the NIC has finished sending and Complete
// has reclaimed. Packets minus Completed is what the NIC still owns.
Completed uint64
// Batches is how many times a batch was published to the hardware: one
// doorbell on mlx5, one kick or suppressed kick on AF_XDP. Packets divided
// by Batches is the effective batch size.
Batches uint64
// Completions is how many completion events were consumed: completion queue
// entries on mlx5, completion ring entries on AF_XDP. On a backend that
// signals once per batch this is far smaller than Completed.
Completions uint64
// RingFull counts calls that could queue nothing because the transmit ring
// had no free slots, and PoolEmpty calls that could allocate nothing
// because every frame was in flight. Both mean the NIC is the limit.
RingFull uint64
PoolEmpty uint64
// Errors counts hardware or kernel errors reported for this queue:
// error completions on mlx5, failed kicks on AF_XDP.
Errors uint64
// Backend holds counters only this backend has. Keys are lowercase and
// stable within a backend, for example mlx5's "cqe_err" or AF_XDP's
// "tx_invalid_descs".
Backend map[string]uint64
}
TxStats counts what one transmit queue has done. Counters are cumulative since the queue was opened and never reset.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit.
|
Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit. |
|
Package afxdp is the packetio backend for Linux AF_XDP.
|
Package afxdp is the packetio backend for Linux AF_XDP. |
|
dpdk
|
|
|
internal/mbuf
Package mbuf reads and writes the DPDK packet buffer header from Go.
|
Package mbuf reads and writes the DPDK packet buffer header from Go. |
|
internal/queue
Package queue holds the frame-ownership logic of the DPDK backend, with no cgo and no DPDK in sight.
|
Package queue holds the frame-ownership logic of the DPDK backend, with no cgo and no DPDK in sight. |
|
examples
|
|
|
hello
command
Command hello is the smallest complete packetio program: it sends one frame and prints the next few it receives.
|
Command hello is the smallest complete packetio program: it sends one frame and prints the next few it receives. |
|
internal/affinity
Package affinity pins a goroutine to a CPU.
|
Package affinity pins a goroutine to a CPU. |
|
internal/frame
Package frame builds the Ethernet frames the examples transmit.
|
Package frame builds the Ethernet frames the examples transmit. |
|
internal/metrics
Package metrics measures what a run cost: how busy the processors were and what the NIC's own counters say.
|
Package metrics measures what a run cost: how busy the processors were and what the NIC's own counters say. |
|
internal/prefetch
Package prefetch hints frame memory into the cache ahead of first use.
|
Package prefetch hints frame memory into the cache ahead of first use. |
|
steer
command
Command filter receives with a filter and reports what arrives, by destination port, so you can see the filter doing its job.
|
Command filter receives with a filter and reports what arrives, by destination port, so you can see the filter doing its job. |
|
sweep
command
sweep drives every backend through the same three loops -- transmit, receive, forward -- so a comparison between them measures the backend and not four different programs.
|
sweep drives every backend through the same three loops -- transmit, receive, forward -- so a comparison between them measures the backend and not four different programs. |
|
timestamps
command
Command timestamps shows when packets actually arrived, using the time the device recorded for each one rather than the time this program got round to looking.
|
Command timestamps shows when packets actually arrived, using the time the device recorded for each one rather than the time this program got round to looking. |
|
internal
|
|
|
affinity
Package affinity decides where a backend's workers run.
|
Package affinity decides where a backend's workers run. |
|
conform
Package conform is the shared test suite every packetio backend must pass.
|
Package conform is the shared test suite every packetio backend must pass. |
|
pool
Package pool holds the free-frame list shared by packetio backends.
|
Package pool holds the free-frame list shared by packetio backends. |
|
mlx5
|
|
|
internal/arch
Package arch holds the memory-ordering and memory-mapped I/O primitives the mlx5 packet path needs.
|
Package arch holds the memory-ordering and memory-mapped I/O primitives the mlx5 packet path needs. |
|
internal/clock
Package clock turns the tick counter in a completion into nanoseconds.
|
Package clock turns the tick counter in a completion into nanoseconds. |
|
internal/mocknic
Package mocknic is a software model of the part of an mlx5 NIC that a send or receive queue talks to.
|
Package mocknic is a software model of the part of an mlx5 NIC that a send or receive queue talks to. |
|
internal/ring
Package ring drives an mlx5 send or receive queue: it decides what goes in the queue, tracks who owns each frame, and reads completions back.
|
Package ring drives an mlx5 send or receive queue: it decides what goes in the queue, tracks who owns each frame, and reads completions back. |
|
internal/wqe
Package wqe encodes and decodes mlx5 work queue entries and completion queue entries.
|
Package wqe encodes and decodes mlx5 work queue entries and completion queue entries. |
|
netstack
module
|