lisaac/blog

PVE 记录 -- 高权限 LXC 容器 及 虚拟机显卡直通 beta

Opened this issue · 0 comments

高权限 LXC 容器配置 --- 只运行 docker 的容器

创建一个基于CT 模板为alpine 的 LXC 容器,启动之后需要修改源和安装 docker

https://mirrors.ustc.edu.cn/alpine/latest-stable/main
https://mirrors.ustc.edu.cn/alpine/latest-stable/community
#https://mirrors.ustc.edu.cn/alpine/edge/main
#https://mirrors.ustc.edu.cn/alpine/edge/community
apk update
apk add docker docker-compose

块设备

将设备挂载入容器的前提,必须对 cgroup2 进行设置,lxc.cgroup2.devices.allow: b *:* rwm,b 表示块设备,设备号可以使用 ls -l /dev 或者 lsblk 查看,: 表示所有设备号

lxc.cgroup2.devices.allow: b *:* rwm
lxc.cgroup2.devices.allow: c *:* rwm
lxc.mount.entry: /dev/sda dev/sda none bind,create=file # 对单个设备挂载入lxc容器

创建设备文件也可以使用 autodev hook 实现,这个脚本实现了所有块设备的创建。

lxc.autodev: 1
lxc.hook.autodev: sh -c 'cat /proc/partitions | awk '"'"'{if ($1~/[0-9]+/) system("mknod -m 777 ${LXC_ROOTFS_MOUNT}/dev/"$4" b "$1" "$2)}'"'"''

后续,容器使用过程中,例如硬盘重新分区,usb 等热插拔设备,想让容器能够获取到设备最新状态,可以运行下列命令:

cat /proc/partitions | awk '{if ($1~/[0-9]+/) system("rm ${LXC_ROOTFS_MOUNT}/dev/"$4" ;; mknod -m 777 ${LXC_ROOTFS_MOUNT}/dev/"$4" b "$1" "$2)}'

字符设备

同样的,与块设备一样,只需对 cgroup2 进行设置 lxc.cgroup2.devices.allow: c *:* rwm,c 表示字符设备,设备号可以使用 ls -l /dev查看,: 表示所有设备号

lxc.cgroup2.devices.allow: c *:* rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file # 将tun设备挂入 lxc 容器, 可以实现 tailscale/wireguard 等
lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file  
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file #intel 核显设备

*所有设备

** 可能会出现意想不到的问题 **

再进一步,如果要对所有设备进行直通,可以使用,lxc.cgroup2.devices.alllow: a 表示允许所有设备,但是如何对所有文件挂载进入 lxc 容器的,同时,如果设备文件发生变化,例如硬盘重新分区,usb 等热插拔设备,需要让容器能够获取到,就需要一些小小的魔法,这里将 /dev 目录挂载为 overlayfs 并挂入容器内部,这样,lxc 容器就能直接使用 /dev 内的内容了。

lxc.cgroup2.devices.alllow: a
lxc.hook.pre-mount: sh -c "umount /tmp/.ct0_dev/merge 2&> /dev/null; rm -fr /tmp/.ct0_dev; mkdir -p /tmp/.ct0_dev/upper /tmp/.ct0_dev/work /tmp/.ct0_dev/merge; mount -t overlay overlay -o lowerdir=/dev,upperdir=/tmp/.ct0_dev/upper,workdir=/tmp/.ct0_dev/work /tmp/.ct0_dev/merge"  # 在/tmp/.ct0_dev/merge 创建 /dev 对 overlayfs
lxc.mount.entry: /tmp/.ct0_dev/merge dev none bind,create=dir # 挂载入容器
lxc.hook.post-stop: sh -c "umount /tmp/.ct0_dev/merge 2&> /dev/null; rm -fr /tmp/.ct0_dev" # 在关闭容器的时候销毁 overlayfs

网卡直通

默认情况下,pve 会给容器通过 veth 网桥来分配虚拟网卡,如果 pve 主机有多个网卡,可以将网卡直通入 lxc:

lxc.net.0.type: phys
lxc.net.0.link: enp1s0
lxc.net.0.name: eth0
lxc.net.1.ipv4.address: 10.1.1.222
lxc.net.1.ipv4.gateway: 10.1.1.1
lxc.net.1.flags: up

macvlan网卡

