pcie、p2p和ATS分析

综述

PCIe协议定义了PCIe设备三种数据传输方式之一(PIO,P2P和DMA),分别对应到CPU访问设备,设备访问设备和设备访问内存/CPU。

pcie data_transfer

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 ecam

完整流程如下:

pcie ecam_overview

设备枚举建链接

建立好配置空间之后,后面就是读取配置空间信息,建立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 ecam_scan

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原有驱动。

  • 用户态通过mmapmunmap从这个设备以类似O_Direct方式来分配和释放内存,并在用户态通过SPDK类似的软件,在另外一个设备中操作该地址,或者直接在用户态读写该内存。

原理

  • mmap怎么从这个bar分配内存呢?

由于bar是作为zone_device注册的,zone_device仍然是基于sparsemem_vmemmap的,它提供了基于struct page的服务,包括pfn_to_page,page_to_pfnget_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报文格式

pcie tlp

ATS域段

pcie tlp_ats

pcie_ats

参考

知道是不会有人点的,但万一有人呢:)