首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
· ⑤PCIe、虚拟化与 DPU
①性能工程基础
②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
条评论
首页
栏目
页面
关于
DPDK 成长路线 /
⑤PCIe、虚拟化与 DPU
共
11
篇
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P4-01 · PCIe Configuration Space 与 BAR 解码:写全 1 读回来,才知道设备多大
对应路线图:Phase 4 (Day 120-140) | 内核 v6.6 | 代码出处:source/linux-stable/drivers/pci/,引用均标 文件:行号上一篇:P3-06 host_if | 下一篇:P4-02 MSI-X一句话定位Config Space 是设备与软件之间的"注册表":软件通过它知道设备是谁(Vendor/Device ID)、有什么能力(Capability 链)、占多大地址(BAR)。而 BAR 解码的玄机在于——BAR 寄存器同时承担"地址寄存器"和"尺寸寄存器"两个身份,内核用"写全 1 读回"让它现出原形。真实源码精读① 64 字节头部:偏移定义在 include/uapi/linux/pci_regs.h:38-125#define PCI_VENDOR_ID 0x00 /* 16 bits */ #define PCI_DEVICE_ID 0x02 /* 16 bits */ #define PCI_COMMAND 0x04 /* 16 bits */ // ...省略(STATUS 0x06、REVISION_ID 0x08、CLASS_DEVICE 0x0a ...) #define PCI_BASE_ADDRESS_0 0x10 /* 32 bits */ // ...省略(BASE_ADDRESS_1..5 依次 0x14/0x18/0x1c/0x20/0x24,pci_regs.h:96-100) #define PCI_CAPABILITY_LIST 0x34 /* Offset of first capability list entry */ #define PCI_INTERRUPT_LINE 0x3c /* 8 bits */ #define PCI_INTERRUPT_PIN 0x3d /* 8 bits */BAR 寄存器的位布局(pci_regs.h:101-110):bit0 区分空间(1=I/O, 0=Memory)、bit1-2 编码类型(PCI_BASE_ADDRESS_MEM_TYPE_64 = 0x04 表示 64 位 BAR 占两个寄存器)、bit3 是否 prefetchable,其余位才是地址。② 枚举第一步:读 Vendor/Device ID 判断设备是否存在(probe.c:2494-2517)static struct pci_dev *pci_scan_device(struct pci_bus *bus, int devfn) { struct pci_dev *dev; u32 l; if (!pci_bus_read_dev_vendor_id(bus, devfn, &l, 60*1000)) return NULL; dev = pci_alloc_dev(bus); if (!dev) return NULL; dev->devfn = devfn; dev->vendor = l & 0xffff; dev->device = (l >> 16) & 0xffff; if (pci_setup_device(dev)) { // ... return NULL; } // ... }一次 dword 读偏移 0x00 就完成"设备在不在"判断:全 0xffffffff 表示该 devfn 悬空。存在则 pci_setup_device(probe.c:1867)继续读 header type、class 等并解析 BAR。③ BAR 尺寸探测:写全 1 读回(probe.c:176-247,__pci_read_base 节选) mask = type ? PCI_ROM_ADDRESS_MASK : ~0; // ... pci_read_config_dword(dev, pos, &l); pci_write_config_dword(dev, pos, l | mask); pci_read_config_dword(dev, pos, &sz); pci_write_config_dword(dev, pos, l); /* * All bits set in sz means the device isn't working properly. * If the BAR isn't implemented, all bits must be 0. If it's a * memory BAR or a ROM, bit 0 must be clear; if it's an io BAR, bit * 1 must be clear. */ if (PCI_POSSIBLE_ERROR(sz)) sz = 0; // ... if (res->flags & IORESOURCE_MEM_64) { pci_read_config_dword(dev, pos + 4, &l); pci_write_config_dword(dev, pos + 4, ~0); pci_read_config_dword(dev, pos + 4, &sz); pci_write_config_dword(dev, pos + 4, l); // ... }四步协议:读原值 → 写全 1 → 读回 → 还原。读回值中低 N 位被硬件"钉死"为 0,N 就是尺寸的对数——设备应答 0xfff00000 说明它是 1MB 的 BAR。注意写全 1 前内核先关掉 decode(probe.c:189-193),防止探测期间把配置写当成正常访问。64 位 BAR 占两个寄存器,第二个寄存器在 pos + 4 重复一遍(probe.c:239-243)。④ 树形枚举:pci_scan_bus → pci_scan_slot → pci_device_addpci_scan_bus(probe.c:3308)挂上 ioport/iomem/busn 三类资源后创建根总线,调 pci_scan_child_bus 深度扫描。pci_scan_slot(probe.c:2745-2770)按 function 递增探测:fn 0 不存在且不在 hypervisor 透传环境就 break——Fn 0 是一个 slot 的"存在性锚点"。每个设备最终进 pci_device_add(probe.c:2609),挂到 bus->devices 链表(:2642)后由 driver core 触发匹配和 probe。🐧 内核机制对照:同一份 BAR 信息,两种消费方式环节内核(drivers/pci/)DPDK(drivers/bus/pci/)枚举入口pci_scan_bus probe.c:3308用户态读 /sys/bus/pci/devices/存在性判断读 offset 0x00 dword,probe.c:2499sysfs vendor/device 文件BAR 尺寸写全 1 读回,probe.c:198-201直接读 sysfs resource 文件BAR 映射ioremap(内核态地址)mmap,pci_common_uio.c:178-179空 BAR 跳过尺寸 0 则 fail,probe.c:253-254phys_addr == 0 continue,pci_common_uio.c:120-121本质区别:内核替所有驱动把 BAR 解析好、放进 pci_dev.resource[](pci.h:421);DPDK 在用户态只能拿到已分配好的物理地址,用 mmap 直接映射设备寄存器——这也是 DPDK 不写内核驱动就能操作网卡的前提。面试 3 问内核怎么测 BAR 尺寸? → 写全 1 读回(probe.c:198-201),读回值取反加一即尺寸;写之前先关 decode 防误访问(probe.c:189-193);64 位 BAR 在 pos+4 再做一遍(probe.c:239-243)。BAR 低 4 位为什么不是地址? → bit0 空间类型(pci_regs.h:101)、bit1-2 类型编码含 64 位标志(pci_regs.h:104-107)、bit3 prefetch(pci_regs.h:108),掩码 PCI_BASE_ADDRESS_MEM_MASK = ~0x0f(pci_regs.h:109)。扫描时如何避免误判"幽灵设备"? → 悬空 devfn 读回 0xffffffff,pci_scan_device 直接返回 NULL(probe.c:2499-2500);读回全 1 也用于识别坏设备(probe.c:203-210 注释)。🔧 动手实验用真实 NIC(如 ixgbe 网卡)做一次"手写 __pci_read_base":lspci -nn -v -s 01:00.0 # 记录 BAR0 地址与尺寸(如 64-bit, 4MB) sudo lspci -xxxx -s 01:00.0 | head # 前 64 字节逐字节对表解码 sudo setpci -s 01:00.0 0x10.L # 读 BAR0 原值 sudo setpci -s 01:00.0 0x10.L=0xffffffff sudo setpci -s 01:00.0 0x10.L # 读回:低 N 位为 0,验证尺寸 sudo setpci -s 01:00.0 0x10.L=<原值> # 必须还原!不还原设备可能失效同时 insmod code/dpu_dev/01_pci/pci_scan.ko 看内核侧 pci_resource_start(pdev, 0) 给出的 BAR0 值是否与 lspci 一致。数据落点 perf-data/P4/01-config-space.txt(lspci 原文 + 手工解码过程 + setpci 三次读数)。下一篇:P4-02 · MSI-X 中断机制:Table 在哪、怎么触发
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P4-02 · MSI-X 中断机制:Table 在哪、怎么触发,一个内存写就够了
对应路线图:Phase 4 (Day 120-140) | 内核 v6.6 | 代码出处 source/linux-stable/drivers/pci/msi/ | 上一篇:P4-01 Config Space 与 BAR | 下一篇:P4-03 最小 PCIe 驱动一句话定位MSI-X 把"中断线"变成了"内存写事务":设备想打断 CPU 时,往一个特殊地址写一个 16 位数据,根复合体(RC)把它翻译成中断投递。每个向量在 MSI-X Table 里有一张 16 字节的表项,存着自己的"地址+数据"。所以问题拆成两个:Table 在哪(Capability 链 → BAR 内偏移),谁写表项(内核用 ioremap 后的 MMIO 直接写)。真实源码精读① 表在哪:从 Capability 链到 BAR 内偏移(msi.c:561-580)static void __iomem *msix_map_region(struct pci_dev *dev, unsigned int nr_entries) { resource_size_t phys_addr; u32 table_offset; u8 bir; pci_read_config_dword(dev, dev->msix_cap + PCI_MSIX_TABLE, &table_offset); bir = (u8)(table_offset & PCI_MSIX_TABLE_BIR); // ...省略(pci_resource_flags 检查,:572-574) table_offset &= PCI_MSIX_TABLE_OFFSET; phys_addr = pci_resource_start(dev, bir) + table_offset; return ioremap(phys_addr, nr_entries * PCI_MSIX_ENTRY_SIZE); }找 Capability:内核沿 Capability 链表(起点 PCI_CAPABILITY_LIST,pci_regs.h:121)找到 ID 为 PCI_CAP_ID_MSIX = 0x11(pci_regs.h:226)的能力结构,缓存偏移到 dev->msix_cap(pci.h:359);读 Table dword:PCI_MSIX_TABLE(能力内偏移 4,pci_regs.h:327)低 3 位是 BIR(PCI_MSIX_TABLE_BIR,pci_regs.h:328),高 29 位是 BAR 内偏移(PCI_MSIX_TABLE_OFFSET,pci_regs.h:329);BAR 基址 + 偏移 → ioremap:pci_resource_start(dev, bir) 拿到 P4-01 里解析好的 BAR 物理地址,加偏移直接映射。MSI-X 表项数与 PCI_MSIX_FLAGS(pci_regs.h:323)里的 QSIZE 相关:msi.h:6 定义 msix_table_size(flags) = (flags & PCI_MSIX_FLAGS_QSIZE) + 1——QSIZE 存的是"个数减 1"。② 表项布局与触发:16 字节一张"名片"(pci_regs.h:336-342)/* MSI-X Table entry format (in memory mapped by a BAR) */ #define PCI_MSIX_ENTRY_SIZE 16 #define PCI_MSIX_ENTRY_LOWER_ADDR 0x0 /* Message Address */ #define PCI_MSIX_ENTRY_UPPER_ADDR 0x4 /* Message Upper Address */ #define PCI_MSIX_ENTRY_DATA 0x8 /* Message Data */ #define PCI_MSIX_ENTRY_VECTOR_CTRL 0xc /* Vector Control */ #define PCI_MSIX_ENTRY_CTRL_MASKBIT 0x00000001中断触发不需要任何"电平拉低":设备做一次 Memory Write TLP,写地址 = 表项里的 address、数据 = data,RC/IOMMU 拦截后路由到本地 APIC/ITS,最终给 CPU 一个向量号。INTx 是物理线共享,MSI-X 是每个向量独享一张名片——这就是多队列网卡每队列一个中断还互不打扰的原因。③ 内核怎么写表项:先 mask 再改(msi.c:205-227,pci_write_msg_msix 节选) /* * The specification mandates that the entry is masked * when the message is modified: * * "If software changes the Address or Data value of an * entry while the entry is unmasked, the result is * undefined." */ if (unmasked) pci_msix_write_vector_ctrl(desc, ctrl | PCI_MSIX_ENTRY_CTRL_MASKBIT); writel(msg->address_lo, base + PCI_MSIX_ENTRY_LOWER_ADDR); writel(msg->address_hi, base + PCI_MSIX_ENTRY_UPPER_ADDR); writel(msg->data, base + PCI_MSIX_ENTRY_DATA);写完 address/data 再解除 mask,并用一次读强制写可见(:231-232)。消息本身来自 msi_desc.msg(msi.h:193),由中断子系统(x86 上多为 irq_matrix / MSI domain)分配。④ 使能全流程:mask 一切,再逐步放开(msi.c:712-766,msix_capability_init 节选) /* * Some devices require MSI-X to be enabled before the MSI-X * registers can be accessed. Mask all the vectors to prevent * interrupts coming in before they're fully set up. */ pci_msix_clear_and_set_ctrl(dev, 0, PCI_MSIX_FLAGS_MASKALL | PCI_MSIX_FLAGS_ENABLE); // ...省略(msix_enabled=1、读 control、msix_table_size) dev->msix_base = msix_map_region(dev, tsize); // ... /* Disable INTX */ pci_intx_for_msi(dev, 0); // ...省略(Marvell 设备 quirk 注释) msix_mask_all(dev->msix_base, tsize); pci_msix_clear_and_set_ctrl(dev, PCI_MSIX_FLAGS_MASKALL, 0);顺序是防御性的:先置 MASKALL+ENABLE(防止设置期间漏进中断)→ map Table → 分配描述符 → 关 INTx(避免与 MSI-X 并存导致双份中断)→ mask 全部表项 → 解除 MASKALL。全部搞定后驱动才逐个 unmask 自己的向量。⑤ 驱动侧入口:MSI-X 优先的 fallback 链(api.c:254-298) if (flags & PCI_IRQ_MSIX) { nvecs = __pci_enable_msix_range(dev, NULL, min_vecs, max_vecs, affd, flags); if (nvecs > 0) return nvecs; } if (flags & PCI_IRQ_MSI) { nvecs = __pci_enable_msi_range(dev, min_vecs, max_vecs, affd); // ... } /* use legacy IRQ if allowed */ if (flags & PCI_IRQ_LEGACY) { if (min_vecs == 1 && dev->irq) { // ... pci_intx(dev, 1); return 1; } }驱动传 PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY(pci.h:1076-1078)时,有多少本事用多少:MSI-X 不够换 MSI,再不行退回共享的 INTx。成功之后用 pci_irq_vector(dev, nr)(api.c:313)把"设备侧向量号"翻译成 Linux IRQ 号传给 request_irq。🐧 内核机制对照:INTx vs MSI vs MSI-X维度INTx(PCI_INTERRUPT_PIN)MSIMSI-X载体物理线 INTA#-INTD#,多设备共享内存写 TLP内存写 TLP向量数1(共享)1/2/4/8/16/32最多 2048(QSIZE+1)每向量独立地址/数据无共享一份每表项一份内核路径传统 irq domainMSI domain(msi.c:270-279)Table ioremap(msi.c:579)亲和性由 APIC 路由每向量可绑核每向量可绑核对 DPDK 的意义:多队列网卡每队列一个 MSI-X 向量,是"每队列独立中断源"的硬件基础;PMD 不想要中断时把 mask 位一置即可。面试 3 问MSI-X Table 在物理内存的哪个位置? → Capability 链找到 PCI_CAP_ID_MSIX(pci_regs.h:226),读 PCI_MSIX_TABLE dword(pci_regs.h:327)拆出 BIR(pci_regs.h:328)与偏移(pci_regs.h:329),BAR 基址加偏移 ioremap(msi.c:569-579)。为什么改表项前必须 mask? → PCI 规范原文:unmasked 状态下改 Address/Data 结果 undefined,内核在 msi.c:213-222 引用并遵守。QSIZE 为什么加 1? → QSIZE 编码的是"表项数 - 1",msix_table_size 统一加 1(msi/msi.h:6),读表项数走 pci_msix_vec_count(api.c:71-80)。🔧 动手实验lspci -vvv -s 01:00.0 | grep -A5 MSI-X # 记下 "Capabilities: [70] MSI-X" 的能力偏移 sudo lspci -xxxx -s 01:00.0 | grep -A4 '^70:' # 手工解析 Table/BIR dword sudo setpci -s 01:00.0 0x72.L # Message Control: QSIZE / Enable / MaskAll 位对照任务:把 0x72.L 读出的 QSIZE 与 lspci -v 的 "Count=xx" 互验;流量打满时观察 /proc/interrupts 各队列分布是否均匀(RSS 生效则各队列计数接近)。数据落点 perf-data/P4/02-msix.txt。下一篇:P4-03 · 从 0 写一个最小 PCIe 内核驱动
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P4-03 · 从 0 写一个最小 PCIe 内核驱动:24 行 probe 一切,然后逐行升级
对应路线图:Phase 4 (Day 120-140) | 内核 v6.6 | 实验代码:code/dpu_dev/01_pci/,引用均标 文件:行号上一篇:P4-02 MSI-X | 下一篇:P5-01 DMA/IOMMU一句话定位PCIe 驱动的最小骨架不是操作寄存器,而是参与内核的"设备-驱动匹配协议":填一张 pci_device_id 表 + 一个 probe 回调,driver core 就会在设备出现时替你调它。code/dpu_dev/01_pci/pci_scan.c 全文件只有 24 行,却能 probe 整条总线上的所有设备——这是每个网卡驱动(ixgbe、mlx5)的同款起点。真实源码精读① 全文件带读:code/dpu_dev/01_pci/pci_scan.c:1-24#include <linux/module.h> #include <linux/pci.h> static int probe(struct pci_dev *pdev, const struct pci_device_id *id) { printk("pci_scan: %04x:%04x (rev %02x) BAR0=%llx IRQ=%d\n", pdev->vendor, pdev->device, pdev->revision, pci_resource_start(pdev, 0), pdev->irq); return 0; } static struct pci_device_id ids[] = { { PCI_ANY_ID, PCI_ANY_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 }, { 0, } }; MODULE_DEVICE_TABLE(pci, ids); static struct pci_driver drv = { .name = "pci_scan", .id_table = ids, .probe = probe, }; module_pci_driver(drv); MODULE_LICENSE("GPL");逐行拆:probe 回调(:4-10):签名 (struct pci_dev *, const struct pci_device_id *) 是 PCI 子系统的硬约定。这里读的 vendor/device/revision 正是 P4-01 里 pci_scan_device 从 Config Space 填进 pci_dev 的(probe.c:2507-2508);pci_resource_start(pdev, 0) 是 BAR0 物理地址(宏定义 pci.h:2107);pdev->irq 是内核分配的 IRQ 号(pci.h:420)。返回 0 = "这个设备我接管了"。ids 表(:12-16):全 PCI_ANY_ID 表示匹配所有设备——所以本模块会 probe 系统里每一个 PCI 设备,只能在学习机上玩。真正的驱动用 PCI_DEVICE(0x8086, 0x10fb) 这类精确匹配(宏在 pci.h:962)。表项结构在 mod_devicetable.h:44-50:vendor/device/subvendor/subdevice/class/class_mask 六维过滤 + driver_data 私有数据。module_pci_driver(drv)(:23):展开成 module_driver(drv, pci_register_driver, pci_unregister_driver)(pci.h:1598-1599),自动生成 init/exit,替换手写 module_init/module_exit 的样板代码。② 匹配与 probe 触发:struct pci_driver 与 driver core(pci.h:928-943)struct pci_driver { struct list_head node; const char *name; const struct pci_device_id *id_table; /* Must be non-NULL for probe to be called */ int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ void (*remove)(struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ // ...省略 suspend/resume/shutdown/sriov/err_handler ... struct device_driver driver; // ... };id_table 注释说得很直白:没有它 probe 就不会被调用。注册入口 __pci_register_driver(pci-driver.c:1451-1467)只做一件事:填 drv->driver.bus = &pci_bus_type 然后 driver_register()——后面就是通用设备模型的 match → probe 机器了。设备侧由 pci_device_probe(pci-driver.c:444-467)接棒:static int pci_device_probe(struct device *dev) { int error; struct pci_dev *pci_dev = to_pci_dev(dev); struct pci_driver *drv = to_pci_driver(dev->driver); // ... pci_assign_irq(pci_dev); error = pcibios_alloc_irq(pci_dev); // ... pci_dev_get(pci_dev); error = __pci_device_probe(drv, pci_dev); // ... }注意顺序:先 pci_assign_irq 分配 IRQ,再进驱动的 probe——所以 probe 里 pdev->irq 一定已经有值。③ remove 与卸载:内核兜底(pci-driver.c:469-510 节选)static void pci_device_remove(struct device *dev) { struct pci_dev *pci_dev = to_pci_dev(dev); struct pci_driver *drv = pci_dev->driver; if (drv->remove) { // ... runtime PM barrier ... drv->remove(pci_dev); // ... } pcibios_free_irq(pci_dev); pci_dev->driver = NULL; // ... }drv->remove 是驱动自己的清理窗口(放走 IRQ、unmap、还资源);窗口之外的事内核兜底:pcibios_free_irq 收尾 IRQ(:486)、解绑 driver = NULL(:487)。rmmod 会经 pci_unregister_driver(pci-driver.c:1480)对每个被该驱动接管的设备各调一次 remove。④ 升级路线:把"只读"变成"拥有"pci_scan.c 只读不占,等于借设备看了一眼。要真正"拥有"一个 BAR(比如网卡的 MMIO 寄存器区),probe 里按序做四件事(API 均已核实存在):步骤API位置作用1pci_enable_device(pdev)pci.c:2191打开 IO/MEM decode,唤醒设备2pci_request_regions(pdev, name)pci.c:4273声明 resource 所有权(/proc/iomem 可见)3pci_ioremap_bar(pdev, bar)pci.c:252映射 BAR 到内核虚拟地址4request_irq(pci_irq_vector(pdev, 0), handler, ...)interrupt.h:165挂中断(配合 P4-02 的 MSI-X)remove 里严格逆序:free_irq(interrupt.h:196)→ iounmap → pci_release_regions(pci.c:4254)→ pci_disable_device(pci.c:2379)。🐧 内核机制对照:驱动协议 vs 用户态"野路子"环节内核驱动(本模块)DPDK 用户态(bus/pci)设备发现driver core 匹配 id_table → probe自己扫 /sys/bus/pci/devices/"接管"动作probe 返回 0,设备归属本驱动bind 到 vfio-pci/uio 驱动后拿 fdBAR 访问ioremap → 内核虚拟地址mmap,pci_common_uio.c:178-179中断request_irq + 上半部/NAPI轮询或 epoll 中断线程资源冲突防护pci_request_regions 互斥依赖内核驱动让位(bound to VFIO)DPDK 不是"没有驱动协议",而是把协议放进了用户态:内核里那个 vfio-pci 驱动替它完成了 probe/接管,用户态只负责 mmap 和轮询。面试 3 问probe 什么时候被调用? → 设备注册或驱动注册后 driver core 完成 match 即调;前提是 id_table 非 NULL(pci.h:931 注释);入口 pci_device_probe(pci-driver.c:444)先 pci_assign_irq 再调驱动的 probe(pci-driver.c:453-460)。PCI_ANY_ID 表项的风险? → 匹配所有设备(pci_scan.c:12-13),probe 返回 0 会接管每一个设备(pci_scan.c:9),真实驱动必须用 PCI_DEVICE 精确匹配(pci.h:962-964)。remove 里不释放资源会怎样? → IRQ 方面内核会兜底 pcibios_free_irq(pci-driver.c:486),但 ioremap 的地址和 request_regions 占用的 resource 泄漏,再加载驱动会 EBUSY——所以 remove 必须逆序归还(见上表)。🔧 动手实验cd code/dpu_dev/01_pci make # Makefile:1-2,obj-m += pci_scan.o sudo insmod pci_scan.ko dmesg | tail -30 # 每个 PCI 设备一行:vendor:device rev BAR0 IRQ sudo rmmod pci_scan升级练习(在实验机上,不要在主力机上):给 probe 加 pci_enable_device + pci_request_regions,验证 /proc/iomem 里出现 pci_scan 名字段;再试 pci_ioremap_bar 读 4 字节网卡寄存器与 lspci -xxxx 对拍。数据落点 perf-data/P4/03-pci-scan-dmesg.txt(dmesg 原文 + iomem 段 + 升级版对比)。下一篇:P5-01 · DMA/IOVA/IOMMU:设备怎么看到内存
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P5-01 · DMA/IOVA/IOMMU:设备看到的地址,从来不是你 malloc 出的那个
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/linux-stable/drivers/iommu/dma-iommu.c、source/dpdk-stable-24.11.6/lib/eal/linux/eal.c,引用均标 文件:行号上一篇:P4-03 最小 PCIe 驱动 | 下一篇:P5-02 VFIO一句话定位CPU 用页表把 VA 变成 PA,设备做 DMA 时也要有一套自己的"页表":IOMMU 页表。IOVA 就是设备视角的虚拟地址——没有 IOMMU 时 IOVA 直接等于 PA,有 IOMMU 时内核替你维护 IOVA→PA 的映射。DPDK 的 mempool 写进网卡描述符的"物理地址",本质都是 IOVA。真实源码精读① 内核的 DMA 抽象:每设备一张 dma_map_ops 虚表(dma-iommu.c:1613-1636)static const struct dma_map_ops iommu_dma_ops = { .flags = DMA_F_PCI_P2PDMA_SUPPORTED, .alloc = iommu_dma_alloc, .free = iommu_dma_free, .alloc_pages = dma_common_alloc_pages, .free_pages = dma_common_free_pages, // ...省略 alloc_noncontiguous / mmap / get_sgtable ... .map_page = iommu_dma_map_page, // :1623 .unmap_page = iommu_dma_unmap_page, .map_sg = iommu_dma_map_sg, // :1625 .unmap_sg = iommu_dma_unmap_sg, // ...省略 sync_* / map_resource ... };驱动代码从来不直接调 iommu_map,而是调 dma_map_sg() 宏 → 落到 dev->dma_ops->map_sg。IOMMU 打开时指向这张表,关掉时指向直通实现——上层驱动一行不用改,这就是内核 DMA-API 分层的意义。② iommu_dma_map_sg:先算总长度,再一次性分配 IOVA(dma-iommu.c:1332-1349) if (!iova_len) return __finalise_sg(dev, sg, nents, 0); iova = iommu_dma_alloc_iova(domain, iova_len, dma_get_mask(dev), dev); if (!iova) { ret = -ENOMEM; goto out_restore_sg; } /* * We'll leave any physical concatenation to the IOMMU driver's * implementation - it knows better than we do. */ ret = iommu_map_sg(domain, iova, sg, nents, prot, GFP_ATOMIC); if (ret < 0 || ret < iova_len) goto out_free_iova; return __finalise_sg(dev, sg, nents, iova);三步走,跟我 P0 学的内存池一个思路——先要一块连续的 IOVA 空间,再把 sg 里散落的物理页逐段映射进去。__finalise_sg(:1349)把算好的 IOVA 写回每个 sg_dma_address,驱动接下来只跟 IOVA 打交道。函数签名在 :1241-1242,开头还要处理 sg 段对齐(:1275 起 for_each_sg 的 pad 逻辑)。③ 挂载点:IOMMU 可用时偷换 dev->dma_ops(dma-iommu.c:1642-1663)void iommu_setup_dma_ops(struct device *dev, u64 dma_base, u64 dma_limit) { struct iommu_domain *domain = iommu_get_domain_for_dev(dev); if (!domain) goto out_err; // ...省略注释... if (iommu_is_dma_domain(domain)) { if (iommu_dma_init_domain(domain, dma_base, dma_limit, dev)) goto out_err; dev->dma_ops = &iommu_dma_ops; // :1656 关键一行 } return; out_err: pr_warn("Failed to set up IOMMU for device %s; retaining platform DMA ops\n", dev_name(dev)); }设备注册时调用一次,之后所有 DMA API 全部改道 IOMMU 路径。注意 :1661 的失败路径:IOMMU 初始化失败就保留 platform DMA ops(直通 PA)——系统不崩,只是没有隔离,这种"静默降级"排查问题时要想到。④ DPDK 侧:启动时三态选择 IOVA 模式(lib/eal/linux/eal.c:1038-1066) /* 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) { // ...省略 build option / phys addrs 判断 ... } else if (is_iommu_enabled()) { /* we have an IOMMU, pick IOVA as VA mode */ iova_mode = RTE_IOVA_VA; // :1054-1057 } else { /* physical addresses available, and no IOMMU * found, so pick IOVA as PA. */ iova_mode = RTE_IOVA_PA; // :1058-1063 } } rte_eal_get_configuration()->iova_mode = iova_mode;IOVA=VA 是默认最优解:有 IOMMU 就选 VA(mempool 里存的是 mbuf 虚拟地址,IOMMU 页表替设备翻译);没有 IOMMU 才退回 PA。所以 DPDK 起不来的常见报错之一就是 PA 模式 + 物理地址不可得(eal.c:1072-1076 的检查)。🐧 内核对照:同一件事,谁来做翻译维度内核驱动DPDK抽象层DMA-API(dma_map_ops 虚表,dma-iommu.c:1613)IOVA 全局三态(PA/VA/DC),无 per-device 虚表选择时机设备注册时挂表(:1656)EAL 启动时一次选定(eal.c:1066)映射粒度sg 段级,按需 map/unmapmempool 大块预映射,mbuf 只记 IOVA 偏移失败语义降级直通 + pr_warn(:1661)启动即报错退出(:1072-1076)内核把 DMA 当"每次 I/O 都建立的一段契约",DPDK 把 IOVA 当"一次启动定死的世界设定"。性能取向不同:内核要通用和隔离,DPDK 要热路径零开销。面试 3 问IOVA 和 PA、VA 什么关系? → 有 IOMMU 时 IOVA 是设备视角的虚拟地址,由 IOMMU 页表映射到 PA(iommu_map_sg 建表,dma-iommu.c:1345);无 IOMMU 时 IOVA 就是 PA。VA 是 CPU 视角的虚拟地址,和 IOVA 是两套体系。内核怎么决定走不走 IOMMU? → 设备注册时 iommu_setup_dma_ops 检查默认域,可用就把 dev->dma_ops 换成 iommu_dma_ops(:1656),失败则保留直通 ops 并打 warning(:1661)。DPDK 的 --iova-mode 为什么默认选 VA? → 有 IOMMU 时选 VA 可让 mbuf 直接存虚拟地址、避免 PA 碎片问题(eal.c:1054-1057);没有 IOMMU 且能读 /proc/self/pagemap 才退 PA(:1058-1063)。🔧 动手实验J3455 支持 VT-d,做一次"开关 IOMMU"的对照实验:# 1. 默认开启:dmesg 确认 IOMMU 生效 dmesg | grep -i -E "iommu|dmar" | head # 期望看到 DMAR: IOMMU enabled / dmar irq 等字样 # 2. 跑 testpmd,看 EAL 启动日志的 IOVA 选择 ./testpmd -c 0x3 -n 2 -- -i # 期望日志:"Selected IOVA mode 'VA'" # 3. 重启,Grub 内核参数加 intel_iommu=off(关掉 IOMMU) # 再跑 testpmd,日志应变为 "Selected IOVA mode 'PA'" # 4. 两种模式下各跑 5 分钟 rxonly,记录 PPS/CPU数据落点:perf-data/P5/01-iommu-mode.md(两种模式的 EAL 日志、dmesg 证据、PPS 对比)。预期:小包转发下 VA/PA 吞吐基本持平(IOMMU 页表走 cache),但 VA 模式是生产部署的默认答案。下一篇:P5-02 · VFIO 源码深读:把设备安全地交给用户态
2026年08月18日
0 阅读
0 评论
0 点赞
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
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 点赞
⑤PCIe、虚拟化与 DPU
· 第 1/1 页 · 本页 11 条 · 共 11 条