只有一张网卡的情况,使用 macvlan 会比 veth 带来更好的性能表现:

xc.net.1.type: macvlan
lxc.net.1.link: vmbr0
lxc.net.1.name: eth0
lxc.net.1.ipv4.address: 10.1.1.222
lxc.net.1.ipv4.gateway: 10.1.1.1
lxc.net.1.flags: up

将 /volumes 及 fstab 挂入容器

挂载 /volumes 以及 fstab 的目的是,计划通过编辑容器内的 /tmp/fstab 来实现PVE宿主开机自动挂载到 /volumes 目录中,使用 rbind参数使其,具有挂载传播性,即:宿主机挂载硬盘,在容器中也能看到。

lxc.mount.entry: /volumes volumes none rbind,create=dir
lxc.mount.entry: /etc/fstab tmp/fstab none bind,create=file

创建挂载的目录,同时为了保护 /meida 目录下的 disk* 目录,可以使用chattr修改权限

mkdir -p /volumes/disk{0..9}
chattr +i /volumes/disk{0..9}

挂载硬盘

使用fstab

创建完目录之后,可以挂载硬盘,当然如果是新硬盘,要分区格式化

mount /dev/sdXn /volumes/disk* # 根据自己情况修改

挂载完成所有硬盘之后,可以将相关挂载信息写入fstab,以便下次重启,自动挂载,这里写了一个小脚本来帮助填写

#!/bin/bash
MEDIA=/volumes
# 获取所有分区设备
partitions=$(lsblk -o UUID,MOUNTPOINT -n -l | grep -E "\s+${MEDIA}/\w+$")
# 遍历每个分区
while IFS= read -r partition; do
    # 提取分区名称、UUID 和挂载点
    uuid=$(echo $partition | awk '{print $1}')
    mountpoint=$(echo $partition | awk '{print $2}')
    # 添加分区挂载信息到 fstab
    echo "UUID=$uuid $mountpoint auto defaults 0 0"
done <<< "$partitions"

执行之后,会输出相应的配置,只要追加复制到 /etc/fstab 就行了

使用脚本

考虑到写入 fstab后,如果硬盘出现问题或者硬盘拔出后,会在启动时报错,所以考虑在容器启动时执行用脚本挂载操作。
而使用 mp0=/dev/sdaX...则会导致每次重启容器都需要卸载和挂载,带来不必要的操作。
同样创建完目录之后,可以挂载硬盘,当然如果是新硬盘,要分区格式化

mount /dev/sdXn /volumes/disk* # 根据自己情况修改
lsblk -o UUID,MOUNTPOINT -n -l | grep -E "\s+/volumes/\w+$" > /root/mttab # 创建uuid和挂载目录一一对应的文件

cat /root/mount.sh    # mount.sh 内容:
#!/bin/sh

DF=$(mount)
MEDIA="/volumes"
LSBLK=$(lsblk -o UUID,MOUNTPOINT -n -l | grep -E "\s+${MEDIA}/\w+$")
cat /root/mttab | while read line
do
  uuid=$(echo $line | awk '{print $1}')
  mountpoint=$(echo $line | awk '{print $2}')
  [ -n "$(echo ${LSBLK} | grep ${uuid})" ] || mount /dev/disk/by-uuid/$uuid $mountpoint
done

# 修改lxc 配置文件:
lxc.hook.pre-start: /root/mount.sh

挂载 proc sys cgroup tmpfs 等

lxc.mount.auto: proc:rw sys:rw cgroup-full:rw
lxc.mount.entry: tmp tmp tmpfs rw,nodev,relatime,mode=1777 0 0

权限设置

禁用 AppArmor 提升容器权限,lxc.cap.drop 空表示容器支持所有 Linux Capabilities 权限,进一步提升权限,可以使用 capsh --print 查看,可以使用smartctl 查看磁盘 S.M.A.R.T. 信息

lxc.apparmor.profile: unconfined
lxc.cap.drop:

使用部分宿主 namespaces

lxc 可以支持 clone/keep/share 等方式将宿主机的 namespaces 与容器共用。
可以使用 lxc.namespace.keep lxc.namespace.clone lxc.namespace.share.* 这三个参数进行设置,详情可以查看 man page。

但是 pve 的 pct 配置文件并不支持者三个参数,可以使用下面这个补丁来解除限制:

