综述
PCIe协议定义了PCIe设备三种数据传输方式之一(PIO,P2P和DMA),分别对应到CPU访问设备,设备访问设备和设备访问内存/CPU。
CPU访问设备-PCIe设备枚举建链
PCI设备的地址空间
PCI协议定义了三种地址空间:mmio地址空间(memory address space),io地址空间(io address space)和配置空间(configure address space)。
CPU要访问前面两种地址空间分别通过mmio和pio,而对于配置空间则使用io端口 CF8/CFC或者ECAM方式访问,在arm64平台上只支持ECAM机制。
ECAM机制
ECAM其实也是基于mmio访问的,以ACPI启动举例,ACPI的MCFG(memory-maped configuration)表格中会配置ECAM的基地址。
完整流程如下:
设备枚举建链接
建立好配置空间之后,后面就是读取配置空间信息,建立pcie设备树,具体流程如下:
acpi_init
acpi_scan_init
*acpi_pci_root_init
acpi_pci_root_add
pci_acpi_scan_root
acpi_pci_root_create
pci_create_root_bus
pci_scan_child_bus
pci_scan_single_device
pci_scan_device
pci_setup_device
pci_device_add
*acpi_pci_link_init
其中acpi_pci_root_init
完成pci设备的相关操作,包括设备枚举、配置空间设置等,最终完成物理设备到逻辑设备的映射:
PCIe热插拔
热插拔分为2种:通知式和暴力热插拔,差异体现在拔插动作上,暴力热插拔是没有事先通知的情况下,直接插拔。
热插拔依赖PCIe硬件的实现,内核中的驱动在driver/pci/hotplug
里面,其中:
pciehp_hpc.c
主要负责控制器的初始化以及检测设备在位变化、attention button pressed、电源错误等事件检测,检测到这些事件后,会上报热插拔中断。pciehp_ctrl.c
主要是对热插拔各个events 的具体处理。如果pcieslot槽位处于上电状态,却产生了在位状态改变的event,说明产生了暴力热拔操作,此时直接将槽位下电。
pciehp_handle_presence_or_link_change()
slot->state = POwEROFF_STATE; pciehp_disable_slot() remove_board()
如果链路状态正常并且pcie卡处于在位状态,进行热插的处理。
present || link_active
pciehp_enable_slot() board_added() pciehp_configure_device() pcie_scan_single_device //和枚举流程一样,调用pci_setup_divice读取设备config space pci_bus_add_devices //调用设备驱动,使能设备
热插的卡的bar空间,一般是在BIOS/UEFI阶段枚举过程中预留的地址空间,但随着pcie nvme盘的流程,动态bar空间的技术也被引入了,具体参考The modernization of PCIe hotplug in Linux。
设备访问内存、CPU
这块涉及到的技术主要包括IOMMU,SVA,DDIO等,就不在本文描述了。
设备访问设备(P2P)
PCIe P2P是指两个PCIe设备直接通信,通信的数据不经过CPU处理从一个PCIe设备交换到另外一个PCIe设备。
因此有两个隐藏的前提条件:
- 至少某一个PCIe设备拥有内存,而不是简单的一个PCIe设备去访问另外一个PCIe设备的BAR空间或者寄存器
- 至少某一个PCIe设备拥有算力
前面已经写过一篇P2P的文章,
现在6.2内核已经把p2pdma
接口合并到dma
接口,并通过vfio接口export到了用户态。
简单来讲,主要实现以下2点:
写一个pcie的设备驱动,实现p2pdma provider的
pci_p2pdma_add_resource
接口,将bar作为zone_device
注册成为DMA内存资源,
并通过pci_p2pm_publish
发布这个资源,允许p2pdma的orchestrator调用。注意如果该设备已有驱动匹配了,要先unbind原有驱动。用户态通过
mmap
和munmap
从这个设备以类似O_Direct
方式来分配和释放内存,并在用户态通过SPDK类似的软件,在另外一个设备中操作该地址,或者直接在用户态读写该内存。
原理
- mmap怎么从这个bar分配内存呢?
由于bar是作为zone_device
注册的,zone_device
仍然是基于sparsemem_vmemmap
的,它提供了基于struct page
的服务,包括pfn_to_page
,page_to_pfn
和get_user_pages
等。
但它的分配和释放并不是当做普通内存来管理的,而是通过generic purpose allocator来管理分配的,并会针对该设备创建一个内存池。
当map的时候,会调用p2pmem_alloc_mmap
,并通过vm_insert_page
把这个地址映射到用户态的va。
- 对端设备怎么用这个地址呢?
另外一个设备拿到这个地址之后,怎么找到这个地址对应的设备的bar offset呢?
这就有回到类似dma的作用,把dma地址映射到内存的物理地址,而在p2p场景中,则是把dma地址,转换成bar offset。
要理解这个细节,可以看pci_p2pmem_virt_to_bus
,它会调用gen_pool_virt_to_phys
,返回pcie bar的offset,于是对端设备实际上配置的是pcie的地址空间。
于是当对端设备针对这个地址发起访问的时候,就没有上pcie root port,而是直接到该设备了。
注意:
当前内核一旦开启了P2P,默认会关掉ACS,也可能会影响到SVA。
原则上ATS/ACS和P2P是有些冲突的,因为当ATS打开之后,设备发出的PCIe TLP报文会声称该报文的地址是否是翻译过的。
如果没有翻译,则先路由到RC的TA处进行地址翻译;如果翻译过,则直接使用,绕过了IOMMU的隔离,直接访问这个物理地址了,导致安全风险。
比如说,开启了P2P和ATS以后,同一个PCIe Switch后的所有EP设备,必须都分给同一个虚拟机,不然分给不同虚拟机的话,可以从这个PCIe设备的另外一个Function攻击到其它的虚拟机。
于是呢,就引入了ACS(访问控制)来决定一个TLP是否能正常路由,还是被阻塞或者重定向。
可以参考这个patch的评论PCI/P2PDMA: Clear ACS P2P flags for all devices behind switches。
也可以参考SBSA的测试用例SBSA PCIe ATS test。
其它
PCIe TLP报文格式
ATS域段
参考
- PCIe扫盲系列博文连载目录篇
- PCIe扫盲——PCI总线的三种传输模式
- PCIe ECAM机制
- Linux源码阅读——PCI总线驱动代码(三)PCI设备枚举过程
- Writing a PCI device driver for Linux
- How To Write Linux PCI Drivers
- A Practical Tutorial on PCIe for Total Beginners on Windows (Part 1)
- UEFI——PCIe子系统(I)
- Introduction to PCIe Address Translation Services
- PCIe TLP Header 中的常见 Feild 及其释义
- PCIe地址转换服务(ATS)详解
- PCIe地址转换服务(ATS)详解
- PCIe访问控制服务(ACS)
- PCI设备驱动(二)
- P2P DMA
- PCI Peer-to-Peer DMA Support
- Peer-to-peer DMA
- PCIe扫盲——一个Memory Read操作的例子
- Down to the TLP: How PCI express devices talk
- 颠覆性技术!你NRZ相守20年又怎样?看我PAM4如何上位PCIe 6.0
- p2pmem-test
- Linux SVA特性分析
- How can my PCI device driver remap PCI memory to userspace
- Linux | PCIe Hotplug | 概念及工作原理的不完全总结
- PCI hotplug: movable BARs and bus numbers