在 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 拓扑图(省略了具体型号)
添加 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 中,包括以下功能:
- 连接 CPU 和内存:根复合体通常直接连接 CPU 和内存,负责处理从 PCIe 设备到 CPU 的请求和数据传输。
- PCIe 总线管理:根复合体初始化和配置 PCIe 总线,分配地址空间,管理设备枚举和资源分配。
- 生成根端口:根复合体生成一个或多个根端口(Root Ports),每个根端口连接到一个 PCIe 设备或 PCIe 交换机,扩展 PCIe 总线。
- 中断处理:根复合体处理从 PCIe 设备发送到 CPU 的中断请求。
根端口(Root Port)是根复合体的一部分,负责连接具体的 PCIe 设备或 PCIe 交换机。每个根端口提供一个 PCIe 链路,具有以下功能:
- 连接 PCIe 设备:根端口通过 PCIe 链路直接连接到 PCIe 设备(如显卡、网络卡、存储设备)或 PCIe 交换机(用于连接更多的 PCIe 设备)。
- 链路初始化和管理:根端口负责 PCIe 链路的初始化、配置和带宽分配,确保数据可以高效传输。
- 热插拔支持:根端口支持热插拔功能,允许在系统运行时插拔 PCIe 设备。
- 中断信号传递:根端口传递 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 拓扑图
此时在 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
后续待补充
本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。