首页
/ Linux 内核 PCI Endpoint 实战指南:搭建 vNTB 虚拟非透明桥(pci-epf-vntb + ntb_hw_epf)

Linux 内核 PCI Endpoint 实战指南:搭建 vNTB 虚拟非透明桥(pci-epf-vntb + ntb_hw_epf)

2026-09-06 15:45:48作者:毕习沙Eudora

本篇基于 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)组成,其内存布局是理解后续配置的前提:

  1. Config Region(控制区):存放命令、状态、拓扑、窗口地址等控制寄存器;
  2. Self Scratchpad Registers(本地暂存寄存器):本端读写;
  3. Peer Scratchpad Registers(对端暂存寄存器):对端读写;
  4. Doorbell (DB) Registers(门铃寄存器):两端互相发中断通知;
  5. 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_ENDPOINTNTBselect 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 生成。

两侧关键源码:

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 置为 0xffffinterrupt_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_headerbaseclass_code = PCI_BASE_CLASS_MEMORYinterrupt_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)
mw1mw4 各内存窗口大小(字节,十六进制可写) 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:即我们配置的功能设备——vendor 0x1957(Freescale Semiconductor Inc)、device 0x0809,类码为 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..MW4epf_ntb_init_epc_bar() 统一规划,前两个为强制项,缺 BAR 时整体初始化失败;
  • 门铃采用保留槽位布局:槽 0 留给链路事件,槽 1 为历史遗留空槽,槽 2 起才是真正可用的 DB#0、DB#1……,因此最小 db_count = 3MIN_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):

  1. 注册内置 PCI 驱动vntb_pci_driver(设备名 pci-vntb)的匹配表初始为 0xffff:0xffff,绑定阶段改写为 configfs 写入的 vntb_vid/vntb_pid
  2. 扫描虚拟总线vpci_scan_bus() 用自定义的 pci_opspci_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 时建议遵循以下顺序与检查点:

  1. 顺序不可乱mkdir 创建功能 → 写标准头(vendorid/deviceid)→ 写 vNTB 属性(db_count/spad_count/num_mws/mw1)→ 写虚拟总线参数(vntb_vid/vntb_pid/vbus_number)→ ln -s 绑定控制器 → echo 1 > start。绑定后写属性会被内核拒绝(-EOPNOTSUPP);
  2. db_count 至少为 3:这不是笔误,而是“链路事件槽 + 历史保留槽 + 至少 1 个可用门铃”的协议要求;宿主驱动同样按此下限校验;
  3. num_mws 上限 4:受 BAR 数量(BAR2~BAR5)约束;mw1 示例值 0x100000(1MB)应小于等于 EPC 可提供的出站地址空间;
  4. 链路未起的常见原因start 写入前未建立 primary 符号链接、控制器名拼写错误(对照 ls /sys/class/pci_epc/)、或平台要求跳过/依赖设备树初始化(如 imx8);
  5. 宿主侧看不到设备:先确认 lspci 是否出现 RAM memory 类设备(类码来自 PCI_BASE_CLASS_MEMORY),再确认 CONFIG_NTB_EPF 模块已加载;
  6. 回收资源:反向操作即可——echo 0 > controllers/.../startrm 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.cntb_hw_epf.c 的实现,补充了属性取值边界、BAR/门铃槽位规则、命令协议与虚拟总线创建机制。相关背景可进一步阅读 PCI Endpoint 总览NTB 驱动 API

登录后查看全文
热门项目推荐
相关项目推荐