jklincn


在 WSL 中使用 QEMU 搭建 PCIe 模拟环境


本文介绍了如何在 Windows Subsystem for Linux(WSL) 上使用 QEMU 来模拟 PCIe 系统。选用 WSL 的初衷是方便开发,无需远程连接到物理 Linux 主机即可在 Windows 上做测试。

基础环境(可以使用 wsl –version 查看):

  • Windows 版本:10.0.22621.3880
  • WSL 版本: 2.2.4.0

选用的 WSL 发行版:Ubuntu-22.04

创建 WSL

以下命令在 Windows CMD 或者 Windows PowerShell 中执行

wsl --update
wsl install Ubuntu-22.04

虚拟机设置

此处的“虚拟机”指的是 QEMU 运行的虚拟机实例

以下命令在 WSL 终端中执行

安装 QEMU

本文选择的 QEMU 版本为 8.2.5

# 安装一系列依赖(部分软件包可能用不上,这里是一个超集),参考 https://wiki.qemu.org/Hosts/Linux
sudo apt update
sudo apt install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev ninja-build git-email libaio-dev libbluetooth-dev libcapstone-dev libbrlapi-dev libbz2-dev libcap-ng-dev libcurl4-gnutls-dev libgtk-3-dev libibverbs-dev libjpeg8-dev libncurses5-dev libnuma-dev librbd-dev librdmacm-dev libsasl2-dev libsdl2-dev libseccomp-dev libsnappy-dev libssh-dev libvde-dev libvdeplug-dev libvte-2.91-dev libxen-dev liblzo2-dev valgrind xfslibs-dev libnfs-dev libiscsi-dev wget build-essential automake autoconf ninja-build make cmake python3-dev python3-venv python-is-python3 libslirp-dev

# 下载源代码
wget https://download.qemu.org/qemu-8.2.5.tar.xz
tar xf qemu-8.2.5.tar.xz
cd qemu-8.2.5

# 只构建 x86,编译并安装
./configure --target-list=x86_64-softmmu --enable-debug
make -j $(nproc)
sudo make install

使用 KVM 加速

# 安装依赖包
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager

# 把用户加入到 kvm 组中
sudo adduser $(id -un) kvm

经实践,最新的 WSL 内核已经包含了 KVM,因此不需要再手动编译内核。

运行 adduser 后需要重新打开 WSL 界面以使组成员身份的更改生效。

安装操作系统

本文选择的虚拟机操作系统为 Ubuntu 22.04.4 Server

# 下载镜像
wget https://releases.ubuntu.com/22.04/ubuntu-22.04.4-live-server-amd64.iso

# 创建虚拟磁盘
qemu-img create -f qcow2 ubuntu.qcow2 80G

# 安装操作系统
qemu-system-x86_64 -enable-kvm \
	-M q35 \
	-cpu SapphireRapids-v2 \
	-smp 8 \
	-m 16G \
	-hda ubuntu.qcow2 \
	-cdrom ubuntu-22.04.4-live-server-amd64.iso \
	-boot d
  • -M q35:使用 q35 机器类型,这会模拟了更现代的硬件,支持较新的硬件特性。
  • -cpu SapphireRapids-v2:模拟 Intel Sapphire Rapids 架构的处理器(2023 年发布的服务器 CPU)
  • -enable-kvm:启用 KVM 支持
  • -smp 8:虚拟机配置 8 核 (可自行修改)
  • -m 16G:虚拟机配置 16G 内存 (可自行修改)
  • -hda ubuntu.qcow2:使用虚拟磁盘镜像作为硬盘
  • -cdrom ubuntu-22.04.4-live-server-amd64.iso:使用 iso 镜像文件

QEMU 图形化界面可能会在选择 Try or Install Ubuntu Server 后一直黑屏,等待即可,大概两三分钟后即有输出。

