| // 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; |
| } |