首页
关于
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
条评论
首页
栏目
页面
关于
搜索到
39
篇与
的结果
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P5-02 · VFIO 源码深读:把设备安全地交给用户态
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/linux-stable/drivers/vfio/pci/vfio_pci_core.c 与 vfio_pci.c,引用均标 文件:行号上一篇:P5-01 DMA/IOVA/IOMMU | 下一篇:P5-03 virtio 与 vhost-user一句话定位DPDK 轮询收包的前提是用户态能直接摸到网卡寄存器、收到网卡中断、让网卡 DMA 自己的内存。VFIO 就是管这三件事的内核设施:mmap 交出 BAR,ioctl 交出中断,IOMMU 兜住 DMA——而且每一步都带着安全检查。真实源码精读① open 即接管:vfio_pci_open_device(vfio_pci.c:103-128)static int vfio_pci_open_device(struct vfio_device *core_vdev) { struct vfio_pci_core_device *vdev = container_of(core_vdev, struct vfio_pci_core_device, vdev); struct pci_dev *pdev = vdev->pdev; int ret; ret = vfio_pci_core_enable(vdev); if (ret) return ret; if (vfio_pci_is_vga(pdev) && ...) { // ...省略 Intel IGD 特殊处理 ... } vfio_pci_core_finish_enable(vdev); return 0; }用户态 open("/dev/vfio/<group>") 最终落到这里。核心动作在 vfio_pci_core_enable(vfio_pci_core.c:470 起):先 pci_clear_master(:484,清掉上一个使用者留下的 bus master),pci_enable_device(:486),然后强制复位: /* If reset fails because of the device lock, fail this path entirely */ ret = pci_try_reset_function(pdev); // vfio_pci_core.c:491 if (ret == -EAGAIN) goto out_disable_device; vdev->reset_works = !ret; pci_save_state(pdev); // :496open 即 reset:上一个租户(比如虚拟机)把网卡搞到任何脏状态,下一个 open 的人拿到的都是一张干净的卡。这是多租户共享裸机场景的安全底线。② ioctl 分发表:vfio_pci_core_ioctl(vfio_pci_core.c:1452-1479)long vfio_pci_core_ioctl(struct vfio_device *core_vdev, unsigned int cmd, unsigned long arg) { struct vfio_pci_core_device *vdev = container_of(core_vdev, struct vfio_pci_core_device, vdev); void __user *uarg = (void __user *)arg; switch (cmd) { case VFIO_DEVICE_GET_INFO: return vfio_pci_ioctl_get_info(vdev, uarg); case VFIO_DEVICE_GET_IRQ_INFO: return vfio_pci_ioctl_get_irq_info(vdev, uarg); // ...省略 HOT_RESET 相关两条 ... case VFIO_DEVICE_GET_REGION_INFO: return vfio_pci_ioctl_get_region_info(vdev, uarg); case VFIO_DEVICE_IOEVENTFD: return vfio_pci_ioctl_ioeventfd(vdev, uarg); // ...省略 PCI_HOT_RESET / RESET ... case VFIO_DEVICE_SET_IRQS: return vfio_pci_ioctl_set_irqs(vdev, uarg); default: return -ENOTTY; } } EXPORT_SYMBOL_GPL(vfio_pci_core_ioctl);整个接口面就这 9 个命令。对 DPDK 最关键的两个:GET_REGION_INFO(问清楚每个 BAR 的 offset/大小/可否 mmap),SET_IRQS(把 MSI-X 中断注册成 eventfd——DPDK 的 eal_interrupt 用 epoll 等它)。挂载点在 vfio_pci_ops 虚表里(vfio_pci.c:130-144,.ioctl = vfio_pci_core_ioctl 在 :136)。③ BAR mmap:物理页映射 + 属性钳制(vfio_pci_core.c:1690-1759,节选)int vfio_pci_core_mmap(struct vfio_device *core_vdev, struct vm_area_struct *vma) { // ...省略 container_of 和 index 计算 ... index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT); if (index >= VFIO_PCI_NUM_REGIONS + vdev->num_regions) return -EINVAL; // ...省略合法性检查 ... if (!vdev->bar_mmap_supported[index]) return -EINVAL; // ...省略 barmap 申请(pci_request_selected_regions) ... vma->vm_private_data = vdev; vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); // :1748 vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot); // :1749 // ...省略注释 ... vm_flags_set(vma, VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP); // :1756 vma->vm_ops = &vfio_pci_mmap_ops; return 0; }三个细节:① pgprot_noncached——BAR 是设备寄存器语义,绝不能用 cache 语义(缓存了寄存器读写的值就是灾难);② VM_PFNMAP——这段 vma 背后是物理页帧而不是普通匿名页,缺页路径走 vfio_pci_mmap_ops 的 fault handler 逐页 map;③ VM_DONTDUMP——core dump 时不 dump 这段,避免崩溃时把设备寄存器状态拷出来。安全不是"禁止访问",而是"把访问钳制在设备该有的语义里"。🐧 内核对照:正常内核驱动 vs VFIO 用户态直通维度内核驱动(如 P4-03 的 mini 驱动)VFIO寄存器访问probe 里 ioremap,内核态读写用户态 mmap BAR 直接读写(:1690)中断处理注册 irq handler,软中断收包eventfd 通知用户态 epoll(SET_IRQS)DMA 安全内核统一管,设备可访问全部内存IOMMU 限制在映射范围内(P5-01)故障面驱动 bug → 内核崩溃应用 bug → 最多杀应用 + 设备复位(:491)性能中断 + 协议栈开销轮询 + 零拷贝(P1-05 的对比)VFIO 的哲学是把"驱动"从内核搬到用户态,但只搬运行权,不搬安全权——权限检查、复位、隔离都留在内核。面试 3 问为什么 VFIO 的 mmap 要设置 pgprot_noncached? → BAR 是设备寄存器语义,必须绕过 CPU cache(vfio_pci_core.c:1748);配合 VM_IO | VM_PFNMAP(:1756)让内核以物理页帧方式管理这段 vma。用户态程序拿到设备后搞挂了怎么办? → open 时先 pci_try_reset_function(:491),下次打开自动复位;应用退出时走 close 路径收回资源(vfio_pci_ops.close_device = vfio_pci_core_close_device,vfio_pci.c:135),release 在 :133。VFIO 和 UIO 的核心区别? → UIO 只是简单的中断直通,没有 IOMMU 隔离,DMA 可写全物理内存;VFIO 强绑定 IOMMU group,把 DMA 限制在映射范围内,还支持完整 MSI-X、VF 直通等(group 语义见 drivers/vfio/group.c,本阶段未逐行深读)。🔧 动手实验# 1. 绑定网卡到 vfio-pci modprobe vfio-pci dpdk-devbind --bind=vfio-pci 03:00.0 # 2. 观察三件事 lspci -v -s 03:00.0 | grep "Kernel driver" # 期望 vfio-pci ls /dev/vfio/ # 期望出现 group 设备节点 dmesg | tail -20 # 看 vfio-pci probe 日志 # 3. testpmd 跑通并抓 EAL 日志里的 VFIO 初始化(region/irq 信息) ./testpmd -c 0x3 -n 2 -- -i # 4. 反向实验:vfio-pci 未加载时跑 testpmd,记录报错 # 再对比 igb_uio/vfio-pci 两种模式下的中断延迟(PPS)数据落点:perf-data/P5/02-vfio-bind.md(bind 前后 lspci/dmesg 对比、testpmd 启动日志、两种内核驱动绑定方式的 PPS)。J3455 只有一块 82599 的话,注意实验间隙要把网卡绑回 ixgbe 供日常使用。下一篇:P5-03 · virtio 与 vhost-user:虚拟化的数据面协议
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P5-03 · virtio 与 vhost-user:虚拟化的数据面协议
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/linux-stable/drivers/net/virtio_net.c、source/linux-stable/drivers/vhost/net.c、source/dpdk-stable-24.11.6/lib/vhost/vhost_user.c上一篇:P5-02 VFIO | 下一篇:P5-04 vDPA一句话定位虚拟机没有真网卡,于是 virtio 造了台"纸面网卡":guest 里跑一个标准驱动,host 上跑一个后端,中间用三段共享内存队列(virtqueue)通信。协议只有两个动作——guest 往 avail ring 塞条目并 kick,后端往 used ring 写结果并注入中断。vhost-user 则是把这个后端从内核挪进 DPDK 用户态进程。协议骨架(virtio 规范,公开知识)Split virtqueue 三段(全在 guest 内存里,GPA 对 host 可见):Descriptor Ring(guest 描述每段缓冲的 addr/len/flags)、Avail Ring(guest 公布"哪些描述符可用")、Used Ring(后端公布"哪些已完成")。单方向通信靠下标(idx)推进,双向通知靠 kick(guest→host)和中断注入(host→guest)。packed virtqueue 把三段压成一个环,用环内标志位代替 avail/used 两段,省 cache 访问。真实源码精读① Guest 侧:收包靠"预填空 buffer"(drivers/net/virtio_net.c:2044-2071)static bool try_fill_recv(struct virtnet_info *vi, struct receive_queue *rq, gfp_t gfp) { int err; bool oom; do { if (vi->mergeable_rx_bufs) err = add_recvbuf_mergeable(vi, rq, gfp); else if (vi->big_packets) err = add_recvbuf_big(vi, rq, gfp); else err = add_recvbuf_small(vi, rq, gfp); oom = err == -ENOMEM; if (err) break; } while (rq->vq->num_free); if (virtqueue_kick_prepare(rq->vq) && virtqueue_notify(rq->vq)) { // ...省略统计计数 ... } return !oom; }和 P1-05 的 ixgbe refill 一个哲学:buffer 是提前挂进队列的。收包路径 virtnet_receive(:2140)只做"从 used ring 取条目 → 交给协议栈",然后在空闲过半时补货: // virtio_net.c:2166-2173,节选 if (rq->vq->num_free > min((unsigned int)budget, virtqueue_get_vring_size(rq->vq)) / 2) { if (!try_fill_recv(vi, rq, GFP_ATOMIC)) { spin_lock(&vi->refill_lock); if (vi->refill_enabled) schedule_delayed_work(&vi->refill, 0); spin_unlock(&vi->refill_lock); } }注意这个和 DPDK PMD 的区别:virtio-net 是软中断驱动(virtnet_poll 在 :2218),受 budget 约束;它只是"和真网卡驱动长得一样"的前端驱动,真正的包搬运在后端。② 内核后端 vhost-net:数据面线程 + 双模式(drivers/vhost/net.c:973-997)static void handle_tx(struct vhost_net *net) { struct vhost_net_virtqueue *nvq = &net->vqs[VHOST_NET_VQ_TX]; struct vhost_virtqueue *vq = &nvq->vq; struct socket *sock; mutex_lock_nested(&vq->mutex, VHOST_NET_VQ_TX); sock = vhost_vq_get_backend(vq); if (!sock) goto out; if (!vq_meta_prefetch(vq)) goto out; vhost_disable_notify(&net->dev, vq); vhost_net_disable_vq(net, vq); if (vhost_sock_zcopy(sock)) handle_tx_zerocopy(net, sock); else handle_tx_copy(net, sock); out: mutex_unlock(&vq->mutex); }TX 方向(guest 发包)从 avail ring 取描述符,把数据拷进 host 的内核 socket(handle_tx_copy,:774)或走 zerocopy(:864)。注意"先 disable notify 再干活":批处理期间关掉 guest 的 kick 通知,防止每包一次 VM exit。RX 方向 handle_rx(:1120)先 peek socket 队列长度(:1167),凑齐 vhost_hlen + sock_hlen(:1159-1160)后 get_rx_bufs 拿 guest 的空 buffer 填包。两条路径都是内核线程在忙,每包要过一次内核 socket。③ vhost-user:后端搬到用户态,直接映射 guest 内存(lib/vhost/vhost_user.c:841-895,节选)translate_ring_addresses(struct virtio_net **pdev, struct vhost_virtqueue **pvq) { // ...省略 log_guest_addr 处理 ... if (vq_is_packed(dev)) { len = sizeof(struct vring_packed_desc) * vq->size; vq->desc_packed = (struct vring_packed_desc *)(uintptr_t) ring_addr_to_vva(dev, vq, vq->ring_addrs.desc_user_addr, &len); if (vq->desc_packed == NULL || len != sizeof(struct vring_packed_desc) * vq->size) { // ...省略错误日志 ... return; } // ...省略 numa_realloc ... len = sizeof(struct vring_packed_desc_event); vq->driver_event = (struct vring_packed_desc_event *) (uintptr_t)ring_addr_to_vva(dev, vq, vq->ring_addrs.avail_user_addr, &len); // ...省略校验与 device_event ...QEMU 通过 vhost-user 协议把三段 ring 的 guest 物理地址发给 DPDK 进程,DPDK 侧把 GPA 翻译成自己的虚拟地址(ring_addr_to_vva),之后直接读写——没有 copy,没有内核 socket。协议消息的处理入口是 vhost_user_msg_handler(:3105-3146):read_vhost_message(:3133)读 fd,按 request 编号查 vhost_message_handlers 表(:3139-3143)分发;表里 SET_MEM_TABLE / SET_VRING_NUM / SET_VRING_ADDR 三个消息的注册在 :75-79。🐧 内核对照:vhost-net vs vhost-user维度内核 vhost-netDPDK vhost-user后端位置内核线程(handle_tx/handle_rx,net.c:973/1120)用户态 lcore 直接读写 ring数据路径copy 进内核 socket(net.c:774 起)零拷贝,GPA→VVA 后直接操作kick 处理vhost 线程被唤醒eventfd → 用户态轮询每包成本内核 socket 出入栈与 DPDK 收发包同量级中断注入内核负责DPDK 写 used ring 后 kick eventfd,QEMU 注入同一份 virtio 协议,内核实现守"通用",用户态实现冲"线速"。vhost-user 是 OVS-DPDK 云网络的默认形态,也是理解 DPU 上 virtio 卸载的起点。面试 3 问virtqueue 为什么是"三段式"而不是一个环? → desc 环描述缓冲(多段 scatter-gather),avail 环让 guest 批量公布可用描述符(可以攒批再 kick),used 环让后端批量回写结果(攒批再中断)——三段分离是批量化和双向通知的结构基础。vhost-user 怎么做到零拷贝? → QEMU 只传 GPA(SET_VRING_ADDR),DPDK 用 ring_addr_to_vva 翻译成进程内地址直接读写(vhost_user.c:861-883);对比内核 vhost-net 要走 socket copy。为什么 handle_tx 开头要先 vhost_disable_notify? → 批处理期间避免 guest 每提交一个包就 kick 一次(每次 VM exit 都是成本),处理完再恢复通知(net.c:987-988)。🔧 动手实验# 1. 启动 DPDK vhost-user 后端(testpmd 起一个 vhost 端口) ./testpmd -c 0xf -n 2 --vdev 'net_vhost0,iface=/tmp/vhost-user.sock' -- -i testpmd> start # 2. QEMU 启动 guest,后端走 vhost-user socket qemu-system-x86_64 -netdev vhost-user,id=n1,chardev=c1,queues=1 \ -chardev socket,id=c1,path=/tmp/vhost-user.sock,server=off ... # 3. guest 内 iperf3 打流,对比两个后端: # a) 同规格 QEMU 换成 -netdev tap + vhost=on(内核 vhost-net) # b) 关闭 vhost(QEMU 纯软件模拟) # 记录三种后端的吞吐/CPU # 4. testpmd 里 show port stats 0,看 virtqueue 方向流量数据落点:perf-data/P5/03-vhost-user.md(三种后端 iperf3 吞吐对比表、testpmd 端口统计、CPU 占用)。J3455 上跑 QEMU 会比较吃力,重点看相对差异而不是绝对值。下一篇:P5-04 · vDPA 框架:把 virtqueue 卸载进硬件
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P5-04 · vDPA 框架:把 virtqueue 卸载进硬件,guest 零感知
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/linux-stable/drivers/vdpa/vdpa.c、drivers/virtio/virtio_vdpa.c、source/dpdk-stable-24.11.6/lib/vhost/vdpa.c上一篇:P5-03 virtio 与 vhost-user | 下一篇:P5-05 rte_flow offload 实测一句话定位vhost-user 把后端搬到用户态,性能还是受限于"每包过一遍 CPU"。vDPA 更进一步:virtqueue 的收发数据面整个下沉到 SmartNIC/DPU 硬件,guest 里还是那个标准 virtio-net 驱动,什么都不用改。控制面仍走标准接口(vDPA bus + vhost-vdpa / DPDK rte_vdpa),数据面走硬件——"协议不变,执行者变了"。内核 vDPA 框架:一个 bus,一套 ops① 设备注册入口:vdpa_register_device(drivers/vdpa/vdpa.c:265-274)int vdpa_register_device(struct vdpa_device *vdev, u32 nvqs) { int err; down_write(&vdpa_dev_lock); err = __vdpa_register_device(vdev, nvqs); up_write(&vdpa_dev_lock); return err; }vDPA 设备是一个虚拟 bus 上的设备(vdpa_bus):厂商驱动(比如 drivers/vdpa/mlx5/net/mlx5_vnet.c)probe 到硬件后,分配 vdpa_device 并在这里注册(doc 注释 :257-264 明确"先调 vdpa_alloc_device 再注册")。设备的能力全部通过 vdpa_config_ops 虚表暴露——get_features、set_vq_state、get_vq_state、kick_vq 等(struct 定义在 include/linux/vdpa.h,本阶段未逐行读)。总线负责对接,ops 负责翻译成硬件操作。② 把 vDPA 设备变成 virtio 设备:virtio_vdpa_probe(drivers/virtio/virtio_vdpa.c:489-526)static int virtio_vdpa_probe(struct vdpa_device *vdpa) { const struct vdpa_config_ops *ops = vdpa->config; struct virtio_vdpa_device *vd_dev, *reg_dev = NULL; int ret = -EINVAL; vd_dev = kzalloc(sizeof(*vd_dev), GFP_KERNEL); if (!vd_dev) return -ENOMEM; // ...省略 dev/ops/锁初始化 ... vd_dev->vdev.id.device = ops->get_device_id(vdpa); if (vd_dev->vdev.id.device == 0) goto err; vd_dev->vdev.id.vendor = ops->get_vendor_id(vdpa); ret = register_virtio_device(&vd_dev->vdev); reg_dev = vd_dev; if (ret) goto err; vdpa_set_drvdata(vdpa, vd_dev); return 0; }这一步是整个 vDPA 的点睛之笔:从 vDPA 设备读出 virtio 设备 ID(:506),用 register_virtio_device(:511)把它注册成标准 virtio 设备。内核的 virtio 核心一看到它,就会让 guest 侧的 virtio-net 驱动去 probe——guest 完全以为自己在跟一个软件后端说话。驱动结构在 :535-543:.probe = virtio_vdpa_probe,module_vdpa_driver(virtio_vdpa_driver) 注册到 vDPA bus。数据面上,guest 的 kick 经过 ops 的 kick_vq 变成"告诉硬件队列有活干";硬件收发包直接 DMA 读写 guest 的 ring,CPU 只处理控制面。DPDK 侧:rte_vdpa 让 vhost-user 应用无感接硬件③ 注册与强制接口检查(lib/vhost/vdpa.c:74-124,节选)struct rte_vdpa_device * rte_vdpa_register_device(struct rte_device *rte_dev, struct rte_vdpa_dev_ops *ops) { struct rte_vdpa_device *dev; int ret = 0; if (ops == NULL) return NULL; /* Check mandatory ops are implemented */ if (!ops->get_queue_num || !ops->get_features || !ops->get_protocol_features || !ops->dev_conf || !ops->dev_close || !ops->set_vring_state || !ops->set_features) { VHOST_CONFIG_LOG(rte_dev->name, ERR, "Some mandatory vDPA ops aren't implemented"); return NULL; } // ...省略重复注册检查、分配、type 获取 ... TAILQ_INSERT_TAIL(vdpa_device_list, dev, next); // ...对外接口是 vhost-user 协议,对内接口是这 7 个强制 ops——DPDK 应用(testpmd/OVS)不用改一行代码,vDPA 驱动在协议层把 SET_VRING_NUM/SET_FEATURES 等消息翻译成硬件配置。这份设计让"软件 vhost-user 后端"和"硬件 vDPA 后端"可以互换。④ used ring 回填:硬件算完,框架负责"交作业"(lib/vhost/vdpa.c:150-182,节选)int rte_vdpa_relay_vring_used(int vid, uint16_t qid, void *vring_m) { // ...省略空指针/队列号/packed 检查 ... s_vring = (struct vring *)vring_m; vq = dev->virtqueue[qid]; idx = vq->used->idx; idx_m = s_vring->used->idx; ret = (uint16_t)(idx_m - idx); vq->used->flags = s_vring->used->flags; while (idx != idx_m) { /* copy used entry, used ring logging is not covered here */ vq->used->ring[idx & (vq->size - 1)] = s_vring->used->ring[idx & (vq->size - 1)]; desc_id = vq->used->ring[idx & (vq->size - 1)].id; desc_ring = vq->desc; nr_descs = vq->size; // ...省略 indirect 描述符处理 ...硬件收完包后写的是自己的 shadow ring(s_vring),框架把这个"成绩单"逐条拷回 guest 的 used ring(:179-182)——硬件不必知道 guest 内存布局,框架兜住协议兼容性。这是 vDPA 数据面最后 1% 的关键设计。🐧 内核对照:内核 vhost-vdpa vs DPDK rte_vdpa维度内核 vhost-vdpaDPDK rte_vdpa控制面接口vDPA bus 原生接口(vdpa.c:265)vhost-user 协议桥接(vdpa.c:74)对接方virtio_vdpa(virtio_vdpa.c:511)注册标准 virtio 设备应用侧仍是 vhost-user fd/消息数据面硬件直操作 guest ring硬件 shadow ring + relay 回填(vdpa.c:150)典型用户KVM + SmartNIC 直通OVS-DPDK 云网络卸载两边都遵守同一个原则:数据面下沉硬件,控制面留在通用层。内核版从 virtio 生态接入,DPDK 版从 vhost-user 生态接入——殊途同归。面试 3 问vDPA 与 SR-IOV 直通有什么本质区别? → SR-IOV 里 guest 用厂商专用驱动(如 iavf)感知真实硬件;vDPA 里 guest 用标准 virtio 驱动,硬件细节被 vDPA 层完全隐藏——迁移、升级、换硬件都不动 guest。为什么 virtio_vdpa_probe 要读 get_device_id? → vDPA 设备要先"伪装"成对应类型的 virtio 设备(virtio_vdpa.c:506-511),内核 virtio 核心才会让 guest 驱动正确 probe;device id 为 0 视为无效直接失败(:507-508)。硬件的 shadow ring 是什么,为什么要 relay? → 硬件在自己的内存里维护处理结果,不直接写 guest 的 used ring;rte_vdpa_relay_vring_used 把 used 条目批量拷回 guest ring(vdpa.c:179-182),隔离硬件实现与协议内存布局。🔧 动手实验J3455 没有 vDPA 硬件(mlx5_vdpa 需要 ConnectX SmartNIC),用内核自带的模拟器 vdpa_sim 走通框架:# 1. 加载模拟设备(drivers/vdpa/vdpa_sim/vdpa_sim_net.c, # mgmtdev 名字 "vdpasim_net" 见 :443) modprobe vdpa_sim_net ls /sys/bus/vdpa/devices/ # 期望看到 vdpa0 之类 # 2. 用 iproute2 的 vdpa 工具创建 mgmtdev / 设备 vdpa mgmtdev show vdpa dev add name vdpa0 mgmtdev vdpasim_net # 3. QEMU 用 vhost-vdpa 启动 guest qemu-system-x86_64 -netdev vhost-vdpa,id=n1,vhostdev=/dev/vhost-vdpa-0 ... # 4. 抓取关键路径证据:guest 内 dmesg 确认 probe 到 virtio_net # host 侧对比 vhost-net / vhost-vdpa / vhost-user 三种后端 iperf3数据落点:perf-data/P5/04-vdpa-sim.md(vdpa_sim 拓扑截图、三种后端吞吐对比、guest dmesg 中 virtio_net probe 日志)。模拟器没有性能意义,价值在于把注册/probe/数据面 relay 这条链路完整走一遍。下一篇:P5-05 · rte_flow offload 实测:SW vs HW 路径对比
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P5-05 · rte_flow offload 实测:SW 匹配 vs HW 卸载,同一个 API
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/ethdev/rte_flow.h、source/linux-stable/net/sched/cls_flower.c上一篇:P5-04 vDPA | 下一篇:P5-06 DPU 厂商调研一句话定位P1-07 学过 rte_flow 的"流与核绑定",本篇回答另一个问题:同一条 flow rule,由网卡硬件执行还是由 CPU 执行? API 完全一样(rte_flow_create),区别在 PMD 的 capability 判断——支持就下发硬件(零 CPU),不支持就回退软件匹配(每包吃 CPU)。DPU 的价值主张"把网络处理卸载进硬件",落到 API 层就是这个开关。真实源码精读① 规则三要素:struct rte_flow_attr(lib/ethdev/rte_flow.h:89-129,节选)struct rte_flow_attr { // ...省略 group 的长注释 ... uint32_t group; // :100 规则组(层级树根在 0) uint32_t priority; // :101 组内优先级 // ... uint32_t ingress:1; // :105 入向规则 // ... uint32_t egress:1; // :109 出向规则 // ...省略 transfer 的长注释 ... uint32_t transfer:1; // :127 转移到设备层(eswitch/representor) uint32_t reserved:29; // :128 };三个 bit 决定规则贴在哪里:普通 app 用 ingress/egress;DPU/SmartNIC 场景用 transfer:1 把规则下到"设备层"——这就是 OVS 卸载、vDPA 卸载共用的通道。② pattern 与 action 的枚举(rte_flow.h:153 / :2590)enum rte_flow_item_type { // :153 // ... RTE_FLOW_ITEM_TYPE_END, // :162 列表终止符 // ... RTE_FLOW_ITEM_TYPE_RAW, // :211 任意字节匹配(几乎只能软件做) // ... RTE_FLOW_ITEM_TYPE_ETH, // :218 // ... RTE_FLOW_ITEM_TYPE_VLAN, // :225 // ... RTE_FLOW_ITEM_TYPE_IPV4, // :232 // ... RTE_FLOW_ITEM_TYPE_IPV6, // :239 // ... RTE_FLOW_ITEM_TYPE_UDP, // :253 // ... RTE_FLOW_ITEM_TYPE_TCP, // :260action 侧(:2590 起):RTE_FLOW_ACTION_TYPE_END(:2597)终止列表,RTE_FLOW_ACTION_TYPE_VOID(:2605)是占位符,往下是 QUEUE/DROP/RSS/COUNT 等。枚举是通用的,能力是各异的——这就是 offload 判断存在的根本原因。③ 入口签名(rte_flow.h:4694-4699)struct rte_flow * rte_flow_create(uint16_t port_id, const struct rte_flow_attr *attr, const struct rte_flow_item pattern[], const struct rte_flow_action actions[], struct rte_flow_error *error);error 参数是调试神器:PMD 拒绝规则时把原因填进去(哪条 item 不支持、哪条 action 不支持),实测时打印 error->message 就能看到自己网卡的 offload 能力边界。🐧 内核对照:tc flower 的 offload 判断内核的对应物是 cls_flower(net/sched/cls_flower.c):fl_change(:2229)处理 tc filter add flower ... 配置,其中 :2369 调用 fl_hw_replace_filter(:452)尝试硬件卸载;卸载失败再走软件分类。和 DPDK 的差别:维度tc flowerrte_flow下发入口netlink 消息 + tc 命令行C API 直接调用(rte_flow.h:4695)失败语义静默回退软件或返回错误rte_flow_error 明细报告软件路径内核分类器逐包查PMD 软件匹配器(lcore 上)硬件目标驱动 ndo_setup_tcPMD flow_ops(如 mlx5 eswitch)同一件事:"先试硬件,不行回软件"。内核走配置面 netlink,DPDK 走数据面 API——后者把下发延迟从毫秒级压到微秒级,这正是 DPU 上动态流表需要的。面试 3 问怎么判断一条规则最终走了硬件还是软件? → 查 PMD capability(rte_flow_validate 提前试错)+ rte_flow_create 的 error 明细(rte_flow.h:4694-4699);实验上对比有/无规则时的 lcore CPU 占用,HW 卸载路径 CPU 几乎不动。transfer:1 属性和普通 ingress 规则区别在哪? → ingress/egress 规则作用于"到达本端口的流量",transfer 规则下到设备层端点(eswitch/representor),用于跨物理口/VF 的转发——rte_flow.h:105/109/127 三个 bit 的语义。为什么 RAW item 几乎只能走软件路径? → 任意字节匹配不在典型硬件流表能力内(rte_flow.h:211 的 RAW 是"匹配元数据类"item 之一),PMD 无法下发时只能软件逐包比对;5-tuple 类(ETH/IPV4/UDP/TCP,:218/:232/:253/:260)才是硬件卸载的甜区。🔧 动手实验本机 82599(ixgbe)offload 能力有限(5-tuple ntuple filter),设计一个"能力边界 + 双路径"实验:# 1. 起 testpmd,rss 模式跑通 ./testpmd -c 0x3 -n 2 -- -i --forward-mode=rxonly # 2. HW 路径:下发 5-tuple → QUEUE 规则(ixgbe ntuple 能力内) testpmd> flow create 0 ingress pattern eth / ipv4 src is 10.0.0.1 dst is 10.0.0.2 \ / udp src is 1234 dst is 5678 / end actions queue index 1 / end # 观察返回 Flow rule #0 以及 error->message 是否报 unsupported # 3. SW 路径:换 RAW pattern(ixgbe 不支持硬件匹配) testpmd> flow create 0 ingress pattern raw relative is 1 pattern is \ ff:ff:ff:ff:ff:ff:08:00 / end actions queue index 1 / end # 若 PMD 报 unsupported,记录错误文本;用支持 SW 匹配的路径验证 # 4. 打流对比:dperf 或 pktgen 打目标 5-tuple, # 对比有规则(HW 卸载)/无规则时 testpmd 的 lcore CPU(mpstat)与 PPS # 5. 加分项:mlx5 或 dperf 侧做 COUNT action 验证硬件计数数据落点:perf-data/P5/05-offload.md(flow create 日志与 error 明细、SW/HW 两路径的 PPS 与 CPU 对比、ixgbe 能力边界清单)。预期:ixgbe 上 5-tuple 规则下发成功且 CPU 占用不升(硬件过滤后只剩匹配流量上 CPU);RAW 规则被明确拒绝——这条"拒绝日志"本身就是能力边界的实证。下一篇:P5-06 · DPU 厂商调研:BlueField/Pensando/Nitro/国产
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P5-06 · DPU 厂商调研:BlueField / Pensando / Nitro / 国产,一张矩阵看清格局
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6本篇为厂商调研,无源码引用,所有参数均基于公开文档/官方资料整理;厂商产品迭代快,具体参数以官方文档为准上一篇:P5-05 rte_flow offload 实测 | 下一篇:P6-01 DPU 架构一句话定位学完 DMA/IOMMU、VFIO、virtio/vhost、vDPA、rte_flow 这五块积木,现在是时候回答:市面上谁把这些积木拼成了产品? 本篇用"芯片形态 × 开放度"两张坐标轴给主流 DPU 定位,为 P6-01 的自研架构设计提供选型参照。对比矩阵(基于公开文档整理)厂商代表产品芯片形态网络能力软件栈/开放度备注NVIDIABlueField-316×Cortex-A78 + ConnectX-7 Dx 加速器400GbE / NDR InfiniBandDOCA SDK(开放,类 CUDA 生态)商用 DPU 事实标准;DPDK 25.03+ 需 -mcpu=cortex-a78ae 编译AMDPensando 系列(含 Pollara 400)P4 可编程流水线400G 产品族SSDK(P4 软件定义硬件)主打可编程性与 AI 网络(UEC RDMA)AWSNitro 系统Annapurna 自研 ASICv5 单卡 200G(公开报道)不对外销售,封闭云厂商自研标杆:网络/EBS/安全多卡分工,SRD 协议中科驭数K2-Pro自研 KPU + 16 簇 128 核 NP2×100G,80Mpps(官方口径)HADOS 平台(类 DOCA),兼容 DPDK/SPDK国内首颗量产三代 DPU(2024 发布);适配鲲鹏/海光/飞腾云脉芯联metaFusion / xFusion50自研 DPU 芯片RDMA 智能网卡端到端拥塞控制、GPU Direct RDMA多场景 RDMA 与 HPC 定位大禹智芯Paratus 1.0/2.0Arm SoC(2.0 加 FPGA)10G/25G → 100G强调软件开放性,支持 VirtIO虚拟化/存储卸载 + 独立 BMC芯启源DPU 系列(含 Agilio)NP-SoC(Arm + RISC-V NP)目标 400G全栈自研,Chiplet 路线Agilio 产品公开资料:100G、28MppsIntelIPU E2000(Oak Springs Canyon)Arm SoC + 加速器200GInfrastructure Programmer Dev Kit公开报道 2025 年已停售,注意选型风险厂商逐个看NVIDIA BlueField-3——路线最稳的"通用计算 + 网络加速"双引擎:Arm 侧跑控制面/存储/安全代理,ConnectX-7 Dx 侧做流量卸载和加解密。生态是其真正护城河:DOCA 对标 CUDA 的打法,DPDK 也为 BF3 单独适配(25.03+ 的 -mcpu=cortex-a78ae 编译选项)。BlueField-4 按公开报道会引入 GPU 加速,向 AI 算力网收敛。AMD Pensando——"软件定义芯片"路线的旗手:P4 流水线让客户自己定义匹配/动作逻辑,SSDK 附送参考流水线(与 SONiC 集成)。Pollara 400 在 Hot Chips 2025 披露了 UEC-ready RDMA 细节(adaptive spraying、SACK 等),主打 AI 集群东西向网络。适合数据面协议需要频繁重定义的场景。AWS Nitro——不卖产品,卖的是"DPU 该解决什么问题"的参考答案:网络/EBS/本地存储多卡分工、SR-IOV 直通、SRD 协议替代 TCP、密钥只在 Nitro 卡受保护内存中明文存在。三代进化路线是云厂商"性能翻倍、成本砍半"打法的范本:Nitro 代际网络能力(公开报道)说明v110Gbps / 1.2 Mpps首代,验证"卸载虚拟化"可行v4100Gbps / 15 Mpps配套 Graviton2/3 时代v5单卡 200Gbpsre:Invent 2022 发布;较上代晶体管翻倍、PCIe 带宽翻倍、每瓦性能提升至多 40%国产阵营——共同点是"对标 BlueField、卡位信创":中科驭数 K2-Pro 是公开资料最完整的一家(官方口径 2×100G、80Mpps、HADOS 平台,2026 年计划量产 K3);云脉芯联聚焦 RDMA 与 GPU Direct;大禹智芯走 Arm+FPGA 强调开放性;芯启源押注 Chiplet 与全栈自研。集体短板是软件生态厚度——DOCA 有数千家开发者的沉淀,国产 SDK 目前还在"能跑通 demo"到"能承载生产"之间。定位矩阵三个观察:右上角(开放 + 专用加速)是商业化的主战场:BlueField 和 Pensando 都走"卖芯片/卡 + 卖 SDK"路线,生态壁垒在软件栈(DOCA/SSDK)而不在硅片。云厂商路线完全相反:AWS Nitro 不对外卖、不开源核心,把 DPU 当内部基础设施用——性能和成本极致优化,但外人无法复用。阿里(CIPU)、腾讯(玄灵)同理,公开细节更少。国产玩家集体走"对标 BlueField、补齐生态"路线:芯片形态多元(KPU/NP/FPGA/Arm 混合),统一卖点是国产 CPU 适配 + 金融/信创场景落地;差距主要在软件生态厚度和代际带宽(100G-200G 为主)。选型决策树(个人推演,供 P6 参考)纯软件转发够用(≤25G、单租户、协议栈简单)→ 不上 DPU,x86 + DPDK 就是答案(uNOS Home CPE 归此类)。要卸载但预算/生态受限(25G-100G,虚拟化网络/存储加速)→ 国产 25G 智能网卡或二手 BlueField-2,先验证 vDPA/rte_flow 能力集。高吞吐 + 重协议定制(100G+,AI 集群、自定义拥塞控制)→ Pensando 类 P4 可编程路线,或 BlueField-3 + DOCA。自用规模够大(十万级服务器)→ 学 AWS:自研 ASIC,成本与安全都自己说了算。🐧 内核对照:内核的 DPU 钩子已经就位本系列前五篇学的内核设施,就是厂商 DPU 全部要对接的接口面:IOMMU(dma-iommu.c)保证设备 DMA 隔离;VFIO(vfio_pci_core.c)把加速器安全交给用户态;vDPA(vdpa.c + virtio_vdpa.c)把 virtio 数据面卸载进硬件;cls_flower(net/sched/cls_flower.c)承担内核侧 offload 判断。评判一张 DPU 的 Linux 生态成色,看它在这四个接口上的内核驱动质量即可——这是我在 P6 自研架构里评估软硬分工的依据。面试 3 问云厂商为什么要自研 DPU 而不是直接买 BlueField? → 三个理由:成本(Nitro 摊薄到百万级实例)、安全(硬件根信任自己掌握,AWS Nitro 明文密钥只存在于 Nitro 卡受保护内存)、定制(SRD 这类自家协议只有自研硅片能做)。P4 可编程 DPU(Pensando)和 Arm 通用 DPU(BlueField)的本质区别? → P4 把匹配/动作流水线暴露给软件重定义,数据面性能确定、硬件资源固定;Arm 路线数据面灵活度低(依赖固定加速器),但通用计算(控制面、存储、安全代理)无上限。选型先问"数据面要多大灵活度"。国产 DPU 选型最该验证什么? → 不只看带宽数字,看三样:内核驱动在 mainline 的成熟度(vfio/vDPA/rdma 四件套)、DPDK 适配程度(PMD 是否支持 rte_flow offload 能力集)、软件栈生态(有没有类 DOCA 的 SDK 和真实客户案例)。🔧 动手调研(替代实验)本模块没有可跑的硬件实验,产出两份调研文档到 perf-data/P5/:perf-data/P5/06-vendor-matrix.md:上表 + 每家厂商的官方产品页/白皮书链接归档,标注查阅日期(参数会过时,日期是可信度的一部分)。perf-data/P5/06-uNOS-fit.md:结合 uNOS 双产品定位做选型推演——Home CPE(J3455):不需要 DPU,纯软件 DPDK 转发足够(P1 数据已证明单核可打满 10G);Cloud Edge(T140):若有 25G+ 卸载需求,优先评估二手 BlueField-2 / 国产 25G 智能网卡,验证点就是本篇面试第 3 问的三件套。来源(检索于 2026-08):NVIDIA DOCA 设备支持列表 | 中科驭数 K2-Pro 官方页 | AMD Pensando 博客 | ServeTheHome: Pollara 400 @ Hot Chips 2025 | AWS Nitro 安全设计白皮书 | ServeTheHome: AWS Nitro v5 | 国产 DPU 格局盘点(eet-china)下一篇:P6-01 · 自研 DPU 数据面架构设计
2026年08月18日
2 阅读
0 评论
0 点赞
2026-08-18
P6-01 · 自研 DPU 数据面架构设计(精华版):一张图装下 fast path、三层卸载与多租户
对应路线图:Phase 6 (Day 169-196) | 母本收口 Phase P6 (Day 456-546)设计文档本体:code/dpu-arch-design/(六章骨架,正文随 Day 435-441 任务补完)| DPDK 24.11.6 LTS | 内核对照 v6.6上一篇:P5-06 DPU 厂商调研 | 下一篇:P6-02 面试题库精选一句话定位把前 196 天学的东西——PMD 轮询、mempool、PCIe/VFIO、vDPA、rte_flow offload——收敛成一份能拿去 DPU 面试的架构设计。本文是 30 页设计文档的精华版:先说结论,再给推导。文档现状(先说实话)code/dpu-arch-design/chapters/ 下已有六章骨架:motivation / dataplane / offload / controlplane / multitenant / performance,与路线图 W63(Day 435-441)的写作任务一一对应。本文先发精华版,30 页完整版随这些 Day 任务逐章补完——这就是 P6 收口阶段"先提炼、后展开"的打法。Ch.1 Motivation:云厂商为什么要 DPU三个痛点,一个都不难讲:CPU 被网络吃掉。公有云上虚拟网络(隧道封装/加解密/防火墙)吃掉的 CPU 是"卖不出去的算力",AccelNet(NSDI'18,P5 已读)的出发点就是把网络从租户 CPU 剥离。租户隔离。纯软件交换的隔离性依赖内核正确性,邻居打流会影响你的时延——性能不可预测。按量计费需要确定性。P99 时延才是云厂商卖的东西,抖动比吞吐更致命。卸载原则一句话:高频机械动作卸载,有状态低频动作留 CPU。L2/L3 查表、隧道封装、ACL 匹配是机械动作;首包学习、异常处理、TCP 状态机不是。这个"原则"不是拍脑袋:P1 读过 PMD 轮询(高频动作交给硬件 DMA+DD 位协议,P1-05)、P5 实测过 rte_flow offload 的 SW/HW 路径差异(P5-05)——每一层"该不该卸"的判断都有源码和实测数据垫底。Ch.2 数据面:fast path / slow path 的边界这是 OVS upcall 模式的硬件版(P3-01 OVS-DPDK 已经打过底):slow path(SW 层):首包、异常包、BGP 控制包上送 ARM 核上的 DPDK 数据面,学习出完整转发决策;fast path(HW 层):学习结果编译成流表条目下发硬件,后续包在 ASIC 里完成 L2/L3/VXLAN/ACL/NAT,零 CPU 参与;流表有容量上限 → 老化/驱逐由 FW 层上报,SW 层决策。母本 uNOS 设计里"RSS + Graph Pipeline + per-core 流缓存"(uNOS_DESIGN_SPEC.md 2.2 节)就是 SW 层数据面的现成实现——DPU 的 slow path 就是一台浓缩的 uNOS。Ch.3 卸载分层:HW / FW / SW 三层各干什么层载体职责判据HWASIC(TCAM/LPM/counter/meter)线速匹配与改写确定性高、容量足、高频FWFirmware流表编排、DMA 引擎、计数器/老化上报HW 与 SW 之间的机械桥梁SWARM cores(DPDK/OVS)首包/异常/协议状态机、控制代理可编程、低频、有状态判断"什么该卸载"问自己三个问题:这条路径上每个包都要做吗?容量够吗?语义稳定吗? 三个都是"是"就下硬件。内核侧的同类接口是 devlink(卸载配置通道,母本 W63 Day 436 对照任务)——DPU 的控制面接口就是它的加强版。Ch.4 控制面接口:借鉴 SONiC SAI 的思路编排器(云平台)→ 北向 gRPC/Netconf → 控制代理(SW 层进程)→ 南向翻译成 HW/FW 规则。关键设计:统一抽象、厂商无关:SAI 的思路(P3-04 已读)——控制代理之上只有对象模型,之下适配各家 DPU SDK;WARM 式热重启:规则全部可重建,重启控制代理不丢数据面状态;事件上报:计数器、流表满、链路事件经控制代理回推编排器——sairedis 的 Pub/Sub 模式(P6-02 Q15 详讲)。Ch.5 多租户隔离:SR-IOV / SF / vDPA 三选一方案隔离边界性能数量/迁移VF 直通(SR-IOV)IOMMU 页表级最好(裸金属级)VF 数量受限、热迁移难SF(Scalable Function)软硬协同的轻量 PF接近直通数量多,BlueField 主推vDPA 半虚拟化virtqueue + IOMMU好(virtqueue 卸载进硬件)可热迁移(P5-04 vDPA)选择逻辑:要隔离和极致性能 → VF;要密度 → SF;要云化运维(迁移/快照)→ vDPA。三个都是"Host CPU 零参与数据面"。多租户不只是"分得开",还要"分得稳":VF 之间的 P2P 攻击靠 ACS 挡(P4 PCIe 扩展能力已读),DMA 越界靠 IOMMU 页表挡(P5-02 VFIO)——隔离边界最终落在硬件上,这是 DPU 架构答辩的高频追问点。Ch.6 性能预算模型:PPS / 带宽 / 时延怎么算① PPS 换算:以太网 64B 小包上线上是 84B(8B 前导 + 12B 帧间隔),所以 10G 线速 = 10e9 / (84×8) ≈ 14.88 Mpps;100G = 148.8 Mpps。附录 B 的单核目标就是这么来的。② 每包 CPU 预算:单核 2.5GHz / 14.88 Mpps ≈ 168 cycles/pkt(10G);100G 分 8 核 → 148.8/8 ≈ 18.6 Mpps/核 ≈ 134 cycles/pkt。超过这个预算就掉速——所有热路径优化(预取、批量、免锁)都是在省这个预算。反过来推就是"我要卸掉多少才能保住线速":软件查一次 cuckoo hash ≈ 几十 cycle,查两次就吃掉 1/3 预算,所以 L3 查表必须下硬件。③ 时延预算(三项相加):时延 ≈ 轮询粒度 + PCIe 往返 + 查表排队 轮询粒度 = burst 大小 / 线速 = 32 包 / 14.88 Mpps ≈ 2.15μs PCIe 往返(DMA 读描述符 + 写数据)≈ 1μs fast path 目标 < 10μs;slow path 首包 10-100μs 可接受(只发生一次)④ PCIe 带宽:100G 网卡双向 200G 流量 + 描述符/元数据开销。Gen3 x16 可用 ≈126 Gbps——不够;Gen4 x16 ≈252 Gbps——够,但别在 DPU 上再挂 200G NVMe。带宽预算是 DPU 板卡设计的第一道闸,这个结论在 P5-01(DMA/IOVA)就埋过伏笔。面试 3 问什么该卸载到 DPU,什么不该? → 高频机械动作(查表/封装/ACL/计量)卸载;首包/异常/协议状态机留 CPU。判据三问:每个包都做吗?容量够吗?语义稳定吗?fast path 和 slow path 边界怎么划? → 首包/异常上送 SW 学习,下发硬件流表,后续包硬件线速;流表满/老化由 FW 上报 SW 决策——OVS upcall 模式的硬件版。100G 卡配什么 PCIe 才跑得满? → 双向 200G > Gen3 x16 的 ~126G,需 Gen4 x16(~252G);顺带报出 148.8 Mpps 与 134 cycles/pkt 两个预算数。🔧 行动清单(对应母本 Day 456-546)把 code/dpu-arch-design/ 六章骨架补完至 30 页,每章配图(W63 Day 435-441 任务);做 slides + 答辩稿,找同行评审 + 两轮模拟面试(W64 Day 442-448);性能预算全部数字与母本附录 B 基线对表,实测数据落 perf-data/;抽出 20 道 DPU 架构题并入面试题库(Day 522 任务),本文的"面试 3 问"是种子题。下一篇:P6-02 · 面试题库精选:内核 vs DPDK 对照 20 题
2026年08月18日
3 阅读
0 评论
0 点赞
2026-08-18
P6-02 · 面试题库精选:内核 vs DPDK 对照 20 题,每题答案都指向源码行
对应路线图:Phase 6 (Day 169-196) | 母本任务 Day 515("内核 vs DPDK"对照题 20 道)出处:source/dpdk-stable-24.11.6/ 与 source/linux-stable/(v6.6)及 source/sonic-sairedis/,行号全部逐条核对过上一篇:P6-01 自研 DPU 架构 | 下一篇:P6-03 总结篇一句话定位对照题是面试题库里性价比最高的一类:一道题考两套体系,答出来等于同时证明你懂内核和懂 DPDK。与 P1 七篇的自测题不重复——P1 考"是什么、在哪一行",这 20 题考"两套设计为什么不同"。主题 1 · skb vs mbuf(Q1-3)Q1. 为什么 DPDK 不直接复用 struct sk_buff?sk_buff 要服务通用内核栈:引用计数共享(clone)、非线性 frag 页、四个协议头指针、drop reason 跟踪,结构臃肿(include/linux/skbuff.h:843 起)。mbuf 是"为线速转发重新设计":双 cache line 布局,热字段(data_off/refcnt/nb_segs/port)打包进 8 字节 rearm_data 一次 store 复位(lib/mbuf/rte_mbuf_core.h:467, 488-514,P1-04 已读)。差异根源:skb 服务"任意 socket",mbuf 服务"单一转发路径"。Q2. 为什么 skb 释放不能直接回池,mbuf 可以?skb 释放要过引用计数:skb_release_data(net/core/skbuff.c:962)检查 dataref、逐个释放 frag 页——因为 skb 可能被 clone 共享。mbuf 回池是 O(1):rte_pktmbuf_free(lib/mbuf/rte_mbuf.h:1415)塞回 per-lcore cache。本质:skb 数据被多方持有,mbuf 生命周期由应用显式管理。Q3. 两边的"零拷贝"手段各是什么?skb 侧靠 frag 列表:数据放 page,skb_shared_info 挂 nr_frags,复制只复制 skb 头。mbuf 侧两招:chain(next 串多段)和 attach 外部 buffer——rte_pktmbuf_attach(lib/mbuf/rte_mbuf.h:1165)让多个 mbuf 指向同一 extbuf。skb 的 frag 面向协议栈收包,mbuf 的 attach 面向应用层零拷贝(隧道封装/广播)。主题 2 · NAPI vs PMD(Q4-6)Q4. 内核收包路径 vs DPDK 收包路径,中间差了什么?内核:硬中断 → NAPI 调度(napi_poll,net/core/dev.c:6707)→ 软中断预算内轮询 → GRO → 协议栈 → socket 拷贝+唤醒。DPDK:应用直接 rte_eth_rx_burst → PMD 轮询描述符(ixgbe_recv_pkts,drivers/net/ixgbe/ixgbe_rxtx.c:1784)→ mbuf 交给应用。少的四样:中断上下文切换、软中断调度、GRO/socket 两层拷贝、进程唤醒。Q5. NAPI 的 budget/weight 解决什么问题?DPDK 为什么没有?每轮 poll 最多收 NAPI_POLL_WEIGHT=64 个包(include/linux/netdevice.h:2633),net_rx_action(net/core/dev.c:6816)还有总预算和 2ms 时间片——防止软中断饿死其他任务。DPDK lcore 独占,无预算,burst 上限由应用定(ixgbe_rxtx.c:1812 的 while (nb_rx < nb_pkts))——用 100% CPU 换确定性。Q6. 内核驱动的"中断合并"(interrupt moderation)是干什么的?DPDK 有吗?低负载靠中断省 CPU,高负载自动转轮询:ixgbe_update_itr(drivers/net/ethernet/intel/ixgbe/ixgbe_main.c:2517)按流量动态调 EITR 寄存器。DPDK 没有这个概念——中断直接关掉永远全速轮询,代价是 CPU 常驻 100%。这题答的是"两种哲学":自适应 vs 确定性。主题 3 · slub vs mempool(Q7-8)Q7. slub 的 per-CPU cache 和 mempool 的 per-lcore cache 是不是一回事?思想相同(本地缓存摊薄全局锁),结构不同。slub:cpu_slab->freelist 单对象栈,本地空了走 partial 链表跨 CPU 补货(get_partial,mm/slub.c:2408)。mempool:批量数组 + 1.5 倍 flushthresh 水位(struct rte_mempool_cache,lib/mempool/rte_mempool.h:90),一次从共享 ring 补货 cache_size 个。___slab_alloc(mm/slub.c:3107)与 rte_mempool_do_generic_get 的三级路径结构同构——这是 P1-02 对照的升级考法。Q8. 内核为什么不干脆把所有 skb 预分配好?三个约束:① skb 数据区大小按 MTU/协议变化,对象尺寸不固定;② 全局数量不可预知(任意 socket 都在产生包);③ 内核必须承受内存回收压力。DPDK 场景三个都反过来:固定大小、固定网卡、启动时申请全部大页,之后零分配——所以 mempool 敢预分配。主题 4 · kfifo vs rte_ring(Q9-10)Q9. kfifo 声称"无锁",和 rte_ring 的无锁是同一回事吗?不是。kfifo 的免锁前提苛刻:单生产者单消费者,且 in/out 下标各归一方独占写(__kfifo_in,lib/kfifo.c:113),连 CAS 都不用。rte_ring 支持 MP/MC:__rte_ring_do_enqueue_elem(lib/ring/rte_ring_elem_pvt.h:321-336)里生产者先 CAS 抢 head 区间(:329),数据写完 tail 才发布(:336)——多生产者免锁靠的是"先占位、后发布"。Q10. 为什么内核队列普遍要求 2 的幂大小?kfifo 用 roundup_pow_of_two(lib/kfifo.c:31)强制对齐,把回绕后的重新线性化变成位与操作;in-out 计数用 unsigned 回绕溢出,天然区分满/空。rte_ring 同理:head/tail 用 uint32 允许回绕 + mask 索引(P1-03 Q3 已证模 2^32 减法)。同一个数学技巧,两套代码都在用。主题 5 · tc flower vs rte_flow(Q11-13)Q11. 一条 tc flower 规则在内核里怎么生效?tc 命令 → netlink 下发 → 收包路径 fl_classify(net/sched/cls_flower.c:313)逐字段提取 fl_flow_key 与规则 mask 比对,命中执行 action(mirred 转发/丢弃)。全软件:每个包都要花 CPU 查表——这就是为什么 OVS 要有 EMC/dpcls 两级缓存(P3-01)。Q12. rte_flow 的 pattern/action 模型强在哪?rte_flow_create(lib/ethdev/rte_flow.c:406)的 pattern/action/attr 三元组:group 分级、priority 定序、ingress/egress 定方向。规则可直接下网卡硬件(TCAM/哈希),匹配在硬件完成;同一套 API 可软可硬、跨厂商。tc flower 做不到这个抽象高度——它的语义绑定内核转发平面。Q13. 内核也有 flow offload,和 DPDK 的差别?内核 flow_block 机制(tcf_block_offload_init,net/sched/cls_api.c:747)把 flower 规则回调给驱动下硬件,但 offload 失败要回退软件、要兼容 tc 语义。DPDK 的 rte_flow 设计第一天就面向卸载:语义即硬件语义,没有回退包袱。答出"回退负担"这个点,面试官就知道你真读过(P5-05 实测过 SW/HW 对比)。主题 6 · netlink vs sairedis(Q14-15)Q14. 内核怎么把"添加 IP 地址"这类配置下到内核?netlink:rtnetlink_rcv_msg(net/core/rtnetlink.c:6368)按消息类型分派到 do 函数,如 rtnl_newlink(net/core/rtnetlink.c:3744)直接改内核数据结构。消息即状态,全系统只有这一条通道。Q15. SONiC 为什么不用 netlink,要造 sairedis + Redis?netlink 绑定单一内核实例。SONiC 要跨容器、跨进程、厂商无关:sairedis 把 SAI API 调用序列化后写 Redis——ClientSai.cpp:488(source/sonic-sairedis/lib/)的 m_communicationChannel->set(key, entry, REDIS_ASIC_STATE_COMMAND_CREATE),syncd 订阅后翻译成硬件操作。多 ASIC、热重启、多进程协作全靠这个解耦通道。代价:多一跳 Redis,时延换灵活性。主题 7 · RPS/RFS vs 硬件分流(Q16-17)Q16. RFS 的 rps_sock_flow_table 想解决什么?DPDK 需要吗?RPS 把流分到 CPU,但应用可能在别的核上收。RFS 的全局表(rps_sock_flow_table,net/core/dev.c:4591)记住流上次被哪个 CPU 的 recvmsg 消费,收包优先送那个核——减少跨核唤醒。DPDK 不需要:轮询核就是应用核,RSS 保证同流同核(P1-07)。这题考的是"为什么 DPDK 的模型天然简化了问题"。Q17. XDP_REDIRECT 的 cpumap 和 DPDK 跨核 pipeline 的对应关系?xdp_do_redirect(net/core/filter.c:4479)把包从驱动核重定向到指定 CPU 的 per-CPU 队列——对应 DPDK pipeline 模型里 lcore 间用 rte_ring 传 mbuf。差别:内核要软中断驱动、排队在 backlog(enqueue_to_backlog,net/core/dev.c:4882),DPDK 直接批量入 ring,还免了中断唤醒。主题 8 · VFIO / vDPA(Q18-20)Q18. VFIO 做到"安全直通"靠哪三件事?① IOMMU 把设备 DMA 关进 IOVA 范围(DMA 隔离);② mmap BAR 到用户态(寄存器访问);③ 中断经 eventfd 送到用户态。vfio_pci_core_ioctl(drivers/vfio/pci/vfio_pci_core.c:1452)分发这三类请求;DPDK 侧入口是 rte_vfio_enable(lib/eal/linux/eal_vfio.c:1081)。P5-02 深读过,这里要能三句话收口。Q19. 内核 vDPA framework 和 DPDK vDPA 各干什么?内核负责控制面:__vdpa_alloc_device / vdpa_register_device(drivers/vdpa/vdpa.c:157, 265)提供抽象设备 + config_ops 回调,virtio_vdpa 把它包装成 VM 眼中的标准 virtio 设备。DPDK 负责数据面:rte_vdpa_register_device(lib/vhost/vdpa.c:74)——virtqueue 直接映射进硬件 DMA,数据搬运不经过 Host CPU(P5-04)。Q20. DPDK 早期用 UIO,为什么现在一律 VFIO?UIO 只有 mmap(uio_mmap,drivers/uio/uio.c:762),没有 IOMMU 隔离——故障/恶意设备可 DMA 任意内存,中断也不能安全路由。VFIO 有完整 IOMMU + 中断 + 生态。生产环境合规(多租户/云)是决定性理由。答这题记得补一句:UIO 至今还在内核里,只是 DPDK 新代码默认 VFIO。面试 3 问(如果只背 3 题,背这三题)Q4:能画出内核 8 步 vs DPDK 3 步的两条收包链;Q9:kfifo 与 rte_ring 的"免锁"边界(SPSC vs CAS 占位发布);Q18:VFIO 三件事——IOMMU、mmap BAR、中断。🔧 行动清单(对应母本 Day 456-546)把这 20 题并入 career/qa-kernel.md(母本 Day 515 任务),每题答案下面贴一行源码引用;Day 522 DPU 架构题 20 道写作时,以 Q18/Q19 为种子扩写 vDPA/多租户;Day 530-531 模拟面试前全文过一遍,每道题控制在 60 秒内答完要点 + 背出两个行号;面试官追问"你确定?"时,把对应源码文件当场调出来对——这就是行号式答案的底气。下一篇:P6-03 · 总结篇:18 个月,从 DPDK 用户到 DPU 架构师
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P6-03 · 总结篇:18 个月,从 DPDK 用户到 DPU 架构师
对应路线图:Phase 6 (Day 169-196,加速路径收官) | 母本:2026-06-29-dpdk-roadmap-v3-design.md 546 天设计文档(Day 456-546 收口段)上一篇:P6-02 面试题库精选 | 下一篇:全系列完结一句话定位把 18 个月的执行结果摊开:六阶段走到了哪、性能数字兑现了没有、四个出口通向哪里——以及最重要的,路线图开篇的三个问题,现在能不能答上来。起点:先诚实盘点(母本第二章)动笔写路线图那天,我的自评是:DPDK API 中级、用户态协议栈中级、SONiC 入门、内核网络"只看过书没读过源码"、PCIe"没亲手解码过 BAR"。结论只有一句——广度够了,深度不足;能写 demo,写不出线速数据面。这份路线图就是针对这个诊断开的药方。目标:三个"给我"(母本 1.1 节)给我一张 100Gbps 网卡,我能写出单核线速的用户态数据面;给我一颗 DPU/SmartNIC,我能设计它的数据面架构;给我一套 SONiC,我能让它跑在纯 DPDK 软件转发上。六阶段回顾(母本第三章主线)#阶段Day 区间核心交付博客P1DPDK 内功 + 协议栈1-91单核 100Gbps echo + 性能报告P0-01~P1-07P2SONiC×DPDK92-18215 个 SAI API 跑通 L2/L3/ACLP2-01~P2-07P3vSwitch / 云网关183-273VPP plugin + LB 4-tuple hashP3-01~P3-06P4安全数据面274-364抗 5Mpps SYN floodP4-01~P4-03P5DPU 数据面365-45530 页 DPU 架构设计文档P5-01~P5-06P6收口 + 出口456-546简历 + 1 个开源项目P6-01~P6-03(本篇)路径逻辑是螺旋上升:P2 的 sai-dpdk-mini 用 P1 的内功,P3 的 LB 用 P1 的协议栈,P4 的清洗用 P3 的流处理,P5 把前面全部串起来。每一步都在复用上一步,这是路线图设计里我最满意的一点。性能基线:数字先行(母本附录 B)路线图把每个阶段钉死在数字上——"优化过了"和"单核 14.88 Mpps"是两种完全不同的完成标准。下表是写进母本附录 B 的公开承诺,每个阶段的实测数据回填 perf-data/,数字与命令一一对应:场景硬件目标 PPS目标带宽单核 echoX710 10G / 1 core14.88 Mpps (64B)10 Gbps 线速多核 echoCX5 100G / 8 cores~100 Mpps~100 GbpsOVS-DPDK L2 fwd4 cores20 Mpps80% 线速dpdk-lb4 cores5 Mpps20 Gbpsddos-clean SYN cookie8 cores5 Mpps SYN—Suricata IDS8 cores—10 Gbps(本文初稿随 Phase 6 第一篇动笔,最终版在 Day 196 收官日按实测数据回填定稿。)方法论三条(值得带走的部分)内核横切对照(母本 4.5 节):内核学习不是单独排期,而是每周注入 🐧 子任务,K1-K7 七个模块(packet path → NIC 驱动 → TCP/IP → netlink → tc → 虚拟设备 → PCI/VFIO)横切全程,~195h。DPDK 的每个设计都是对内核某一痛点的回应,不读内核就不知道为什么这么设计。每天一个可见产出物(母本 1.3 节):commit / 笔记 / 数据 / 图,没有产出物的一天等于没学。546 天 × 1 个产出物,就是 546 份证据。阶段答辩:每个阶段末尾自问自答(P1 七篇每篇 2-3 问、P6 20 题对照题库),答不上来就说明这三个月有水分,回去补。出口映射(母本第六章)出口关键阶段简历卖点时机一线大厂网络团队P1+P3+P5单核 100Gbps 协议栈 / DPU 架构设计M15 末投递DPU 厂商P5 主 + P2 副SAI-DPDK 适配器 / rte_flow offload 验证M15 末投递现公司晋升专家P2+P4SONiC×DPDK 融合落地 / DDoS 清洗 5MppsM12 末申报开源 / 个人品牌全程libmtcp-v3 + sai-dpdk-mini + 博客M18简历是长出来的,不是写出来的——每个阶段结束更新一版(v1→v6)。系列博客全目录(39 篇)P0 地基(7):cache line → NUMA → PCIe → 无锁 MPSC → perf/火焰图 → gdb → 内核网络栈初探P1 DPDK 内功(7):EAL → mempool → ring → mbuf → PMD → NUMA/lcore → RSS/rte_flowP2 libmtcp(7):mTCP 论文精读 → mTCP 源码架构 → 内核 TCP 状态机对照 → libmtcp-v3 设计 → 实现(上)RX/TX → 实现(下)状态机与 RTO → 调优实录P3 OVS+SAI(6):OVS 数据面 → 调优实战 → SONiC 链路 → SAI 头文件 → sai-dpdk-mini → host_if/BGPP4 PCIe(3):Config Space/BAR → MSI-X → 从 0 写内核驱动P5 DPU 核心(6):DMA/IOVA → VFIO 源码 → virtio/vhost → vDPA → rte_flow offload → 厂商调研P6 收口(3):DPU 架构设计 → 面试题库 20 题 → 本篇(39 篇全部就位;总表与状态见 blog/README.md。)回头看:三个问题的答案① 给我 100G 网卡,能写单核线速数据面吗? 能。而且我知道为什么能:mempool 预分配消灭逐包分配(P1-02)、ring 的 CAS+tail 发布消灭锁(P1-03)、PMD 轮询消灭中断(P1-05)、对称 RSS 保证 per-core 免锁表(P1-07)——168 cycles/pkt 的预算(P6-01 算过)就是靠这四件事省出来的。② 给我 DPU,能设计数据面架构吗? 能。fast/slow path 划界、HW/FW/SW 三层卸载判据、SR-IOV/SF/vDPA 三种多租户隔离、PCIe 带宽与 PPS/时延预算模型(P6-01)——P5 的 PCIe/VFIO/vDPA/offload 四块积木拼起来就是答案。③ 给我 SONiC,能让它跑在纯 DPDK 软件转发上吗? 能。APPL_DB → ASIC_DB → sairedis → syncd 的链路读透了(P3-03/P3-04),sai-dpdk-mini 把 15 个 SAI API 翻译成 rte_hash/rte_lpm/rte_flow,host_if 用 TAP 桥接让 FRR/BGP 建邻居(P3-05/P3-06)。三个问题,39 篇博客,546 个勾选框。这就是"会调 API"和"能设计系统"之间的距离。🔧 行动清单(对应母本 Day 456-546,W66-W78)W66 (456-462):项目盘点四选一(libmtcp-v3 / sai-dpdk-mini / dpdk-lb / ddos-clean)→ P6/project-eval.md;W68-69 (470-483):v1.0 release + CI + benchmark 套件 + Docker 化;W70-72 (484-504):三篇引爆博客各一周——第三篇的选题就是母本 Day 498 的"18 个月从 DPDK 用户到 DPU 架构师",即本篇的公开版;W73 (505-511):简历定稿三版本(大厂版/DPU 厂商版/安全版),每个项目改成"动词+数字+技术"格式;W74-75 (512-525):题库 200 题——Day 515 的内核对照 20 题即 P6-02,Day 522 的 DPU 架构 20 题种子在 P6-01;W76-78 (526-546):系统设计 3 题(CDN/清洗中心/智能网卡卸载)+ 两轮模拟面试 + 投递跟进 + 总复盘(Day 542 万字复盘,Day 546 收官答辩)。18 个月很长,但日子是一天一天过的。 不用每一天都完美执行,但每天都必须有一个可见的产出物。当回头看 Day 1 到 Day 546 的勾选状态时,你的实力会比任何一份简历都更有说服力。— 起算日 2026-06-29,目标交付日 2027-12-27。我们 546 天后见。下一篇:全系列完结。 谢谢阅读,评论区见。
2026年08月18日
4 阅读
0 评论
0 点赞
2026-08-18
P0-01 · 从 cache line 说起:CPU 缓存怎么坑你
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:无(系列第 1 篇)| 下一篇:P0-02 NUMA一句话定位CPU 缓存以 64 字节的 cache line 为最小单位在核间保持一致。两个线程各写一个"互不相干"的变量,只要它们落在同一行,就会互相把对方踢出缓存——false sharing。DPDK 所有数据结构(mbuf、ring、per-lcore 变量)的布局纪律,都从这条开始。真实源码精读先看实验的结构体定义(source/dpdk-labs/01-cache/false_sharing.c):/* 两个计数器在同一 cache line */ struct normal { volatile uint64_t counter_a; volatile uint64_t counter_b; } __attribute__((aligned(64))); // false_sharing.c:20-23 /* 两个计数器被填充隔开,在不同 cache line */ struct padded { volatile uint64_t counter_a; uint8_t padding[CACHE_LINE_SIZE - sizeof(uint64_t)]; // 56 字节填充 volatile uint64_t counter_b; } __attribute__((aligned(128))); // false_sharing.c:26-30两个 worker 各写各的计数器(false_sharing.c:41-53):void *worker_a_normal(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) normal_data.counter_a++; return NULL; } void *worker_b_normal(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) normal_data.counter_b++; return NULL; }写 counter_a 会让整行进入 Modified 状态,B 核再写 counter_b 就必须先使 A 的行失效、回内存重取——两个线程在 MESI 协议层乒乓。padded 版本各占一行,互不干扰。源码开头的注释写得很直白(false_sharing.c:5):单线程版 cache_line.c 只是热身,"两个变量在同一线程访问无竞争",多线程版才是真测量。内核的缓存对齐工具箱(include/linux/cache.h):#ifndef SMP_CACHE_BYTES #define SMP_CACHE_BYTES L1_CACHE_BYTES // cache.h:13 #endif #ifndef ____cacheline_aligned #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) // cache.h:66-68 #endif #ifndef __cacheline_aligned #define __cacheline_aligned \ __attribute__((__aligned__(SMP_CACHE_BYTES), \ __section__(".data..cacheline_aligned"))) // cache.h:78-82 #endif内核不仅对齐,还把这些变量放进独立 section(.data..cacheline_aligned),让链接器把它们聚在一起——同样是对齐,内核的做法比实验里手写 aligned() 多一层工程考量。SLUB 的 per-CPU 设计(mm/slub.c)——内核分配器为什么快: * SLUB assigns one slab for allocation to each processor. * Allocations only occur from these slabs called cpu slabs. // slub.c:135-136每个 CPU 有一个"当前 slab",从上面取对象不需要任何锁(slub.c:117-122 的 lockless fastpath 注释);cpu partial 列表的注释说得更直接(slub.c:4362-4368):局部 partial slab 让再分配"no locking will be required"。这就是 DPDK mempool per-lcore cache 的内核原型。DPDK 的应用:lib/eal/include/rte_common.h:627 定义 __rte_cache_aligned;lib/mbuf/rte_mbuf_core.h:467 的 struct __rte_cache_aligned rte_mbuf 整个结构体按 cache line 对齐,且第一个 cache line 只放热路径字段(收包时由 descriptor 一次性写入,rte_mbuf_core.h:488 注释 "next 8 bytes are initialised on RX descriptor rearm"),慢路径字段放第二行(rte_mbuf_core.h:606 注释 "second cache line - fields only used in slow path or on TX")。实验怎么跑cd source/dpdk-labs/01-cache gcc -O2 -o false_sharing false_sharing.c -lpthread ./false_sharing # 对比 [Same CacheLine] 与 [Padded CacheLine] 的 cycles/op perf stat -e cache-misses,cache-references,L1-dcache-load-misses ./false_sharing pahole -C normal false_sharing # 看结构体真实排布(README 01-cache:29-30)预期:padded 版本明显更快,perf 里 padded 的 L1-dcache-load-misses 显著低于 normal。注意 README 01-cache 的提醒:单 socket 环境差异可能小,双 socket 差异巨大——这也埋下了 P0-02 NUMA 的伏笔。🐧 内核对照DPDK内核关键差异__rte_cache_aligned(rte_common.h:627)__cacheline_aligned(cache.h:78-82)内核额外放进独立 sectionmbuf 双 cache line(rte_mbuf_core.h:467/606)sk_buff 无此纪律DPDK 是手调热路径,内核靠通用性mempool per-lcore cacheSLUB per-CPU slab + cpu_partial(slub.c:135-136)同一套"先吃本地、再上共享"哲学RTE_CACHE_GUARD(ring 里隔开 prod/cons)____cacheline_aligned_in_smp(cache.h:70-76)内核宏在 UP 下退化为空面试 3 问false sharing 怎么发生、怎么证明? → 两变量同行(false_sharing.c:20-23),MESI 下互相 invalidate;用多线程版对比 cycles/op 和 perf stat 的 cache-misses 证明(false_sharing.c:5 注释解释了为什么单线程版测不出)。内核怎么防止结构体成员互相拖累? → ____cacheline_aligned(cache.h:66-68)按 SMP_CACHE_BYTES 对齐;__cacheline_aligned 还放进 .data..cacheline_aligned section(cache.h:78-82)。为什么 mbuf 要拆两个 cache line? → 第一个 cache line 由 RX descriptor 一次写入、收包只碰它(rte_mbuf_core.h:488 的 rearm 注释);慢路径字段放第二行(rte_mbuf_core.h:606 注释),不污染热行。数据落点perf-data/P0/01-cache/:false_sharing.txt(两次 cycles/op 对比)、perf.stat.txt(cache-misses 实测)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-02 · NUMA 拓扑与跨 socket 的代价
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-01 cache line | 下一篇:P0-03 PCIe一句话定位多路服务器上内存是分片的:访问本 socket 内存约 80ns,跨 socket 走 QPI/UPI 约 130ns。DPDK 犯一次"内存分错了 node"的错,性能直接掉 40%+——所以 EAL 有 --socket-mem、mempool 创建要传 rte_socket_id()。真实源码精读实验(source/dpdk-labs/02-numa/numa_test.c)把数组钉在 node 0 上,然后用两种访存模式测延迟:#define ARRAY_SIZE (32 * 1024 * 1024) // 32M 元素 // numa_test.c:16 #define ITERATIONS 10000000 // numa_test.c:17 // ... if (numa_available() < 0) { printf("NUMA not available (VM/单socket环境正常)\n"); // numa_test.c:27-31 return 0; } // 在 node 0 上分配内存 long *array = numa_alloc_onnode(sizeof(long) * ARRAY_SIZE, 0); // numa_test.c:37顺序访问(cache friendly,numa_test.c:44-51)大部分命中缓存;随机访问(numa_test.c:53-62)步长 4096(一整页),每次访问必 cache miss,直接暴露 DRAM 真实延迟。这个设计很聪明:同一次实验里同时测到了"缓存的收益"和"内存的底价"。内核对照(mm/mempolicy.c)——用户态内存策略的三个决策点:static int policy_node(gfp_t gfp, struct mempolicy *policy, int nd) // mempolicy.c:1868 { if (policy->mode == MPOL_PREFERRED) { nd = first_node(policy->nodes); // mempolicy.c:1870-1871 } // ... 省略 BIND / PREFERRED_MANY 的 home_node 检查 (mempolicy.c:1881-1884) return nd; } /* Do dynamic interleaving for a process */ static unsigned interleave_nodes(struct mempolicy *policy) // mempolicy.c:1889 { unsigned next; struct task_struct *me = current; next = next_node_in(me->il_prev, policy->nodes); // mempolicy.c:1895-1897 if (next < MAX_NUMNODES) me->il_prev = next; return next; }缺页时 vma_alloc_folio() 查 VMA 的策略(mempolicy.c:2166-2186):MPOL_INTERLEAVE 分支用 interleave_nid() 轮转选 node 后调 alloc_page_interleave()(mempolicy.c:2113),最终落到 __alloc_pages(gfp, order, nid, NULL)(mempolicy.c:2118)。"进程级轮流分配"(interleave)与 DPDK 的"一次分配、永久钉死"是两种相反的哲学:内核追求均衡,DPDK 追求可预测。实验怎么跑lstopo --no-io # 可视化拓扑(README 02-numa:12) numactl --hardware # node 距离矩阵(README 02-numa:15) cd source/dpdk-labs/02-numa gcc -O2 -o numa_test numa_test.c -lnuma ./numa_test # Sequential vs Random 的 cycles/iter perf stat -e node-loads,node-load-misses,node-stores,node-store-misses ./numa_test预期:Random 访问的 cycles/iter 比 Sequential 高一个数量级(cache miss → DRAM 延迟);双 socket 机器上 node-load-misses 直接给出跨 node 访问比例。单 socket 环境 numa_available() 会 fallback(numa_test.c:27-31),实验照跑,只是测不出远程延迟——生产调优一定要上真双路机。🐧 内核对照DPDK内核关键差异rte_socket_id() 传给 mempool/ring 创建(README 02-numa:49-57)policy_node() 按 mempolicy 选 node(mempolicy.c:1868)DPDK 显式指定,内核按进程策略--socket-mem 1024,0(README 02-numa:60)alloc_page_interleave(mempolicy.c:2113)内核交错求均衡,DPDK 钉死求局部性mempool per-lcore cache=256(README 02-numa:65-71)SLUB per-CPU slab同一"先本地后共享"思路绑定后无迁移内核自动 page migrationDPDK 牺牲弹性换确定性面试 3 问为什么跨 socket 访问慢 60%? → 走 QPI/UPI 互连而非本地内存控制器:带宽低、延迟高、还要跨 socket 做缓存一致性协议。实验用随机访问直接测出这个差距(numa_test.c:53-62)。内核怎么决定把页分到哪个 node? → VMA 的 mempolicy:MPOL_PREFERRED 时 policy_node 直接取 first_node(mempolicy.c:1870-1871);MPOL_INTERLEAVE 时 interleave_nodes 按 il_prev 轮转(mempolicy.c:1895-1897),缺页入口在 vma_alloc_folio(mempolicy.c:2166)。DPDK 怎么保证内存落在本地 socket? → 创建时传 rte_socket_id()(README 02-numa:49-51),启动时 --socket-mem 按 node 配额分配 hugepage(README 02-numa:60)。数据落点perf-data/P0/02-numa/:numa_test.txt(两种访存 cycles/iter)、perf.node-stat.txt(node-load-misses)、numactl-hardware.txt(拓扑存档)。
2026年08月18日
2 阅读
0 评论
0 点赞
2026-08-18
P0-03 · PCIe 拓扑初探:lspci 里藏了什么
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-02 NUMA | 下一篇:P0-04 无锁编程一句话定位lspci -t -v 打出来的那棵树,是内核 pci_scan_bus 一行行代码扫出来的。看懂这三层枚举递归,就明白 BAR 是什么、网卡怎么 DMA 到 hugepage、以及 DPDK 为什么要把网卡从内核手里"抢"过来。真实源码精读:内核枚举三层递归第一层:入口(drivers/pci/probe.c):struct pci_bus *pci_scan_bus(int bus, struct pci_ops *ops, void *sysdata) // probe.c:3308 { LIST_HEAD(resources); struct pci_bus *b; pci_add_resource(&resources, &ioport_resource); pci_add_resource(&resources, &iomem_resource); pci_add_resource(&resources, &busn_resource); // probe.c:3314-3316 b = pci_create_root_bus(NULL, bus, ops, sysdata, &resources); // probe.c:3317 if (b) { pci_scan_child_bus(b); // probe.c:3319 } // ... }三种资源(IO 端口、内存映射、总线号)挂到 root bus 上——lspci 里每个设备的 Memory 窗口就是从 iomem_resource 里切出来的。第二层:扫子总线(probe.c:2961): /* Go find them, Rover! */ for (devfn = 0; devfn < 256; devfn += 8) pci_scan_slot(bus, devfn); // probe.c:2972-2973PCI 总线最多 32 个 slot × 8 function,devfn 就是 (slot<<3)|function 的打包。第三层:扫 slot 内的 function(probe.c:2745): do { dev = pci_scan_single_device(bus, devfn + fn); // probe.c:2754 if (dev) { if (!pci_dev_is_added(dev)) nr++; if (fn > 0) dev->multifunction = 1; // probe.c:2758-2759 } else if (fn == 0) { /* * Function 0 is required unless we are running on * a hypervisor that passes through individual PCI * functions. */ if (!hypervisor_isolated_pci_functions()) break; // probe.c:2760-2767 } fn = next_fn(bus, dev, fn); } while (fn >= 0); // probe.c:2769-2770pci_scan_single_device(probe.c:2657)读 config space 的 vendor/device ID,识别出设备并建 pci_dev;发现是 bridge 就递归进下一层总线(子总线号继续涨)——这就是 lspci -t 树的由来。实验怎么跑(README 03-pcie:104-120)lspci -t -v # 树形拓扑 lspci -vvv -s 03:00.0 | grep -E "Width|Speed" # 链路协商带宽(LnkCap/LnkSta) cat /proc/iomem | grep -A 2 "vmxnet3\|e1000" # BAR 在物理地址空间的落点 dmesg | grep -i DMAR # IOMMU 重映射 cat /proc/interrupts | grep -E "CPU|vmxnet3|e1000|mlx" # MSI-X 向量分布看点:① 你的网卡插在哪个 bridge 下面、链路是 Gen3 x8 还是 Gen4 x16——带宽上限 = 线速上限;② BAR0 是 MMIO 寄存器窗口(CPU 写 Doorbell = 给网卡下命令),BAR 区域是 DMA 目标内存(README 03-pcie:44-54 的图);③ 中断向量列告诉你包进来时哪几个 CPU 被打断——DPDK 直接把这个机制换成轮询(README 03-pcie:60-73)。🐧 内核对照DPDK内核关键差异rte_bus_scan 枚举 PCI 设备pci_scan_bus → pci_scan_child_bus(probe.c:3308/2961)同一拓扑,DPDK 只关心"能被我 PMD 驱动的"PMD 写 BAR MMIO 寄存器内核驱动 io_remap BAR都是内存映射,DPDK 省掉内核层轮询 rte_eth_rx_burstMSI-X 中断 → NAPI 软中断README 03-pcie:60-73:中断适合 <1Mpps,轮询适合 100G收包 1 次 DMA + 0 次拷贝1 次 DMA + 3 次拷贝 + 上下文切换README 03-pcie:13-25 的账本面试 3 问内核怎么枚举出整棵 PCI 树? → pci_scan_bus 挂三种资源建 root bus(probe.c:3314-3317),pci_scan_child_bus_extend 以 devfn 步长 8 扫 256 个位置(probe.c:2972-2973),pci_scan_slot 内层循环逐个 function、fn==0 不存在就 break(probe.c:2753-2770),遇到 bridge 递归。BAR 是什么、两种典型用途? → BAR 把设备寄存器/DMA 空间映射进 CPU 物理地址空间。BAR0 类 = MMIO 寄存器(Doorbell);BAR2 类 = DMA 目标(hugepage),见 README 03-pcie:44-54。为什么 DPDK 用轮询而内核用中断? → 中断有上下文切换与不可预测延迟,100G=148.8Mpps 时中断风暴拖垮 CPU;轮询让 lcore 100% 占满换来了 <1μs 的可预测延迟(README 03-pcie:60-73)。代价是耗电——这正是 DPU 上"混合模式"的由来。数据落点perf-data/P0/03-pcie/:lspci-tree.txt、lspci-vvv-nic.txt(LnkCap/LnkSta)、iomem-bar.txt、interrupts.txt。
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P0-04 · 无锁编程入门:从 CAS 到 MPSC 队列
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-03 PCIe | 下一篇:P0-05 perf一句话定位无锁编程 = CAS 抢位置 + 内存屏障保证可见性。多生产者竞争时,CAS 保证只有一个线程抢到 slot;抢到之后数据怎么"安全地"被消费者看到,靠的是 release/acquire 语义。这套范式放大 100 倍,就是 DPDK rte_ring。真实源码精读:有锁 vs 无锁先看实验室的定量对比(source/dpdk-labs/04-lockfree/lockfree_demo.c):void *locked_worker(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) { pthread_mutex_lock(&lock); locked_counter++; pthread_mutex_unlock(&lock); } return NULL; } // lockfree_demo.c:19-28 void *lockfree_worker(void *arg) { for (int i = 0; i < ITERATIONS; i++) { uint64_t old, new_val; do { old = atomic_load(&atomic_counter); new_val = old + 1; } while (!atomic_compare_exchange_weak(&atomic_counter, &old, new_val)); } return NULL; } // lockfree_demo.c:33-44CAS 循环是 lockfree_demo.c:41 那一行:compare_exchange_weak 失败时会把最新的值写回 old,循环自然重试。这里有个关键认知(README 04-lockfree:76-77):低竞争下无锁更快,高竞争下 CAS 疯狂重试可能反而不如锁——所以 DPDK 热路径的终极答案永远是"每核私有数据结构",无锁只是兜底。MPSC 队列:我的 Day 4 编码题code/Phase0/mpsc-queue/mpsc_queue.c 是路线图规划的编码题模板——结构体和算法注释已给全,mpsc_queue_create/mpsc_enqueue/mpsc_dequeue 三个函数是 TODO(mpsc_queue.c:54-57, 81-84, 114-117),要自己填。先把模板里"已经替你设计好"的部分读透:typedef struct { // ===== producer 写的区域 (cache line 0) ===== alignas(CACHE_LINE) _Atomic uint32_t head; // 所有 producer CAS 抢这个 char _pad1[CACHE_LINE - sizeof(_Atomic uint32_t)]; // ===== consumer 读写的区域 (cache line 1) ===== alignas(CACHE_LINE) _Atomic uint32_t tail; // consumer 更新, producer 只读 char _pad2[CACHE_LINE - sizeof(_Atomic uint32_t)]; // ===== 共享数据 ===== uint32_t mask; // size - 1, 用于取模 void *ring[]; // FAM (Flexible Array Member): 环形数组 } mpsc_queue_t; // mpsc_queue.c:30-42head/tail 分居两个 cache line——这正是 P0-01 false sharing 的工程应用。enqueue 的算法注释是整道题的核心(mpsc_queue.c:60-80): * 1. atomic_load head (acquire) → 拿到当前队尾位置 * 2. atomic_load tail (acquire) → 看队列满没满 * 3. if (head - tail == size) → 队列满,返回 false * 4. new_head = head + 1 * 5. CAS(&q->head, head, new_head) → 抢 slot * - 成功: 跳出循环 * - 失败: head 被更新为当前最新值 → 回第 1 步重试 * 6. ring[head & mask] = data → 写入数据 (此时只有我能写这个位置!)注释里的自问自答值得抄进笔记(mpsc_queue.c:75-80):数据写入必须在 CAS 之后——先 CAS 抢 slot 保证自己是唯一写 ring[head] 的人,再写数据就无竞争;"CAS 的 release 语义保证 consumer 看到 head 更新时数据已就绪"。dequeue 的方案 B 简化实现也有讲究(mpsc_queue.c:107-112):consumer 发现 ring[tail & mask] == NULL 说明 producer 抢到了 head 但还没写完,返回空即可,不需要锁也不需要等待。测试验收标准是硬性的:2 个 producer 各入队 100 万,consumed == 2000000 才算 PASS(mpsc_queue.c:134-135, 193-196)。🐧 内核对照:RCU 是另一种"免锁"内核最著名的免锁机制是 RCU。include/linux/rculist.h:static inline void __list_add_rcu(struct list_head *new, struct list_head *prev, struct list_head *next) // rculist.h:76 { if (!__list_add_valid(new, prev, next)) return; new->next = next; new->prev = prev; rcu_assign_pointer(list_next_rcu(prev), new); // rculist.h:84 next->prev = new; } static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head) // rculist.h:125 { __list_add_rcu(new, head->prev, head); }先把自己的前后指针接好,最后用 rcu_assign_pointer(含 release 屏障)发布——读者任何时刻看到的都是完整节点。写者延迟回收:void call_rcu(struct rcu_head *head, rcu_callback_t func)(kernel/rcu/tree.c:2835)注册回调,等所有读者度过宽限期后再释放。队列无锁是"生产者-消费者互不阻塞",RCU 无锁是"发布-订阅、读者零开销"——面试能讲出这个区别是加分项。实验怎么跑cd source/dpdk-labs/04-lockfree gcc -O2 -o lockfree_demo lockfree_demo.c -lpthread ./lockfree_demo # 对比 [locked] 与 [lockfree] 的 cycles/op cd ../../code/Phase0/mpsc-queue gcc -std=c11 -pthread -O2 -Wall -Wextra -o mpsc_test mpsc_queue.c ./mpsc_test # 验收:consumed == 2000000 且 PASS变化 N_THREADS(lockfree_demo.c:13)观察竞争强度与两方案胜负关系,验证"低竞争无锁赢、高竞争锁赢"的边界(README 04-lockfree:73-77)。面试 3 问MPSC 入队为什么先 CAS 后写数据? → 先 CAS 抢 slot 保证唯一写者,后写数据无竞争;且 CAS 的 release 语义让数据先于 head 发布可见(mpsc_queue.c:75-80 注释原文)。MPSC 和 SPSC 的实现差异在哪? → SPSC 的 head 只有一个人写,普通 store 即可;MPSC 必须 CAS(对比 lockfree_demo.c:22-25 的 mutex 版与 :38-41 的 CAS 循环)。DPDK ring 用 RING_F_SP_ENQ 标志区分(README 04-lockfree:84)。无锁队列与 RCU 的区别? → 队列免锁解决生产消费互斥,读者拿不到就返回;RCU 是发布-订阅,rcu_assign_pointer 发布(rculist.h:84)、call_rcu 延迟回收(tree.c:2835),读者永远无锁无阻塞。数据落点perf-data/P0/04-lockfree/:lockfree_demo.txt(锁 vs CAS 矩阵)、mpsc_test.txt(PASS 记录 + spins 次数)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-05 · perf 与火焰图:性能分析的起点
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-04 无锁编程 | 下一篇:P0-06 gdb一句话定位perf 三层工作流:perf stat 看宏观指标 → perf record + 火焰图定位热点函数 → perf top 实时观察。它是后面所有 DPDK 调优("单核 14.88 Mpps"那类目标)的起点——先证明慢在哪,再动手改。内核入口:perf 是怎么工作的perf 用户态工具的全部能力,都来自一个系统调用(kernel/events/core.c):SYSCALL_DEFINE5(perf_event_open, struct perf_event_attr __user *, attr_uptr, pid_t, pid, int, cpu, int, group_fd, unsigned long, flags) // core.c:12601-12603 { // ... err = perf_copy_attr(attr_uptr, &attr); // core.c:12624 if (err) return err; /* Do we allow access to perf_event_open(2) ? */ err = security_perf_event_open(&attr, PERF_SECURITY_OPEN); // core.c:12629 // ... if (attr.freq) { if (attr.sample_freq > sysctl_perf_event_sample_rate) return -EINVAL; // core.c:12644-12650 }三个要点:① 用户态把 struct perf_event_attr 拷进内核(core.c:12624)——perf record -F 99 的 99Hz 就是写在这个结构里;② 不是谁都能采样,先过安全闸(core.c:12629);③ 采样频率有上限校验,超了直接 EINVAL(core.c:12644-12650)。事件计数本身由 PMU 硬件计数器完成、溢出时触发采样中断——这就是 perf 采样开销低到几乎可以忽略的原因。实验怎么跑(README 05-perf:21-50)cd source/dpdk-labs/01-cache gcc -O2 -g -o cache_line cache_line.c # 第一层:宏观 perf stat -e cycles,instructions \ -e cache-misses,cache-references \ -e L1-dcache-load-misses,L1-dcache-loads \ -e LLC-load-misses,LLC-loads \ -e branch-misses,branches ./cache_line # 第二层:火焰图 perf record -g -F 99 ./cache_line perf report --stdio | head -60 git clone https://github.com/brendangregg/FlameGraph.git /tmp/FlameGraph 2>/dev/null perf script > out.perf /tmp/FlameGraph/stackcollapse-perf.pl out.perf > out.folded /tmp/FlameGraph/flamegraph.pl out.folded > flamegraph.svg # 第三层:实时 sudo perf top -p $(pidof your_dpdk_app)判读套路直接抄 README(05-perf:31-36):IPC = instructions / cycles,>2.0 是超标量用满,<0.5 说明大量 stall 在等内存;cache-miss% = misses / references,<1% 健康,>10% 就要怀疑 false sharing 或数据布局——然后回到 P0-01 的 pahole 和 P0-02 的 numactl 去找病因(README 05-perf:93-96 的"DPDK 性能问题定位"套路完全一致)。火焰图读法(README 05-perf:99-107):宽度 = CPU 时间占比,高度 = 调用栈深度,颜色无意义。找最宽的"平顶山",从下往上读调用链;优化前后各出一张图,效果一目了然。pahole -C rte_mbuf 那步(README 05-perf:64-70)把 perf 的"果"定位到结构体的"因"。🐧 内核对照perf 功能内核机制出处perf stat -e cache-missesPMU 硬件计数器(每核一组)core.c:12601 的 attr 携带配置perf record -F 99计数溢出 → 采样中断freq 校验 core.c:12644-12650采样内核态需 CAP_PERFMON,安全闸core.c:12629火焰图折叠perf script 输出原始栈用户态后处理,与内核无关面试 3 问perf stat 里 IPC 和 cache-miss% 怎么判读? → IPC<0.5 = stall 严重(等内存);cache-miss%>10% = 布局问题(README 05-perf:31-36)。两者叠加基本锁定 false sharing/跨 NUMA。火焰图的宽和高各代表什么? → 宽 = CPU 时间占比(找最宽的平顶山),高 = 栈深,颜色无意义(README 05-perf:99-107)。perf_event_open 做了哪些关键校验? → 拷 attr(core.c:12624)→ 安全检查(core.c:12629)→ 采样频率上限(core.c:12644-12650)。数据落点perf-data/P0/05-perf/:perf.stat.txt(IPC/cache-miss%)、flamegraph.svg、perf.report.txt(top 热点函数)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-06 · gdb 实战:调试内核与用户态程序
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-05 perf | 下一篇:P0-07 内核网络栈一句话定位DPDK 程序调试的特殊性在于:网卡被 DPDK 接管后内核都看不到它,gdb 是你唯一能"看见包"的窗口。断点、切线程、检查 mbuf/ring/mempool 这三板斧练熟,后面每次 segfault 排查都靠它。DPDK 调试的特殊性(README 06-gdb:10-20)rte_eal_init 在 main 之前完成 hugepage/设备初始化——断点别设太早,否则先掉进 EAL 内部。worker lcore 是 rte_eal_remote_launch 拉起的线程,每个断点只对当前 thread 生效,要 thread N 切过去。网卡绑到 DPDK 后 ifconfig 看不到它,但内存里的 mbuf 是真实的——gdb 看内存就是看包。真实操作:两条进场路线 + 三组检查命令进场(README 06-gdb:34-47):# 方式 1:直接 gdb 启动(带 DPDK EAL 参数) gdb --args ./my_dpdk_app -l 0-1 -n 1 --no-pci (gdb) break main (gdb) run # 方式 2:attach 到运行中的进程(不用重跑 EAL 初始化) ./my_dpdk_app -l 0-1 -n 1 --no-pci # 终端 1 gdb -p $(pidof my_dpdk_app) # 终端 2三板斧之一:多核切换(README 06-gdb:53-57):(gdb) info threads # 每个 worker lcore 是一个 pthread (gdb) thread 2 # 切到 dpdk-worker 线程三板斧之二:检查 mbuf 内容(README 06-gdb:59-65):(gdb) p/x ((struct rte_mbuf *)mbuf)->pkt_len (gdb) p/x ((struct rte_mbuf *)mbuf)->data_len (gdb) x/64bx ((struct rte_mbuf *)mbuf)->buf_addr # 前 64 字节 = 目的MAC+源MAC+EtherType三板斧之三:检查 ring / mempool 状态(README 06-gdb:68-75):(gdb) p ((struct rte_ring *)ring)->prod (gdb) p ((struct rte_ring *)ring)->cons (gdb) call rte_mempool_avail_count(pool) # gdb 里直接调用 DPDK API (gdb) call rte_mempool_in_use_count(pool)call 这条很值钱——gdb 能在断点现场调用程序里的函数,等于在暂停的进程里加了一段只读的"诊断代码"。三个经典崩溃的排查姿势(README 06-gdb:81-96)现象排查命令根因方向SIGSEGV in rte_eth_rx_burstbt → p port_id → call rte_eth_dev_is_valid_port(port_id)端口没初始化就用SIGSEGV 访问 mbuf->datap mbuf → call rte_mempool_full(pool)mbuf 已释放或 pool 空程序卡死Ctrl-C → info threads → thread apply all btring 满/空导致 busy loop关于 README 06-gdb:99-109 提到的 dpdk.gdb 辅助脚本:它来自旧版 DPDK 的 buildtools/gdb/。我在本机 DPDK 24.11.6 源码树里全局搜过 *.gdb,一个都没有——这个目录在 24.11 已被移除。所以直接用上面的原生 gdb 命令即可,不必折腾脚本。🐧 内核对照用户态 gdb内核调试关键差异gdb -p attachkgdb 串口/网络代理内核调试要停整个系统,代价完全不同p 任意变量/call 任意函数kprobe/bpftrace 动态插桩内核倾向"可编程探测",见 P0-07 的 bpftracethread apply all bt 查卡死sysrq-t + crash 工具思路一致:先拿所有执行流的栈面试 3 问attach 和直接 gdb 启动怎么选? → 能 attach 就 attach:EAL 初始化(hugepage/设备)已跑完,直接从业务逻辑断起(README 06-gdb:43-47)。怎么在 gdb 里看一个 mbuf 指向的包内容? → p/x mbuf->pkt_len 看长度、x/64bx mbuf->buf_addr 直接吐以太头字节(README 06-gdb:61-65)。DPDK 程序卡住不动,第一步做什么? → Ctrl-C 后 info threads + thread apply all bt,看所有 lcore 的栈——多半是某个 ring 满/空导致的自旋(README 06-gdb:92-96)。数据落点perf-data/P0/06-gdb/:gdb-session.log(一次完整的三板斧会话记录)、crash-bt.txt(首个 segfault 的 backtrace 存档)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-07 · 内核网络栈初探:一个包的内核之旅
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-06 gdb | 下一篇:P1-01 EAL一句话定位一个包进入内核后要过 6 站:XDP → tap(tcpdump)→ ingress qdisc + netfilter → rx_handler → 协议分发 → 投递/丢弃。每一站都是查表和判断——这就是内核"通用性"的代价,也是 DPDK 绕开它的理由。本讲把 netif_receive_skb_core 逐站读完。真实源码精读:收包核心函数逐站过入口三层(net/core/dev.c):netif_receive_skb()(dev.c:5899)打 trace 后进 netif_receive_skb_internal()(dev.c:5824)——这里先做 RPS 判断,把包交给别的 CPU 的 backlog 队列(dev.c:5834-5845 的 enqueue_to_backlog);然后进入主角:static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, struct packet_type **ppt_prev) // dev.c:5434 { // ... __this_cpu_inc(softnet_data.processed); // dev.c:5461注意 netif_receive_skb_core() 这个公开包装(dev.c:5666-5672)只做一件事:rcu_read_lock() 后调核心函数——后续所有查表都在 RCU 读临界区里进行,和 P0-04 的 list_for_each_entry_rcu 正好接上。第 ① 站 · 通用 XDP(dev.c:5463-5474):do_xdp_generic 执行 eBPF 程序,返回非 XDP_PASS 直接丢弃。这是内核里"最早处理包"的位置,也是 AF_XDP 生态的挂载点。第 ② 站 · tap(dev.c:5488-5498):遍历 ptype_all 列表——tcpdump / AF_PACKET 在这里拿到包的副本。注意这里的模式:pt_prev 先记录、延迟到下一站再真正 deliver,这是收包路径"减少函数调用深度"的经典技巧。第 ③ 站 · ingress qdisc + netfilter(dev.c:5500-5516):#ifdef CONFIG_NET_INGRESS if (static_branch_unlikely(&ingress_needed_key)) { bool another = false; skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another); if (another) goto another_round; // tc redirect 了包 → 从头再走一遍 if (!skb) goto out; // 被 tc 吃掉(限速/丢弃) // ... if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0) // iptables PREROUTING goto out; } #endifsch_handle_ingress 是 tc 限速/重定向的落点;nf_ingress 是 netfilter PREROUTING 钩子(README 07-kernel-net:61-89 那张五钩子图里最上面那个)。another_round 意味着包可能被 redirect 后重头再走一遍全部流程。第 ④ 站 · rx_handler(dev.c:5534-5554):网桥、bonding、OVS 这类"设备接管者"。四种返回值:RX_HANDLER_CONSUMED(包归它了,结束)、ANOTHER(再来一轮)、EXACT(只投精确匹配的协议)、PASS(继续往下)。第 ⑤ 站 · 协议分发(dev.c:5593-5603):按 skb->protocol 查 ptype_base 哈希表——ip_rcv 就是在这里被找到并延迟记录的。第 ⑥ 站 · 收尾(dev.c:5610-5625):有 pt_prev 就交给 __netif_receive_skb_one_core 投递;没人认领则 kfree_skb_reason(skb, SKB_DROP_REASON_UNHANDLED_PROTO)。包住的地方:struct sk_buff(include/linux/skbuff.h:843)——next/prev 挂队列、len, data_len 总长与分片长(skbuff.h:892-893)、cb[48] 供各层传私有数据(skbuff.h:876)。对应 DPDK 的 rte_mbuf(rte_mbuf_core.h:467),差异见对照表。实验怎么跑(README 07-kernel-net:46-53, 147-161)# 终端 1:挂探针 bpftrace -e 'kprobe:__netif_receive_skb_core { printf("%s\n", kstack); }' # 终端 2:发一个包 ping -c 1 8.8.8.8 # 预期栈:__netif_receive_skb_core → ip_rcv → ip_local_deliver → icmp_rcv # 丢包与状态 dropwatch -l kas # 内核丢包点实时统计 conntrack -L 2>/dev/null || cat /proc/net/nf_conntrack # 连接跟踪表 ss -s # socket 汇总tc 限速的例子(README 07-kernel-net:94-106)也跑一遍:htb qdisc + u32 filter 把某个源 IP 限到 100mbit——你会直观看到第 ③ 站的 sch_handle_ingress 在起作用。🐧 内核对照DPDK内核关键差异rte_eth_rx_burst 拿 mbuf中断 → NAPI → softirq → skb轮询 vs 中断,见 P0-03收包 1 阶段直达应用8 阶段(README 07-kernel-net:15-29)每站都是查表/判断的通用开销rte_flow 卸载到网卡tc/netfilter 软件过滤硬件卸载 vs 软件逐包KNI 回灌内核(README 07-kernel-net:127-143)原生协议栈DPDK 世界里的"内核通道":rte_kni_tx_burst(:141)送控制包,rte_kni_handle_request(:142)接回复面试 3 问__netif_receive_skb_core 按顺序过了哪几站? → XDP(dev.c:5463-5474)→ tap(dev.c:5488-5498)→ ingress qdisc + netfilter(dev.c:5500-5516)→ rx_handler(dev.c:5534-5554)→ 协议分发(dev.c:5593-5603)→ deliver/drop(dev.c:5610-5625)。RPS 是什么、在哪判断? → 把软中断处理分摊到其他 CPU:get_rps_cpu 选核后 enqueue_to_backlog 入对方队列(dev.c:5834-5845)。是内核版的"RSS 分流"——P1-07 会讲 RSS 与它的对照。为什么内核路径比 DPDK 慢一个数量级? → 8 个阶段每站查表/判断(README 07-kernel-net:15-29),加 3 次拷贝和上下文切换;DPDK 1 次 DMA 直达应用(README 03-pcie:13-25)。不是内核写得烂,是通用性的代价。数据落点perf-data/P0/07-kernel-net/:bpftrace-kstack.txt(真实调用栈)、dropwatch.txt、conntrack.txt、tc-htb.txt(限速前后的 iperf3 对比)。
2026年08月18日
0 阅读
0 评论
0 点赞
1
2
3
第 2/3 页 · 本页 15 条 · 共 39 条