根据界面进行安装系统,具体步骤:

  • 选择语言 English(默认选择,直接 Enter)
  • 检查可用更新(默认选择,直接 Continue without updating)
  • 选择键盘布局 English (US) (默认选择,直接 Done)
  • 安装 Ubuntu Server(默认选择,直接 Done)
  • 网络设置(直接 Done)
  • 代理设置(直接 Done)
  • 软件包镜像地址(直接 Done,后续可根据需要在系统安装后自行换源)
  • 存储设置(推荐取消 Set up this disk as an LVM group)
  • 分区设置(使用默认分区,直接 Done,再选择 Continue 确认)
  • 设置主机名,用户名和密码(自行设置)
  • 升级到 Ubuntu Pro(直接 Continue)
  • SSH 设置(勾选 Install OpenSSH server,然后 Done)
  • 安装 Snap 应用(全都不勾选,直接 Done)

安装完成后选择 Reboot Now 会看到 [FAILED] Failed unmounting /cdrom,此时可以关闭 QEMU 界面

在操作系统安装完成后可以将虚拟磁盘文件拷贝一份, 以便未来复用

cp ubuntu.qcow2 ubuntu.qcow2.ori

注意:在重置虚拟硬盘后需要手动删除 Windows 下 known_hosts 中的条目,文件一般位于 C:\Users\<username>\.ssh,否则 ssh 连接会报错。

运行 QEMU

qemu-system-x86_64 -enable-kvm \
	-M q35 \
	-cpu SapphireRapids-v2 \
	-smp 8 \
	-m 16G \
	-hda ubuntu.qcow2 \
	-netdev user,id=net0,hostfwd=tcp::10022-:22 \
	-device e1000,netdev=net0

参数解释:

  • -netdev user,id=net0,hostfwd=tcp::10022-:22

    指定网络后端类型为 user,设置标识符为 net0(用于后续网络设备关联),将宿主机的 10022(可自行更改)端口流量转发到虚拟机的 22 端口。

  • -device e1000,netdev=net0

    向虚拟机添加一个设备 e1000,这是非常常见的虚拟以太网卡,使用 netdev=net0 将网卡和前面定义的网络后端关联。这样,e1000 网卡就会使用我们定义的用户模式网络后端来处理网络数据。

QEMU 图形化界面可能会在引导后一直黑屏,等待即可,大概两三分钟后即有输出。

至此,QEMU 虚拟机已经成功安装并运行。

由于我们在虚拟机中安装了 SSH 服务器并设置了端口转发,因此可以直接在 vscode 的远程资源管理器中打开虚拟机进行开发。hostname 为 WSL 所对应的 IP,可以在 WSL 终端中使用 ip addr show 查看 eth0 网卡的地址,一般是 172.xxx.xxx.xxx,Port 即上述设置的 10022。

PCIe 系统

当前 PCIe 拓扑

首先我们在虚拟机中使用 lspci 查看当前的 PCIe 设备:

00:00.0 Host bridge: Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller
00:01.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:02.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
00:1f.0 ISA bridge: Intel Corporation 82801IB (ICH9) LPC Interface Controller (rev 02)
00:1f.2 SATA controller: Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)
00:1f.3 SMBus: Intel Corporation 82801I (ICH9 Family) SMBus Controller (rev 02)
  • 00:00.0 Host bridge: Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller

    这是芯片组的 DRAM 控制器,主要负责管理和控制系统内存(DRAM)的访问和通信。它是整个系统架构的桥梁,将 CPU 和内存连接在一起。

  • 00:01.0 VGA compatible controller: Device 1234:1111 (rev 02)

    这是 QEMU 提供的标准 VGA 显卡设备,通常是一个虚拟显卡,用于基本的图形显示。设备 ID 1234:1111 是 QEMU 虚拟 VGA 设备的标识。

  • 00:02.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

    这是 Intel 的千兆以太网控制器,对应启动命令中使用的 -device e1000,netdev=net0 参数。

  • 00:1f.0 ISA bridge: Intel Corporation 82801IB (ICH9) LPC Interface Controller (rev 02)

    这是一个 LPC 接口控制器,LPC(Low Pin Count)总线用于连接较低速率的设备。它作为 ISA 桥,负责将 ISA 设备连接到 PCI 总线,主要用于连接低速设备如键盘控制器和实时钟(RTC)。

  • 00:1f.2 SATA controller: Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)

    这是一个六端口 SATA 控制器,工作在 AHCI 模式下。它管理 SATA 接口,连接硬盘和光驱等 SATA 设备。

  • 00:1f.3 SMBus: Intel Corporation 82801I (ICH9 Family) SMBus Controller (rev 02)

    这是一个 SMBus 控制器。SMBus(System Management Bus)是一种简单的双向串行总线,主要用于系统管理和监控任务,如温度监控、电源管理和其他低速系统管理通信。

