首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
①性能工程基础
②DPDK 源码深读
③用户态协议栈
④软件交换与 SONiC
⑤PCIe、虚拟化与 DPU
⑥面试与复盘
登录
Search
标签搜索
DPDK→DPU 学习路线
P0
P1
P2
P3
P5
PCIe
P4
P6
libmtcp
NUMA
mTCP
OVS
SONiC
SAI
DPU
perf
gdb
EAL
mempool
PacketForge
累计撰写
53
篇文章
累计收到
3
条评论
首页
栏目
页面
关于
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P1-07 · RSS 与 rte_flow:流与核的绑定
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/ 与 source/linux-stable/,引用均标 文件:行号上一篇:P1-06 NUMA | 下一篇:P2-01 mTCP 论文精读(Phase 2 学到时发布)一句话定位多核数据面的第一个问题不是"怎么写快",而是"谁处理哪个流"。RSS 让硬件替你把流量平均分到 N 个核,且同一 5-tuple 永远落同一核——这是 per-core 免锁数据结构的物理前提。RSS 流水线Toeplitz 哈希:NIC 对 5-tuple 做硬件哈希(rss_key 可编程),低 N 位经 RETA 表映射到 RX 队列 → 每队列一个 lcore。哈希结果在收包时已灌进 mbuf(ixgbe_rxtx.c:1943-1945,P1-05 已读): if (likely(pkt_flags & RTE_MBUF_F_RX_RSS_HASH)) rxm->hash.rss = rte_le_to_cpu_32( rxd.wb.lower.hi_dword.rss);对称 RSS:用户态协议栈的基石普通 RSS 下,一个 TCP 连接双向流会 hash 到不同核——连接表必须跨核共享(加锁)。对称 RSS 用特殊 key 让"src→dst"与"dst→src"哈希值相同 → 双向流同核 → 每核连接表完全独立,零共享。这是 Phase 2 libmtcp-v3 和 P3 自研 LB 的 per-core conn table 基石。P1 实验:testpmd --rxq=16 --rss 观察队列分布,再换对称 key 验证双向流落同队列。rte_flow:不想平均,要精确控制pattern + action + attr:pattern 描述匹配(ETH/IPV4/UDP + 5-tuple),action 定动作(QUEUE/RSS/DROP/COUNT/MARK),attr 定方向优先级。rte_flow_create 下发网卡(硬件 offload)或软件模拟。P1 实验:写一条 rule 把特定 5-tuple 导到指定队列;P2 的 SAI ACL 就是把 SAI 规则翻译成 rte_flow。🐧 内核对照:RPS = 软件版 RSS真实代码(net/core/dev.c:4651-4734,节选):static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb, struct rps_dev_flow **rflowp) { // ... flow_table = rcu_dereference(rxqueue->rps_flow_table); map = rcu_dereference(rxqueue->rps_map); if (!flow_table && !map) goto done; skb_reset_network_header(skb); hash = skb_get_hash(skb); if (!hash) goto done; sock_flow_table = rcu_dereference(rps_sock_flow_table); if (flow_table && sock_flow_table) { struct rps_dev_flow *rflow; u32 next_cpu; u32 ident; /* First check into global flow table if there is a match. ... */ ident = READ_ONCE(sock_flow_table->ents[hash & sock_flow_table->mask]); if ((ident ^ hash) & ~rps_cpu_mask) goto try_rps; next_cpu = ident & rps_cpu_mask; // ... /* * If the desired CPU (where last recvmsg was done) is * different from current CPU ..., switch if one of the * following holds: * - Current CPU is unset (>= nr_cpu_ids). * - Current CPU is offline. * - The current CPU's queue tail has advanced beyond the * last packet that was enqueued using this table entry. * This guarantees that all previous packets for the flow * have been dequeued, thus preserving in order delivery. */ if (unlikely(tcpu != next_cpu) && (tcpu >= nr_cpu_ids || !cpu_online(tcpu) || ((int)(per_cpu(softnet_data, tcpu).input_queue_head - rflow->last_qtail)) >= 0)) { tcpu = next_cpu; rflow = set_rps_cpu(dev, skb, rflow, next_cpu); } // ...三个对比点:RSS 在硬件做(零 CPU 开销,哈希在 NIC),RPS 在软中断做(skb_get_hash 软件算,吃软中断 CPU)。RFS 更进一步:把流绑到应用所在 CPU(sock_flow_table 记住上次 recvmsg 的 CPU)。迁移 CPU 必须保证不乱序:内核要求"目标 CPU 的队列尾部已越过上次入队位置"(dev.c:4708-4718 注释)——而 RSS 硬件天然保证同流同队列,没有乱序问题。这就是硬件分流的真正价值。面试 2 问RSS 和 RPS 区别? → 硬件哈希分流(零 CPU、同流同队列、天然不乱序)vs 软件哈希分流(软中断开销、迁移 CPU 要保证顺序,dev.c:4708-4718)。为什么要对称 RSS? → 双向流同核 → per-core 连接表免锁;代价是队列负载可能略不均。🔧 动手实验testpmd 配置 RSS 观察队列分布;用对称 key 验证双向流同队列;写一条 rte_flow 规则导流验证(code/P1/rte-flow-demo/);实现 16 队列多队列 echo,测单核 vs 16 核扩展曲线,数据留档 perf-data/P1/。Phase 1 七篇到此完结。 下一篇是 Phase 2 的第一篇:P2-01 · mTCP 论文精读(NSDI'14)——学到 Day 50 时动笔。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-06 · NUMA 与 lcore 调度:性能的第一条纪律
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/ 与 source/linux-stable/,引用均标 文件:行号上一篇:P1-05 PMD | 下一篇:P1-07 RSS一句话定位NUMA 是 DPDK 性能模型的第一条纪律:lcore 只用本 socket 的内存和队列。违反了它,前面学的 mempool/ring 免锁优化全部白费——跨 socket 的 UPI 延迟比共享缓存行更疼。拓扑与代价lstopo 画拓扑;/sys/devices/system/node/node*/distance 看距离矩阵。跨 socket 内存访问延迟 +50%~100%。实验:同 socket vs 跨 socket 跑 mempool alloc 延迟对比,数据留档 perf-data/P1/。三条纪律的代码依据纪律 1:lcore 绑核worker 线程创建即绑核(lib/eal/linux/eal.c:1197-1224),主线程自己也绑(eal.c:1183-1188): if (rte_thread_set_affinity_by_id(rte_thread_self(), &lcore_config[config->main_lcore].cpuset) != 0) { rte_eal_init_alert("Cannot set affinity"); rte_errno = EINVAL; return -1; }绑核 → 线程不迁移 → per-lcore 结构免锁成立(P1-02/03 的所有"免锁"都建立在这条之上)。纪律 2:内存对齐mempool 创建必须传目标 socket_id(文档见 rte_mempool.h:1061-1064): * @param socket_id * The *socket_id* argument is the socket identifier in the case of * NUMA. The value can be *SOCKET_ID_ANY* if there is no NUMA * constraint for the reserved zone.mempool 结构体本身也记录了这个值:int socket_id; /**< Socket id passed at create. */(rte_mempool.h:239)。纪律 3:队列对齐用 rte_eth_dev_socket_id(port) 校验网卡队列与 lcore 同 socket;跨 socket 收包同样有 UPI 延迟惩罚。绑核策略实验:HT 兄弟核 vs 物理核的 PPS 差异——别把两个数据面 lcore 绑在同一物理核的 HT 对上(共享执行单元)。service core:管理任务的家rte_service 框架:统计收集、链路检测、定时任务注册成 service,映射到专用 core(rte_service_map_lcore_set)。在 rte_eal_init 中,service 初始化安排在 bus_probe 之前(eal.c:1234),因为 vdev 设备探测时就可能注册 service。数据面 lcore 保持纯净 run-to-completion,管理任务绝不混入热路径。🐧 内核对照mm/mempolicy.c:set_mempolicy/mbind 是内核的 NUMA 绑内存 API;调度域(kernel/sched/topology.c)让调度器感知 NUMA 距离。内核靠调度器"尽量"局部化,DPDK 靠程序员"强制"局部化——这是性能模型的分水岭。内核里 mbind 是"建议",DPDK 里 socket_id 是"契约"。面试 2 问DPDK 应用怎么保证内存不跨 socket? → 创建 mempool/ring 时传目标 socket_id;lcore 绑核到该 socket;分配前 rte_socket_id() 校验。service core 解决什么问题? → 让管理/控制任务不打断数据面轮询节奏,保持热路径无杂务。🔧 动手实验lstopo 画本机拓扑图,标出网卡所在 socket;同/跨 socket mempool alloc 延迟对比 + ring 吞吐对比,数据留档 perf-data/P1/。下一篇:P1-07 · RSS 与 rte_flow:流与核的绑定
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-05 · PMD:把中断换成轮询,把每包分配换成预分配
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/drivers/net/ixgbe/,引用均标 文件:行号上一篇:P1-04 mbuf | 下一篇:P1-06 NUMA一句话定位ethdev 是抽象层,PMD 是轮询驱动。热路径只有一次函数指针间接调用,没有其他分支。热路径真实代码(lib/ethdev/rte_ethdev.h:6261-6294)static inline uint16_t rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id, struct rte_mbuf **rx_pkts, const uint16_t nb_pkts) { uint16_t nb_rx; struct rte_eth_fp_ops *p; void *qd; // ...(DEBUG 编译下的参数检查,省略)... /* fetch pointer to queue data */ p = &rte_eth_fp_ops[port_id]; qd = p->rxq.data[queue_id]; // ... nb_rx = p->rx_pkt_burst(qd, rx_pkts, nb_pkts); // ... return nb_rx; }关键结构 rte_eth_fp_ops:每端口一份、按 cache line 对齐的函数指针表,声明在 lib/ethdev/rte_ethdev_core.h:129,实例在 lib/ethdev/rte_ethdev.c:42:struct rte_eth_fp_ops rte_eth_fp_ops[RTE_MAX_ETHPORTS];rx/tx 热路径只碰这张表,不碰 rte_eth_dev 大结构——热路径和配置路径的数据彻底分离。PMD 启动时把函数填进来(ixgbe 注册点,ixgbe_ethdev.c:1112):eth_dev->rx_pkt_burst = &ixgbe_recv_pkts;ixgbe 还会按特性换实现(ixgbe_rxtx.c:4940/4947/4954):启用 vector → ixgbe_recv_pkts_vec;启用 bulk alloc → ixgbe_recv_pkts_bulk_alloc;默认 → ixgbe_recv_pkts。RX 路径逐步走:DD 位协议 + refill + RDT 回写① 轮询 DD 位(ixgbe_rxtx.c:1812-1836,节选) while (nb_rx < nb_pkts) { /* * The order of operations here is important as the DD status * bit must not be read after any other descriptor fields. * rx_ring and rxdp are pointing to volatile data so the order * of accesses cannot be reordered by the compiler. If they were * not volatile, they could be reordered which could lead to * using invalid descriptor fields when read from rxd. * ... */ rxdp = &rx_ring[rx_id]; staterr = rxdp->wb.upper.status_error; if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD))) break; /* * Use acquire fence to ensure that status_error which includes * DD bit is loaded before loading of other descriptor words. */ rte_atomic_thread_fence(rte_memory_order_acquire); rxd = *rxdp;三个层次:volatile 约束编译器不重排;acquire fence 约束 CPU 不把 DD 位之外的字段提前读进来;DD=0 立即 break——没包就收工,绝不自旋空等。② refill:上一轮就挂好下一轮的 mbuf(ixgbe_rxtx.c:1870-1903,节选) nmb = rte_mbuf_raw_alloc(rxq->mb_pool); if (nmb == NULL) { // ... 计数 + break:描述符留着,下轮再试 break; } nb_hold++; rxe = &sw_ring[rx_id]; rx_id++; if (rx_id == rxq->nb_rx_desc) rx_id = 0; /* Prefetch next mbuf while processing current one. */ rte_ixgbe_prefetch(sw_ring[rx_id].mbuf); // ... rxm = rxe->mbuf; // 取走旧的 rxe->mbuf = nmb; // 挂上新的 dma_addr = rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb)); rxdp->read.hdr_addr = 0; rxdp->read.pkt_addr = dma_addr; // 新 mbuf 地址写给 NICmbuf 不是"收到包才分配",而是本轮补下一轮:从 mempool 拿新 mbuf 写回描述符,旧 mbuf 直接交给应用。这就是 mempool 存在的意义闭环——把分配移出热路径。③ mbuf 字段初始化(ixgbe_rxtx.c:1918-1945,节选) pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) - rxq->crc_len); rxm->data_off = RTE_PKTMBUF_HEADROOM; rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off); rxm->nb_segs = 1; rxm->next = NULL; rxm->pkt_len = pkt_len; rxm->data_len = pkt_len; rxm->port = rxq->port_id; pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data); // ... pkt_flags = rx_desc_status_to_pkt_flags(staterr, vlan_flags); pkt_flags = pkt_flags | rx_desc_error_to_pkt_flags(staterr, (uint16_t)pkt_info, rxq->rx_udp_csum_zero_err); pkt_flags = pkt_flags | ixgbe_rxd_pkt_info_to_pkt_flags((uint16_t)pkt_info); rxm->ol_flags = pkt_flags; rxm->packet_type = ixgbe_rxd_pkt_info_to_pkt_type(...); if (likely(pkt_flags & RTE_MBUF_F_RX_RSS_HASH)) rxm->hash.rss = rte_le_to_cpu_32( rxd.wb.lower.hi_dword.rss);硬件 offload 信息(校验和/VLAN/RSS 哈希/packet type)从描述符直接灌进 mbuf——零软件解析开销。④ RDT 回写:把描述符还给 NIC(ixgbe_rxtx.c:1959-1982,节选) rxq->rx_tail = rx_id; /* * If the number of free RX descriptors is greater than the RX free * threshold of the queue, advance the Receive Descriptor Tail (RDT) * register. * Update the RDT with the value of the last processed RX descriptor * minus 1, to guarantee that the RDT register is never equal to the * RDH register, which creates a "full" ring situation from the * hardware point of view... */ nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold); if (nb_hold > rxq->rx_free_thresh) { // ... rx_id = (uint16_t) ((rx_id == 0) ? (rxq->nb_rx_desc - 1) : (rx_id - 1)); IXGBE_PCI_REG_WC_WRITE(rxq->rdt_reg_addr, rx_id); nb_hold = 0; } rxq->nb_rx_hold = nb_hold;三个细节:① RDT 故意写 rx_id - 1——保证 RDT ≠ RDH,否则硬件误判 ring 满停收(PMD 最经典的 bug);② 攒批回写——nb_hold 累计超过 rx_free_thresh 才写一次寄存器,不逐包写 MMIO;③ _WC_WRITE——写合并(Write Combining)放宽序,批量 MMIO 写更快。批量预分配版本(ixgbe_rxtx.c:1629-1670,节选) /* allocate buffers in bulk directly into the S/W ring */ alloc_idx = rxq->rx_free_trigger - (rxq->rx_free_thresh - 1); rxep = &rxq->sw_ring[alloc_idx]; diag = rte_mempool_get_bulk(rxq->mb_pool, (void *)rxep, rxq->rx_free_thresh); if (unlikely(diag != 0)) return -ENOMEM; rxdp = &rxq->rx_ring[alloc_idx]; for (i = 0; i < rxq->rx_free_thresh; ++i) { /* populate the static rte mbuf fields */ mb = rxep[i].mbuf; if (reset_mbuf) { mb->port = rxq->port_id; } rte_mbuf_refcnt_set(mb, 1); mb->data_off = RTE_PKTMBUF_HEADROOM; /* populate the descriptors */ dma_addr = rte_cpu_to_le_64(rte_mbuf_data_iova_default(mb)); rxdp[i].read.hdr_addr = 0; rxdp[i].read.pkt_addr = dma_addr; }一次 rte_mempool_get_bulk 预分配 rx_free_thresh 个 mbuf 直接铺进软件环——P1-02 学的 mempool 批量 API,在这里兑现成"收包零分配"。vector PMD:SIMD 批处理ixgbe_recv_pkts_vec(ixgbe_rxtx_vec_sse.c:599 起):一次循环处理 8/16 个描述符,SIMD 批量搬运字段,movemask 一条指令提取多个 DD 位。本质:把逐包的分支和标量搬运变成定长批量操作——DPDK "burst" 哲学的硬件层实现。选择逻辑在 ixgbe_rxtx.c:4940。🐧 内核对照:同一块硬件,两种活法真实内核代码(ixgbe_main.c:2306-2345,节选): while (likely(total_rx_packets < budget)) { // budget 上限! // ... /* return some buffers to hardware, one at a time is too slow */ if (cleaned_count >= IXGBE_RX_BUFFER_WRITE) { ixgbe_alloc_rx_buffers(rx_ring, cleaned_count); cleaned_count = 0; } rx_desc = IXGBE_RX_DESC(rx_ring, rx_ring->next_to_clean); size = le16_to_cpu(rx_desc->wb.upper.length); if (!size) break; /* This memory barrier is needed to keep us from reading * any other fields out of the rx_desc until we know the * descriptor has been written back */ dma_rmb(); rx_buffer = ixgbe_get_rx_buffer(rx_ring, rx_desc, &skb, size, &rx_buffer_pgcnt); // ... XDP 处理 ... skb = ixgbe_construct_skb(rx_ring, rx_buffer, &xdp, rx_desc); // ... ixgbe_rx_skb(q_vector, skb); // ... total_rx_packets++; }环节内核 ixgbe(软中断里)DPDK ixgbe PMD(lcore 里)触发硬中断(MSI-X 每队列)→ NAPI纯轮询预算budget 限制(防饿死其他 CPU)无预算,burst 打满屏障dma_rmb()acquire fence(等价物)补缓冲ixgbe_alloc_rx_buffers 页引用rte_mbuf_raw_alloc mbuf 池包容器构造 skb(ixgbe_construct_skb)预分配 mbuf 直接换指针协议完全一样(DD 位 + RDT),差异在运行模型:内核在软中断里受预算约束地做,DPDK 在应用线程里无约束地做。10G 线速(14.88 Mpps)下,内核单核百万级 PPS,DPDK 单核可达线速——差距就是中断、skb 构造和协议栈的代价。面试 3 问描述 DD 位与 RDT 回写的协议。 → ① NIC 写 DD 通知就绪(单向);② 软件写 RDT 归还描述符(单向);③ RDT 写 rx_id-1 防等于 RDH(ixgbe_rxtx.c:1965-1968 注释)。为什么收包路径的 mbuf 是"上一轮"分配的? → refill 提前挂 mbuf 进描述符,收包时只换指针(ixgbe_rxtx.c:1898-1903);批量版一次 mempool_get_bulk 铺 rx_free_thresh 个(ixgbe_rxtx.c:1641-1642)。rte_eth_fp_ops 表解决什么问题? → 热路径只碰每端口对齐的小表(rte_ethdev_core.h:129),与配置路径的 rte_eth_dev 大结构分离,避免每次收包都踩冷数据。🔧 动手实验testpmd 跑 io / mac / macswap / rxonly / txonly 五种 forward mode 记录 PPS;再对比 scalar 与 vector PMD 的小包 PPS。产出 perf-data/P1/W1-testpmd-modes.md。下一篇:P1-06 · NUMA 与 lcore 调度
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P1-04 · mbuf:包的一生都住在里面
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/mbuf/,引用均标 文件:行号上一篇:P1-03 ring | 下一篇:P1-05 PMD一句话定位mbuf 之于 DPDK,等于 sk_buff 之于内核:数据面所有结构都围绕它转。mbuf 的设计处处为 cache line 优化:一个包的处理全程只碰 1-2 个 cache line。真实布局(lib/mbuf/rte_mbuf_core.h:467-674)第一缓存行的真实定义(节选,rte_mbuf_core.h:468-518):struct __rte_cache_aligned rte_mbuf { void *buf_addr; /**< Virtual address of segment buffer. */ #if RTE_IOVA_IN_MBUF alignas(sizeof(rte_iova_t)) rte_iova_t buf_iova; // ... #endif /* next 8 bytes are initialised on RX descriptor rearm */ union { uint64_t rearm_data[1]; __extension__ struct { uint16_t data_off; RTE_ATOMIC(uint16_t) refcnt; // 引用计数(原子性可配) uint16_t nb_segs; // 仅首段有效 uint16_t port; // 入端口号 }; }; uint64_t ol_flags; /**< Offload features. */ /* remaining 24 bytes are set on RX when pulling packet from descriptor */ union { void *rx_descriptor_fields1[24 / sizeof(void *)]; __extension__ struct { union { uint32_t packet_type; ... }; uint32_t pkt_len; /**< Total pkt len: sum of all segments. */ uint16_t data_len; /**< Amount of data in segment buffer. */ uint16_t vlan_tci; union { uint32_t rss; ... } hash; /**< RSS hash result */ // ... }; }; struct rte_mempool *pool; /**< Pool from which mbuf was allocated. */第二缓存行(节选,rte_mbuf_core.h:606-673): /* second cache line - fields only used in slow path or on TX */ #if RTE_IOVA_IN_MBUF alignas(RTE_CACHE_LINE_MIN_SIZE) struct rte_mbuf *next; // 分段链(IOVA_IN_MBUF=1 时在第二行) #endif /* fields to support TX offloads */ union { uint64_t tx_offload; /**< combined for easy fetch */ __extension__ struct { uint64_t l2_len:RTE_MBUF_L2_LEN_BITS; uint64_t l3_len:RTE_MBUF_L3_LEN_BITS; uint64_t l4_len:RTE_MBUF_L4_LEN_BITS; uint64_t tso_segsz:RTE_MBUF_TSO_SEGSZ_BITS; uint64_t outer_l3_len:RTE_MBUF_OUTL3_LEN_BITS; uint64_t outer_l2_len:RTE_MBUF_OUTL2_LEN_BITS; }; }; struct rte_mbuf_ext_shared_info *shinfo; // 外部缓冲共享信息 uint16_t priv_size; uint16_t timesync; uint32_t dynfield1[9]; /**< Reserved for dynamic fields. */ };三个设计点:rearm_data 打包:收包时要复位的 data_off/refcnt/nb_segs/port 打包成 64 位,一条 8B store 完成 mbuf 重装。实测证据在 ixgbe_rx_alloc_bufs(ixgbe_rxtx.c:1654-1655):rte_mbuf_refcnt_set(mb, 1); mb->data_off = RTE_PKTMBUF_HEADROOM;。headroom = 128B(build/rte_build_config.h:454):留给封装。VXLAN 外层头 = ETH14 + IP20 + UDP8 + VXLAN8 = 50B,128 绰绰有余。rte_pktmbuf_prepend() 向前扩 data_off,零拷贝加隧道头。默认缓冲 RTE_MBUF_DEFAULT_BUF_SIZE = 2048 + 128(rte_mbuf_core.h:405-407)。pool 指针:mbuf 记得自己的 mempool(rte_mbuf_core.h:604),rte_pktmbuf_free 才能按池归还。外部缓冲:rte_mbuf_ext_shared_info(rte_mbuf_core.h:684-688)struct rte_mbuf_ext_shared_info { rte_mbuf_extbuf_free_callback_t free_cb; /**< Free callback function */ void *fcb_opaque; /**< Free callback argument */ RTE_ATOMIC(uint16_t) refcnt; };rte_pktmbuf_attach_extbuf() 把用户自己的内存挂进 mbuf:refcnt 原子计数管共享,归零时调用户的 free_cb——生命周期完全由用户控制。典型场景:TLS 记录、文件缓存直接交给网卡 DMA 收发,全程不拷贝。这是 P1 零拷贝 echo 的技术基础。🐧 内核对照:sk_buff 的真实结构head 区模型(skbuff.h:3122-3124):#ifndef NET_SKB_PAD #define NET_SKB_PAD max(32, L1_CACHE_BYTES) #endif(x86 上 L1_CACHE_BYTES=64,即 64B headroom——和 mbuf 的 128B 是同一思想,留的余量不同。)非线性数据(include/linux/skbuff.h:572-597,全文):struct skb_shared_info { __u8 flags; __u8 meta_len; __u8 nr_frags; __u8 tx_flags; unsigned short gso_size; unsigned short gso_segs; struct sk_buff *frag_list; struct skb_shared_hwtstamps hwtstamps; unsigned int gso_type; u32 tskey; /* * Warning : all fields before dataref are cleared in __alloc_skb() */ atomic_t dataref; unsigned int xdp_frags_size; void * destructor_arg; /* must be last field, see pskb_expand_head() */ skb_frag_t frags[MAX_SKB_FRAGS]; };struct sk_buff 开头(skbuff.h:843-876 节选):struct sk_buff { union { struct { /* These two members must be first to match sk_buff_head. */ struct sk_buff *next; struct sk_buff *prev; // ... }; struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */ struct list_head list; struct llist_node ll_node; }; struct sock *sk; // ... /* * This is the control buffer. It is free to use for every * layer. ... This is owned by whoever has the skb queued ATM. */ char cb[48] __aligned(8); // ... unsigned int len, data_len;对照图:DPDK mbuf内核 sk_buff差异本质数据区独立,buf_addr 指针引用head 区 kmalloc 一段(末端挂 shared_info)都要 headroom(128 vs 64)多段 = mbuf 链(next 指针)非线性数据 = frags[] 页数组段链表 vs 页片段数组refcnt(rearm_data 内)skb_shared_info.dataref(原子计数)同为引用计数,skb 放在数据区末端共享结构mbuf 记得 pool,free 即归还每包进 slab/page_pool 分配池化预分配 vs 按需分配+池化回收面试金句:mbuf 把"段"建模为元数据链表,适配用户态预分配池 + 批量轮询;skb 把"非线性数据"建模为页数组,适配内核页缓存零拷贝生态——不是谁好谁坏,是谁服务谁的场景。面试 3 问rearm_data 打包 4 字段的意义? → 收包热路径一次 8B store 重置 4 个字段;对应 rte_mbuf_core.h:488-516。headroom 为什么 128B? → 隧道封装零拷贝 prepend;VXLAN 外层 50B 加得下。extbuf 与 clone 的计数分别在哪? → extbuf 在 rte_mbuf_ext_shared_info.refcnt(用户内存共享);clone 在 mbuf 自身 refcnt。🔧 动手实验写零拷贝 echo(code/P1/zerocopy-echo/):不分配新 mbuf,in-place 交换 src/dst MAC+IP 原路发回。dperf 打满对比"重新分配 mbuf"版本,量化省掉的 mempool get/put 值多少 Mpps。数据落点 perf-data/P1/。下一篇:P1-05 · PMD:把中断换成轮询
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-03 · ring:免锁队列如何可能
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/ring/,引用均标 文件:行号上一篇:P1-02 mempool | 下一篇:P1-04 mbuf一句话定位rte_ring 是 DPDK 的"万能胶":mempool 的默认后端是它,进程间通信用它,P3 的 LB 分发用它。理解 ring = 理解 CAS 抢占 + tail 有序发布 这套无锁队列范式。真实结构(lib/ring/rte_ring_core.h)/** prod/cons sync types */ enum rte_ring_sync_type { // rte_ring_core.h:53 RTE_RING_SYNC_MT, /**< multi-thread safe (default mode) */ RTE_RING_SYNC_ST, /**< single thread only */ RTE_RING_SYNC_MT_RTS, /**< multi-thread relaxed tail sync */ RTE_RING_SYNC_MT_HTS, /**< multi-thread head/tail sync */ }; struct rte_ring_headtail { // rte_ring_core.h:65 volatile RTE_ATOMIC(uint32_t) head; /**< prod/consumer head. */ volatile RTE_ATOMIC(uint32_t) tail; /**< prod/consumer tail. */ union { enum rte_ring_sync_type sync_type; uint32_t single; }; }; struct rte_ring { // rte_ring_core.h:116 alignas(RTE_CACHE_LINE_SIZE) char name[RTE_RING_NAMESIZE]; int flags; const struct rte_memzone *memzone; uint32_t size; /**< Size of ring. */ uint32_t mask; /**< Mask (size-1) of ring. */ uint32_t capacity; /**< Usable size of ring */ RTE_CACHE_GUARD; /** Ring producer status. */ union __rte_cache_aligned { struct rte_ring_headtail prod; struct rte_ring_hts_headtail hts_prod; struct rte_ring_rts_headtail rts_prod; }; RTE_CACHE_GUARD; /** Ring consumer status. */ union __rte_cache_aligned { struct rte_ring_headtail cons; struct rte_ring_hts_headtail hts_cons; struct rte_ring_rts_headtail rts_cons; }; RTE_CACHE_GUARD; };结构注释里藏着核心设计(rte_ring_core.h:106-115):head/tail 不是 0..size-1 之间的值,而是 0..2^32-1,访问 ring[] 时才用 mask。因此两个 uint32 索引的减法在模 32 位意义下永远正确,溢出不是问题。布局要点:prod 和 cons 各占独立 cache line(RTE_CACHE_GUARD 隔离)——生产者只写 prod 行,消费者只写 cons 行,双方互不挤占对方的缓存行。size 必须 2 的幂 → & mask 替代 % size。入队四步:对真实代码逐行讲第 ① 步 · 抢占区间(lib/ring/rte_ring_generic_pvt.h:55-101,节选):static __rte_always_inline unsigned int __rte_ring_move_prod_head(struct rte_ring *r, unsigned int is_sp, unsigned int n, enum rte_ring_queue_behavior behavior, uint32_t *old_head, uint32_t *new_head, uint32_t *free_entries) { const uint32_t capacity = r->capacity; // ... do { n = max; *old_head = r->prod.head; /* add rmb barrier to avoid load/load reorder in weak * memory model. It is noop on x86 */ rte_smp_rmb(); // :74 /* * The subtraction is done between two unsigned 32bits value * (the result is always modulo 32 bits ...). So 'free_entries' * is always between 0 and capacity (which is < size). */ *free_entries = (capacity + r->cons.tail - *old_head); // :82 if (unlikely(n > *free_entries)) n = (behavior == RTE_RING_QUEUE_FIXED) ? 0 : *free_entries; if (n == 0) return 0; *new_head = *old_head + n; if (is_sp) { r->prod.head = *new_head; // SP:普通写即可 success = 1; } else success = rte_atomic32_cmpset((uint32_t *)&r->prod.head, *old_head, *new_head); // MP:CAS 抢区间 } while (unlikely(success == 0)); return n; }第 ② 步 · 写入对象:__rte_ring_enqueue_elems(rte_ring_elem_pvt.h:129-152)按元素大小分派 32/64/128 字节三档批量复制——定长批量 copy,编译器可向量化。第 ③④ 步 · 屏障 + 发布 tail(rte_ring_generic_pvt.h:13-30,全文):static __rte_always_inline void __rte_ring_update_tail(struct rte_ring_headtail *ht, uint32_t old_val, uint32_t new_val, uint32_t single, uint32_t enqueue) { if (enqueue) rte_smp_wmb(); else rte_smp_rmb(); /* * If there are other enqueues/dequeues in progress that preceded us, * we need to wait for them to complete */ if (!single) rte_wait_until_equal_32((volatile uint32_t *)&ht->tail, old_val, rte_memory_order_relaxed); ht->tail = new_val; }两个知识点:wmb/rmb 的选择:入队前 wmb(对象内容先于 tail 发布可见),出队前 rmb(读到发布后的 tail 才读对象)。MP 场景的按序发布:rte_wait_until_equal_32 等前一个生产者的 tail 到 old_val 再写自己的——即使 P2 先写完数据,tail 也只能按序发布,消费者看到的永远是有序完整的数据。这就是"head 无锁抢占、tail 顺序发布"的完整语义。入队主循环(rte_ring_elem_pvt.h:320-341,全文):static __rte_always_inline unsigned int __rte_ring_do_enqueue_elem(struct rte_ring *r, const void *obj_table, unsigned int esize, unsigned int n, enum rte_ring_queue_behavior behavior, unsigned int is_sp, unsigned int *free_space) { uint32_t prod_head, prod_next; uint32_t free_entries; n = __rte_ring_move_prod_head(r, is_sp, n, behavior, &prod_head, &prod_next, &free_entries); if (n == 0) goto end; __rte_ring_enqueue_elems(r, prod_head, obj_table, esize, n); __rte_ring_update_tail(&r->prod, prod_head, prod_next, is_sp, 1); end: // ... return n; }出队是完美镜像(rte_ring_elem_pvt.h:367-389):move_cons_head → dequeue_elems → update_tail(cons, ..., 0)。变体一句话RTS(Relaxed Tail Sync):head/tail 以 (cnt, pos) 打包成 64 位原子(rte_ring_core.h:76-83),生产者可提前发布,靠 watermark 背压——用容量利用率换更少的缓存行竞争。HTS(Head/Tail Sync):head/tail 打包进一个 64 位原子(rte_ring_core.h:92-104),一次原子操作同时读两个值。两阶段 peek API:rte_ring_enqueue_peek 占位 → 逐包填充(填不下就丢)→ rte_ring_enqueue_do_commit 发布,适合网卡收包场景。🐧 内核对照:kfifo 与 RCUkfifo(lib/kfifo.c) 是内核最接近 ring 的通用无锁 FIFO,屏障用法一一对应:static void kfifo_copy_in(struct __kfifo *fifo, const void *src, unsigned int len, unsigned int off) // kfifo.c:88 { // ... memcpy 两段(处理回绕) ... /* * make sure that the data in the fifo is up to date before * incrementing the fifo->in index counter */ smp_wmb(); // kfifo.c:110 } unsigned int __kfifo_in(struct __kfifo *fifo, const void *buf, unsigned int len) // kfifo.c:113 { unsigned int l; l = kfifo_unused(fifo); if (len > l) len = l; kfifo_copy_in(fifo, buf, len, fifo->in); fifo->in += len; // "发布" return len; }出队侧 kfifo_copy_out 同样在 out 前 smp_wmb()(kfifo.c:145-150)。与 ring 的差异:kfifo 是 SPSC 假设(in/out 各一人写),ring 用 CAS 把同样的屏障范式扩展到了 MP/MC。RCU(kernel/rcu/):另一种"免锁"哲学——读方零开销,写方延迟回收。ring 的免锁是队列语义(生产者-消费者互不阻塞);RCU 的免锁是发布-订阅语义(读者随时读,写者保证读者不踩到已释放内存)。面试能讲出这个区别是加分项。面试 3 问画 4 线程并发 enqueue 时序图,tail 最后是多少? → 各线程 CAS 抢到互不重叠区间;tail 按 wait_until_equal_32 顺序推进,最终 = 实际入队总数。少一条 wmb 会怎样? → 消费者可能先看到 tail 发布再读到未写完的对象(数据竞争),且只在多核乱序窗口偶现,极难复现——所以源码注释必须写清屏障"保护什么"。head/tail 为什么用 uint32 且允许回绕? → 模 32 位减法天然正确(rte_ring_core.h:106-115 注释),省掉回绕判断。🔧 动手实验扩展 code/ring-bench/:SPSC/MPSC/MPMC × 1/2/4/8 producer 吞吐+延迟矩阵。预期曲线:SPSC 最快(无 CAS 无等待),MPMC 在 8 生产者时见顶——数据留档 perf-data/P1/,这就是"热路径要 per-core 数据结构"的实证。下一篇:P1-04 · mbuf:包的一生都住在里面
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-02 · mempool:预分配 + 每核缓存,消灭逐包分配
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/mempool/,引用均标 文件:行号上一篇:P1-01 EAL | 下一篇:P1-03 ring一句话定位内核每收一个包要 kmalloc 一个 skb(快路径进 slab per-CPU 缓存,慢路径仍有锁和页分配)。DPDK 的回答:启动时一次性造好 N 个 mbuf 放池里,每核再缓存一批,热路径拿/还只碰自己核的缓存,零竞争。架构图真实数据结构(lib/mempool/rte_mempool.h)/** * A structure that stores a per-core object cache. */ struct __rte_cache_aligned rte_mempool_cache { // rte_mempool.h:90 uint32_t size; /**< Size of the cache */ uint32_t flushthresh; /**< Threshold before we flush excess elements */ uint32_t len; /**< Current cache count */ // ... 统计字段(编译开关控制) /** * Cache objects * * Cache is allocated to this size to allow it to overflow in certain * cases to avoid needless emptying of cache. */ alignas(RTE_CACHE_LINE_SIZE) void *objs[RTE_MEMPOOL_CACHE_MAX_SIZE * 2]; }; // rte_mempool.h:114注意 objs 数组是 MAX_SIZE * 2——故意留一倍溢出空间,"overflow to avoid needless emptying":put 时即使超过 flushthresh 也能先落进数组,下次再整批回刷,而不是立即碰后端。池本体(rte_mempool.h:230-272 节选):struct __rte_cache_aligned rte_mempool { char name[RTE_MEMPOOL_NAMESIZE]; union { void *pool_data; /**< Ring or pool to store objects. */ uint64_t pool_id; /**< External mempool identifier. */ }; unsigned int flags; /**< Flags of the mempool. */ int socket_id; /**< Socket id passed at create. */ uint32_t size; /**< Max size of the mempool. */ uint32_t cache_size; /**< Size of per-lcore default local cache. */ uint32_t elt_size; /**< Size of an element. */ int32_t ops_index; /**< ops 表索引(ring/stack/lf-stack) */ struct rte_mempool_cache *local_cache; /**< Per-lcore local cache */ // ... };两个必须记住的常量// lib/mempool/rte_mempool.c:56 #define CALC_CACHE_FLUSHTHRESH(c) (((c) * 3) / 2)// config/rte_config.h:61 #define RTE_MEMPOOL_CACHE_MAX_SIZE 512flushthresh = cache_size × 1.5:缓存水位保持在 [size, 1.5×size]——既能吸收突发,又不囤积(囤积会让别的核饿死)。初始化在 rte_mempool.c:746-757(mempool_cache_init)。创建时强校验(rte_mempool.c:837-842): /* asked cache too big */ if (cache_size > RTE_MEMPOOL_CACHE_MAX_SIZE || CALC_CACHE_FLUSHTHRESH(cache_size) > n) { rte_errno = EINVAL; return NULL; }头文件文档再补一刀(rte_mempool.h:1033-1038): * RTE_MEMPOOL_CACHE_MAX_SIZE and n / 1.5. It is advised to choose * cache_size to have "n modulo cache_size == 0": if this is * not the case, some elements will always stay in the pool and will * never be used.——cache_size 必须 ≤ n/1.5(否则回刷阈值超过池容量),且建议整除 n。put 热路径:逐行读 rte_mempool_do_generic_put(rte_mempool.h:1377-1426,节选核心逻辑)static __rte_always_inline void rte_mempool_do_generic_put(struct rte_mempool *mp, void * const *obj_table, unsigned int n, struct rte_mempool_cache *cache) { void **cache_objs; /* No cache provided */ if (unlikely(cache == NULL)) goto driver_enqueue; // ... /* The request itself is too big for the cache */ if (unlikely(n > cache->flushthresh)) goto driver_enqueue_stats_incremented; // rte_mempool.h:1392-1393 /* * The cache follows the following algorithm: * 1. If the objects cannot be added to the cache without crossing * the flush threshold, flush the cache to the backend. * 2. Add the objects to the cache. */ if (cache->len + n <= cache->flushthresh) { cache_objs = &cache->objs[cache->len]; cache->len += n; } else { cache_objs = &cache->objs[0]; rte_mempool_ops_enqueue_bulk(mp, cache_objs, cache->len); // 整批回刷 cache->len = n; } /* Add the objects to the cache. */ rte_memcpy(cache_objs, obj_table, sizeof(void *) * n); // ... driver_enqueue: /* push objects to the backend */ rte_mempool_ops_enqueue_bulk(mp, obj_table, n); }三条规则:① 请求太大直接走后端;② 缓存放得下就只改 len(一次 memcpy);③ 放不下先把旧缓存整批刷回后端,再收下新的。全程无锁——cache 是每核私有的。get 热路径:逐行读 rte_mempool_do_generic_get(rte_mempool.h:1506-1590,节选核心逻辑) /* The cache is a stack, so copy will be in reverse order. */ cache_objs = &cache->objs[cache->len]; // :1521-1522 if (__rte_constant(n) && n <= cache->len) { /* * The request size is known at build time, and * the entire request can be satisfied from the cache, * so let the compiler unroll the fixed length copy loop. */ cache->len -= n; for (index = 0; index < n; index++) *obj_table++ = *--cache_objs; // :1524-1538 return 0; } /* Use the cache as much as we have to return hot objects first. */ len = __rte_constant(n) ? cache->len : RTE_MIN(n, cache->len); cache->len -= len; remaining = n - len; for (index = 0; index < len; index++) *obj_table++ = *--cache_objs; // :1545-1549 // ... /* if dequeue below would overflow mem allocated for cache */ if (unlikely(remaining > RTE_MEMPOOL_CACHE_MAX_SIZE)) goto driver_dequeue; // :1566-1567 /* Fill the cache from the backend; fetch size + remaining objects. */ ret = rte_mempool_ops_dequeue_bulk(mp, cache->objs, cache->size + remaining); // :1570-1571 // ... /* Satisfy the remaining part of the request from the filled cache. */ cache_objs = &cache->objs[cache->size + remaining]; for (index = 0; index < remaining; index++) *obj_table++ = *--cache_objs; cache->len = cache->size; // :1582-1587四个要点:cache 是栈(LIFO):注释明说 "The cache is a stack"——刚放回的对象最热,最先被取走(cache 友好)。编译期常量优化:__rte_constant(n) 为真时循环展开——收包路径 nb_pkts=32 这种常量调用会展开成无循环代码。miss 时补货量是 cache->size + remaining:先把缓存补满再满足请求。把对共享 ring 的竞争摊销到每 cache_size 次访问一次。请求巨大(>512)直接走后端,绕过缓存——避免缓存被一次性掏空。🐧 内核对照:SLUB 的 per-CPU 缓存真实结构(include/linux/slub_def.h:50-66):struct kmem_cache_cpu { union { struct { void **freelist; /* Pointer to next available object */ unsigned long tid; /* Globally unique transaction id */ }; freelist_aba_t freelist_tid; }; struct slab *slab; /* The slab from which we are allocating */ #ifdef CONFIG_SLUB_CPU_PARTIAL struct slab *partial; /* Partially allocated frozen slabs */ #endif local_lock_t lock; /* Protects the fields above */ };DPDK mempool内核 SLUB差异本质rte_mempool_cache(每 lcore)kmem_cache_cpu(per-CPU freelist)同一哲学:per-CPU 缓存吸收竞争miss → ring 批量补 size+remainingfreelist 空 → 从 per-node partial 摘 slab都是"一次慢路径换多次快路径"无锁 ring 共享池,无锁全程partial 链表 + local_lock_t lock内核慢路径仍要锁;DPDK 靠"绑核不迁移"免锁固定大小对象任意大小内核服务通用分配器,DPDK 只服务 mbufSLUB 的 tid 用于检测抢占迁移(线程被切走又回来时发现 freelist 被别的 CPU 动过)。DPDK 没有这个烦恼——lcore 绑核,线程根本不迁移,这是 EAL 绑核的连锁收益。面试 3 问cache miss 时为什么一次补 cache->size + remaining 个? → 把共享 ring 的竞争摊销为每 cache_size 次访问一次;对应 rte_mempool.h:1570-1571。flushthresh 为什么是 1.5 倍? → CALC_CACHE_FLUSHTHRESH = c*3/2(rte_mempool.c:56);水位 [size, 1.5×size]。objs 数组为什么开 MAX×2? → 允许溢出暂存,"avoid needless emptying of cache"(rte_mempool.h:110-113 注释)。🔧 动手实验写 code/P1/mempool-bench/:8 核并发 alloc/free,cache_size = 0 / 128 / 512 对比吞吐与延迟,出火焰图。cache=0 时 CAS 竞争会给出这道题的标准答案。 数据落点 perf-data/P1/。下一篇:P1-03 · ring:免锁队列如何可能
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-01 · EAL:用户态的"迷你内核启动器"
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/eal/,引用均标 文件:行号上一篇:无(系列第 1 篇)| 下一篇:P1-02 mempool一句话定位EAL(Environment Abstraction Layer)在用户态复刻了内核启动时做的那些事:CPU/拓扑检测、内存管理、设备枚举、中断处理、线程管理。rte_eal_init()(lib/eal/linux/eal.c:918)就是 DPDK 的 start_kernel。启动主线:对着真实代码走一遍步骤真实调用行号内核对应① 日志eal_log_init()eal.c:937printk 日志子系统② CPU/lcore 检测rte_eal_cpu_init()eal.c:965setup_arch 拓扑发现③ 参数解析eal_parse_args()eal.c:971内核 cmdline 解析④ 运行时配置rte_config_init()eal.c:998编译期 .config⑤ 中断线程rte_eal_intr_init()eal.c:1003irq 子系统⑥ 总线扫描rte_bus_scan()eal.c:1025pci_scan_bus(drivers/pci/probe.c)⑦ IOVA 模式选择自动探测逻辑eal.c:1032-1086IOMMU 使能检测⑧ 大页+内存rte_eal_memory_init()eal.c:1087/1136memblock → buddy⑨ worker 线程eal_worker_thread_create()eal.c:1197-1224kthread + 调度器⑩ 设备探测rte_bus_probe()eal.c:1242driver probe(drivers/base/dd.c)第 ⑦ 步:IOVA 自动选择(eal.c:1037-1064)/* if no EAL option "--iova-mode=<pa|va>", use bus IOVA scheme */ if (internal_conf->iova_mode == RTE_IOVA_DC) { /* autodetect the IOVA mapping mode */ enum rte_iova_mode iova_mode = bus_iova_mode; if (iova_mode == RTE_IOVA_DC) { EAL_LOG(DEBUG, "Buses did not request a specific IOVA mode."); if (!RTE_IOVA_IN_MBUF) { iova_mode = RTE_IOVA_VA; EAL_LOG(DEBUG, "IOVA as VA mode is forced by build option."); } else if (!phys_addrs) { /* if we have no access to physical addresses, * pick IOVA as VA mode. */ iova_mode = RTE_IOVA_VA; EAL_LOG(DEBUG, "Physical addresses are unavailable, selecting IOVA as VA mode."); } else if (is_iommu_enabled()) { /* we have an IOMMU, pick IOVA as VA mode */ iova_mode = RTE_IOVA_VA; EAL_LOG(DEBUG, "IOMMU is available, selecting IOVA as VA mode."); } else { /* physical addresses available, and no IOMMU * found, so pick IOVA as PA. */ iova_mode = RTE_IOVA_PA; EAL_LOG(DEBUG, "IOMMU is not available, selecting IOVA as PA mode."); } } rte_eal_get_configuration()->iova_mode = iova_mode; }读出三层信息:① 有 IOMMU 就用 VA(网卡 DMA 走虚拟地址,P5 讲 DPU 时回来复习);② 优先级是"构建选项 > 物理地址可用性 > IOMMU 检测";③ 这就是 VFIO 绑定后 DPDK 默认跑 VA 模式的原因——rte_vfio_enable("vfio") 在 eal.c:1117。第 ⑨ 步:worker lcore 怎么诞生(eal.c:1197-1224,节选)RTE_LCORE_FOREACH_WORKER(i) { /* * create communication pipes between main thread * and children */ if (pipe(lcore_config[i].pipe_main2worker) < 0) rte_panic("Cannot create pipe\n"); if (pipe(lcore_config[i].pipe_worker2main) < 0) rte_panic("Cannot create pipe\n"); lcore_config[i].state = WAIT; /* create a thread for each lcore */ ret = eal_worker_thread_create(i); if (ret != 0) rte_panic("Cannot create thread\n"); // ... 设置线程名 "dpdk-worker%d" ... ret = rte_thread_set_affinity_by_id(lcore_config[i].thread_id, &lcore_config[i].cpuset); if (ret != 0) rte_panic("Cannot set affinity\n"); }两个细节:每个 worker lcore 有一对 pipe 与主线程通信(rte_eal_remote_launch 的底层机制);线程创建后立即绑核——lcore 模型的第一条纪律。之后主线程用 rte_eal_mp_remote_launch(sync_func, ...) 同步确认所有 worker 就绪(eal.c:1230-1231)。三个关键概念lcore:逻辑核。rte_lcore_id() 返回 EAL 编号;--lcores 可把 lcore 映射到任意物理核。IOVA 模式:见上文判定链——RTE_IOVA_VA(需 IOMMU)vs RTE_IOVA_PA(无 IOMMU 兜底)。大页:DPDK 内存全部来自 hugepage(2M/1G),页表项少 → TLB miss 少。eal_hugepage_info_init() 读 /sys/kernel/mm/hugepages/(eal.c:1087-1098)。🐧 内核对照DPDK内核关键差异rte_eal_init 一步步初始化start_kernel → rest_init内核在特权态,DPDK 全靠 sysfs/procfs 读信息memseg 表 + 大页buddy allocator + zoneDPDK 内存一次性申请,之后零分配轮询收包中断 + 软中断见 [P1-05 PMD 对照](/index.php/archives/29/)lcore 绑核sched domain / CPU affinity内核调度器会迁移任务,DPDK 主动绑死面试 3 问EAL 启动做了哪些事? → 背十步表,并说出每步的内核对应物。IOVA VA/PA 何时切换? → 复述 eal.c:1037-1064 判定链:构建选项 → 物理地址可用性 → IOMMU 检测。worker lcore 是怎么被拉起的? → 启动时创建线程 + 双向 pipe + 绑核 + 置 WAIT 态;运行时 rte_eal_remote_launch 经 pipe 下发函数。🔧 动手实验改造 examples/helloworld:每个 lcore 打印 rte_lcore_id()、物理 CPU、NUMA node。产出 code/P1/helloworld-plus/。下一篇:P1-02 · mempool:预分配 + 每核缓存,消灭逐包分配
2026年08月18日
2 阅读
0 评论
0 点赞
第 1/1 页 · 本页 7 条 · 共 7 条