802 lines
21 KiB
C
802 lines
21 KiB
C
|
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
|
||
|
|
||
|
#include <linux/dma-mapping.h>
|
||
|
#include <linux/ip.h>
|
||
|
#include <linux/pci.h>
|
||
|
#include <linux/skbuff.h>
|
||
|
#include <linux/tcp.h>
|
||
|
#include <uapi/linux/udp.h>
|
||
|
#include "funeth.h"
|
||
|
#include "funeth_ktls.h"
|
||
|
#include "funeth_txrx.h"
|
||
|
#include "funeth_trace.h"
|
||
|
#include "fun_queue.h"
|
||
|
|
||
|
#define FUN_XDP_CLEAN_THRES 32
|
||
|
#define FUN_XDP_CLEAN_BATCH 16
|
||
|
|
||
|
/* DMA-map a packet and return the (length, DMA_address) pairs for its
|
||
|
* segments. If a mapping error occurs -ENOMEM is returned. The packet
|
||
|
* consists of an skb_shared_info and one additional address/length pair.
|
||
|
*/
|
||
|
static int fun_map_pkt(struct device *dev, const struct skb_shared_info *si,
|
||
|
void *data, unsigned int data_len,
|
||
|
dma_addr_t *addr, unsigned int *len)
|
||
|
{
|
||
|
const skb_frag_t *fp, *end;
|
||
|
|
||
|
*len = data_len;
|
||
|
*addr = dma_map_single(dev, data, *len, DMA_TO_DEVICE);
|
||
|
if (dma_mapping_error(dev, *addr))
|
||
|
return -ENOMEM;
|
||
|
|
||
|
if (!si)
|
||
|
return 0;
|
||
|
|
||
|
for (fp = si->frags, end = fp + si->nr_frags; fp < end; fp++) {
|
||
|
*++len = skb_frag_size(fp);
|
||
|
*++addr = skb_frag_dma_map(dev, fp, 0, *len, DMA_TO_DEVICE);
|
||
|
if (dma_mapping_error(dev, *addr))
|
||
|
goto unwind;
|
||
|
}
|
||
|
return 0;
|
||
|
|
||
|
unwind:
|
||
|
while (fp-- > si->frags)
|
||
|
dma_unmap_page(dev, *--addr, skb_frag_size(fp), DMA_TO_DEVICE);
|
||
|
|
||
|
dma_unmap_single(dev, addr[-1], data_len, DMA_TO_DEVICE);
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
/* Return the address just past the end of a Tx queue's descriptor ring.
|
||
|
* It exploits the fact that the HW writeback area is just after the end
|
||
|
* of the descriptor ring.
|
||
|
*/
|
||
|
static void *txq_end(const struct funeth_txq *q)
|
||
|
{
|
||
|
return (void *)q->hw_wb;
|
||
|
}
|
||
|
|
||
|
/* Return the amount of space within a Tx ring from the given address to the
|
||
|
* end.
|
||
|
*/
|
||
|
static unsigned int txq_to_end(const struct funeth_txq *q, void *p)
|
||
|
{
|
||
|
return txq_end(q) - p;
|
||
|
}
|
||
|
|
||
|
/* Return the number of Tx descriptors occupied by a Tx request. */
|
||
|
static unsigned int tx_req_ndesc(const struct fun_eth_tx_req *req)
|
||
|
{
|
||
|
return DIV_ROUND_UP(req->len8, FUNETH_SQE_SIZE / 8);
|
||
|
}
|
||
|
|
||
|
/* Write a gather list to the Tx descriptor at @req from @ngle address/length
|
||
|
* pairs.
|
||
|
*/
|
||
|
static struct fun_dataop_gl *fun_write_gl(const struct funeth_txq *q,
|
||
|
struct fun_eth_tx_req *req,
|
||
|
const dma_addr_t *addrs,
|
||
|
const unsigned int *lens,
|
||
|
unsigned int ngle)
|
||
|
{
|
||
|
struct fun_dataop_gl *gle;
|
||
|
unsigned int i;
|
||
|
|
||
|
req->len8 = (sizeof(*req) + ngle * sizeof(*gle)) / 8;
|
||
|
|
||
|
for (i = 0, gle = (struct fun_dataop_gl *)req->dataop.imm;
|
||
|
i < ngle && txq_to_end(q, gle); i++, gle++)
|
||
|
fun_dataop_gl_init(gle, 0, 0, lens[i], addrs[i]);
|
||
|
|
||
|
if (txq_to_end(q, gle) == 0) {
|
||
|
gle = (struct fun_dataop_gl *)q->desc;
|
||
|
for ( ; i < ngle; i++, gle++)
|
||
|
fun_dataop_gl_init(gle, 0, 0, lens[i], addrs[i]);
|
||
|
}
|
||
|
|
||
|
return gle;
|
||
|
}
|
||
|
|
||
|
static __be16 tcp_hdr_doff_flags(const struct tcphdr *th)
|
||
|
{
|
||
|
return *(__be16 *)&tcp_flag_word(th);
|
||
|
}
|
||
|
|
||
|
static struct sk_buff *fun_tls_tx(struct sk_buff *skb, struct funeth_txq *q,
|
||
|
unsigned int *tls_len)
|
||
|
{
|
||
|
#if IS_ENABLED(CONFIG_TLS_DEVICE)
|
||
|
const struct fun_ktls_tx_ctx *tls_ctx;
|
||
|
u32 datalen, seq;
|
||
|
|
||
|
datalen = skb->len - skb_tcp_all_headers(skb);
|
||
|
if (!datalen)
|
||
|
return skb;
|
||
|
|
||
|
if (likely(!tls_offload_tx_resync_pending(skb->sk))) {
|
||
|
seq = ntohl(tcp_hdr(skb)->seq);
|
||
|
tls_ctx = tls_driver_ctx(skb->sk, TLS_OFFLOAD_CTX_DIR_TX);
|
||
|
|
||
|
if (likely(tls_ctx->next_seq == seq)) {
|
||
|
*tls_len = datalen;
|
||
|
return skb;
|
||
|
}
|
||
|
if (seq - tls_ctx->next_seq < U32_MAX / 4) {
|
||
|
tls_offload_tx_resync_request(skb->sk, seq,
|
||
|
tls_ctx->next_seq);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
FUN_QSTAT_INC(q, tx_tls_fallback);
|
||
|
skb = tls_encrypt_skb(skb);
|
||
|
if (!skb)
|
||
|
FUN_QSTAT_INC(q, tx_tls_drops);
|
||
|
|
||
|
return skb;
|
||
|
#else
|
||
|
return NULL;
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/* Write as many descriptors as needed for the supplied skb starting at the
|
||
|
* current producer location. The caller has made certain enough descriptors
|
||
|
* are available.
|
||
|
*
|
||
|
* Returns the number of descriptors written, 0 on error.
|
||
|
*/
|
||
|
static unsigned int write_pkt_desc(struct sk_buff *skb, struct funeth_txq *q,
|
||
|
unsigned int tls_len)
|
||
|
{
|
||
|
unsigned int extra_bytes = 0, extra_pkts = 0;
|
||
|
unsigned int idx = q->prod_cnt & q->mask;
|
||
|
const struct skb_shared_info *shinfo;
|
||
|
unsigned int lens[MAX_SKB_FRAGS + 1];
|
||
|
dma_addr_t addrs[MAX_SKB_FRAGS + 1];
|
||
|
struct fun_eth_tx_req *req;
|
||
|
struct fun_dataop_gl *gle;
|
||
|
const struct tcphdr *th;
|
||
|
unsigned int l4_hlen;
|
||
|
unsigned int ngle;
|
||
|
u16 flags;
|
||
|
|
||
|
shinfo = skb_shinfo(skb);
|
||
|
if (unlikely(fun_map_pkt(q->dma_dev, shinfo, skb->data,
|
||
|
skb_headlen(skb), addrs, lens))) {
|
||
|
FUN_QSTAT_INC(q, tx_map_err);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
req = fun_tx_desc_addr(q, idx);
|
||
|
req->op = FUN_ETH_OP_TX;
|
||
|
req->len8 = 0;
|
||
|
req->flags = 0;
|
||
|
req->suboff8 = offsetof(struct fun_eth_tx_req, dataop);
|
||
|
req->repr_idn = 0;
|
||
|
req->encap_proto = 0;
|
||
|
|
||
|
if (likely(shinfo->gso_size)) {
|
||
|
if (skb->encapsulation) {
|
||
|
u16 ol4_ofst;
|
||
|
|
||
|
flags = FUN_ETH_OUTER_EN | FUN_ETH_INNER_LSO |
|
||
|
FUN_ETH_UPDATE_INNER_L4_CKSUM |
|
||
|
FUN_ETH_UPDATE_OUTER_L3_LEN;
|
||
|
if (shinfo->gso_type & (SKB_GSO_UDP_TUNNEL |
|
||
|
SKB_GSO_UDP_TUNNEL_CSUM)) {
|
||
|
flags |= FUN_ETH_UPDATE_OUTER_L4_LEN |
|
||
|
FUN_ETH_OUTER_UDP;
|
||
|
if (shinfo->gso_type & SKB_GSO_UDP_TUNNEL_CSUM)
|
||
|
flags |= FUN_ETH_UPDATE_OUTER_L4_CKSUM;
|
||
|
ol4_ofst = skb_transport_offset(skb);
|
||
|
} else {
|
||
|
ol4_ofst = skb_inner_network_offset(skb);
|
||
|
}
|
||
|
|
||
|
if (ip_hdr(skb)->version == 4)
|
||
|
flags |= FUN_ETH_UPDATE_OUTER_L3_CKSUM;
|
||
|
else
|
||
|
flags |= FUN_ETH_OUTER_IPV6;
|
||
|
|
||
|
if (skb->inner_network_header) {
|
||
|
if (inner_ip_hdr(skb)->version == 4)
|
||
|
flags |= FUN_ETH_UPDATE_INNER_L3_CKSUM |
|
||
|
FUN_ETH_UPDATE_INNER_L3_LEN;
|
||
|
else
|
||
|
flags |= FUN_ETH_INNER_IPV6 |
|
||
|
FUN_ETH_UPDATE_INNER_L3_LEN;
|
||
|
}
|
||
|
th = inner_tcp_hdr(skb);
|
||
|
l4_hlen = __tcp_hdrlen(th);
|
||
|
fun_eth_offload_init(&req->offload, flags,
|
||
|
shinfo->gso_size,
|
||
|
tcp_hdr_doff_flags(th), 0,
|
||
|
skb_inner_network_offset(skb),
|
||
|
skb_inner_transport_offset(skb),
|
||
|
skb_network_offset(skb), ol4_ofst);
|
||
|
FUN_QSTAT_INC(q, tx_encap_tso);
|
||
|
} else if (shinfo->gso_type & SKB_GSO_UDP_L4) {
|
||
|
flags = FUN_ETH_INNER_LSO | FUN_ETH_INNER_UDP |
|
||
|
FUN_ETH_UPDATE_INNER_L4_CKSUM |
|
||
|
FUN_ETH_UPDATE_INNER_L4_LEN |
|
||
|
FUN_ETH_UPDATE_INNER_L3_LEN;
|
||
|
|
||
|
if (ip_hdr(skb)->version == 4)
|
||
|
flags |= FUN_ETH_UPDATE_INNER_L3_CKSUM;
|
||
|
else
|
||
|
flags |= FUN_ETH_INNER_IPV6;
|
||
|
|
||
|
l4_hlen = sizeof(struct udphdr);
|
||
|
fun_eth_offload_init(&req->offload, flags,
|
||
|
shinfo->gso_size,
|
||
|
cpu_to_be16(l4_hlen << 10), 0,
|
||
|
skb_network_offset(skb),
|
||
|
skb_transport_offset(skb), 0, 0);
|
||
|
FUN_QSTAT_INC(q, tx_uso);
|
||
|
} else {
|
||
|
/* HW considers one set of headers as inner */
|
||
|
flags = FUN_ETH_INNER_LSO |
|
||
|
FUN_ETH_UPDATE_INNER_L4_CKSUM |
|
||
|
FUN_ETH_UPDATE_INNER_L3_LEN;
|
||
|
if (shinfo->gso_type & SKB_GSO_TCPV6)
|
||
|
flags |= FUN_ETH_INNER_IPV6;
|
||
|
else
|
||
|
flags |= FUN_ETH_UPDATE_INNER_L3_CKSUM;
|
||
|
th = tcp_hdr(skb);
|
||
|
l4_hlen = __tcp_hdrlen(th);
|
||
|
fun_eth_offload_init(&req->offload, flags,
|
||
|
shinfo->gso_size,
|
||
|
tcp_hdr_doff_flags(th), 0,
|
||
|
skb_network_offset(skb),
|
||
|
skb_transport_offset(skb), 0, 0);
|
||
|
FUN_QSTAT_INC(q, tx_tso);
|
||
|
}
|
||
|
|
||
|
u64_stats_update_begin(&q->syncp);
|
||
|
q->stats.tx_cso += shinfo->gso_segs;
|
||
|
u64_stats_update_end(&q->syncp);
|
||
|
|
||
|
extra_pkts = shinfo->gso_segs - 1;
|
||
|
extra_bytes = (be16_to_cpu(req->offload.inner_l4_off) +
|
||
|
l4_hlen) * extra_pkts;
|
||
|
} else if (likely(skb->ip_summed == CHECKSUM_PARTIAL)) {
|
||
|
flags = FUN_ETH_UPDATE_INNER_L4_CKSUM;
|
||
|
if (skb->csum_offset == offsetof(struct udphdr, check))
|
||
|
flags |= FUN_ETH_INNER_UDP;
|
||
|
fun_eth_offload_init(&req->offload, flags, 0, 0, 0, 0,
|
||
|
skb_checksum_start_offset(skb), 0, 0);
|
||
|
FUN_QSTAT_INC(q, tx_cso);
|
||
|
} else {
|
||
|
fun_eth_offload_init(&req->offload, 0, 0, 0, 0, 0, 0, 0, 0);
|
||
|
}
|
||
|
|
||
|
ngle = shinfo->nr_frags + 1;
|
||
|
req->dataop = FUN_DATAOP_HDR_INIT(ngle, 0, ngle, 0, skb->len);
|
||
|
|
||
|
gle = fun_write_gl(q, req, addrs, lens, ngle);
|
||
|
|
||
|
if (IS_ENABLED(CONFIG_TLS_DEVICE) && unlikely(tls_len)) {
|
||
|
struct fun_eth_tls *tls = (struct fun_eth_tls *)gle;
|
||
|
struct fun_ktls_tx_ctx *tls_ctx;
|
||
|
|
||
|
req->len8 += FUNETH_TLS_SZ / 8;
|
||
|
req->flags = cpu_to_be16(FUN_ETH_TX_TLS);
|
||
|
|
||
|
tls_ctx = tls_driver_ctx(skb->sk, TLS_OFFLOAD_CTX_DIR_TX);
|
||
|
tls->tlsid = tls_ctx->tlsid;
|
||
|
tls_ctx->next_seq += tls_len;
|
||
|
|
||
|
u64_stats_update_begin(&q->syncp);
|
||
|
q->stats.tx_tls_bytes += tls_len;
|
||
|
q->stats.tx_tls_pkts += 1 + extra_pkts;
|
||
|
u64_stats_update_end(&q->syncp);
|
||
|
}
|
||
|
|
||
|
u64_stats_update_begin(&q->syncp);
|
||
|
q->stats.tx_bytes += skb->len + extra_bytes;
|
||
|
q->stats.tx_pkts += 1 + extra_pkts;
|
||
|
u64_stats_update_end(&q->syncp);
|
||
|
|
||
|
q->info[idx].skb = skb;
|
||
|
|
||
|
trace_funeth_tx(q, skb->len, idx, req->dataop.ngather);
|
||
|
return tx_req_ndesc(req);
|
||
|
}
|
||
|
|
||
|
/* Return the number of available descriptors of a Tx queue.
|
||
|
* HW assumes head==tail means the ring is empty so we need to keep one
|
||
|
* descriptor unused.
|
||
|
*/
|
||
|
static unsigned int fun_txq_avail(const struct funeth_txq *q)
|
||
|
{
|
||
|
return q->mask - q->prod_cnt + q->cons_cnt;
|
||
|
}
|
||
|
|
||
|
/* Stop a queue if it can't handle another worst-case packet. */
|
||
|
static void fun_tx_check_stop(struct funeth_txq *q)
|
||
|
{
|
||
|
if (likely(fun_txq_avail(q) >= FUNETH_MAX_PKT_DESC))
|
||
|
return;
|
||
|
|
||
|
netif_tx_stop_queue(q->ndq);
|
||
|
|
||
|
/* NAPI reclaim is freeing packets in parallel with us and we may race.
|
||
|
* We have stopped the queue but check again after synchronizing with
|
||
|
* reclaim.
|
||
|
*/
|
||
|
smp_mb();
|
||
|
if (likely(fun_txq_avail(q) < FUNETH_MAX_PKT_DESC))
|
||
|
FUN_QSTAT_INC(q, tx_nstops);
|
||
|
else
|
||
|
netif_tx_start_queue(q->ndq);
|
||
|
}
|
||
|
|
||
|
/* Return true if a queue has enough space to restart. Current condition is
|
||
|
* that the queue must be >= 1/4 empty.
|
||
|
*/
|
||
|
static bool fun_txq_may_restart(struct funeth_txq *q)
|
||
|
{
|
||
|
return fun_txq_avail(q) >= q->mask / 4;
|
||
|
}
|
||
|
|
||
|
netdev_tx_t fun_start_xmit(struct sk_buff *skb, struct net_device *netdev)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(netdev);
|
||
|
unsigned int qid = skb_get_queue_mapping(skb);
|
||
|
struct funeth_txq *q = fp->txqs[qid];
|
||
|
unsigned int tls_len = 0;
|
||
|
unsigned int ndesc;
|
||
|
|
||
|
if (IS_ENABLED(CONFIG_TLS_DEVICE) && skb->sk &&
|
||
|
tls_is_sk_tx_device_offloaded(skb->sk)) {
|
||
|
skb = fun_tls_tx(skb, q, &tls_len);
|
||
|
if (unlikely(!skb))
|
||
|
goto dropped;
|
||
|
}
|
||
|
|
||
|
ndesc = write_pkt_desc(skb, q, tls_len);
|
||
|
if (unlikely(!ndesc)) {
|
||
|
dev_kfree_skb_any(skb);
|
||
|
goto dropped;
|
||
|
}
|
||
|
|
||
|
q->prod_cnt += ndesc;
|
||
|
fun_tx_check_stop(q);
|
||
|
|
||
|
skb_tx_timestamp(skb);
|
||
|
|
||
|
if (__netdev_tx_sent_queue(q->ndq, skb->len, netdev_xmit_more()))
|
||
|
fun_txq_wr_db(q);
|
||
|
else
|
||
|
FUN_QSTAT_INC(q, tx_more);
|
||
|
|
||
|
return NETDEV_TX_OK;
|
||
|
|
||
|
dropped:
|
||
|
/* A dropped packet may be the last one in a xmit_more train,
|
||
|
* ring the doorbell just in case.
|
||
|
*/
|
||
|
if (!netdev_xmit_more())
|
||
|
fun_txq_wr_db(q);
|
||
|
return NETDEV_TX_OK;
|
||
|
}
|
||
|
|
||
|
/* Return a Tx queue's HW head index written back to host memory. */
|
||
|
static u16 txq_hw_head(const struct funeth_txq *q)
|
||
|
{
|
||
|
return (u16)be64_to_cpu(*q->hw_wb);
|
||
|
}
|
||
|
|
||
|
/* Unmap the Tx packet starting at the given descriptor index and
|
||
|
* return the number of Tx descriptors it occupied.
|
||
|
*/
|
||
|
static unsigned int fun_unmap_pkt(const struct funeth_txq *q, unsigned int idx)
|
||
|
{
|
||
|
const struct fun_eth_tx_req *req = fun_tx_desc_addr(q, idx);
|
||
|
unsigned int ngle = req->dataop.ngather;
|
||
|
struct fun_dataop_gl *gle;
|
||
|
|
||
|
if (ngle) {
|
||
|
gle = (struct fun_dataop_gl *)req->dataop.imm;
|
||
|
dma_unmap_single(q->dma_dev, be64_to_cpu(gle->sgl_data),
|
||
|
be32_to_cpu(gle->sgl_len), DMA_TO_DEVICE);
|
||
|
|
||
|
for (gle++; --ngle && txq_to_end(q, gle); gle++)
|
||
|
dma_unmap_page(q->dma_dev, be64_to_cpu(gle->sgl_data),
|
||
|
be32_to_cpu(gle->sgl_len),
|
||
|
DMA_TO_DEVICE);
|
||
|
|
||
|
for (gle = (struct fun_dataop_gl *)q->desc; ngle; ngle--, gle++)
|
||
|
dma_unmap_page(q->dma_dev, be64_to_cpu(gle->sgl_data),
|
||
|
be32_to_cpu(gle->sgl_len),
|
||
|
DMA_TO_DEVICE);
|
||
|
}
|
||
|
|
||
|
return tx_req_ndesc(req);
|
||
|
}
|
||
|
|
||
|
/* Reclaim completed Tx descriptors and free their packets. Restart a stopped
|
||
|
* queue if we freed enough descriptors.
|
||
|
*
|
||
|
* Return true if we exhausted the budget while there is more work to be done.
|
||
|
*/
|
||
|
static bool fun_txq_reclaim(struct funeth_txq *q, int budget)
|
||
|
{
|
||
|
unsigned int npkts = 0, nbytes = 0, ndesc = 0;
|
||
|
unsigned int head, limit, reclaim_idx;
|
||
|
|
||
|
/* budget may be 0, e.g., netpoll */
|
||
|
limit = budget ? budget : UINT_MAX;
|
||
|
|
||
|
for (head = txq_hw_head(q), reclaim_idx = q->cons_cnt & q->mask;
|
||
|
head != reclaim_idx && npkts < limit; head = txq_hw_head(q)) {
|
||
|
/* The HW head is continually updated, ensure we don't read
|
||
|
* descriptor state before the head tells us to reclaim it.
|
||
|
* On the enqueue side the doorbell is an implicit write
|
||
|
* barrier.
|
||
|
*/
|
||
|
rmb();
|
||
|
|
||
|
do {
|
||
|
unsigned int pkt_desc = fun_unmap_pkt(q, reclaim_idx);
|
||
|
struct sk_buff *skb = q->info[reclaim_idx].skb;
|
||
|
|
||
|
trace_funeth_tx_free(q, reclaim_idx, pkt_desc, head);
|
||
|
|
||
|
nbytes += skb->len;
|
||
|
napi_consume_skb(skb, budget);
|
||
|
ndesc += pkt_desc;
|
||
|
reclaim_idx = (reclaim_idx + pkt_desc) & q->mask;
|
||
|
npkts++;
|
||
|
} while (reclaim_idx != head && npkts < limit);
|
||
|
}
|
||
|
|
||
|
q->cons_cnt += ndesc;
|
||
|
netdev_tx_completed_queue(q->ndq, npkts, nbytes);
|
||
|
smp_mb(); /* pairs with the one in fun_tx_check_stop() */
|
||
|
|
||
|
if (unlikely(netif_tx_queue_stopped(q->ndq) &&
|
||
|
fun_txq_may_restart(q))) {
|
||
|
netif_tx_wake_queue(q->ndq);
|
||
|
FUN_QSTAT_INC(q, tx_nrestarts);
|
||
|
}
|
||
|
|
||
|
return reclaim_idx != head;
|
||
|
}
|
||
|
|
||
|
/* The NAPI handler for Tx queues. */
|
||
|
int fun_txq_napi_poll(struct napi_struct *napi, int budget)
|
||
|
{
|
||
|
struct fun_irq *irq = container_of(napi, struct fun_irq, napi);
|
||
|
struct funeth_txq *q = irq->txq;
|
||
|
unsigned int db_val;
|
||
|
|
||
|
if (fun_txq_reclaim(q, budget))
|
||
|
return budget; /* exhausted budget */
|
||
|
|
||
|
napi_complete(napi); /* exhausted pending work */
|
||
|
db_val = READ_ONCE(q->irq_db_val) | (q->cons_cnt & q->mask);
|
||
|
writel(db_val, q->db);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/* Reclaim up to @budget completed Tx packets from a TX XDP queue. */
|
||
|
static unsigned int fun_xdpq_clean(struct funeth_txq *q, unsigned int budget)
|
||
|
{
|
||
|
unsigned int npkts = 0, ndesc = 0, head, reclaim_idx;
|
||
|
|
||
|
for (head = txq_hw_head(q), reclaim_idx = q->cons_cnt & q->mask;
|
||
|
head != reclaim_idx && npkts < budget; head = txq_hw_head(q)) {
|
||
|
/* The HW head is continually updated, ensure we don't read
|
||
|
* descriptor state before the head tells us to reclaim it.
|
||
|
* On the enqueue side the doorbell is an implicit write
|
||
|
* barrier.
|
||
|
*/
|
||
|
rmb();
|
||
|
|
||
|
do {
|
||
|
unsigned int pkt_desc = fun_unmap_pkt(q, reclaim_idx);
|
||
|
|
||
|
xdp_return_frame(q->info[reclaim_idx].xdpf);
|
||
|
|
||
|
trace_funeth_tx_free(q, reclaim_idx, pkt_desc, head);
|
||
|
|
||
|
reclaim_idx = (reclaim_idx + pkt_desc) & q->mask;
|
||
|
ndesc += pkt_desc;
|
||
|
npkts++;
|
||
|
} while (reclaim_idx != head && npkts < budget);
|
||
|
}
|
||
|
|
||
|
q->cons_cnt += ndesc;
|
||
|
return npkts;
|
||
|
}
|
||
|
|
||
|
bool fun_xdp_tx(struct funeth_txq *q, struct xdp_frame *xdpf)
|
||
|
{
|
||
|
unsigned int idx, nfrags = 1, ndesc = 1, tot_len = xdpf->len;
|
||
|
const struct skb_shared_info *si = NULL;
|
||
|
unsigned int lens[MAX_SKB_FRAGS + 1];
|
||
|
dma_addr_t dma[MAX_SKB_FRAGS + 1];
|
||
|
struct fun_eth_tx_req *req;
|
||
|
|
||
|
if (fun_txq_avail(q) < FUN_XDP_CLEAN_THRES)
|
||
|
fun_xdpq_clean(q, FUN_XDP_CLEAN_BATCH);
|
||
|
|
||
|
if (unlikely(xdp_frame_has_frags(xdpf))) {
|
||
|
si = xdp_get_shared_info_from_frame(xdpf);
|
||
|
tot_len = xdp_get_frame_len(xdpf);
|
||
|
nfrags += si->nr_frags;
|
||
|
ndesc = DIV_ROUND_UP((sizeof(*req) + nfrags *
|
||
|
sizeof(struct fun_dataop_gl)),
|
||
|
FUNETH_SQE_SIZE);
|
||
|
}
|
||
|
|
||
|
if (unlikely(fun_txq_avail(q) < ndesc)) {
|
||
|
FUN_QSTAT_INC(q, tx_xdp_full);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (unlikely(fun_map_pkt(q->dma_dev, si, xdpf->data, xdpf->len, dma,
|
||
|
lens))) {
|
||
|
FUN_QSTAT_INC(q, tx_map_err);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
idx = q->prod_cnt & q->mask;
|
||
|
req = fun_tx_desc_addr(q, idx);
|
||
|
req->op = FUN_ETH_OP_TX;
|
||
|
req->len8 = 0;
|
||
|
req->flags = 0;
|
||
|
req->suboff8 = offsetof(struct fun_eth_tx_req, dataop);
|
||
|
req->repr_idn = 0;
|
||
|
req->encap_proto = 0;
|
||
|
fun_eth_offload_init(&req->offload, 0, 0, 0, 0, 0, 0, 0, 0);
|
||
|
req->dataop = FUN_DATAOP_HDR_INIT(nfrags, 0, nfrags, 0, tot_len);
|
||
|
|
||
|
fun_write_gl(q, req, dma, lens, nfrags);
|
||
|
|
||
|
q->info[idx].xdpf = xdpf;
|
||
|
|
||
|
u64_stats_update_begin(&q->syncp);
|
||
|
q->stats.tx_bytes += tot_len;
|
||
|
q->stats.tx_pkts++;
|
||
|
u64_stats_update_end(&q->syncp);
|
||
|
|
||
|
trace_funeth_tx(q, tot_len, idx, nfrags);
|
||
|
q->prod_cnt += ndesc;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int fun_xdp_xmit_frames(struct net_device *dev, int n,
|
||
|
struct xdp_frame **frames, u32 flags)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(dev);
|
||
|
struct funeth_txq *q, **xdpqs;
|
||
|
int i, q_idx;
|
||
|
|
||
|
if (unlikely(flags & ~XDP_XMIT_FLAGS_MASK))
|
||
|
return -EINVAL;
|
||
|
|
||
|
xdpqs = rcu_dereference_bh(fp->xdpqs);
|
||
|
if (unlikely(!xdpqs))
|
||
|
return -ENETDOWN;
|
||
|
|
||
|
q_idx = smp_processor_id();
|
||
|
if (unlikely(q_idx >= fp->num_xdpqs))
|
||
|
return -ENXIO;
|
||
|
|
||
|
for (q = xdpqs[q_idx], i = 0; i < n; i++)
|
||
|
if (!fun_xdp_tx(q, frames[i]))
|
||
|
break;
|
||
|
|
||
|
if (unlikely(flags & XDP_XMIT_FLUSH))
|
||
|
fun_txq_wr_db(q);
|
||
|
return i;
|
||
|
}
|
||
|
|
||
|
/* Purge a Tx queue of any queued packets. Should be called once HW access
|
||
|
* to the packets has been revoked, e.g., after the queue has been disabled.
|
||
|
*/
|
||
|
static void fun_txq_purge(struct funeth_txq *q)
|
||
|
{
|
||
|
while (q->cons_cnt != q->prod_cnt) {
|
||
|
unsigned int idx = q->cons_cnt & q->mask;
|
||
|
|
||
|
q->cons_cnt += fun_unmap_pkt(q, idx);
|
||
|
dev_kfree_skb_any(q->info[idx].skb);
|
||
|
}
|
||
|
netdev_tx_reset_queue(q->ndq);
|
||
|
}
|
||
|
|
||
|
static void fun_xdpq_purge(struct funeth_txq *q)
|
||
|
{
|
||
|
while (q->cons_cnt != q->prod_cnt) {
|
||
|
unsigned int idx = q->cons_cnt & q->mask;
|
||
|
|
||
|
q->cons_cnt += fun_unmap_pkt(q, idx);
|
||
|
xdp_return_frame(q->info[idx].xdpf);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Create a Tx queue, allocating all the host resources needed. */
|
||
|
static struct funeth_txq *fun_txq_create_sw(struct net_device *dev,
|
||
|
unsigned int qidx,
|
||
|
unsigned int ndesc,
|
||
|
struct fun_irq *irq)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(dev);
|
||
|
struct funeth_txq *q;
|
||
|
int numa_node;
|
||
|
|
||
|
if (irq)
|
||
|
numa_node = fun_irq_node(irq); /* skb Tx queue */
|
||
|
else
|
||
|
numa_node = cpu_to_node(qidx); /* XDP Tx queue */
|
||
|
|
||
|
q = kzalloc_node(sizeof(*q), GFP_KERNEL, numa_node);
|
||
|
if (!q)
|
||
|
goto err;
|
||
|
|
||
|
q->dma_dev = &fp->pdev->dev;
|
||
|
q->desc = fun_alloc_ring_mem(q->dma_dev, ndesc, FUNETH_SQE_SIZE,
|
||
|
sizeof(*q->info), true, numa_node,
|
||
|
&q->dma_addr, (void **)&q->info,
|
||
|
&q->hw_wb);
|
||
|
if (!q->desc)
|
||
|
goto free_q;
|
||
|
|
||
|
q->netdev = dev;
|
||
|
q->mask = ndesc - 1;
|
||
|
q->qidx = qidx;
|
||
|
q->numa_node = numa_node;
|
||
|
u64_stats_init(&q->syncp);
|
||
|
q->init_state = FUN_QSTATE_INIT_SW;
|
||
|
return q;
|
||
|
|
||
|
free_q:
|
||
|
kfree(q);
|
||
|
err:
|
||
|
netdev_err(dev, "Can't allocate memory for %s queue %u\n",
|
||
|
irq ? "Tx" : "XDP", qidx);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
static void fun_txq_free_sw(struct funeth_txq *q)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(q->netdev);
|
||
|
|
||
|
fun_free_ring_mem(q->dma_dev, q->mask + 1, FUNETH_SQE_SIZE, true,
|
||
|
q->desc, q->dma_addr, q->info);
|
||
|
|
||
|
fp->tx_packets += q->stats.tx_pkts;
|
||
|
fp->tx_bytes += q->stats.tx_bytes;
|
||
|
fp->tx_dropped += q->stats.tx_map_err;
|
||
|
|
||
|
kfree(q);
|
||
|
}
|
||
|
|
||
|
/* Allocate the device portion of a Tx queue. */
|
||
|
int fun_txq_create_dev(struct funeth_txq *q, struct fun_irq *irq)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(q->netdev);
|
||
|
unsigned int irq_idx, ndesc = q->mask + 1;
|
||
|
int err;
|
||
|
|
||
|
q->irq = irq;
|
||
|
*q->hw_wb = 0;
|
||
|
q->prod_cnt = 0;
|
||
|
q->cons_cnt = 0;
|
||
|
irq_idx = irq ? irq->irq_idx : 0;
|
||
|
|
||
|
err = fun_sq_create(fp->fdev,
|
||
|
FUN_ADMIN_EPSQ_CREATE_FLAG_HEAD_WB_ADDRESS |
|
||
|
FUN_ADMIN_RES_CREATE_FLAG_ALLOCATOR, 0,
|
||
|
FUN_HCI_ID_INVALID, ilog2(FUNETH_SQE_SIZE), ndesc,
|
||
|
q->dma_addr, fp->tx_coal_count, fp->tx_coal_usec,
|
||
|
irq_idx, 0, fp->fdev->kern_end_qid, 0,
|
||
|
&q->hw_qid, &q->db);
|
||
|
if (err)
|
||
|
goto out;
|
||
|
|
||
|
err = fun_create_and_bind_tx(fp, q->hw_qid);
|
||
|
if (err < 0)
|
||
|
goto free_devq;
|
||
|
q->ethid = err;
|
||
|
|
||
|
if (irq) {
|
||
|
irq->txq = q;
|
||
|
q->ndq = netdev_get_tx_queue(q->netdev, q->qidx);
|
||
|
q->irq_db_val = FUN_IRQ_SQ_DB(fp->tx_coal_usec,
|
||
|
fp->tx_coal_count);
|
||
|
writel(q->irq_db_val, q->db);
|
||
|
}
|
||
|
|
||
|
q->init_state = FUN_QSTATE_INIT_FULL;
|
||
|
netif_info(fp, ifup, q->netdev,
|
||
|
"%s queue %u, depth %u, HW qid %u, IRQ idx %u, eth id %u, node %d\n",
|
||
|
irq ? "Tx" : "XDP", q->qidx, ndesc, q->hw_qid, irq_idx,
|
||
|
q->ethid, q->numa_node);
|
||
|
return 0;
|
||
|
|
||
|
free_devq:
|
||
|
fun_destroy_sq(fp->fdev, q->hw_qid);
|
||
|
out:
|
||
|
netdev_err(q->netdev,
|
||
|
"Failed to create %s queue %u on device, error %d\n",
|
||
|
irq ? "Tx" : "XDP", q->qidx, err);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
static void fun_txq_free_dev(struct funeth_txq *q)
|
||
|
{
|
||
|
struct funeth_priv *fp = netdev_priv(q->netdev);
|
||
|
|
||
|
if (q->init_state < FUN_QSTATE_INIT_FULL)
|
||
|
return;
|
||
|
|
||
|
netif_info(fp, ifdown, q->netdev,
|
||
|
"Freeing %s queue %u (id %u), IRQ %u, ethid %u\n",
|
||
|
q->irq ? "Tx" : "XDP", q->qidx, q->hw_qid,
|
||
|
q->irq ? q->irq->irq_idx : 0, q->ethid);
|
||
|
|
||
|
fun_destroy_sq(fp->fdev, q->hw_qid);
|
||
|
fun_res_destroy(fp->fdev, FUN_ADMIN_OP_ETH, 0, q->ethid);
|
||
|
|
||
|
if (q->irq) {
|
||
|
q->irq->txq = NULL;
|
||
|
fun_txq_purge(q);
|
||
|
} else {
|
||
|
fun_xdpq_purge(q);
|
||
|
}
|
||
|
|
||
|
q->init_state = FUN_QSTATE_INIT_SW;
|
||
|
}
|
||
|
|
||
|
/* Create or advance a Tx queue, allocating all the host and device resources
|
||
|
* needed to reach the target state.
|
||
|
*/
|
||
|
int funeth_txq_create(struct net_device *dev, unsigned int qidx,
|
||
|
unsigned int ndesc, struct fun_irq *irq, int state,
|
||
|
struct funeth_txq **qp)
|
||
|
{
|
||
|
struct funeth_txq *q = *qp;
|
||
|
int err;
|
||
|
|
||
|
if (!q)
|
||
|
q = fun_txq_create_sw(dev, qidx, ndesc, irq);
|
||
|
if (!q)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
if (q->init_state >= state)
|
||
|
goto out;
|
||
|
|
||
|
err = fun_txq_create_dev(q, irq);
|
||
|
if (err) {
|
||
|
if (!*qp)
|
||
|
fun_txq_free_sw(q);
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
out:
|
||
|
*qp = q;
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/* Free Tx queue resources until it reaches the target state.
|
||
|
* The queue must be already disconnected from the stack.
|
||
|
*/
|
||
|
struct funeth_txq *funeth_txq_free(struct funeth_txq *q, int state)
|
||
|
{
|
||
|
if (state < FUN_QSTATE_INIT_FULL)
|
||
|
fun_txq_free_dev(q);
|
||
|
|
||
|
if (state == FUN_QSTATE_DESTROYED) {
|
||
|
fun_txq_free_sw(q);
|
||
|
q = NULL;
|
||
|
}
|
||
|
|
||
|
return q;
|
||
|
}
|