然后使用 lspci -t -v 查看当前的 PCIe 拓扑:

-[0000:00]-+-00.0  Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller
           +-01.0  Device 1234:1111
           +-02.0  Intel Corporation 82540EM Gigabit Ethernet Controller
           +-1f.0  Intel Corporation 82801IB (ICH9) LPC Interface Controller
           +-1f.2  Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode]
           \-1f.3  Intel Corporation 82801I (ICH9 Family) SMBus Controller

可以得到当前 PCIe 拓扑图(省略了具体型号)

PCIe 拓扑图1

添加 NVMe SSD 设备

# 创建虚拟磁盘文件作为 NVMe 设备
qemu-img create -f qcow2 nvme1.qcow2 10G
qemu-img create -f qcow2 nvme2.qcow2 10G
qemu-img create -f qcow2 nvme3.qcow2 10G

# 运行
qemu-system-x86_64 -enable-kvm \
    -trace events=trace-events \
    -M q35 \
    -cpu SapphireRapids-v2 \
    -smp 8 \
    -m 16G \
    -hda ubuntu.qcow2 \
    -netdev user,id=net0,hostfwd=tcp::10022-:22 \
    -device e1000,netdev=net0 \
    -drive file=nvme1.qcow2,if=none,id=nvme1 \
    -device nvme,drive=nvme1,serial=0000,cmb_size_mb=2048 \
    -device pcie-root-port,id=rp1,slot=0,chassis=1 \
    -device x3130-upstream,id=upstream1,bus=rp1 \
    -device xio3130-downstream,id=downstream1,bus=upstream1,chassis=2,slot=0 \
    -device xio3130-downstream,id=downstream2,bus=upstream1,chassis=3,slot=1 \
    -drive file=nvme2.qcow2,if=none,id=nvme2 \
    -device nvme,drive=nvme2,serial=0001,cmb_size_mb=2048,bus=downstream1 \
    -drive file=nvme3.qcow2,if=none,id=nvme3 \
    -device nvme,drive=nvme3,serial=0002,cmb_size_mb=2048,bus=downstream2

