Linux 内核 PCI Endpoint 实战指南:搭建 vNTB 虚拟非透明桥(pci-epf-vntb + ntb_hw_epf)
本篇基于 Linux 内核官方用户指南 pci-vntb-howto.rst,系统讲解如何在 PCI Endpoint(EP)侧创建并配置 vNTB(虚拟非透明桥)功能设备、在 RootComplex(RC)宿主侧识别设备,并使用标准 NTB 客户端完成点对点数据通道搭建。读完本文,你将能够独立完成 vNTB 全链路部署(configfs 创建 → 属性配置 → 绑定控制器 → 链路建立 → 宿主侧 lspci 验证),并理解控制区命令协议、BAR/Doorbell 布局与虚拟 PCI 总线在源码中的实现机制。
vNTB 是什么:单宿主 NTB 拓扑
NTB(Non-Transparent Bridge,非透明桥)用于在两个 PCI 域之间建立内存映射的直接数据传输通道,典型硬件拓扑需要两个 Host 各经一个 NTB 实例连接到一个 Endpoint 设备。而 vNTB(virtual NTB)的关键差异在于:
- PCI NTB function:需要两个 endpoint 实例,连接 HOST1 和 HOST2;
- PCI vNTB function:只使用一个 Host + 一个 EP,用 NTB 连接 EP 与 PCI 宿主,同时在 EP 侧虚拟出一条“虚拟 PCI 总线”(Virtual PCI Bus),NTB 实际建立在宿主与这条虚拟总线之间。
整体拓扑(引自 pci-vntb-function.rst):
+------------+ +---------------------------------------+
| | | |
+------------+ | +--------------+
| NTB | | | NTB |
| NetDev | | | NetDev |
+------------+ | +--------------+
| NTB | | | NTB |
| Transfer | | | Transfer |
+------------+ | +--------------+
| | | | |
| PCI NTB | | | PCI Virtual |
| EPF | +---------------+ | NTB Driver |
| Driver | | PCI EP NTB |<------>| |
| | | FN Driver | | |
+------------+ +---------------+ +--------------+
| | | | | |
| PCI BUS | <-----> | PCI EP BUS | | Virtual PCI |
| | PCI | | | BUS |
+------------+ +---------------+--------+--------------+
PCI RC PCI EP
vNTB 功能由五类结构(Constructs)组成,其内存布局是理解后续配置的前提:
- Config Region(控制区):存放命令、状态、拓扑、窗口地址等控制寄存器;
- Self Scratchpad Registers(本地暂存寄存器):本端读写;
- Peer Scratchpad Registers(对端暂存寄存器):对端读写;
- Doorbell (DB) Registers(门铃寄存器):两端互相发中断通知;
- Memory Window (MW)(内存窗口):承载两端之间的实际数据搬运。
控制区紧跟配置头之后,两个 Scratchpad 区各分成 Peer 空间和 Span 空间两段,按 Base → Base + spad_offset → Base + spad_offset + spad_count * 4 的顺序排布(布局图见 pci-vntb-function.rst)。
BAR 建模上,默认采用 32 位 BAR:
| BAR | 用途 |
|---|---|
| BAR0 | Config Region(vNTB 中与 Scratchpad 合并,见源码) |
| BAR1 | Doorbell |
| BAR2 | Memory Window 1 |
| BAR3 | Memory Window 2 |
| BAR4 | Memory Window 3 |
| BAR5 | Memory Window 4 |
64 位 BAR 方案下则使用 BAR0/BAR1 对承载 Config+Scratchpad、BAR2/BAR3 承载 Doorbell、BAR4/BAR5 承载 MW1。
内核配置与驱动组成
vNTB 涉及三组内核配置:
| 配置项 | 位置 | 说明 |
|---|---|---|
PCI_ENDPOINT / PCI_ENDPOINT_CONFIGFS |
drivers/pci/endpoint/Kconfig | Endpoint 框架及 configfs 接口 |
PCI_EPF_VNTB |
drivers/pci/endpoint/functions/Kconfig | EP 侧 vNTB 功能驱动,tristate,依赖 PCI_ENDPOINT 和 NTB,select CONFIGFS_FS |
NTB + NTB_EPF |
drivers/ntb/hw/epf/Kconfig | 标准 NTB 核心与宿主侧 ntb_hw_epf 驱动 |
PCI_EPF_VNTB 的 help 文本明确其定位:“enable the Non-Transparent Bridge (NTB) driver for PCIe Endpoint. NTB driver implements NTB between PCI Root Port and PCIe Endpoint.”,对应编译产物由 drivers/pci/endpoint/functions/Makefile 中的 obj-$(CONFIG_PCI_EPF_VNTB) += pci-epf-vntb.o 生成。
两侧关键源码:
- EP 侧功能驱动:drivers/pci/endpoint/functions/pci-epf-vntb.c(模块名
pci_epf_vntb) - 宿主侧硬件驱动:drivers/ntb/hw/epf/ntb_hw_epf.c(模块名
ntb_hw_epf)
Endpoint 侧操作:从零建立 vNTB 功能设备
1. 发现 Endpoint 控制器与功能驱动
列出系统中已注册的 endpoint 控制器设备:
# ls /sys/class/pci_epc/
5f010000.pcie_ep
若开启了 PCI_ENDPOINT_CONFIGFS,控制器同样出现在 configfs 下:
# ls /sys/kernel/config/pci_ep/controllers
5f010000.pcie_ep
列出当前系统支持的 endpoint 功能驱动:
# ls /sys/bus/pci-epf/drivers
pci_epf_ntb pci_epf_test pci_epf_vntb
# ls /sys/kernel/config/pci_ep/functions
pci_epf_ntb pci_epf_test pci_epf_vntb
三个功能驱动分别对应 drivers/pci/endpoint/functions/ 下的 pci-epf-ntb.c(双宿主 NTB)、pci-epf-test.c(测试)与 pci-epf-vntb.c(vNTB)。
2. 通过 configfs 创建功能设备
# mount -t configfs none /sys/kernel/config
# cd /sys/kernel/config/pci_ep/
# mkdir functions/pci_epf_vntb/func1
mkdir func1 即创建一个将被 pci_epf_vntb 驱动探测的功能设备。PCI endpoint 框架会自动填充标准配置项:
# ls functions/pci_epf_vntb/func1
baseclass_code deviceid msi_interrupts pci-epf-vntb.0
progif_code secondary subsys_id vendorid
cache_line_size interrupt_pin msix_interrupts primary
revid subclass_code subsys_vendor_id
功能驱动绑定后会写入默认值。pci-epf-vntb 驱动将 vendorid 置为 0xffff、interrupt_pin 置为 0x0001:
# cat functions/pci_epf_vntb/func1/vendorid
0xffff
# cat functions/pci_epf_vntb/func1/interrupt_pin
0x0001
这与源码中的默认头一致——pci-epf-vntb.c 里定义 epf_ntb_header,baseclass_code = PCI_BASE_CLASS_MEMORY、interrupt_pin = PCI_INTERRUPT_INTA(即 INTA,编码 1),vendorid/deviceid 未指定时保持 PCI_ANY_ID。
3. 配置 PCIe 头:vendorid / deviceid
# echo 0x1957 > functions/pci_epf_vntb/func1/vendorid
# echo 0x0809 > functions/pci_epf_vntb/func1/deviceid
这里 0x1957 是 Freescale/NXP 的 PCI vendor ID,0x0809 作为设备 ID,将决定宿主侧 lspci 中的显示内容(见后文)。
4. 配置 vNTB 专属属性
PCI endpoint 框架还会在功能目录下自动创建一个与功能设备同名的子目录(本例为 pci-epf-vntb.0),其中包含 vNTB 专属可配置属性:
# ls functions/pci_epf_vntb/func1/pci_epf_vntb.0/
ctrl_bar db_count mw1_bar mw2_bar mw3_bar mw4_bar spad_count
db_bar mw1 mw2 mw3 mw4 num_mws vbus_number
vntb_vid vntb_pid
官方指南给出的示例配置:
# echo 4 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/db_count
# echo 128 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/spad_count
# echo 1 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/num_mws
# echo 0x100000 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/mw1
为虚拟 PCI 总线上的 NTB 设备设置 vendor/device ID 与总线号:
# echo 0x1957 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/vntb_vid
# echo 0x080A > functions/pci_epf_vntb/func1/pci_epf_vntb.0/vntb_pid
# echo 0x10 > functions/pci_epf_vntb/func1/pci_epf_vntb.0/vbus_number
结合 pci-epf-vntb.c 中的 store 实现,可以给出每个属性的取值边界与行为:
| 属性 | 含义 | 取值约束(源码) |
|---|---|---|
db_count |
门铃数量(含保留槽位) | [MIN_DB_COUNT=3, MAX_DB_COUNT=32],非法返回 -EINVAL |
spad_count |
单侧 scratchpad 32 位寄存器个数 | 无显式上限;分配大小 = 2 * spad_count * 4 字节 |
num_mws |
内存窗口数量 | 必须 ≤ MAX_MW(4) |
mw1–mw4 |
各内存窗口大小(字节,十六进制可写) | 仅 num_mws 范围内的窗口有效,越界返回 -ERANGE |
vntb_vid / vntb_pid |
虚拟总线上 NTB 设备的 vendor/device ID | 16 位 |
vbus_number |
虚拟 PCI 总线号(probe 时默认 0xff) |
决定 EP 侧 lspci 的总线前缀 |
ctrl_bar/db_bar/mwX_bar |
为各构造强制指定 BAR 号 | 取值 NO_BAR(-1)(自动分配)~ BAR_5 |
两个重要的行为细节:
- BAR 自动分配:默认“每个构造按顺序各占一个 BAR”;若平台需要特定 BAR 布局,才通过
XYZ_bar条目显式指定。源码中epf_ntb_find_bar()从 BAR0 起依序为BAR_CONFIG → BAR_DB → BAR_MW1..寻找空闲 BAR,可选窗口(MW2 之后)分配不到 BAR 时会自动缩减num_mws而不是报错。 - 绑定后不可再改:所有写入属性的 store 回调都会先检查
epf_ntb_epc_attached(),一旦功能设备已绑定到 EPC,修改将返回-EOPNOTSUPP。因此必须先配置、后绑定。
5. 绑定到 Endpoint 控制器
vNTB 只需一个 EP 实例,将其 primary 接口链接到面向宿主的控制器即可:
# ln -s controllers/5f010000.pcie_ep functions/pci_epf_vntb/func1/primary
完成后,PCI endpoint 控制器即具备与宿主建立链路的能力。
6. 启动链路
向 _start_ 字段写入 1 使 EP 与宿主建立链路:
# echo 1 > controllers/5f010000.pcie_ep/start
官方文档特别注明:对 NTB 场景,两个 endpoint 控制器都应与宿主建立链路,但 imx8 平台不需要这一步(imx8 的控制器在设备树/平台初始化时即处于 ready 状态,这一点从文档措辞可作推断,具体以所用平台的 EPC 驱动实现为准)。
RootComplex(宿主)侧验证:lspci 输出
链路建立后,在宿主侧执行 lspci,会看到与上面 configfs 写入值对应的设备:
# lspci
00:00.0 PCI bridge: Freescale Semiconductor Inc Device 0000 (rev 01)
01:00.0 RAM memory: Freescale Semiconductor Inc Device 0809
解读:
00:00.0 PCI bridge:EP 控制器呈现的桥;01:00.0 RAM memory ... Device 0809:即我们配置的功能设备——vendor0x1957(Freescale Semiconductor Inc)、device0x0809,类码为 Memory Controller(RAM memory),与源码默认头baseclass_code = PCI_BASE_CLASS_MEMORY一致。
宿主侧内核中的 ntb_hw_epf 驱动(CONFIG_NTB_EPF)会识别该 RAM memory 设备,把它作为标准 NTB 控制器管理:解析控制区、读取 scratchpad/doorbell 布局、下发配置命令,命令超时时间为 1 秒(NTB_EPF_COMMAND_TIMEOUT,见 ntb_hw_epf.c)。
Endpoint 侧虚拟 PCI 总线:lspci 输出
在 EP 侧(虚拟 PCI 总线所在内核)执行 lspci,可看到虚拟总线上的 NTB 设备:
# lspci
10:00.0 Unassigned class [ffff]: Dawicontrol Computersysteme GmbH Device 1234 (rev ff)
10:前缀即vbus_number = 0x10的效果,该虚拟总线号由pci_scan_bus(vbus_number, ...)创建;Unassigned class [ffff]对应虚拟设备配置空间中 class code 未被赋值的默认态(pci_space[]初始化为0xffffffff模式,见 pci-epf-vntb.c);- vendor/device 字段跟随写入的
vntb_vid/vntb_pid(文档示例输出为历史样例值,实际显示随你 echo 的值而变)。
使用 ntb_hw_epf:标准 NTB 软件栈
宿主侧软件遵循 Linux 标准 NTB 架构:ntb_hw_epf 作为硬件驱动注册 ntb_dev,上层的 NTB Transport Client(ntb_transport)、NTB Netdev(ntb_netdev)、NTB Ping Pong Test Client 以及 NTB Tool Test Client 等通用客户端均可直接用于 vNTB 功能设备,无需任何 vNTB 专属修改——这正是 vNTB 方案的卖点:用一条真实 PCIe 链路,同时获得“宿主—EP”物理 NTB 通道与“EP 侧虚拟总线” NTB 通道。更多 NTB 客户端用法参见 NTB 驱动 API 文档。
内核自带自测脚本 ntb_test.sh 可用于在配置完成后做端到端回归验证。
源码级纵深:vNTB 功能驱动的三大机制
1. 控制区命令协议:5ms 轮询的状态机
EP 侧与宿主侧共享同一套控制区布局(struct epf_ntb_ctrl,__packed,见 pci-epf-vntb.c),宿主侧用偏移宏访问同一结构(ntb_hw_epf.c):
偏移 字段 含义
0x00 command 宿主下发的命令字
0x04 argument 命令参数(如 MW 索引)
0x08 command_status 1 = OK,2 = ERROR
0x0A link_status BIT(0) = 链路 UP
0x0C topology 拓扑类型
0x10~ addr/size 要映射的物理地址与大小
0x20 num_mws 内存窗口数量
0x28 spad_offset scratchpad 区偏移
0x2C spad_count scratchpad 数量
0x34+ db_data[] MSI 门铃写数据
0xB4+ db_offset[] MSI 门铃写偏移
EP 侧由延迟工作队列 epf_ntb_cmd_handler 轮询处理,支持 6 条命令:CONFIGURE_DOORBELL(1)、TEARDOWN_DOORBELL(2)、CONFIGURE_MW(3)、TEARDOWN_MW(4)、LINK_UP(5)、LINK_DOWN(6)。其中 CONFIGURE_MW 会调用 pci_epc_map_addr() 配置 EP 的出站地址转换(Outbound ATU),把虚拟 NTB 驱动的出站窗口映射到宿主内存——这是 vNTB 能“把 EP 内存直接暴露给虚拟总线侧”的核心。轮询周期是动态的:MSI 门铃模式下每 500ms 轮询一次(门铃走中断),轮询模式下每 5ms 一次(门铃状态也要靠轮询发现)。
2. BAR 分配与门铃槽位布局
BAR_CONFIG(控制区+scratchpad)、BAR_DB(门铃)、BAR_MW1..MW4由epf_ntb_init_epc_bar()统一规划,前两个为强制项,缺 BAR 时整体初始化失败;- 门铃采用保留槽位布局:槽 0 留给链路事件,槽 1 为历史遗留空槽,槽 2 起才是真正可用的 DB#0、DB#1……,因此最小
db_count = 3(MIN_DB_COUNT = EPF_IRQ_DB_START + 1);宿主侧ntb_hw_epf同样以 3 为下限(NTB_EPF_MIN_DB_COUNT); - 门铃优先走 MSI:
epf_ntb_db_bar_init_msi_doorbell()会为每个门铃槽请求 IRQ,并向控制区回写 MSI 的db_data/db_offset供宿主按地址/数据写 MSI;任何一步失败都会自动回退到轮询模式(分配一段普通 BAR 空间让宿主轮询),保证功能可用。
3. 虚拟 PCI 总线的创建与 NTB 注册
epf_ntb_bind() 完成 BAR 规划与控制区分配后,做了两件 vNTB 特有的事(pci-epf-vntb.c):
- 注册内置 PCI 驱动:
vntb_pci_driver(设备名pci-vntb)的匹配表初始为0xffff:0xffff,绑定阶段改写为 configfs 写入的vntb_vid/vntb_pid; - 扫描虚拟总线:
vpci_scan_bus()用自定义的pci_ops(pci_read只认devfn==0的配置空间,其余返回设备不存在)执行pci_scan_bus(vbus_number),于是虚拟总线上出现一个“设备 0”,即我们lspci看到的那条 NTB 记录。
pci_vntb_probe() 随后为该虚拟设备调用 ntb_register_device(),注册 vntb_epf_ops 全套 NTB 操作:scratchpad 读写(直接映射到控制区后的 scratchpad 物理内存,spad_read 读后半段、peer_spad_read 读前半段,与上文布局图对应)、MW 传输设置(vntb_epf_mw_set_trans 重配 EPC BAR)、对端地址查询(vntb_epf_peer_mw_get_addr 返回 EPC 出站分配的物理地址)。
门铃转发路径体现了 vNTB 的“单宿主”本质:虚拟总线侧客户端调用 ntb_db_set() 时,vntb_epf_peer_db_set() 把门铃位先合并在 peer_db_pending 原子变量中(允许原子上下文调用),再由 kpcintb 工作队列(WQ_MEM_RECLAIM | WQ_HIGHPRI | WQ_PERCPU)逐位调用 pci_epc_raise_irq(PCI_IRQ_MSI) 向宿主发 MSI,中断号按 db_bit + EPF_IRQ_DB_START + 1 的 1 基编码映射,且注释明确要求不得改动该映射以兼容旧对端。每次运行最多处理 5 个批次的门铃(VNTB_PEER_DB_WORK_BUDGET),避免门铃风暴独占 kworker。
实操要点与排障清单
结合文档与源码,部署 vNTB 时建议遵循以下顺序与检查点:
- 顺序不可乱:
mkdir创建功能 → 写标准头(vendorid/deviceid)→ 写 vNTB 属性(db_count/spad_count/num_mws/mw1)→ 写虚拟总线参数(vntb_vid/vntb_pid/vbus_number)→ln -s绑定控制器 →echo 1 > start。绑定后写属性会被内核拒绝(-EOPNOTSUPP); - db_count 至少为 3:这不是笔误,而是“链路事件槽 + 历史保留槽 + 至少 1 个可用门铃”的协议要求;宿主驱动同样按此下限校验;
- num_mws 上限 4:受 BAR 数量(BAR2~BAR5)约束;
mw1示例值0x100000(1MB)应小于等于 EPC 可提供的出站地址空间; - 链路未起的常见原因:
start写入前未建立primary符号链接、控制器名拼写错误(对照ls /sys/class/pci_epc/)、或平台要求跳过/依赖设备树初始化(如 imx8); - 宿主侧看不到设备:先确认
lspci是否出现RAM memory类设备(类码来自PCI_BASE_CLASS_MEMORY),再确认CONFIG_NTB_EPF模块已加载; - 回收资源:反向操作即可——
echo 0 > controllers/.../start、rm functions/pci_epf_vntb/func1/primary(符号链接)、rmdir functions/pci_epf_vntb/func1,unbind 时驱动会清理 MSI、释放 BAR 空间并注销虚拟 PCI 驱动(epf_ntb_unbind())。
小结
vNTB 是 Linux PCI Endpoint 框架中一个“一石二鸟”的功能:一条 PCIe 链路、一个 EP 实例,既让宿主按标准 NTB 协议(ntb_hw_epf + 通用 NTB 客户端)直达 EP 内存,又让 EP 侧内核多出一条可承载任意 PCI 设备的虚拟总线。本文完整继承了 pci-vntb-howto.rst 的六步操作流程与全部命令、配置项,并结合 pci-vntb-function.rst 的构造/布局定义与 pci-epf-vntb.c、ntb_hw_epf.c 的实现,补充了属性取值边界、BAR/门铃槽位规则、命令协议与虚拟总线创建机制。相关背景可进一步阅读 PCI Endpoint 总览 与 NTB 驱动 API。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00