Linux PCI Endpoint Framework(PCI EP)实战:EPC/EPF 双库 API 全解析与 configfs 绑定机制
本文基于内核文档 pci-endpoint.rst 及其对应实现源码,系统讲解 Linux PCI Endpoint Framework 的三层架构(EPC 库、EPF 库、configfs 层)。读完本文,你将能够独立编写一个 Endpoint Controller 驱动和一个 Endpoint Function 驱动,理解 pci_epc_set_bar() 等关键 API 的底层校验逻辑,并掌握通过 configfs 将功能驱动绑定到控制器、最终让主机侧 lspci 枚举出该 PCIe 设备的完整操作流程。
1. 背景:为什么 Linux 需要 Endpoint 模式
Linux 拥有成熟的 PCI 子系统,它面向工作在 Root Complex(RC)模式的 PCI 控制器:扫描 PCI 总线、分配内存与 IRQ 资源、按 vendor ID / device ID 加载 PCI 驱动,并提供热插拔、电源管理、高级错误报告、虚拟通道等服务。
然而,部分 SoC 集成的 PCI 控制器 IP 既可以工作在 Root Complex 模式,也可以工作在 Endpoint 模式。PCI Endpoint Framework 为 Linux 补充了 endpoint 模式支持,使得 Linux 可以运行在"EP 系统"中,覆盖测试验证、协处理器加速器等多种用例。该框架的核心由三个组件构成:
- Endpoint Controller(EPC)库:为可工作在 endpoint 模式的控制器提供 API,同时也为功能驱动实现特定 endpoint 功能提供 API;
- Endpoint Function(EPF)库:为功能驱动和 EPC 库提供 endpoint 模式功能 API;
- configfs 层:负责把 endpoint function 与 endpoint controller 绑定在一起。
实现代码集中在 drivers/pci/endpoint/ 目录:
| 文件 | 职责 |
|---|---|
| pci-epc-core.c | EPC 库核心,EPC 设备类、各 API 的实现 |
| pci-epc-mem.c | EPC 地址空间分配管理 |
| pci-epf-core.c | EPF 库核心,EPF 总线、BAR 寄存器空间管理 |
| pci-ep-cfs.c | configfs 接口,完成 function 与 controller 的绑定 |
| functions/ | 内置功能驱动(pci-epf-test、pci-epf-ntb 等) |
头文件方面,EPC 侧接口声明在 pci-epc.h,EPF 侧接口声明在 pci-epf.h。
2. EPC 库 API(一):控制器驱动视角
2.1 创建与销毁 EPC 设备:devm_pci_epc_create() / pci_epc_create()
PCI 控制器驱动需要先实现一组回调 ops,然后调用 devm_pci_epc_create() 或 pci_epc_create() 创建 EPC 设备。文档中列出的基础 ops 为:
write_header:填充配置空间头(configuration space header);set_bar:配置 BAR;clear_bar:复位 BAR;alloc_addr_space:在 PCI 控制器地址空间中分配地址;free_addr_space:释放已分配的地址空间;raise_irq:触发 Legacy、MSI 或 MSI-X 中断;start:启动 PCI link;stop:停止 PCI link。
对照 pci-epc.h 中的 struct pci_epc_ops 定义,当前内核的完整 ops 集还包括:align_addr(返回映射 RC PCI 地址所需的控制器内存窗口地址、大小与偏移)、map_addr/unmap_addr(CPU 地址与 PCI 地址互映射)、set_msi/get_msi、set_msix/get_msix(向 MSI/MSI-X capability 寄存器设置或读取中断数量)、map_msi_irq(把物理地址映射为 MSI 地址并返回 MSI data,主要用于 NTB 的 doorbell 场景)、get_features(按 func_no/vfunc_no 返回 struct pci_epc_features)、get_aux_resources_count/get_aux_resources(查询控制器自带的辅助资源,如 DMA 引擎寄存器窗口),以及记录模块属主的 owner 字段。
从源码结构看,__pci_epc_create()(见 pci-epc-core.c)会为 EPC 分配 struct pci_epc,将其挂入名为 pci_epc 的设备类(class),设备名取父设备名(即 /sys/class/pci_epc/ 下看到的名称,如 51000000.pcie_ep),并通过 pci_ep_cfs_add_epc_group() 在 configfs 中自动注册一个同名 controller 组。devm_pci_epc_create() 的 devres 变体(L1123-L1143)则把 pci_epc_destroy() 注册为 devres 释放回调,驱动 detach 时自动销毁。
struct pci_epc_features 是理解各 API 能力位的关键,它包含:
linkup_notifier:EPC 能否在 link up 时通知 EPF 驱动;dynamic_inbound_mapping:是否支持在不先pci_epc_clear_bar()的情况下对已配置 BAR 再次调用pci_epc_set_bar()以更新 inbound 地址翻译;subrange_mapping:是否支持对单个 BAR 建立多个入站子区映射(依赖dynamic_inbound_mapping);msi_capable/msix_capable/intx_capable:中断能力位;bar[6]:每个 BAR 的硬件描述(enum pci_epc_bar_type:BAR_PROGRAMMABLE、BAR_FIXED、BAR_RESIZABLE、BAR_RESERVED、BAR_DISABLED,以及only_64bit约束);align:BAR 缓冲区分配要求的对齐大小。
2.2 销毁与 link 通知
pci_epc_destroy():销毁 EPC 设备,源码(L1031-L1039)中依次执行移除 configfs 组、释放 PCI domain、device_unregister();pci_epc_linkup():当 EPC 与 host 建立链路后,控制器驱动调用它通知所有已链接的功能设备。实现(L881-L897)遍历epc->pci_epf链表,对每个 EPF 调用其event_ops->link_up回调;与之配套的还有pci_epc_linkdown()、pci_epc_init_notify()、pci_epc_bus_master_enable_notify()等通知函数,对应 EPF 侧struct pci_epc_event_ops中的epc_init、epc_deinit、link_up、link_down、bus_master_enable五个回调(定义见 pci-epf.h)。
2.3 EPC 地址空间管理
pci_epc_mem_init():初始化struct pci_epc_mem(含struct pci_epc_mem_window地址窗口 + bitmap 分页管理),用于后续 EPC 地址空间分配;pci_epc_mem_exit()为其清理函数。多窗口控制器可使用pci_epc_multi_mem_init()。- 该地址空间即控制器 inbound 窗口中可映射给 RC PCI 地址的区域,
pci_epc_mem_alloc_addr()基于其中的 bitmap 按页分配。
3. EPC 库 API(二):功能驱动视角
3.1 配置空间头:pci_epc_write_header()
功能驱动用 pci_epc_write_header() 把标准配置头写入 endpoint controller。对应的数据结构是 struct pci_epf_header(pci-epf.h):vendorid、deviceid、revid、progif_code、subclass_code、baseclass_code、cache_line_size、subsys_vendor_id、subsys_id、interrupt_pin。
实现(L756-L777)会先校验功能号合法性,并且只允许 vfunc_no <= 1 的函数写入 deviceID("Only Virtual Function #1 has deviceID"),这与 PCIe SRIOV 规范一致。
3.2 BAR 配置:pci_epc_set_bar() / pci_epc_clear_bar()
功能驱动用 pci_epc_set_bar() 配置 Base Address Register,以便 host 在枚举时分配 PCI 地址空间——功能驱动的寄存器空间通常就是经由此 API 配置的。
文档特别强调了一个进阶流程:BAR 子区映射(subrange mapping)。当 pci_epf_bar.num_submap 非零时,功能驱动是在请求基于 pci_epf_bar.submap 数组的 BAR 子区间映射,这要求 EPC 通过 subrange_mapping 能力位声明支持。由于子区映射要求 host 在枚举时先给 BAR 编程基址,因此 EPF 驱动需要对同一 BAR 两次调用 pci_epc_set_bar()(需要 dynamic_inbound_mapping 能力):第一次 num_submap 置 0、只配置 BAR 大小;第二次在 PCIe link up 且 host 完成 BAR 基址编程后,将 num_submap 置为非零。且两次调用之间不得调用 pci_epc_clear_bar(),因为清空 BAR 可能清掉/禁用 endpoint 侧仍被 host 认为有效的 BAR 地址解码。
struct pci_epf_bar(pci-epf.h)携带这些字段:phys_addr、addr、size、mem_size(为满足 iATU 对齐实际分配的大小)、barno、flags,以及可选的 num_submap/submap(struct pci_epf_bar_submap 数组,按顺序铺满整个 BAR,submap[0] 位于偏移 0)。
内核在 pci_epc_set_bar() 中对 epf_bar 做了一组硬性校验(L668-L714),写功能驱动时值得对照:
num_submap非零时submap必须非空,且 EPC 必须同时具备dynamic_inbound_mapping与subrange_mapping能力,否则返回-EINVAL;BAR_RESIZABLE类型 BAR 大小必须在 1 MB ~ 128 TB 之间(对应 PCIe r6.0 §7.8.6.2 对 Resizable BAR 的最小/最大尺寸限制);BAR_FIXED类型 BAR 请求大小必须等于fixed_size;size必须是 2 的幂;- BAR_5 不能配置为 64 位(64 位 BAR 占用相邻两个 BAR),IO 类型不能与内存地址掩码位混用,超过 4 GB 的大 BAR 必须声明 64 位。
pci_epc_bar_size_to_rebar_cap()(L725-L742)则负责把 BAR 尺寸换算成 Resizable BAR Capability 寄存器的编码(BIT(ilog2(size) - ilog2(1M) + 4)),供 BAR_RESIZABLE 控制器使用。pci_epc_clear_bar() 用于复位 BAR,源码中对 "BAR_5 + 64 位" 的组合直接跳过(该组合本就非法)。
3.3 中断:pci_epc_raise_irq()
功能驱动用 pci_epc_raise_irq(epc, func_no, vfunc_no, type, interrupt_num) 触发 Legacy(INTx)、MSI 或 MSI-X 中断。实现(L290-L307)先做功能号合法性检查,再进入控制器 ops;若控制器未实现 raise_irq 则静默返回 0。与之配套的还有 pci_epc_set_msi()(请求 1~32 个 MSI 中断号)、pci_epc_get_msi()(读取 RC 实际分配的 MSI 中断数)、pci_epc_set_msix()(1~2048 个,需指定 MSI-X 表所在 BAR 与偏移)与 pci_epc_get_msix()。
3.4 地址映射:从 alloc_addr 到 mem_map
pci_epc_mem_alloc_addr():从 EPC 地址空间分配内存地址,这是访问 RC 侧缓冲区的必备地址;pci_epc_mem_free_addr()用于释放;pci_epc_map_addr():把pci_epc_mem_alloc_addr()得到的本地 CPU 物理地址映射为一个 RC PCI 地址;pci_epc_unmap_addr()解除映射;pci_epc_mem_map():一些 endpoint controller 对可映射的 RC PCI 地址存在约束。该函数让功能驱动在考虑这些约束的前提下"分配 + 映射"控制器内存,它会返回实际映射成功的 PCI 地址范围大小(可能小于请求值)以及应使用的本地内存偏移。实现(L553-L603)的核心逻辑:若控制器实现了ops->align_addr,先由它算出对齐后的map_pci_addr、map_size与map_offset;随后按map_size调用pci_epc_mem_alloc_addr()分配本地内存,最后pci_epc_map_addr()建立翻译;失败则回滚释放。输出结构struct pci_epc_map同时给出pci_addr/pci_size(请求范围)、map_pci_addr/map_size(实际映射范围与所需内存)、phys_base/virt_base与phys_addr/virt_addr(对齐偏移后的实际访问点)。pci_epc_mem_unmap()完成对应的解除映射与释放。
4. EPC 库"其他 API":EPF 与 EPC 的绑定
文档指出以下 API 用于把 EPF 设备绑定到 EPC 设备,可参考 pci-ep-cfs.c 的用法:
pci_epc_get(epc_name):按设备名获取 EPC 引用。实现(L51-L68)通过class_find_device_by_name()在pci_epc类中查找,并try_module_get防止控制器模块被卸载;pci_epc_put()释放该引用;pci_epc_add_epf():把一个 PCI endpoint function 加到 EPC 上。按 PCIe 规范,一个 PCIe 设备最多可有 8 个 function。实现(L790-L836)用function_num_map位图分配空闲 func_no,并受epc->max_functions限制;该 API 还接受enum pci_epc_interface_type(PRIMARY_INTERFACE/SECONDARY_INTERFACE),以支持 NTB 这类 EPF 同时挂到两个 EPC(主/从接口)的场景;pci_epc_remove_epf():把 EPF 从 EPC 移除,清位并从链表摘除;pci_epc_start():功能驱动完成 endpoint function 配置后调用以启动 PCI link(未实现ops->start时返回 0);pci_epc_stop():停止 PCI link。
5. EPF 库 API
5.1 注册功能驱动:pci_epf_register_driver()
功能驱动需要实现以下 ops(struct pci_epf_ops,pci-epf.h):
bind:EPC 设备与 EPF 设备建立绑定时的操作;unbind:绑定关系解除时的操作;add_cfs:可选,创建功能特定的 configfs 属性。
随后用 pci_epf_register_driver() 宏(展开为 __pci_epf_register_driver(driver, THIS_MODULE))注册,pci_epf_unregister_driver() 反注册。
从源码结构看,EPF 体系是一条名为 pci-epf 的自定义总线(pci-epf-core.c):EPF 设备挂在这条总线上,EPF 驱动通过 pci_epf_match_id() 按名称匹配——优先用驱动的 id_table(pci_epf_device_id 列表)做精确匹配,否则回退到驱动名与设备名比较(L625-L646)。struct pci_epf_driver 还提供 probe/remove 回调,probe 时传入匹配到的设备 ID。注册时 __pci_epf_register_driver() 强制要求 ops->bind 与 ops->unbind 均非空(L487-L508),并会遍历 id_table 为每个函数名在 configfs functions/ 下创建目录。
5.2 BAR 寄存器空间:pci_epf_alloc_space() / pci_epf_free_space()
功能驱动用 pci_epf_alloc_space() 为指定 BAR 分配寄存器空间,用 pci_epf_free_space() 释放。实现细节(L305-L347)值得注意:
- 内部
pci_epf_get_required_bar_size()先把请求大小取整:最小 128 字节;BAR_RESIZABLE时最小 1 MB;BAR_FIXED时不得超过固定尺寸且最终采用固定尺寸;否则向上取 2 的幂; - 使用
dma_alloc_coherent()在 EPC 父设备上分配内存(mem_size会按epc_features->align对齐),保证 host 通过 BAR 读写的地址对本地 CPU 也是相干可访问的; - 若请求尺寸超过 4 GB 或该 BAR 是
only_64bit,自动置PCI_BASE_ADDRESS_MEM_TYPE_64,否则置 32 位。
此外还有两个配套 API:pci_epf_assign_bar_space() 用于控制器侧已经给定 BAR 基址的场景(如 NTB),pci_epf_align_inbound_addr() 用于按 BAR 对齐要求对齐外设寄存器空间等非 pci_epf_alloc_space() 分配的入站地址。
5.3 供 EPC 库调用:pci_epf_linkup()
当 EPC 设备与 host 建立连接时,EPC 库调用 pci_epf_linkup(),最终触达功能驱动的 event_ops->link_up 回调——这是功能驱动感知 "host 已上线" 的标准入口。
5.4 其他 EPF API
pci_epf_create(name):按名称创建 EPF 设备,该名称用于与 EPF 驱动匹配。实现(L531-L573)会把名称按第一个.截断存储(configfs 创建的设备实际名为<funcname>.<index>),并将 VF 号位图初值置 1(VF 编号从 1 开始);pci_epf_destroy():销毁 EPF 设备;pci_epf_bind():EPF 与 EPC 绑定成功时调用,会先绑定所有挂在该 PF 下的虚拟功能(校验epc->max_vfs),再调用驱动ops->bind;pci_epf_unbind():绑定丢失时调用,反向通知所有 VF 与 PF。
6. configfs 层:把功能驱动绑定到控制器驱动
6.1 挂载与目录结构
PCI Endpoint Core 在 configfs 下注册 pci_ep 子系统(pci-ep-cfs.c),包含两个根目录:
/sys/kernel/config/pci_ep/
.. controllers/ # 每个 EPC 设备一个条目
.. functions/ # 每个 EPF 驱动一个条目
挂载命令:
# mount -t configfs none /sys/kernel/config
查看系统中可用的 endpoint 控制器与功能驱动:
# ls /sys/class/pci_epc/
51000000.pcie_ep
# ls /sys/kernel/config/pci_ep/controllers
51000000.pcie_ep
# ls /sys/bus/pci-epf/drivers
pci_epf_test
# ls /sys/kernel/config/pci_ep/functions
pci_epf_test
6.2 创建 EPF 设备
在 functions/<EPF 驱动名>/ 下 mkdir 即创建一个 EPF 设备,它随后会被同名 EPF 驱动 probe:
# cd /sys/kernel/config/pci_ep/
# mkdir functions/pci_epf_test/func1
源码侧对应 pci_epf_make()(L599-L659):为设备分配 IDR 序号,生成 <funcname>.<index> 名称调用 pci_epf_create(),并自动创建 primary/ 与 secondary/ 两个子组(对应主/次 EPC 接口),若驱动实现了 add_cfs 还会追加功能特定的属性组。
每个 EPF 设备目录会填充一组标准配置头属性(由 L418-L475 的属性表生成):vendorid、deviceid、revid、progif_code、subclass_code、baseclass_code、cache_line_size、subsys_vendor_id、subsys_id、interrupt_pin,以及 msi_interrupts、msix_interrupts。EPF 驱动在 bind 时填充默认值,例如 pci-epf-test 驱动把 vendorid 设为 0xffff、interrupt_pin 设为 0x0001:
# cat functions/pci_epf_test/func1/vendorid
0xffff
# cat functions/pci_epf_test/func1/interrupt_pin
0x0001
用户可在绑定前覆盖这些值:
# echo 0x104c > functions/pci_epf_test/func1/vendorid
# echo 0xb500 > functions/pci_epf_test/func1/deviceid
# echo 32 > functions/pci_epf_test/func1/msi_interrupts
# echo 2048 > functions/pci_epf_test/func1/msix_interrupts
以 pci-epf-test 为例,其默认 BAR 大小为 bar0~bar4 各 128 KB(131072)、bar5 为 1 MB(1048576),定义见 pci-epf-test.c;绑定前可通过 bar?_size 覆盖(例如 # echo 1048576 > functions/pci_epf_test/func1/pci_epf_test.0/bar1_size)。注意:具有固定尺寸或保留 BAR 的控制器会忽略 configfs 中对应的 BAR 大小设置。
6.3 绑定与启动 link
要让 endpoint function 发挥作用,必须把它绑定到某个 PCI endpoint 控制器。做法是在 controllers 目录下建立符号链接:
# ln -s functions/pci_epf_test/func1 controllers/51000000.pcie_ep/
该符号链接触发 pci_primary_epc_epf_link()(L110-L133),内核依次执行 pci_epc_add_epf() → pci_epf_bind() → pci_epc_notify_pending_init()(补发可能早于 bind 发生的 EPC 初始化完成事件)。对于需要连接两个 EPC 的场景(如 Non-transparent bridge),主接口 controller 的符号链接放在 primary/ 子目录、次接口放在 secondary/ 子目录。此外,把某个 EPF 设备符号链接到另一个 EPF 设备表示 VF 与 PF 的关联,一旦 EPF 被链接为 VF,就不能再链接到 EPC。
绑定完成后,向 controller 的 start 字段写 1 即可建立与 host 的链路:
# echo 1 > controllers/51000000.pcie_ep/start
start 属性的 store 回调(L173-L204)解析布尔值后调用 pci_epc_start()/pci_epc_stop(),并在成功启动时置位 epc_group->start;unlink 时若 link 处于启动状态会触发 WARN_ON_ONCE——即必须先停 link(写 0)再解除绑定。
6.4 主机侧验证
host 枚举后 lspci 应显示 EP 侧配置的值(对应 6.2 节写入的 vendorid/deviceid):
00:00.0 PCI bridge: Texas Instruments Device 8888 (rev 01)
01:00.0 Unassigned class [ff00]: Texas Instruments Device b500
内核自带 Kselftest 可跑通完整的 BAR、中断与数据拷贝测试,详见 pci-test-howto.rst:
# cd <kernel-dir>
# make -C tools/testing/selftests/pci_endpoint
# pci_endpoint_test
TAP version 13
1..16
ok 1 pci_ep_bar.BAR0.BAR_TEST
...
ok 8 pci_ep_basic.LEGACY_IRQ_TEST
ok 9 pci_ep_basic.MSI_TEST
ok 10 pci_ep_basic.MSIX_TEST
ok 14 pci_ep_data_transfer.dma.READ_TEST
...
# PASSED: 16 / 16 tests passed.
其中 pci_ep_data_transfer.dma.COPY_TEST 在大多数 DMA 控制器上会因缺少 MEMCPY over DMA 而失败,可用 pci_endpoint_test -f pci_ep_bar -f pci_ep_basic -v memcpy -T COPY_TEST -v dma 跳过。doorbell 用例则用 pci_endpoint_test -f pcie_ep_doorbell。
7. 小结:一次完整的 EP 上线时序
把三层串起来,一个最小 endpoint 系统的完整时序为:
- 控制器驱动 probe:实现
pci_epc_ops(write_header、set_bar、clear_bar、raise_irq、start/stop等),调用devm_pci_epc_create()创建 EPC,pci_epc_mem_init()初始化 inbound 地址窗口,并在/sys/class/pci_epc/与configfs/controllers/出现同名条目; - 功能驱动 通过
pci_epf_register_driver()注册,其id_table中的函数名出现在configfs/functions/下; - 用户在 configfs 中
mkdir创建 EPF 设备、写入vendorid/deviceid等配置头字段(驱动 bind 回调中通常执行pci_epf_alloc_space()分配寄存器空间、pci_epc_set_bar()编程 BAR、pci_epc_write_header()写配置头); - 用户
ln -s把 EPF 链接到 controller(触发pci_epc_add_epf()+pci_epf_bind()),echo 1 > .../start拉起 link(触发ops->start,随后控制器收到 link up 时调用pci_epc_linkup()通知功能驱动); - host 侧
lspci枚举出该设备,主机驱动加载后通过 BAR 读写寄存器、经中断与 DMA 通路交互,即可用 Kselftest 完成回归验证。
框架的其余文档——pci-endpoint-cfs.rst(configfs 目录结构细节)、pci-ntb-howto.rst 与 pci-vntb-howto.rst(NTB 双接口桥接)、pci-nvme-function.rst(NVMe 功能)——可在此骨架上进一步深入具体功能驱动的实现。
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