参数解释:

  • -drive file=nvme1.qcow2,if=none,id=nvme1

    指定第一个 NVMe 设备的磁盘文件为 nvme1.qcow2,不使用默认接口,ID 为 nvme1

  • -device nvme,drive=nvme1,serial=0000,cmb_size_mb=2048

    添加第一个 NVMe 设备,连接到默认总线(pcie.0),CMD 大小为 2048 MB(大于 1GB 以便后续可以映射 1GB 的空间),序列号为 0000

  • -device pcie-root-port,id=rp1,slot=0,chassis=1

    添加一个 PCIe 根端口,ID 为 rp1,插槽为 0,机箱号为 1。

    根复合体和根端口概念辨析:

    根复合体(Root Complex)是 PCIe 体系结构的起始点,负责管理和控制整个 PCIe 总线系统。它通常集成在主板的芯片组或 CPU 中,包括以下功能:

    1. 连接 CPU 和内存:根复合体通常直接连接 CPU 和内存,负责处理从 PCIe 设备到 CPU 的请求和数据传输。
    2. PCIe 总线管理:根复合体初始化和配置 PCIe 总线,分配地址空间,管理设备枚举和资源分配。
    3. 生成根端口:根复合体生成一个或多个根端口(Root Ports),每个根端口连接到一个 PCIe 设备或 PCIe 交换机,扩展 PCIe 总线。
    4. 中断处理:根复合体处理从 PCIe 设备发送到 CPU 的中断请求。

    根端口(Root Port)是根复合体的一部分,负责连接具体的 PCIe 设备或 PCIe 交换机。每个根端口提供一个 PCIe 链路,具有以下功能:

    1. 连接 PCIe 设备:根端口通过 PCIe 链路直接连接到 PCIe 设备(如显卡、网络卡、存储设备)或 PCIe 交换机(用于连接更多的 PCIe 设备)。
    2. 链路初始化和管理:根端口负责 PCIe 链路的初始化、配置和带宽分配,确保数据可以高效传输。
    3. 热插拔支持:根端口支持热插拔功能,允许在系统运行时插拔 PCIe 设备。
    4. 中断信号传递:根端口传递 PCIe 设备生成的中断信号到根复合体,由根复合体进一步处理和传递给 CPU。

    关系图示:

    CPU / 内存
       |
    根复合体(Root Complex)
       |
       +-- 根端口1(Root Port 1)-- PCIe设备1(例如显卡)
       |
       +-- 根端口2(Root Port 2)-- PCIe设备2(例如网络卡)
       |
       +-- 根端口3(Root Port 3)-- PCIe交换机
                                   |
                                   +-- PCIe设备3(例如SSD)
                                   |
                                   +-- PCIe设备4(例如额外的网络卡)
    
  • -device x3130-upstream,id=upstream1,bus=rp1

    使用 x3130-upstream 设备模型来模拟 PCIe 上行端口,ID 为 upstream1,连接到根端口 rp1。

  • -device xio3130-downstream,id=downstream1,bus=upstream1,chassis=2,slot=0

    使用 xio3130-downstream 设备模型来模拟 PCIe 下行端口,ID 为 downstream1,连接到上行端口 upstream1,机箱号为 2,插槽为 0。

  • -device xio3130-downstream,id=downstream2,bus=upstream1,chassis=3,slot=1

    使用 xio3130-downstream 设备模型来模拟另一个 PCIe 下行端口,ID 为 downstream2,连接到上行端口 upstream1,机箱号为 3,插槽为 1。

  • -drive file=nvme2.qcow2,if=none,id=nvme2

    指定第二个 NVMe 设备的磁盘文件为 nvme2.qcow2,不使用默认接口,ID 为 nvme2

  • -device nvme,drive=nvme2,serial=0001,cmb_size_mb=2048,bus=downstream1

    添加第二个 NVMe 设备,连接到 downstream1 下行端口,CMD 大小为 2048 MB,序列号为 0001

  • -drive file=nvme3.qcow2,if=none,id=nvme3

    指定第三个 NVMe 设备的磁盘文件为 nvme3.qcow2,不使用默认接口,ID 为 nvme3

  • -device nvme,drive=nvme3,serial=0002,cmb_size_mb=2048,bus=downstream2

    添加第三个 NVMe 设备,连接到 downstream2 下行端口,CMD 大小为 2048 MB,序列号为 0002

首先我们在虚拟机中使用 lspci 查看当前的 PCIe 设备:

00:00.0 Host bridge: Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller
00:01.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:02.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
00:03.0 Non-Volatile memory controller: Red Hat, Inc. QEMU NVM Express Controller (rev 02)
00:04.0 PCI bridge: Red Hat, Inc. QEMU PCIe Root port
00:1f.0 ISA bridge: Intel Corporation 82801IB (ICH9) LPC Interface Controller (rev 02)
00:1f.2 SATA controller: Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)
00:1f.3 SMBus: Intel Corporation 82801I (ICH9 Family) SMBus Controller (rev 02)
01:00.0 PCI bridge: Texas Instruments XIO3130 PCI Express Switch (Upstream) (rev 02)
02:00.0 PCI bridge: Texas Instruments XIO3130 PCI Express Switch (Downstream) (rev 01)
02:01.0 PCI bridge: Texas Instruments XIO3130 PCI Express Switch (Downstream) (rev 01)
03:00.0 Non-Volatile memory controller: Red Hat, Inc. QEMU NVM Express Controller (rev 02)
04:00.0 Non-Volatile memory controller: Red Hat, Inc. QEMU NVM Express Controller (rev 02)

然后使用 lspci -t 查看当前的 PCIe 拓扑:

-[0000:00]-+-00.0
           +-01.0
           +-02.0
           +-03.0
           +-04.0-[01-04]----00.0-[02-04]--+-00.0-[03]----00.0
           |                               \-01.0-[04]----00.0
           +-1f.0
           +-1f.2
           \-1f.3

可以得到 PCIe 拓扑图

PCIe 拓扑图2

此时在 P2PDMA 的概念中,NVMe 2 设备到 NVMe 3 设备的距离是 4(NVMe 2 —— Downstream 1 —— Upstream —— Downstream 2 —— NVMe 3)

P2PDMA

