Files
genode/repos/os/src/drivers/nic/virtio/component.h
Piotr Tworek 8d5005e03a os: add VirtIO nic driver
The driver is faily simple and does not support fancy features like
TCP checksum offloading or vlan filtering, but it is fully capable of
running every Genode network based scenario I've tried. Its currently
known to work on virt_qemu arm platforms and x86_64.

Fix #3825
2020-10-09 13:35:57 +02:00

524 lines
15 KiB
C++

/*
* \brief VirtIO NIC driver component
* \author Piotr Tworek
* \date 2019-09-27
*/
/*
* Copyright (C) 2019 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* Need to come before attached_rom_dataspace.h */
#include <base/attached_rom_dataspace.h>
#include <base/component.h>
#include <base/heap.h>
#include <base/log.h>
#include <base/signal.h>
#include <irq_session/client.h>
#include <nic/component.h>
#include <root/component.h>
#include <util/misc_math.h>
#include <util/register.h>
#include <virtio/queue.h>
namespace Virtio_nic {
using namespace Genode;
struct Main;
class Root;
class Session_component;
}
class Virtio_nic::Session_component : public Nic::Session_component
{
private:
/*
* Noncopyable
*/
Session_component(Session_component const &);
Session_component &operator = (Session_component const &);
struct Unsupported_version : Genode::Exception { };
struct Device_init_failed : Genode::Exception { };
struct Features_init_failed : Genode::Exception { };
struct Queue_init_failed : Genode::Exception { };
struct Hardware_features
{
Nic::Mac_address mac = { };
bool link_status_available = false;
};
/**
* See section 5.1.6 of VirtIO 1.0 specification.
*/
struct Virtio_net_header
{
enum Flags : Genode::uint16_t
{
NEEDS_CSUM = 1,
};
enum GSO : Genode::uint16_t
{
NONE = 0,
TCPV4 = 1,
UDP = 3,
TCPV6 = 4,
ECN = 0x80,
};
uint8_t flags = 0;
uint8_t gso_type = GSO::NONE;
uint16_t hdr_len = 0;
uint16_t gso_size = 0;
uint16_t csum_start = 0;
uint16_t csum_offset = 0;
uint16_t num_buffers = 0;
};
/**
* VirtIO feature bits relevant to this VirtIO net driver implementation.
*/
struct Features : Genode::Register<64>
{
struct CSUM : Bitfield<0, 1> { };
struct GUEST_CSUM : Bitfield<1, 1> { };
struct MTU : Bitfield<3, 1> { };
struct MAC : Bitfield<5, 1> { };
struct GSO : Bitfield<6, 1> { };
struct GUEST_TSO4 : Bitfield<7, 1> { };
struct GUEST_TSO6 : Bitfield<8, 1> { };
struct GUEST_ECN : Bitfield<9, 1> { };
struct GUEST_UFO : Bitfield<10, 1> { };
struct HOST_TSO4 : Bitfield<11, 1> { };
struct HOST_TSO6 : Bitfield<12, 1> { };
struct HOST_ECN : Bitfield<13, 1> { };
struct HOST_UFO : Bitfield<14, 1> { };
struct MRG_RXBUF : Bitfield<15, 1> { };
struct STATUS : Bitfield<16, 1> { };
struct CTRL_VQ : Bitfield<17, 1> { };
struct CTRL_RX : Bitfield<18, 1> { };
struct CTRL_VLAN : Bitfield<19, 1> { };
struct GUEST_ANNOUNCE : Bitfield<21, 1> { };
struct MQ : Bitfield<22, 1> { };
struct CTRL_MAC_ADDR : Bitfield<23, 1> { };
struct EVENT_IDX : Bitfield<29, 1> { };
struct VERSION_1 : Bitfield<32, 1> { };
};
/**
* See section 5.1.4 of VirtIO 1.0 specification.
*/
enum { CONFIG_MAC_BASE = 0, CONFIG_STATUS = 6 };
enum { STATUS_LINK_UP = 1 << 0 };
/**
* Available VirtIO queue numbers, see section 5.1.2 of VirtIO 1.0 specification.
*/
enum Vq_id : Genode::uint16_t { RX_VQ = 0, TX_VQ = 1 };
/**
* Each VirtIO queue contains fixed number of buffers. The most common size of the buffer
* is 1526 bytes (size of ethernet frame + Virtio_net_header). VirtIO queue size must be
* a power of 2. Each VirtIO queue needs some additional space for the descriptor table,
* available and used rings. The default VirtIO queue parameter values defined here have
* been selected to make Ram_dataspace used by both TX and RX VirtIO queues consume around
* 32Kb of RAM.
*/
static const uint16_t DEFAULT_VQ_SIZE = 16;
static const uint16_t DEFAULT_VQ_BUF_SIZE = 2020;
struct Rx_queue_traits
{
static const bool device_write_only = true;
static const bool has_data_payload = true;
};
struct Tx_queue_traits
{
static const bool device_write_only = false;
static const bool has_data_payload = true;
};
typedef Virtio::Queue<Virtio_net_header, Rx_queue_traits> Rx_queue_type;
typedef Virtio::Queue<Virtio_net_header, Tx_queue_traits> Tx_queue_type;
typedef Genode::Signal_handler<Session_component> Signal_handler;
bool const _verbose;
Virtio::Device &_device;
Hardware_features const _hw_features;
Rx_queue_type _rx_vq;
Tx_queue_type _tx_vq;
Irq_session_client _irq;
Signal_handler _irq_handler;
bool _link_up = false;
void _init_virtio_device()
{
using Status = Virtio::Device::Status;
if (!_device.set_status(Status::RESET)) {
error("Failed to reset the device!");
throw Device_init_failed();
}
if (!_device.set_status(Status::ACKNOWLEDGE)) {
error("Failed to acknowledge the device!");
throw Device_init_failed();
}
if (!_device.set_status(Status::DRIVER)) {
_device.set_status(Status::FAILED);
error("Device initialization failed!");
throw Device_init_failed();
}
}
static Nic::Mac_address _read_mac_address(Virtio::Device &device)
{
Nic::Mac_address mac = {};
/* See section 2.3.1 of VirtIO 1.0 spec for rationale. */
uint32_t before = 0, after = 0;
do {
before = device.get_config_generation();
for (size_t idx = 0; idx < sizeof(mac.addr); ++idx) {
mac.addr[idx] = device.read_config(
CONFIG_MAC_BASE + idx, Virtio::Device::ACCESS_8BIT);
}
after = device.get_config_generation();
} while (after != before);
return mac;
}
Hardware_features _init_hw_features(Xml_node const &xml)
{
_init_virtio_device();
using Status = Virtio::Device::Status;
const uint32_t low = _device.get_features(0);
const uint32_t high = _device.get_features(1);
const Features::access_t device_features = ((uint64_t)high << 32) | low;
Features::access_t driver_features = 0;
/* This driver does not support legacy VirtIO versions. */
if (!Features::VERSION_1::get(device_features)) {
error("Unsupprted VirtIO device version!");
throw Features_init_failed();
}
Features::VERSION_1::set(driver_features);
Hardware_features hw_features;
if (Features::MAC::get(device_features)) {
Features::MAC::set(driver_features);
hw_features.mac = _read_mac_address(_device);
}
hw_features.mac = xml.attribute_value("mac", hw_features.mac);
if (hw_features.mac == Nic::Mac_address()) {
error("HW mac address missing and not provided via config!");
throw Features_init_failed();
}
if (Features::STATUS::get(device_features)) {
Features::STATUS::set(driver_features);
hw_features.link_status_available = true;
}
_device.set_features(0, (uint32_t)driver_features);
_device.set_features(1, (uint32_t)(driver_features >> 32));
if (!_device.set_status(Status::FEATURES_OK)) {
_device.set_status(Status::FAILED);
error("Device feature negotiation failed!");
throw Features_init_failed();
}
return hw_features;
}
Genode::uint16_t _vq_size(Vq_id vq, Xml_node const &xml, char const *cfg_attr)
{
const uint16_t max_vq_size = _device.get_max_queue_size(vq);
if (max_vq_size == 0) {
error("VirtIO queue ", (int)vq, " is not available!");
throw Queue_init_failed();
}
const uint16_t vq_size = Genode::min(xml.attribute_value(cfg_attr, DEFAULT_VQ_SIZE),
max_vq_size);
if (_verbose)
log("VirtIO queue ", (int)vq, " size: ", vq_size, " (max: ", max_vq_size, ")");
return vq_size;
}
uint16_t _buf_size(Vq_id vq, Xml_node const &xml, char const *cfg_attr)
{
const uint16_t vq_buf_size = xml.attribute_value(cfg_attr, DEFAULT_VQ_BUF_SIZE );
if (_verbose)
log("VirtIO queue ", (int)vq, " buffer size: ", Number_of_bytes(vq_buf_size), "b");
return vq_buf_size;
}
void _setup_virtio_queues()
{
if (!_device.configure_queue(RX_VQ, _rx_vq.description())) {
error("Failed to initialize rx VirtIO queue!");
throw Queue_init_failed();
}
if (!_device.configure_queue(TX_VQ, _tx_vq.description())) {
error("Failed to initialize tx VirtIO queue!");
throw Queue_init_failed();
}
using Status = Virtio::Device::Status;
if (!_device.set_status(Status::DRIVER_OK)) {
_device.set_status(Status::FAILED);
error("Failed to initialize VirtIO queues!");
throw Queue_init_failed();
}
}
void _handle_irq()
{
const uint32_t reasons = _device.read_isr();
enum { IRQ_USED_RING_UPDATE = 1, IRQ_CONFIG_CHANGE = 2 };
if (_tx_vq.has_used_buffers())
_tx_vq.ack_all_transfers();
if (reasons & IRQ_USED_RING_UPDATE) {
_receive();
}
if ((reasons & IRQ_CONFIG_CHANGE) && _hw_features.link_status_available &&
(link_state() != _link_up)) {
_link_up = !_link_up;
if (_verbose)
log("Link status changed: ", (_link_up ? "on-line" : "off-line"));
_link_state_changed();
}
_irq.ack_irq();
}
bool _send()
{
if (!_tx.sink()->ready_to_ack())
return false;
if (!_tx.sink()->packet_avail())
return false;
auto packet = _tx.sink()->get_packet();
if (!packet.size() || !_tx.sink()->packet_valid(packet)) {
warning("Invalid tx packet");
return true;
}
if (link_state()) {
Virtio_net_header hdr;
auto const *data = _tx.sink()->packet_content(packet);
if (!_tx_vq.write_data(hdr, data, packet.size(), false)) {
warning("Failed to push packet into tx VirtIO queue!");
return false;
}
}
_tx.sink()->acknowledge_packet(packet);
return true;
}
void _receive()
{
auto rcv_func = [&] (Virtio_net_header const &,
char const *data,
size_t size) {
if (!_rx.source()->ready_to_submit()) {
Genode::warning("Not ready to submit!");
return false;
}
try {
auto p = _rx.source()->alloc_packet(size);
char *dst = _rx.source()->packet_content(p);
Genode::memcpy(dst, data, size);
_rx.source()->submit_packet(p);
} catch (Session::Rx::Source::Packet_alloc_failed) {
Genode::warning("Packet alloc failed!");
return false;
}
return true;
};
while (_rx_vq.has_used_buffers())
_rx_vq.read_data(rcv_func);
/**
* Inform the device the buffers we've just consumed are ready
* to used again
*/
_device.notify_buffers_available(RX_VQ);
}
void _handle_packet_stream() override
{
while (_rx.source()->ack_avail())
_rx.source()->release_packet(_rx.source()->get_acked_packet());
/* Reclaim all buffers processed by the device. */
if (_tx_vq.has_used_buffers())
_tx_vq.ack_all_transfers();
bool sent_packets = false;
while (_send())
sent_packets = true;
if (sent_packets) {
_device.notify_buffers_available(TX_VQ);
}
}
public:
bool link_state() override
{
/**
* According to docs when STATUS feature is not available or has not
* been negotiated the driver should assume the link is always active.
* See section 5.1.4.2 of VIRTIO 1.0 specification.
*/
if (!_hw_features.link_status_available)
return true;
uint32_t before = 0, after = 0;
uint8_t status = 0;
do {
before = _device.get_config_generation();
status = _device.read_config(CONFIG_STATUS, Virtio::Device::ACCESS_8BIT);
after = _device.get_config_generation();
} while (after != before);
return status & STATUS_LINK_UP;
}
Nic::Mac_address mac_address() override { return _hw_features.mac; }
Session_component(Genode::Env &env,
Genode::Allocator &rx_block_md_alloc,
Virtio::Device &device,
Irq_session_capability irq_cap,
Genode::Xml_node const &xml,
Genode::size_t const tx_buf_size,
Genode::size_t const rx_buf_size)
try : Nic::Session_component(tx_buf_size, rx_buf_size, Genode::CACHED,
rx_block_md_alloc, env),
_verbose(xml.attribute_value("verbose", false)),
_device(device),
_hw_features(_init_hw_features(xml)),
_rx_vq(env.ram(), env.rm(),
_vq_size(RX_VQ, xml, "rx_queue_size"),
_buf_size(RX_VQ, xml, "rx_buffer_size")),
_tx_vq(env.ram(), env.rm(),
_vq_size(TX_VQ, xml, "tx_queue_size"),
_buf_size(TX_VQ, xml, "tx_buffer_size")),
_irq(irq_cap),
_irq_handler(env.ep(), *this, &Session_component::_handle_irq),
_link_up(link_state())
{
_setup_virtio_queues();
_irq.sigh(_irq_handler);
_irq.ack_irq();
_link_state_changed();
if (_verbose)
Genode::log("Mac address: ", mac_address());
}
catch (Tx_queue_type::Invalid_buffer_size)
{
error("Invalid TX VirtIO queue buffer size specified!");
throw;
}
catch (Rx_queue_type::Invalid_buffer_size)
{
error("Invalid RX VirtIO queue buffer size specified!");
throw;
}
~Session_component() {
_device.set_status(Virtio::Device::Status::RESET);
}
};
class Virtio_nic::Root : public Genode::Root_component<Session_component, Genode::Single_client>
{
private:
struct Device_not_found : Genode::Exception { };
/*
* Noncopyable
*/
Root(Root const &) = delete;
Root &operator = (Root const &) = delete;
Genode::Env &_env;
Virtio::Device &_device;
Irq_session_capability _irq_cap;
Session_component *_create_session(const char *args) override
{
size_t ram_quota = Arg_string::find_arg(args, "ram_quota" ).ulong_value(0);
size_t tx_buf_size = Arg_string::find_arg(args, "tx_buf_size").ulong_value(0);
size_t rx_buf_size = Arg_string::find_arg(args, "rx_buf_size").ulong_value(0);
/*
* Check if donated ram quota suffices for both communication
* buffers and check for overflow
*/
if (tx_buf_size + rx_buf_size < tx_buf_size ||
tx_buf_size + rx_buf_size > ram_quota) {
error("insufficient 'ram_quota', got ", ram_quota, ", "
"need ", tx_buf_size + rx_buf_size);
throw Genode::Insufficient_ram_quota();
}
Attached_rom_dataspace rom(_env, "config");
try {
return new (md_alloc()) Session_component(
_env, *md_alloc(), _device, _irq_cap, rom.xml(),
tx_buf_size, rx_buf_size);
} catch (...) { throw Service_denied(); }
}
public:
Root(Env &env,
Allocator &md_alloc,
Virtio::Device &device,
Irq_session_capability irq_cap)
: Root_component<Session_component, Genode::Single_client>(env.ep(), md_alloc),
_env(env), _device(device), _irq_cap(irq_cap)
{ }
};