/* * Copyright (c) 2023 Enphase Energy * * SPDX-License-Identifier: Apache-2.0 */ #include "eth_ivshmem_priv.h" #include #include #include #include #include /* These defines must match on the peer */ #define ETH_IVSHMEM_VRING_ALIGNMENT 64 #define ETH_IVSHMEM_FRAME_SIZE(len) ROUND_UP(18 + (len), L1_CACHE_BYTES) #define VRING_FLUSH(x) sys_cache_data_flush_range(&(x), sizeof(x)) #define VRING_INVALIDATE(x) sys_cache_data_invd_range(&(x), sizeof(x)) static int calc_vring_size( size_t section_size, uint16_t *vring_desc_len, uint32_t *vring_header_size); static uint32_t tx_buffer_advance(uint32_t max_len, uint32_t *position, uint32_t *len); static int tx_clean_used(struct eth_ivshmem_queue *q); static int get_rx_avail_desc_idx(struct eth_ivshmem_queue *q, uint16_t *avail_desc_idx); int eth_ivshmem_queue_init( struct eth_ivshmem_queue *q, uintptr_t shmem, size_t shmem_section_size, bool tx_buffer_first) { memset(q, 0, sizeof(*q)); uint16_t vring_desc_len; uint32_t vring_header_size; int res = calc_vring_size(shmem_section_size, &vring_desc_len, &vring_header_size); if (res != 0) { return res; } q->desc_max_len = vring_desc_len; q->vring_data_max_len = shmem_section_size - vring_header_size; q->vring_header_size = vring_header_size; if (tx_buffer_first) { q->tx.shmem = (void *)shmem; q->rx.shmem = (void *)(shmem + shmem_section_size); } else { q->rx.shmem = (void *)shmem; q->tx.shmem = (void *)(shmem + shmem_section_size); } /* Init vrings */ vring_init(&q->tx.vring, vring_desc_len, q->tx.shmem, ETH_IVSHMEM_VRING_ALIGNMENT); vring_init(&q->rx.vring, vring_desc_len, q->rx.shmem, ETH_IVSHMEM_VRING_ALIGNMENT); /* Swap "used" pointers. * This is done so that each peer only ever writes to its output section, * while maintaining vring code consistency elsewhere in this file. */ struct vring_used *tmp_used = q->tx.vring.used; q->tx.vring.used = q->rx.vring.used; q->rx.vring.used = tmp_used; eth_ivshmem_queue_reset(q); return 0; } void eth_ivshmem_queue_reset(struct eth_ivshmem_queue *q) { q->tx.desc_head = 0; q->tx.desc_len = 0; q->tx.data_head = 0; q->tx.data_tail = 0; q->tx.data_len = 0; q->tx.avail_idx = 0; q->tx.used_idx = 0; q->tx.pending_data_head = 0; q->tx.pending_data_len = 0; q->rx.avail_idx = 0; q->rx.used_idx = 0; memset(q->tx.shmem, 0, q->vring_header_size); /* Init TX ring descriptors */ for (unsigned int i = 0; i < q->tx.vring.num - 1; i++) q->tx.vring.desc[i].next = i + 1; q->tx.vring.desc[q->tx.vring.num - 1].next = 0; } int eth_ivshmem_queue_tx_get_buff(struct eth_ivshmem_queue *q, void **data, size_t len) { /* Clean used TX buffers */ int res = tx_clean_used(q); if (res != 0) { return res; } if (q->tx.desc_len >= q->desc_max_len) { return -ENOBUFS; } uint32_t head = q->tx.data_head; uint32_t consumed_len = len; uint32_t new_head = tx_buffer_advance(q->vring_data_max_len, &head, &consumed_len); if (q->vring_data_max_len - q->tx.data_len < consumed_len) { return -ENOBUFS; } struct vring_desc *tx_desc = &q->tx.vring.desc[q->tx.desc_head]; tx_desc->addr = q->vring_header_size + head; tx_desc->len = len; tx_desc->flags = 0; VRING_FLUSH(*tx_desc); *data = (uint8_t *)q->tx.shmem + q->vring_header_size + head; q->tx.pending_data_head = new_head; q->tx.pending_data_len = q->tx.data_len + consumed_len; return 0; } int eth_ivshmem_queue_tx_commit_buff(struct eth_ivshmem_queue *q) { /* Ensure that a TX buffer is pending */ if (q->tx.pending_data_len == 0) { return -EINVAL; } uint16_t desc_head = q->tx.desc_head; q->tx.desc_len++; q->tx.desc_head = (q->tx.desc_head + 1) % q->desc_max_len; q->tx.data_head = q->tx.pending_data_head; q->tx.data_len = q->tx.pending_data_len; q->tx.vring.avail->ring[q->tx.avail_idx % q->desc_max_len] = desc_head; VRING_FLUSH(q->tx.vring.avail->ring[q->tx.avail_idx % q->desc_max_len]); atomic_thread_fence(memory_order_seq_cst); q->tx.avail_idx++; q->tx.vring.avail->idx = q->tx.avail_idx; VRING_FLUSH(q->tx.vring.avail->idx); q->tx.pending_data_len = 0; return 0; } int eth_ivshmem_queue_rx(struct eth_ivshmem_queue *q, const void **data, size_t *len) { *data = NULL; *len = 0; uint16_t avail_desc_idx; int res = get_rx_avail_desc_idx(q, &avail_desc_idx); if (res != 0) { return res; } struct vring_desc *desc = &q->rx.vring.desc[avail_desc_idx]; VRING_INVALIDATE(*desc); uint64_t offset = desc->addr - q->vring_header_size; uint32_t rx_len = desc->len; if (offset > q->vring_data_max_len || rx_len > q->vring_data_max_len || offset > q->vring_data_max_len - rx_len) { return -EINVAL; } *data = (uint8_t *)q->rx.shmem + q->vring_header_size + offset; *len = desc->len; return 0; } int eth_ivshmem_queue_rx_complete(struct eth_ivshmem_queue *q) { uint16_t avail_desc_idx; int res = get_rx_avail_desc_idx(q, &avail_desc_idx); if (res != 0) { return res; } uint16_t used_idx = q->rx.used_idx % q->desc_max_len; q->rx.used_idx++; q->rx.vring.used->ring[used_idx].id = avail_desc_idx; q->rx.vring.used->ring[used_idx].len = 1; VRING_FLUSH(q->rx.vring.used->ring[used_idx]); atomic_thread_fence(memory_order_seq_cst); q->rx.vring.used->idx = q->rx.used_idx; VRING_FLUSH(q->rx.vring.used->idx); atomic_thread_fence(memory_order_seq_cst); q->rx.avail_idx++; vring_avail_event(&q->rx.vring) = q->rx.avail_idx; VRING_FLUSH(vring_avail_event(&q->rx.vring)); return 0; } /** * Calculates the vring descriptor length and header size. * This must match what is calculated by the peer. */ static int calc_vring_size( size_t section_size, uint16_t *vring_desc_len, uint32_t *vring_header_size) { static const int eth_min_mtu = 68; uint32_t header_size; int16_t desc_len; for (desc_len = 4096; desc_len > 32; desc_len >>= 1) { header_size = vring_size(desc_len, ETH_IVSHMEM_VRING_ALIGNMENT); header_size = ROUND_UP(header_size, ETH_IVSHMEM_VRING_ALIGNMENT); if (header_size < section_size / 8) { break; } } if (header_size > section_size) { return -EINVAL; } uint32_t vring_data_size = section_size - header_size; if (vring_data_size < 4 * eth_min_mtu) { return -EINVAL; } *vring_desc_len = desc_len; *vring_header_size = header_size; return 0; } static uint32_t tx_buffer_advance(uint32_t max_len, uint32_t *position, uint32_t *len) { uint32_t aligned_len = ETH_IVSHMEM_FRAME_SIZE(*len); uint32_t contiguous_len = max_len - *position; *len = aligned_len; if (aligned_len > contiguous_len) { /* Wrap back to zero */ *position = 0; *len += contiguous_len; } return *position + aligned_len; } static int tx_clean_used(struct eth_ivshmem_queue *q) { while (true) { VRING_INVALIDATE(q->tx.vring.used->idx); if (q->tx.used_idx == q->tx.vring.used->idx) { break; } struct vring_used_elem *used = &q->tx.vring.used->ring[ q->tx.used_idx % q->desc_max_len]; atomic_thread_fence(memory_order_seq_cst); VRING_INVALIDATE(*used); if (used->id >= q->desc_max_len || used->len != 1) { return -EINVAL; } struct vring_desc *desc = &q->tx.vring.desc[used->id]; uint64_t offset = desc->addr - q->vring_header_size; uint32_t len = desc->len; uint32_t tail = q->tx.data_tail; uint32_t consumed_len = len; uint32_t new_tail = tx_buffer_advance(q->vring_data_max_len, &tail, &consumed_len); if (consumed_len > q->tx.data_len || offset != tail) { return -EINVAL; } q->tx.data_tail = new_tail; q->tx.data_len -= consumed_len; q->tx.desc_len--; q->tx.used_idx++; } return 0; } static int get_rx_avail_desc_idx(struct eth_ivshmem_queue *q, uint16_t *avail_desc_idx) { atomic_thread_fence(memory_order_seq_cst); VRING_INVALIDATE(q->rx.vring.avail->idx); uint16_t avail_idx = q->rx.vring.avail->idx; if (avail_idx == q->rx.avail_idx) { return -EWOULDBLOCK; } VRING_INVALIDATE(q->rx.vring.avail->ring[q->rx.avail_idx % q->desc_max_len]); *avail_desc_idx = q->rx.vring.avail->ring[q->rx.avail_idx % q->desc_max_len]; if (*avail_desc_idx >= q->desc_max_len) { return -EINVAL; } return 0; }