P2PDMA 使用 PCIe 端点的内存,比如 NVMe 的 CMB 和 PCIe BAR。

需要一个支持 P2P 的 RC 或者 switch。

结构示意图参见 PDF 第 4 页。

Linux kernel 的 P2PDMA 框架会比 SPDK 更加通用,支持任何 PCIe 设备(贡献 P2PDMA 内存或使用 P2PDMA 内存来做 DMA)

RC 相比 switch 的不足:

  • 性能可能会下降。许多 RC 在路由 P2P 流量方面效率低下。
  • 它可能根本不起作用。许多 RC 阻止 P2P 流量。Linux 内核现在维护一个工作根端口的白名单。(注意:当前 QEMU 配置可能不符合白名单要求)

p2pmem-test 表明还需要关闭 IOMMU 和 PCIe Access Control Services。

具体的测试配置可见 https://github.com/sbates130272/linux-p2pmem/issues/4

更换内核

以下命令在虚拟机中运行,可以选择 ssh 连接虚拟机,使用起来更方便。

# (可选)换 ustc 源
sudo sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
sudo sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
sudo sed -i 's/http:/https:/g' /etc/apt/sources.list

# 更新索引
sudo apt update

# 安装编译所需依赖
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev dwarves bc

内核版本选择:经查询,Userspace P2PDMA 最初由 6.2 内核版本引入。这里选择目前最新的长期支持的内核版本 6.6.43。(2024 年 8 月)

# 获取内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.43.tar.xz
tar xf linux-6.6.43.tar.xz
cd linux-6.6.43

内核配置:

make menuconfig

寻找 Device Drivers –> PCI support –> PCI peer-to-peer transfer support,使用 y 进行选中。

更新内核:

# 处理证书问题,在编译时遇到证书问题输入两次 enter 即可继续
scripts/config --disable SYSTEM_TRUSTED_KEYS
scripts/config --disable SYSTEM_REVOCATION_KEYS

# 编译内核和模块
make -j$(nproc)

# 安装模块
sudo make modules_install

# 安装内核
sudo make install

# 更新 GRUB 引导加载程序
sudo update-grub

# 重启
sudo reboot

默认情况下已经更换为最新的内核。如果没有更换,则在 GRUB 引导界面(在开机过程中按下 Esc 键)选择 Advanced options for Ubuntu,手动选择对应的内核版本启动即可。

测试 NVMe 磁盘性能

可以使用 fio 先对 NVMe 虚拟设备进行性能测试

直接使用 apt 进行安装

sudo apt install fio

创建配置文件 nvme.fio 并写入以下内容

[global]
bs=512M
size=10G
direct=1
ioengine=libaio
iodepth=1
numjobs=1

[read1]
filename=/dev/disk/by-path/pci-0000:03:00.0-nvme-1
rw=read
stonewall

[write1]
filename=/dev/disk/by-path/pci-0000:03:00.0-nvme-1
rw=write
stonewall

[read2]
filename=/dev/disk/by-path/pci-0000:04:00.0-nvme-1
rw=read
stonewall

[write2]
filename=/dev/disk/by-path/pci-0000:04:00.0-nvme-1
rw=write
stonewall

进行测试

sudo fio nvme.fio

可以多运行几次找一个稳定的结果,可以看到顺序读取速度在 1600 MB/s 左右,顺序写入速度在 1300 MB/s 左右。

Run status group 0 (all jobs):
   READ: bw=1530MiB/s (1604MB/s), 1530MiB/s-1530MiB/s (1604MB/s-1604MB/s), io=9216MiB (9664MB), run=6024-6024msec

Run status group 1 (all jobs):
  WRITE: bw=1173MiB/s (1230MB/s), 1173MiB/s-1173MiB/s (1230MB/s-1230MB/s), io=9216MiB (9664MB), run=7859-7859msec

Run status group 2 (all jobs):
   READ: bw=1502MiB/s (1575MB/s), 1502MiB/s-1502MiB/s (1575MB/s-1575MB/s), io=9216MiB (9664MB), run=6135-6135msec

Run status group 3 (all jobs):
  WRITE: bw=1291MiB/s (1354MB/s), 1291MiB/s-1291MiB/s (1354MB/s-1354MB/s), io=9216MiB (9664MB), run=7139-7139msec