sed -i '/my $valid_lxc_conf_keys = {/a\    '"'"'lxc.namespace.keep'"'"' => 1,\n    '"'"'lxc.namespace.clone'"'"' => 1,\n    '"'"'lxc.namespace.share.*'"'"' => 1,' /usr/share/perl5/PVE/LXC/Config.pm
# 与宿主共享网络,相当于 docker 的 host 网络
lxc.namespace.keep: net

# 与共享宿主的 pid,相当于 docker 的 --pid=host,相当于 chroot,但是共享 pid 会导致容器中的 init 程序会无法启动,从而导致容器失败,解决办法是把容器的启动命令改成其他的,例如tini,存在的问题是,无法使用 console 进入,可以使用 pct enter 进入,我这里只直接配置了启动 dockerd:
lxc.namespace.keep:user pid net
lxc.init.cmd: env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /sbin/tini-static -sg -- dockerd

信号

lxc 默认的 stop 信号是 SIGKILL,这会倒是 tini 下面的子程序无法结束,改成SIGTERM,dockerd 的重载信号是 SIGHUP:

lxc.signal.stop: SIGTERM
lxc.signal.reboot: SIGHUP

STOP HOOK

为了保证容器停止的时候杀死容器相关的进程,例如:pct enter 101 进入容器后一些列操作,在容器关闭时,确保这些进程被杀死,添加一个停止钩子:

lxc.hook.stop: sh -c 'kill $(pgrep -f "lxc-attach -n ${LXC_NAME}") 2> /dev/null'

其他配置

  • 默认配置:可以在/usr/share/lxc/config/ 中找到 pve 自带模板的默认配置,如果要确定容器使用来哪个模板,可以在/var/lib/lxc/<CID>/config中的 include 中找到
  • seccomp默认配置: 同样在 /usr/share/lxc/config/common.seccomp 中配置

日志

lxc.log.level: 0
lxc.log.file: /tmp/lxc-100.log

整体配置文件

下载alpine 模板
第一次进入容器安装必要 docker
apk add docker docker-cli tini

# /etc/pve/lxc/100.conf
#net0: name=eth0,bridge=vmbr0,firewall=1,gw=10.1.1.1,hwaddr=BC:24:11:8B:5F:E4,ip=10.1.1.201/24,type=veth
#lxc.cgroup.cpu: /sys/fs/cgroup/cpu,cpuacct
#lxc.cgroup.memory: /sys/fs/cgroup/memory

cores: 20
memory: 81920
swap: 4096
#lxc.namespace.clone: mnt uts
arch: amd64
hostname: CT0
nameserver: 10.1.1.1
ostype: alpine
rootfs: local:101/vm-101-disk-0.raw,size=24G
timezone: Asia/Shanghai
lxc.signal.reboot: SIGHUP
lxc.signal.stop: SIGTERM
lxc.cgroup2.devices.allow: a
lxc.log.level: 0
lxc.log.file: /tmp/lxc-100.log
lxc.apparmor.profile: unconfined
lxc.cap.drop:
lxc.autodev: 1
lxc.hook.autodev: sh -c 'cat /proc/partitions | awk '"'"'{if ($1~/[0-9]+/) system("mknod -m 777 ${LXC_ROOTFS_MOUNT}/dev/"$4" b "$1" "$2)}'"'"''
lxc.mount.auto: proc:rw sys:rw cgroup-full:rw
lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none rbind,optional,create=file
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none rbind,optional,create=file
lxc.mount.entry: /dev/net/tun dev/net/tun none rbind,create=file
lxc.mount.entry: tmp tmp tmpfs rw,nodev,relatime,mode=1777 0 0
lxc.mount.entry: /etc/fstab tmp/fstab none rbind,create=file
lxc.mount.entry: /volumes volumes none rbind,create=dir
lxc.mount.entry: /sys/fs/bpf sys/fs/bpf none rbind,create=dir
lxc.hook.stop: sh -c 'kill $(pgrep -f "lxc-attach -n ${LXC_NAME}") 2> /dev/null'
lxc.seccomp.profile: /root/unconfined.seccomp
lxc.namespace.keep: user pid net ipc
lxc.init.cmd: env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /sbin/tini-static -sg -- dockerd

中文终端 locales

pct start 100 # 启动容器
pct enter 100 # 进入容器
dpkg-reconfigure locales #添加 zh.CN_UTF8

