src/driver/virtio.h
virtio device struct.
Walkthrough, interview notes & deep dive
This header file defines the interface and state tracking for the VirtIO driver within the ixy framework. It provides the necessary abstractions to manage virtualized network interfaces, mirroring the pattern used by physical hardware drivers like ixgbe. The file focuses on defining the concrete device structure and the function prototypes required for the driver's lifecycle and high-performance data path.
The virtio_device structure is the core component of this header. It implements a form of C-style inheritance by embedding struct ixy_device as its first member. This layout is critical for polymorphism: any pointer to a virtio_device can be treated as a pointer to the generic ixy_device base class. To navigate back from the base class to the specific implementation, the header provides the IXY_TO_VIRTIO macro, which utilizes the standard container_of pattern to calculate the base address of the outer structure.
struct virtio_device {
struct ixy_device ixy;
int fd;
void* rx_queue;
void* tx_queue;
void* ctrl_queue;
uint64_t rx_pkts;
uint64_t tx_pkts;
uint64_t rx_bytes;
uint64_t tx_bytes;
};
State management and queues are handled through the fields within struct virtio_device. The fd field typically stores a file descriptor used for device communication, often via VFIO or UIO in a userspace context. Unlike physical NICs that interact with hardware through MMIO registers and rings, VirtIO relies on virt_queue structures. The pointers rx_queue, tx_queue, and ctrl_queue represent these shared memory regions where descriptors are exchanged between the guest driver and the host hypervisor.
Function declarations in this file define the driver's operational API. The virtio_init function is responsible for PCI resource discovery and initializing the VirtQueues. For the data path, virtio_rx_batch and virtio_tx_batch provide vectorized packet processing. These functions are designed for "batching," a key technique in low-level networking that reduces the per-packet overhead of descriptor updates and synchronization. Other prototypes like virtio_read_stats and virtio_set_promisc handle device-specific management tasks.
Interview angles
- Q: How does the driver access implementation-specific data from a generic ixy_device pointer?
- A: It uses the
IXY_TO_VIRTIOmacro, which wrapscontainer_of. This is a standard low-level C technique that usesoffsetofto perform pointer arithmetic, allowing the driver to recover thevirtio_devicecontext from the embeddedixymember safely.
- Q: Why are rx_queue and tx_queue defined as void* in the header?
- A: This provides a layer of encapsulation. The internal structure of a VirtQueue (the available, used, and descriptor rings) is complex and implementation-specific. By using
void*in the public header, the details of the VirtIO ring layout are kept within the.cimplementation file, reducing header pollution and compilation dependencies.
Going deeper
Memory layout and aliasing. The order of members in struct virtio_device facilitates zero-cost polymorphism:
struct virtio_device {
struct ixy_device ixy;
int fd;
void* rx_queue;
// ...
};
#define IXY_TO_VIRTIO(ixy_device) container_of(ixy_device, struct virtio_device, ixy)
Since ixy is the first member, the address of the inner struct is numerically identical to the outer virtio_device. The IXY_TO_VIRTIO macro uses container_of to recover the full driver state from a generic ixy_device pointer. This is more robust than a simple cast because it relies on the ixy member name and offset, ensuring the code remains correct even if the struct layout is reorganized later.
Batching and DMA ownership. The batching interface is designed to minimize the cost of per-packet synchronization:
uint32_t virtio_tx_batch(struct ixy_device* dev, uint16_t queue_id, struct pkt_buf* bufs[], uint32_t num_bufs);
By passing an array of pkt_buf*, the driver can update the VirtQueue "Available" ring index once for multiple descriptors, amortizing the expensive MMIO doorbell write. The driver assumes ownership of these buffers for DMA; the caller must not modify or reuse the memory until the device marks the descriptors as "Used," or they risk silent data corruption during transmission.
Harder interview questions
- How does the placement of statistics in the struct impact multi-core performance? The
rx_pktsandtx_pktscounters reside in the same cache line. If RX and TX are processed on different cores, these updates trigger false sharing, forcing the MESI protocol to bounce the cache line between cores and degrading throughput. - **Why use
void*for the queue pointers instead of concrete ring structures?** VirtIO supports multiple ring layouts (Split vs. Packed) and versions (Legacy vs. 1.0+). Usingvoid*provides an opaque handle that prevents the header from being tied to a specific ring specification, shifting the complexity of version-specific memory mapping to the implementation.
Gotchas
- Implicit Padding: On 64-bit systems, the 4-byte
int fdfollowed by 8-byte pointers creates 4 bytes of hidden padding, which can lead to unexpected struct sizes during manual memory offset calculations. - Partial Batches: If
virtio_tx_batchreturns a value less thannum_bufs, the ring is full. The caller is responsible for handling the remainingpkt_bufpointers; ignoring this count results in a memory leak.
From ixy to a production driver
Production shape: virtio.h exposes the educational version of a virtio-net guest driver: one struct virtio_device, RX/TX/control queue pointers, simple counters, and batched virtio_rx_batch / virtio_tx_batch entry points. In Linux, the same role is spread across drivers/net/virtio_net.c, the virtio core, NAPI, ethtool, XDP, MSI-X, and the networking stack. In DPDK, the virtio PMD keeps the poll-mode API but still has a much larger matrix of negotiated features, queue modes, and host backends.
Virtqueue contract: ixy is intentionally close to the split-ring model from the VirtIO spec: a descriptor table, an available ring written by the driver, and a used ring written by the device. That is ideal for interviews because you can explain ownership transfer without hiding behind helper layers. A production driver must also handle modern virtio 1.1 packed virtqueues, where descriptor state is compressed into one ring and wrap counters replace the separate avail/used indices. Knowing this split-versus-packed distinction shows you understand the ABI, not just the C wrapper.
Feature surface: ixy negotiates a small legacy feature set and even comments out multiqueue. Linux virtio-net and DPDK negotiate many VIRTIO_NET_F_* bits: checksum offload with VIRTIO_NET_F_CSUM, guest segmentation/offload paths such as VIRTIO_NET_F_GUEST_TSO4, mergeable receive buffers via VIRTIO_NET_F_MRG_RXBUF, multiple queue pairs via VIRTIO_NET_F_MQ, and RSS via VIRTIO_NET_F_RSS. Each bit changes packet layout or queue semantics, so a production driver cannot treat rx_queue and tx_queue as opaque single-lane pointers forever.
Receive and transmit policy: ixy busy-polls batches from userspace, disables interrupts on the virtqueue, and keeps stats without locking because the teaching target is one fast forwarding loop. Linux uses NAPI to switch between interrupts and polling, supports mergeable RX buffers for packets spanning multiple buffers, has XDP paths, and protects shared state across CPUs. DPDK is closer to ixy philosophically, but its virtio PMD still handles multiqueue, packed rings, in-order paths, and backend-specific details.
Control plane and backends: the ctrl_queue in ixy is enough to make virtio_set_promisc concrete, but production virtio-net uses the control virtqueue for promiscuous/all-multicast mode, MAC filtering, MQ commands, and RSS configuration when negotiated. On the host side, performance often depends on vhost-net or vhost-user, not just the guest driver. Interviewers expect you to connect those pieces: ixy removes MSI-X routing, locking, RSS steering, offload correctness, and backend compatibility so the core lesson stays visible: descriptor ownership plus batched packet movement.
Sources
Source
#ifndef IXY_VIRTIO_H
#define IXY_VIRTIO_H
#include <stdbool.h>
#include "stats.h"
#include "memory.h"
struct virtio_device {
struct ixy_device ixy;
int fd;
void* rx_queue;
void* tx_queue;
void* ctrl_queue;
uint64_t rx_pkts;
uint64_t tx_pkts;
uint64_t rx_bytes;
uint64_t tx_bytes;
};
#define IXY_TO_VIRTIO(ixy_device) container_of(ixy_device, struct virtio_device, ixy)
struct ixy_device* virtio_init(const char* pci_addr, uint16_t rx_queues, uint16_t tx_queues);
uint32_t virtio_get_link_speed(const struct ixy_device* dev);
void virtio_set_promisc(struct ixy_device* dev, bool enabled);
void virtio_read_stats(struct ixy_device* dev, struct device_stats* stats);
uint32_t virtio_tx_batch(struct ixy_device* dev, uint16_t queue_id, struct pkt_buf* bufs[], uint32_t num_bufs);
uint32_t virtio_rx_batch(struct ixy_device* dev, uint16_t queue_id, struct pkt_buf* bufs[], uint32_t num_bufs);
#endif // IXY_VIRTIO_H