使用 p2pmem-test 测试 P2PDMA

检查系统日志,可以看到三个 NVMe 设备都提供了 1GB 的 P2P 内存。

$ sudo dmesg | grep "peer-to-peer"
[    3.920611] nvme 0000:03:00.0: added peer-to-peer DMA memory 0x580000000-0x5ffffffff
[    3.927298] nvme 0000:04:00.0: added peer-to-peer DMA memory 0x500000000-0x57fffffff
[    3.934847] nvme 0000:00:03.0: added peer-to-peer DMA memory 0x600000000-0x67fffffff

检查 sysfs

$ cd /sys/bus/pci/devices/0000:03:00.0/p2pmem
$ sudo cat size
2147483648
$ sudo cat available
2146959360
$ sudo cat published
1

可以看到一系列有效输出,size 的值是 2147483648,即 2G,available 的值略小于 size,published 的值为 1。

安装 p2pmem-test

git clone https://github.com/sbates130272/p2pmem-test.git ~/p2pmem-test
cd ~/p2pmem-test
git submodule update --init
make
sudo make install

由于设备名称是按照先到先得的原则分配的,因此这里不使用 /dev/nvme0n1 这样的文件,而是使用更可靠的符号链接,即 /dev/disk/by-path/pci-0000:03:00.0-nvme-1

$ ll /dev/disk/by-path
total 0
drwxr-xr-x 2 root root 260 Aug  3 02:31 ./
drwxr-xr-x 6 root root 120 Aug  3 02:31 ../
lrwxrwxrwx 1 root root  13 Aug  3 02:31 pci-0000:00:03.0-nvme-1 -> ../../nvme0n1
lrwxrwxrwx 1 root root   9 Aug  3 02:31 pci-0000:00:1f.2-ata-1 -> ../../sda
lrwxrwxrwx 1 root root   9 Aug  3 02:31 pci-0000:00:1f.2-ata-1.0 -> ../../sda
lrwxrwxrwx 1 root root  10 Aug  3 02:31 pci-0000:00:1f.2-ata-1.0-part1 -> ../../sda1
lrwxrwxrwx 1 root root  10 Aug  3 02:31 pci-0000:00:1f.2-ata-1.0-part2 -> ../../sda2
lrwxrwxrwx 1 root root  10 Aug  3 02:31 pci-0000:00:1f.2-ata-1-part1 -> ../../sda1
lrwxrwxrwx 1 root root  10 Aug  3 02:31 pci-0000:00:1f.2-ata-1-part2 -> ../../sda2
lrwxrwxrwx 1 root root   9 Aug  3 02:31 pci-0000:00:1f.2-ata-3 -> ../../sr0
lrwxrwxrwx 1 root root   9 Aug  3 02:31 pci-0000:00:1f.2-ata-3.0 -> ../../sr0
lrwxrwxrwx 1 root root  13 Aug  3 02:31 pci-0000:03:00.0-nvme-1 -> ../../nvme2n1
lrwxrwxrwx 1 root root  13 Aug  3 02:31 pci-0000:04:00.0-nvme-1 -> ../../nvme1n1

使用 p2pmem-test 测试位于同一 switch 下的 P2PDMA

sudo p2pmem-test \
    /dev/disk/by-path/pci-0000:03:00.0-nvme-1 \
    /dev/disk/by-path/pci-0000:04:00.0-nvme-1 \
    /sys/bus/pci/devices/0000:03:00.0/p2pmem/allocate \
    -c 10000 -s 4k --check

测试结果如下:

Running p2pmem-test: reading /dev/disk/by-path/pci-0000:03:00.0-nvme-1 (10.74GB): writing /dev/disk/by-path/pci-0000:04:00.0-nvme-1 (10.74GB): p2pmem buffer /sys/bus/pci/devices/0000:03:00.0/p2pmem/allocate.
        chunk size = 4096 : number of chunks =  10000: total = 40.96MB : thread(s) = 1 : overlap = OFF.
        skip-read = OFF : skip-write =  OFF : duration = INF sec.
        buffer = 0x7f099eee1000 (p2pmem): mmap = 4.096kB
        PAGE_SIZE = 4096B
        checking data with seed = 1722653174
MATCH on data check, 0x75b90761 = 0x75b90761.
Transfer:
 40.96MB in 3.9    s    10.48MB/s