PVE 虚拟机

独显直通

bios 相关设置

  • 启用 VT-d
  • 禁用 CSM (启用CSM才能提取显卡bios rom)
  • ACS Enable # 如果存在,设置为已启用(自动不起作用)
  • 启用 4G解码 4G Decoding
  • 禁用 Resizable BAR/Smart Access Memory智能访问内存 #(如果启用,AMD GPUS(Vega 及更高版本)会遇到“代码 43 错误”)
  • 启用 IOMMU # 如果存在,主要用于 AMD 主板
  • 将主显示器设置为 CPU/iGPU # 如果您的 CPU 有 iGPU
  • 预分配内存为 64M
  • 核显设置为Enabled,同时设置主显卡为igfx(有独先情况下,在pve下禁用i915驱动,这样才能使用独显输出pve终端画面。如果不配置igfx,在有些主板上会无法使用核显)

内核启动参数

vi /etc/default/grub
# 修改 cmdline 如果是AMD处理器, 将intel_iommu改为iommu
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt initcall_blacklist=sysfb_init pci=assign-busses pcie_acs_override=downstream,multifunction"

相关参数含义

References

https://www.jinbuguo.com/kernel/boot_parameters.html
  • iommu=pt:启用 Intel VT-d 或 AMD-Vi 的 IOMMU。这是一种硬件功能,用于管理设备对系统内存的访问。在虚拟化环境中,启用 IOMMU 后,可以将物理设备直通到虚拟机中,以便虚拟机可以直接访问硬件设备。“iommu=pt”不是必须的,PT模式只在必要的时候开启设备的IOMMU转换,可以提高未直通设备PCIe的性能,建议添加。
  • initcall_blacklist=sysfb_init:禁用 sysfb_init 内核初始化函数。这个函数通常用于在内核启动过程中初始化系统帧缓冲。在使用 GPU 直通的情况下,这个函数可能会干扰直通操作,因此需要禁用它。
  • initcall_blacklist=sysfb_init:屏蔽掉pve7.2以上的一个bug,方便启动时候就屏蔽核显等设备驱动;
  • pcie_acs_override=downstream,multifunction:便于iommu每个设备单独分组,以免直通导致物理机卡死等问题
  • pci=nommconf:意思是禁用pci配置空间的内存映射,所有的 PCI 设备都有一个描述该设备的区域(您可以看到lspci -vv),访问该区域的最初方法是通过 I/O 端口,而 PCIe 允许将此空间映射到内存以便更简单地访问。
  • i915.enable_gvt=1:启用 Intel GVT-g 虚拟 GPU 技术。这个选项用于创建一个虚拟的 Intel GPU 设备,以便多个虚拟机可以共享物理 GPU 设备。启用 GVT-g 需要在支持虚拟 GPU 的 Intel CPU 和主板上运行,并且需要正确配置内核和虚拟机。想开启GVT-g的就添加这条,需要注意的是,11代之后的已经不支持,转为sr-iov

* 启动 VFIO 模块 (非必需,虚拟机直通时会自动加载相关模块)

打开 /etc/modules ,追加添加:

vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd

修改 vfio 配置文件,启用 audio

lspci -nn | grep AMD
01:00.0 PCI bridge [0604]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 10 XL Upstream Port of PCI Express Switch [1002:1478] (rev c7)
02:00.0 PCI bridge [0604]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 10 XL Downstream Port of PCI Express Switch [1002:1479]
03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 23 [Radeon RX 6600/6600 XT/6600M] [1002:73ff] (rev c7)
03:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:ab28]

根据返回结果,得到 id, [1002:73ff],[1002:ab28]

echo "options vfio-pci ids=1002:73ff,1002:ab28" > /etc/modprobe.d/vfio.conf

更新 grub 和 initramfs

update-grub
update-initramfs -u -k all

核显直通

intel gen11+ 核显 sriov

https://github.com/strongtz/i915-sriov-dkms
更新内核后记得重新安装 dkms 内核模块

使用 displaylink 作为显示器

将 displaylink usb 端口通入虚拟机,win11 会自动安装驱动。这样 sriov 核显就可以输出画面了

References

https://linuxcontainers.org/lxc/manpages/man5/lxc.container.conf.5.html
https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pct_settings
https://github.com/strongtz/i915-sriov-dkms