再验证 nvme0n1 是否可以参与到 P2PDMA 中

sudo p2pmem-test \
    /dev/disk/by-path/pci-0000:00:03.0-nvme-1 \
    /dev/disk/by-path/pci-0000:04:00.0-nvme-1 \
    /sys/bus/pci/devices/0000:00:03.0/p2pmem/allocate \
    -c 10000 -s 4k --check

结果是 Remote I/O error,查看系统日志得到:

[  935.223304] nvme 0000:04:00.0: cannot be used for peer-to-peer DMA as the client and provider (0000:00:03.0) do not share an upstream bridge or whitelisted host bridge
[  935.223315] critical target error, dev nvme1n1, sector 0 op 0x1:(WRITE) flags 0x8800 phys_seg 1 prio class 2

这里明确指出参与 P2PDMA 的设备需要在同一个 switch 下或者在白名单内的主桥下。而当前 QEMU 的配置不符合后者,因此发生了错误。

使用自己代码进行测试

由于 p2pmem-test 的结果比较出人意料,因此按照 p2pmem-test 的想法,自己编写代码进行测试。

首先往 /dev/disk/by-path/pci-0000:03:00.0-nvme-1 中填充随机数据,作为源数据

sudo dd if=/dev/urandom of=/dev/disk/by-path/pci-0000:03:00.0-nvme-1 bs=1M status=progress

运行 non-p2pdma.c 测试经过主存的数据传输速度

curl -O https://jklincn.com/posts/wsl-qemu-p2pdma/non-p2pdma.c
gcc -D_GNU_SOURCE -o non-p2pdma non-p2pdma.c
sudo ./non-p2pdma

第一次运行时会受到缓存等机制的影响(尽管已经设置了 O_DIRECT | O_SYNC 标志),建议多运行几次观察稳定的数据,大概在 700MB/s 至 900MB/s。

Chunk Size: 0x20000000 bytes, Total Size: 0x280000000 bytes
Chunk 1: Read time 0.318560 seconds, Write time 0.386249 seconds
Chunk 2: Read time 0.247340 seconds, Write time 0.418205 seconds
Chunk 3: Read time 0.253630 seconds, Write time 0.375333 seconds
Chunk 4: Read time 0.216541 seconds, Write time 0.439899 seconds
Chunk 5: Read time 0.177537 seconds, Write time 0.390869 seconds
Chunk 6: Read time 0.172893 seconds, Write time 0.413571 seconds
Chunk 7: Read time 0.235481 seconds, Write time 0.416622 seconds
Chunk 8: Read time 0.254489 seconds, Write time 0.430132 seconds
Chunk 9: Read time 0.237011 seconds, Write time 0.411117 seconds
Chunk 10: Read time 0.272340 seconds, Write time 0.404093 seconds
Chunk 11: Read time 0.247284 seconds, Write time 0.402645 seconds
Chunk 12: Read time 0.229687 seconds, Write time 0.452526 seconds
Chunk 13: Read time 0.275223 seconds, Write time 0.397991 seconds
Chunk 14: Read time 0.242650 seconds, Write time 0.413598 seconds
Chunk 15: Read time 0.230669 seconds, Write time 0.412335 seconds
Chunk 16: Read time 0.256452 seconds, Write time 0.469918 seconds
Chunk 17: Read time 0.287616 seconds, Write time 0.407811 seconds
Chunk 18: Read time 0.220717 seconds, Write time 0.400929 seconds
Chunk 19: Read time 0.252354 seconds, Write time 0.371079 seconds
Chunk 20: Read time 0.328883 seconds, Write time 0.343349 seconds
Total read time: 4.957357 seconds
Total write time: 8.158271 seconds
Total transfer time: 13.115628 seconds
Average transfer speed: 780.75 MB/s
Data verification successful. All data matches.

运行 p2pdma.c 测试经过主存的数据传输速度

# p2pdma.c 文件已丢失,可以参考运行结果
gcc -D_GNU_SOURCE -o p2pdma p2pdma.c
sudo ./p2pdma

结果也是惨不忍睹,从 CMB 往硬盘中传输速度非常慢

Chunk Size: 0x20000000 bytes, Total Size: 0x280000000 bytes
Chunk 1: Read time 0.217780 seconds, Write time 19.552946 seconds
Chunk 2: Read time 0.185211 seconds, Write time 20.803130 seconds
Chunk 3: Read time 0.189455 seconds, Write time 16.590718 seconds
Chunk 4: Read time 0.187290 seconds, Write time 19.911669 seconds
Chunk 5: Read time 0.191639 seconds, Write time 24.069332 seconds
Chunk 6: Read time 0.197151 seconds, Write time 23.308786 seconds
Chunk 7: Read time 0.192423 seconds, Write time 21.597719 seconds
Chunk 8: Read time 0.203692 seconds, Write time 16.149544 seconds
Chunk 9: Read time 0.207263 seconds, Write time 18.774473 seconds
Chunk 10: Read time 0.222040 seconds, Write time 25.703661 seconds
Chunk 11: Read time 0.420848 seconds, Write time 26.265945 seconds
Chunk 12: Read time 0.352677 seconds, Write time 20.080822 seconds
Chunk 13: Read time 0.231593 seconds, Write time 19.043215 seconds
Chunk 14: Read time 0.209558 seconds, Write time 17.120326 seconds
Chunk 15: Read time 0.222976 seconds, Write time 20.582259 seconds
Chunk 16: Read time 0.256375 seconds, Write time 22.948815 seconds
Chunk 17: Read time 0.192696 seconds, Write time 22.370362 seconds
Chunk 18: Read time 0.189832 seconds, Write time 23.045267 seconds
Chunk 19: Read time 0.210956 seconds, Write time 20.755989 seconds
Chunk 20: Read time 0.235485 seconds, Write time 20.635251 seconds
Total read time: 4.516940 seconds
Total write time: 419.310229 seconds
Total transfer time: 423.827169 seconds
Average transfer speed: 24.16 MB/s
Data verification successful. All data matches.

寻找 P2PDMA 问题

使用 perf 找到瓶颈代码

编译 perf,源代码在我们之前下载的内核代码中

sudo apt install libzstd1 libdwarf-dev libdw-dev binutils-dev libcap-dev libelf-dev libnuma-dev python3 python3-dev python-setuptools libssl-dev libunwind-dev libdwarf-dev zlib1g-dev liblzma-dev libaio-dev libtraceevent-dev debuginfod libpfm4-dev libslang2-dev systemtap-sdt-dev libperl-dev binutils-dev libbabeltrace-dev libiberty-dev libzstd-dev pkg-config

cd ~/linux-6.6.43/tools/perf
make -j$(nproc)
sudo cp perf /usr/bin

重新编译一下测试程序,保留符号表并且不进行优化处理

gcc -D_GNU_SOURCE -g -O0 -o p2pdma p2pdma.c

使用 perf 工具运行 p2pdma,并保存结果

sudo perf record -o perf_p2pdma.data -- ./p2pdma

分析结果

sudo perf report -i perf_p2pdma.data

可以看到大部分的时间花费在 nvme_queue_rqs 函数中

查看反汇编代码

sudo perf annotate -i perf_p2pdma.data nvme_queue_rqs

可以看到绝大部分时间在 nvme_submit_cmds 函数的一系列 mov 指令中,说明确实是访存瓶颈。

mov 0x8(%rbx), %rdx
mov 0x10(%rbx), %rdx
mov 0x18(%rbx), %rdx
mov 0x20(%rbx), %rdx
mov 0x28(%rbx), %rdx
mov 0x30(%rbx), %rdx
mov 0x38(%rbx), %rdx

如果感兴趣也可以对 non-p2pdma 进行相同的处理,进行比较。

使用 blktrace

待补充

分析代码

再来看一下 nvme_queue_rqs 函数,先在根目录下使用 grep 找到其位置 ,即 drivers/nvme/host/pci.c:932

$ grep -rnw drivers/nvme/host/ -e nvme_queue_rqs
drivers/nvme/host/pci.c:932:static void nvme_queue_rqs(struct request **rqlist)
drivers/nvme/host/pci.c:1674:    .queue_rqs      = nvme_queue_rqs,
drivers/nvme/host/pci.o: binary file matches
drivers/nvme/host/nvme.o: binary file matches
gdrivers/nvme/host/nvme.ko: binary file matches

后续待补充


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。