跳转至

software

在 libvirt 中运行 RISC-V 虚拟机

背景

我在 libvirt 中跑了几个 KVM 加速的虚拟机,然后突发奇想,既然 libvirt 背后是 qemu,然后 qemu 是支持跨指令集的,那是否可以让 libvirt 来运行 RISC-V 架构的虚拟机?经过一番搜索,发现可以跑 ARM:How To: Running Fedora-ARM under QEMU,既然如此,我们也可以试试用 libvirt 来运行 RV64 虚拟机。

准备 rootfs

第一步是根据 Debian 的文档 Creating a riscv64 chroot 来创建 rootfs,然后再用 virt-make-fs 来打包。

首先是用 mmdebstrap 来生成一个 chroot:

$ sudo mkdir -p /tmp/riscv64-chroot
$ sudo apt install mmdebstrap qemu-user-static binfmt-support debian-ports-archive-keyring
$ sudo mmdebstrap --architectures=riscv64 --include="debian-ports-archive-keyring" sid /tmp/riscv64-chroot "deb http://deb.debian.org/debian-ports sid main" "deb http://deb.debian.org/debian-ports unreleased main"

进入 chroot 以后,进行一些配置:

$ sudo chroot /tmp/riscv64-chroot
$ apt update
$ apt install linux-image-riscv64 u-boot-menu vim
# set root password
$ passwd

然后修改 /etc/default/u-boot 文件,添加如下的配置:

# change ro to rw, set root device
U_BOOT_PARAMETERS="rw noquiet root=/dev/vda1"
# fdt is provided by qemu
U_BOOT_FDT_DIR="noexist"

然后运行 u-boot-update 生成配置文件 /boot/extlinux/extlinux.conf

到这里,rootfs 已经准备完毕。

尝试在 QEMU 中启动

接下来,可以参考 Setting up a riscv64 virtual machine 先启动一个 qemu 来测试一下是否可以正常工作:

首先制作一个 qcow2 格式的镜像:

$ sudo virt-make-fs --partition=gpt --type=ext4 --size=+10G --format=qcow2 /tmp/riscv64-chroot rootfs.qcow2
$ qemu-img info rootfs.qcow2
image: rootfs.qcow2
file format: qcow2
virtual size: 11.4 GiB (12231971328 bytes)
disk size: 1.33 GiB
cluster_size: 65536
Format specific information:
    compat: 1.1
    compression type: zlib
    lazy refcounts: false
    refcount bits: 16
    corrupt: false
    extended l2: false

然后启动 qemu,配置好 OpenSBI 和 U-Boot 的路径:

$ sudo apt install qemu-system-misc opensbi u-boot-qemu
$ sudo qemu-system-riscv64 -nographic -machine virt -m 8G \
    -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
    -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
    -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-device,rng=rng0 \
    -append "console=ttyS0 rw root=/dev/vda1" \
    -device virtio-blk-device,drive=hd0 -drive file=rootfs.qcow2,format=qcow2,id=hd0 \
    -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::22222-:22

如果系统可以正常工作,看到下面的输出,下一步就可以配置 libvirt 了。

[    6.285024] Run /init as init process
Loading, please wait...
Starting version 251.1-1
[    7.743714] virtio_ring: module verification failed: signature and/or required key missing - tainting kernel
[    8.071762] virtio_blk virtio1: [vda] 23838189 512-byte logical blocks (12.2 GB/11.4 GiB)
[    8.181210]  vda: vda1
Begin: Loading essential drivers ... done.
Begin: Running /scripts/init-premount ... done.
Begin: Mounting root file system ... Begin: Running /scripts/local-top ... done.
Begin: Running /scripts/local-premount ... done.
Warning: fsck not present, so skipping root file system
[    9.003143] EXT4-fs (vda1): mounted filesystem with ordered data mode. Quota mode: none.
done.
Begin: Running /scripts/local-bottom ... done.
Begin: Running /scripts/init-bottom ... done.
[    9.754151] Not activating Mandatory Access Control as /sbin/tomoyo-init does not exist.
[    9.808860] random: fast init done
[   10.651361] systemd[1]: Inserted module 'autofs4'
[   10.735574] systemd[1]: systemd 251.1-1 running in system mode (+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY -P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified)
[   10.736902] systemd[1]: Detected architecture riscv64.

Welcome to Debian GNU/Linux bookworm/sid!

配置 libvirt

首先,打开 virt-manager,在向导中,可以在下拉菜单选择自定义的架构,选择 riscv64 和 virt,然后选择 Import existing disk image,找到刚刚创建的 qcow2 文件。

创建好以后,我们还不能直接启动,因为此时还没有配置 OpenSBI 和 U-Boot。由于 virt-aa-helper 会检查 OpenSBI 和 U-Boot 的路径,要求它们不能在 /usr/lib 路径下

/*
 * Don't allow access to special files or restricted paths such as /bin, /sbin,
 * /usr/bin, /usr/sbin and /etc. This is in an effort to prevent read/write
 * access to system files which could be used to elevate privileges. This is a
 * safety measure in case libvirtd is under a restrictive profile and is
 * subverted and trying to escape confinement.
 *
 * Note that we cannot exclude block devices because they are valid devices.
 * The TEMPLATE file can be adjusted to explicitly disallow these if needed.
 *
 * RETURN: -1 on error, 0 if ok, 1 if blocked
 */
    const char * const restricted[] = {
        "/bin/",
        "/etc/",
        "/lib",
        "/lost+found/",
        "/proc/",
        "/sbin/",
        "/selinux/",
        "/sys/",
        "/usr/bin/",
        "/usr/lib",
        "/usr/sbin/",
        "/usr/share/",
        "/usr/local/bin/",
        "/usr/local/etc/",
        "/usr/local/lib",
        "/usr/local/sbin/"
    };

所以,我手动把 U-Boot 和 OpenSBI 复制一份到 /var/lib 下:

$ sudo mkdir -p /var/lib/custom
$ cd /var/lib/custom
$ sudo cp -r /usr/lib/u-boot/qemu-riscv64_smode .
$ sudo cp -r /usr/lib/riscv64-linux-gnu .

此时,再去配置 libvirt 的 XML 配置文件:

  <os>
    <type arch='riscv64' machine='virt'>hvm</type>
    <loader type='rom'>/var/lib/custom/riscv64-linux-gnu/opensbi/generic/fw_jump.elf</loader>
    <kernel>/var/lib/custom/qemu-riscv64_smode/uboot.elf</kernel>
    <boot dev='hd'/>
  </os>

其余部分不用修改。在下面可以看到 virt-manager 已经设置好了 qemu-system-riscv64:

<devices>
  <emulator>/usr/bin/qemu-system-riscv64</emulator>
  <disk type='file' device='disk'>
    <driver name='qemu' type='qcow2'/>
    <source file='/path/to/rootfs.qcow2'/>
    <target dev='vda' bus='virtio'/>
    <address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
  </disk>

保存以后直接启动,就完成了在 libvirt 中运行 Debian RV64 虚拟机的目的。

LoongArch64 工具链构建

最近因为龙芯杯的原因,想自己搞个 LoongArch64 的交叉编译工具链试试,结果遇到了很多坑,最后终于算是搞出来了。

一开始是想搞一个 newlib 的工具链,比较简单,而且之前做过一个仓库:jiegec/riscv-toolchain,就是构建的 riscv64-unknown-elf 工具链,照着 riscv-gnu-toolchain 就可以了。不过研究发现,newlib 还不支持 loongarch,目前只有 glibc 支持,只好硬着头皮上了。

于是我就在 riscv-toolchain 的基础上搞 loongarch64-unknown-linux-gnu,也就是带 glibc 的工具链,结果发现遇到很多坑。首先编译 libgcc 的时候就找不到头文件,于是先要从 glibc 和 linux 安装头文件到 sysroot 里面,对于 sysroot 里面的头文件路径到底是 include 还是 usr/include 也折腾了半天。然后编译 libgcc 又各种出问题,最后折腾了半天,结果是 gcc stage1 和 glibc 都没问题,gcc stage2 会报链接错误,但是不管它也能用,可以编译出正常的程序,毕竟 libc 是好的。

于是转念一想,要不要试试 crosstool-ng。克隆了一份上游的版本,照着 riscv 的部分抄了一份变成了 loongarch,然后把 config 里面的 linux/glibc/gcc/binutils-gdb 都替换为 custom location,这样我就可以用上游的最新版本了。中途还遇到了 crosstool-ng 对 gcc 12/13 不兼容的 bug,还好下面有人提出了解决方法。这些都搞定以后,终于构建出了一个完整的 loongarch64-unknown-linux-gnu 工具链。仓库地址是 jiegec/ct-ng-loongarch64,需要配合添加了 LoongArch 的 jiegec/crosstool-ng loongarch 分支 使用。

最后得到的工具链各组件版本如下:

loongarch64-unknown-linux-gnu-gcc (crosstool-NG 1.25.0_rc2.1_7e21141) 13.0.0 20220502 (experimental)
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

GNU ld (crosstool-NG 1.25.0_rc2.1_7e21141) 2.38.50.20220502
Copyright (C) 2022 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.
GNU gdb (crosstool-NG 1.25.0_rc2.1_7e21141) 13.0.50.20220502-git
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

之后有时间的话,再把 qemu 和系统搞起来跑跑。

UPDATE: GCC 12.1 发布了,试了一下这个正式版本可以正确地编译。目前还需要使用 HEAD 版本的 binutils 和龙芯的 glibc 和 linux。

参考文档:

RISC-V Vector 1.0 工具链构建

不久前 RVV 1.0 标准终于是出来了,但是工具链的支持目前基本还处于刚 upstream 还没有 release 的状态。而目前 RVV 1.0 的支持主要在 LLVM 上比较活跃,因此也是采用 LLVM Clang + GCC Newlib Toolchain 的方式进行配合,前者做 RVV 1.0 的编译,后者提供 libc 等基础库。

UPDATE: LLVM 14 已经发布,这个版本已经支持 RVV 1.0,直接从 https://apt.llvm.org 等地安装 LLVM 14 即可。

LLVM Clang 直接采用 upstream 即可。编译选项:

$ cmake -G Ninja ../llvm -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_INSTALL_PREFIX=/prefix/llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_TARGETS_TO_BUILD="RISCV"
$ ninja
$ ninja install
$ /prefix/llvm/bin/clang --version
clang version 14.0.0 (https://github.com/llvm/llvm-project.git 8d298355ca3778a47fd6b3110aeee03ea5e8e02b)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /data/llvm/bin

还需要配合一个 GCC 工具链才可以完整地工作。可以直接采用 riscv-gnu-toolchain nightly 版本,比如 riscv64-elf-ubuntu-20.04-nightly-2022.01.17-nightly.tar.gz。下载以后解压,得到 riscv 目录,GCC 版本是比较新的:

$ ~/riscv/bin/riscv64-unknown-elf-gcc --version
riscv64-unknown-elf-gcc (GCC) 11.1.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

但是如果编译 C++ 程序,链接的时候就会报错:prefixed ISA extension must separate with _,这是因为 riscv-gnu-toolchain 仓库的 binutils 版本不够新,在 upstream 的 binutils 里面已经修复了这个问题。所以 clone 下来,然后编译,覆盖掉 riscv-gnu-toolchain 里面的 binutils:

UPDATE: binutils 2.38 已经发布,用这个版本即可。

$ ../configure --target=riscv64-unknown-elf --prefix=$HOME/riscv --disable-gdb --disable-sim --disable-libdecnumber --disable-readline
$ make
$ make install
$ ~/riscv/bin/riscv64-unknown-elf-ld --version
GNU ld (GNU Binutils) 2.38.50.20220125
Copyright (C) 2022 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

然后编译程序的时候,使用 clang,配置参数 --gcc-toolchain=~/riscv 即可让 clang 找到 GNU 工具链。这样就可以编译出来 RVV 1.0 的程序了:

$ /llvm/bin/clang --target=riscv64-unknown-elf -O2 -march=rv64gcv1p0 -menable-experimental-extensions -mllvm --riscv-v-vector-bits-min=256 --gcc-toolchain=$HOME/riscv add.cpp -o add
$ /data/llvm/bin/llvm-objdump --mattr=+v -S add
   1020e: 57 70 04 c5   vsetivli        zero, 8, e32, m1, ta, mu
   10212: 07 64 03 02   vle32.v v8, (t1)
   10216: 87 e4 03 02   vle32.v v9, (t2)
   1021a: 93 07 07 fe   addi    a5, a4, -32
   1021e: 07 e5 07 02   vle32.v v10, (a5)
   10222: 87 65 07 02   vle32.v v11, (a4)
   10226: 57 14 85 02   vfadd.vv        v8, v8, v10
   1022a: d7 94 95 02   vfadd.vv        v9, v9, v11
   1022e: 93 07 05 fe   addi    a5, a0, -32
   10232: 27 e4 07 02   vse32.v v8, (a5)
   10236: a7 64 05 02   vse32.v v9, (a0)

可以看到 llvm 的自动向量化是工作的。此外,也可以编写 rvv intrinsic。

XRDP 和 NVIDIA 显卡兼容性问题

背景

最近在尝试配置 XRDP,发现它在有 NVIDIA 的机器上启动远程桌面后会黑屏,查看错误信息可以看到:

xf86OpenConsole: Cannot open virtual console 1 (Permission denied)

解决方法

XRDP 作者在 issue #2010 中提到了解决方法:

修改 /etc/xrdp/sesman.ini,在 [Xorg] 部分里加上下面的配置:

param=-configdir
param=/

实际上就是不让 Xorg 加载 nvidia xorg 驱动,这样就绕过了问题。

NVIDIA 驱动和 CUDA 版本信息速查

背景

之前和 NVIDIA 驱动和 CUDA 搏斗比较多,因此记录一下一些常用信息,方便查询。

常用地址

CUDA 版本与 NVIDIA 驱动兼容性

可以通过 apt show cuda-runtime-x-x 找到:

  • cuda 12.3 >= 545 (Release Notes: 525)
  • cuda 12.2 >= 535 (Release Notes: 525)
  • cuda 12.1 >= 530 (Release Notes: 525)
  • cuda 12.0 >= 525 (Release Notes: 525)
  • cuda 11.8 >= 520 (Release Notes: 450)
  • cuda 11.7 >= 515 (Release Notes: 450)
  • cuda 11.6 >= 510 (Release Notes: 450)
  • cuda 11.5 >= 495 (Release Notes: 450)
  • cuda 11.4 >= 470 (Release Notes: 450)
  • cuda 11.3 >= 465 (Release Notes: 450)
  • cuda 11.2 >= 460 (Release Notes: 450)
  • cuda 11.1 >= 455 (Release Notes: 450)
  • cuda 11.0 >= 450 (Release Notes: 450)
  • cuda 10.2 >= 440
  • cuda 10.1 >= 418
  • cuda 10.0 >= 410
  • cuda 9.2 >= 396
  • cuda 9.1 >= 390
  • cuda 9.0 >= 384

使用 nvidia-smi 看到的 CUDA 版本,通常就是这个驱动在上表里对应的 CUDA 版本,例如内核驱动版本是 470 的话,看到的 CUDA 版本就是 11.4。

实际上兼容的驱动版本会比 APT 宣称的更多一些:官方文档 里面写了 CUDA 11.x 可以兼容 NVIDIA >= 450。

CUDA 版本和 GCC/Clang 版本兼容性

可以在 cuda/include/crt/host_config.h 文件里找到:

  • cuda 12.1: gcc <= 12, 3.2 < clang < 16
  • cuda 12.0: gcc <= 12, 3.2 < clang < 15
  • cuda 11.8: gcc <= 11, 3.2 < clang < 15
  • cuda 11.5: gcc <= 11
  • cuda 11.4: gcc <= 10
  • cuda 11.3: gcc <= 10, 3.2 < clang < 12
  • cuda 11.1: gcc <= 10, 3.2 < clang < 11
  • cuda 11.0: gcc <= 9, 3.2 < clang < 10
  • cuda 10.2: gcc <= 8, 3.2 < clang < 9
  • cuda 10.1: gcc <= 8, 3.2 < clang < 9
  • cuda 10.0: gcc <= 7
  • cuda 9.1: gcc <= 6

CUDA 版本与显卡兼容性

编译选项与显卡对应关系 https://arnon.dk/matching-sm-architectures-arch-and-gencode-for-various-nvidia-cards/

可以在 nvcc --help 搜索 gpu-architecture 找到:

  • cuda 12.0 sm_50 to sm_90a
  • cuda 11.8 sm_35 to sm_90
  • cuda 11.4 sm_35 to sm_87
  • cuda 11.3 sm_35 to sm_86
  • cuda 11.1 sm_35 to sm_86
  • cuda 11.0 sm_35 to sm_80
  • cuda 10.2 sm_30 to sm_75
  • cuda 10.0 sm_30 to sm_75
  • cuda 9.1 sm_30 to sm_72
  • cuda 9.0 sm_30 to sm_70

显卡的 Compute Capability 可以在 https://developer.nvidia.com/cuda-gpus 找到:

  • H100: 90
  • A100: 80
  • V100: 70
  • P100: 60

升级 NVIDIA 驱动

升级后,需要 rmmod 已有的,再 modprobe 新的:

sudo rmmod nvidia_uvm nvidia_drm nvidia_modeset nvidia && sudo modprobe nvidia

如果发现 rmmod 失败,可以 lsof /dev/nvidiactl 查看谁在占用。DGX OS 上需要停止:

sudo systemctl stop nvsm.service
sudo systemctl stop nvidia-dcgm.service 

除了 /dev/nvidia* 可能被占用以外,还需要用 lsof 检查 /dev/dri/render*

Locale 影响排序的特殊副作用

背景

最近在答疑的时候,发现同一条命令在不同系统上行为不同,一开始以为是 bash 版本问题,排查后最后发现是 locale 的问题。一个例子如下:

$ cat poc.txt | tr '\\n' ' '
1 + - * / \ a b A B     α
$ LANG="" sort poc.txt | tr '\\n' ' '
* + - / 1 A B \ a b α    $ LANG="zh_CN.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1     a A b B α
$ LANG="en_US.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1 a A b B α    

注意 1 a A 的顺序,在不同的 locale 下结果不同。

网上也有关于这个问题的讨论:

  1. https://unix.stackexchange.com/questions/75341/specify-the-sort-order-with-lc-collate-so-lowercase-is-before-uppercase
  2. https://stackoverflow.com/questions/43448655/weird-behavior-of-bash-glob-regex-ranges

背景

最近发现空调遥控器电池有点不足,有时候会自动关机,于是拿出以前买的 Broadlink RM mini 3 充当远程的空调遥控器使用。为了方便手机上配置,分别采用了官方的 App 智慧星和 homebridge 进行配置。

步骤

首先用官方的智慧星配置好 Broadlink RM mini 3 的网络,然后配置 homebridge-broadlink-rm-pro。最早的插件作者不怎么更新了,这个版本是目前用的比较多的一个 fork。

安装好以后,在 Home 里面可以看到 Scan Code 的开关。打开以后,用遥控器在 Broadlink RM mini 3 附近按按键,就可以在 Homebridge 日志里看到 hex code 了。然后,就按照插件教程里的方法写配置,例子如下:

{
        "platform": "BroadlinkRM",
        "name": "Broadlink RM",
        "accessories": [
        {
                "name": "Air Conditioner",
                "type": "air-conditioner",
                "noHumidity": true,
                "minTemperature": 26,
                "maxTemperature": 28,
                "defaultCoolTemperature": 27,
                "data": {
                        "off": "2600...",
                        "cool28": {
                                "data": "2600..."
                        },
                        "cool27": {
                                "data": "2600..."
                        },
                        "cool26": {
                                "data": "2600..."
                        }
                }
        }]
}

这样就可以在手机上方便地控制空调温度了。测试了一下,可以用 Siri 说“设置空调为 XX 度”,也是完全可以工作的。

P.S. 小米的空气净化器现在可以用插件 https://github.com/torifat/xiaomi-mi-air-purifier#readme,之前博客里写的那一个已经不更新了。

Nginx 处理 POST 请求出现 Internal Server Error 排查一则

前言

最近一个服务忽然出现问题,用户反馈,HTTP POST 一个小的 body 不会出错,POST 一个大的 body 就会 500 Internal Server Error。

排查

观察后端日志,发现没有出错的那一个请求。观察 Nginx 日志,发现最后一次日志是几个小时前。最后几条 Nginx 日志写的是 a client request body is buffered to a temporary file

结论

继续研究后,发现是硬盘满了。Nginx 在处理 POST body 的时候,如果 body 超过阈值,会写入到临时文件中:

Syntax: client_body_buffer_size size;
Default: client_body_buffer_size 8k|16k;
Context: http, server, location
Sets buffer size for reading client request body. In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file. By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64. It is usually 16K on other 64-bit platforms.

详见 https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size

这就可以解释为什么 Nginx 返回 500 而且没有转发到后端,也可以解释为什么 Nginx 没有输出新的错误日志。

Gnome 的 Fractional Scaling

背景

最近发现部分软件(包括 Google Chrome,Firefox 和 Visual Studio Code)在 125% 的 Fractional Scaling 模式下会很卡。找到了一些临时解决方法,但是很不优雅,也很麻烦。所以深入研究了一下 Fractional Scaling 的工作方式。

临时解决方法

根据关键字,找到了 Chrome menus too slow after enabling fractional scaling in Ubuntu 20.04。按它的方法,关闭 Google Chrome 的硬件加速,发现卡顿问题确实解决了。

类似地,也可以关闭 VSCode 的硬件加速,在 Firefox 里也可以找到相应的设置。这样操作确实可以解决问题。但是,对于每一个出问题的应用都这样搞一遍,还是挺麻烦的。

另一个思路是,不使用 Fractional Scaling,而只是把字体变大。但毕竟和我们想要的效果不大一样。

一些发现

在物理机进行了一些实验以后,发现一个现象:125% 的时候卡顿,而其他比例(100%,150%,175%,200%)都不卡顿。

网上一顿搜到,找到了 xrandr 工具。下面是观察到的一些现象(GNOME 设置分辨率一直是 1920x1080):

放缩比例 xrandr 显示的分辨率 xrandr 显示的 transform
100% 1920x1080 diag(1.0, 1.0, 1.0)
125% 3072x1728 diag(1.6, 1.6, 1.0)
150% 2560x1440 diag(1.33, 1.33, 1.0)
175% 2208x1242 diag(1.15, 1.15, 1.0)
200% 1920x1080 diag(1.0, 1.0, 1.0)

xrandr 文档 中,写了:transform 是一个 3x3 矩阵,矩阵乘以输出的点的坐标得到图形缓存里面的坐标。

由此可以猜想:fractional scaling 的工作方式是,把绘制的 buffer 调大,然后再用 transform 把最终输出分辨率调成 1920x1080。可以看到,xrandr 显示的分辨率除以 transform 对应的值,就是 1920x1080。但这并不能解释 100% 和 200% 的区别,所以肯定还漏了什么信息。

翻了翻 mutter 实现 fractional scaling 的 pr,找到了实现 scale 的一部分:

if (clutter_actor_get_resource_scale (priv->actor, &resource_scale) &&
    resource_scale != 1.0f)
  {
    float paint_scale = 1.0f / resource_scale;
    cogl_matrix_scale (&modelview, paint_scale, paint_scale, 1);
  }

然后找到了一段对 scale 做 ceiling 的代码

if (_clutter_actor_get_real_resource_scale (priv->actor, &resource_scale))
  {
    ceiled_resource_scale = ceilf (resource_scale);
    stage_width *= ceiled_resource_scale;
    stage_height *= ceiled_resource_scale;
  }

这样,100% 和其他比例就区分开了。

另外,也在代码 中发现:

#define SCALE_FACTORS_PER_INTEGER 4
#define SCALE_FACTORS_STEPS (1.0 / (float) SCALE_FACTORS_PER_INTEGER)
#define MINIMUM_SCALE_FACTOR 1.0f
#define MAXIMUM_SCALE_FACTOR 4.0f

这段代码规定了比例只能是 25% 的倍数。

我也试了一下用 xrandr --scale 1.5x1.5:效果就是窗口看起来都更小了,分辨率变成了 2880x1620,transform 是 diag(1.5, 1.5, 1.0)。

虚拟机测试

接着,用虚拟机做了一些测试。为了在 GNOME over Wayland 上使用 fractional scaling,需要运行:

$ gsettings set org.gnome.mutter experimental-features "['scale-monitor-framebuffer']"

接着又做了类似上面的测试(GNOME 设置分辨率一直是 2560x1600):

放缩比例 xrandr 显示的分辨率
100% 2560x1600
125% 2048x1280
150% 1704x1065
175% 1464x915
200% 1280x800

在这个测试中,xrandr 显示的 transform 一直都是单位矩阵;还用了来自 xyproto/wallutilswayinfo 命令查看输出的分辨率,一直是 2560x1600,DPI 一直是 96。用 wallutils 的 xinfo 看到的结果和 xrandr 一致(通过 XWayland)。但是和物理机有一点不同:物理机有一个选项问要不要打开 fractional scaling,下面还会提示性能下降的问题;但是虚拟机上并没有这个提示,而是直接给了一些 Scale 比例的选项。

尝试了一下,在 GNOME over X11 上是找不到 fractional scaling 的(没有出现设置 scale 的选项)。找到一个实现这个功能的 fork:https://github.com/puxplaying/mutter-x11-scaling,不过没有尝试过。

我也尝试在虚拟机中用 xrandr --scale,结果就是输出黑屏,需要重启 gdm 来恢复到登录界面。

更新:由于物理机使用的是 Ubuntu,想到是不是 Ubuntu 采用了上面那个 fork 的 patch,然后就在 changelog 中看到:

mutter (3.38.1-1ubuntu1) groovy; urgency=medium

  * Merge with debian, including new upstream version, remaining changes:
    - debian/gbp.conf: update upstream branch to point to ubuntu/master
    - debian/patches/x11-Add-support-for-fractional-scaling-using-Randr.patch:
      + X11: Add support for fractional scaling using Randr
  * d/p/clutter-backend-x11-Don-t-set-the-font-dpi-computed-on-X1.patch:
    - Dropped, applied upstream

也找到了对应的 patch 文件。这也就解释了,为什么网上会说 GNOME over X11 支持 fractional scaling,并且需要用 gsettings 打开,而我在 Debian 和 Arch Linux 上设置这个选项也没有用了。原来是 Ubuntu 加的私货啊。

在 patch 中,找到了这么一段配置的解释:

+    <key name="fractional-scale-mode" enum="org.gnome.mutter.X11.scale-mode">
+      <default>"scale-ui-down"</default>
+      <description>
+        Choose the scaling mode to be used under X11 via Randr extension.
+
+        Supported methods are:
+
+        • “scale-up”     — Scale everything up to the requested scale, shrinking
+                           the UI. The applications will look blurry when scaling
+                           at higher values and the resolution will be lowered.
+        • “scale-ui-down — Scale up the UI toolkits to the closest integer
+                           scaling value upwards, while scale down the display
+                           to match the requested scaling level.
+                           It increases the resolution of the logical display.
+      </description>
+    </key>

这样就可以解释前面看到的现象了:默认是 scale-ui-down,也就是先放大到两倍(closest integer scaling value upwards),再缩小(scale down the display to match the requested scaling level)。

使用 SSSD 的 LDAP 认证

前言

最近在研究替换一个老的用户系统,于是顺便学习了一下 LDAP,还有 SSSD。LDAP 是一个目录协议,顺带的,因为用户信息也可以存在里面,所以也就成了一个常见的用户认证协议。SSSD 就是一个 daemon,把系统的 NSS PAM 的机制和 LDAP 连接起来。

配置

其实很简单,安装 sssd 并且配置即可:

$ sudo apt install sssd
$ sudo vim /etc/sssd/sssd.conf
# file content:
[sssd]
config_file_version = 2
services = nss,pam
domains = LDAP

[domain/LDAP]
cache_credentials = true
enumerate = true
entry_cache_timeout = 10
ldap_network_timeout = 2

id_provider = ldap
auth_provider = ldap
chpass_provider = ldap

ldap_uri = ldap://127.0.0.1/
ldap_chpass_uri = ldap://127.0.0.1/
ldap_search_base = dc=example,dc=com
ldap_default_bind_dn = cn=localhost,ou=machines,dc=example,dc=com
ldap_default_authtok = REDACTED
$ sudo systemctl enable --now sssd

一些字段需要按照实际情况编写,请参考sssd.confsssd-ldap

协议

那么,LDAP 里面的用户是如何和 Linux 里的用户对应起来的呢?可以看到,SSSD 会查询 posixAccount:

(&(objectclass=posixAccount)(uid=*)(uidNumber=*)(gidNumber=*))

然后,可以查到 posixAccount 的 schema,里面可以见到对应 /etc/passwd 的各个字段。相应的,也有 shadowAccount 对应 /etc/shadow

按照要求配好以后(建议用 ldapvi 工具),就可以用 getent passwd 看到新增的用户了。

上面的部分是通过 NSS 接口来查询的,除了用户以外,还有其他的一些 NIS 信息可以通过 LDAP 查询。此外,如果要登录的话,则是用 PAM 认证,SSSD 则会把 PAM 认证转换成 LDAP 的 Bind:

$ su test
Password:
# sssd: bind to dn of test user with password

如果 Bind 成功,则认为登录成功;否则就是登录失败。

如果用户要修改密码,SSSD 默认用的是 RFC3062 LDAP Password Modify Extended Operation 的方式;如果服务器不支持的话,可以按照 文档 使用 ldap modify 方式来修改密码。

SSD 还可以配置 sudo 支持,也是用类似的方法,添加 objectClass=sudoRole 的目录项即可。可以参考 man sudoers.ldap 编写对应的目录项。

对于 SSH 配置,可以参考 RedHat 的文档,和参考 man sss_ssh_authorizedkeys 配置 authorized keys 命令。然后,给用户添加 sshPublicKey 属性即可,内容与 ~/.ssh/id_*.pub 一致。

相关 RFC

LDAP-Related RFCs

在 Big Sur(M1) 上解决 LaTeX 找不到楷体字体的问题

背景

最近在尝试移植 MiKTeX 到 Apple Silicon 上,添加了一些 patch 以后就可以工作了,但遇到了新的问题,即找不到 KaiTi

~/Library/Application Support/MiKTeX/texmfs/install/tex/latex/ctex/fontset/ctex-fontset-macnew.def:99:
   Package fontspec Error:
      The font "Kaiti SC" cannot be found.

miktex-fc-list 命令找了一下,确实没有找到:

$ /Applications/MiKTeX\ Console.app/Contents/bin/miktex-fc-list | grep Kaiti
# Nothing

上网搜了一下,找到了一个解决方案:字体在目录 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Support/FontSubsets/Kaiti.ttc 里,所以手动安装一下,就可以让 LaTeX 找到了。但我觉得,与其安装多一份在文件系统里,不如让 LaTeX 去找它。

解决方法

按照上面的线索,找到了 Kaiti.ttc 所在的路径:

$ fd Kaiti.ttc
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc

可以看到,和上面的路径又不大一样。研究了一下 fontconfig,发现可以用 miktex-fc-conflist 找到配置文件的目录:

$ /Applications/MiKTeX\ Console.app/Contents/bin/miktex-fc-conflist
+ ~/Library/Application Support/MiKTeX/texmfs/config/fontconfig/config/localfonts2.conf: No description
+ ~/Library/Application Support/MiKTeX/texmfs/config/fontconfig/config/localfonts.conf: No description
...

看了下第一个文件(localfonts.conf):

<?xml version="1.0" encoding="UTF-8"?>

<!--
  DO NOT EDIT THIS FILE! It will be replaced when MiKTeX is updated.
  Instead, edit the configuration file localfonts2.conf.
-->

<fontconfig>
<include>localfonts2.conf</include>
<dir>/Library/Fonts/</dir>
<dir>/System/Library/Fonts/</dir>
<dir>~/Library/Application Support/MiKTeX/texmfs/install/fonts/type1</dir>
<dir>~/Library/Application Support/MiKTeX/texmfs/install/fonts/opentype</dir>
<dir>~/Library/Application Support/MiKTeX/texmfs/install/fonts/truetype</dir>
</fontconfig>

可以看到,我们可以添加路径,不过建议修改的是 localfonts2.conf。按照类似的格式,修改成:

<?xml version="1.0"?>
<fontconfig>
<dir>/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets</dir>
<!-- REMOVE THIS LINE
<dir>Your font directory here</dir>
<dir>Your font directory here</dir>
<dir>Your font directory here</dir>
     REMOVE THIS LINE -->
</fontconfig>

UPDATE: 新版本 macOS 中,路径建议加上 /System/Library/AssetsV2/com_apple_MobileAsset_Font7

<dir>/System/Library/AssetsV2/com_apple_MobileAsset_Font7</dir>

这样,就可以找到 Kaiti SC 了:

$ miktex-fc-list | grep Kaiti
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti TC,楷體\-繁,楷体\-繁:style=Regular,標準體,常规体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti SC,楷體\-簡,楷体\-简:style=Regular,標準體,常规体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti SC,楷體\-簡,楷体\-简:style=Bold,粗體,粗体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti TC,楷體\-繁,楷体\-繁:style=Bold,粗體,粗体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti SC,楷體\-簡,楷体\-简:style=Black,黑體,黑体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: Kaiti TC,楷體\-繁,楷体\-繁:style=Black,黑體,黑体
/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/Resources/Fonts/Subsets/Kaiti.ttc: STKaiti:style=Regular,標準體,Ordinær,Normal,Normaali,Regolare,レギュラー,일반체,Regulier,Обычный,常规体

这样就搞定了,用 LaTeX 找字体的时候也没有出现问题了。

如果你用的是 TeX Live,那么直接把上面的 Kaiti.ttc 路径复制到 ~/Library/Fonts 下即可。

COMMON 符号

背景

在编译一个程序的时候,遇到了 undefined symbol 的问题。具体情况是这样的:

  1. 一开始的时候,直接把所有的源代码编译成 .o,再一次性链接,这样不会报错
  2. 后来,把一些代码编译成静态库,即把其中一部分源代码编译成 .o 后,用 ar 合并到一个 .a 中,再和其余的 .o 链接在一起,这时候就报错了:
Undefined symbols for architecture arm64:
  "_abcd", referenced from:
    ...

如果换台机器,编译(使用的是 gcc 10.2.0)就没有问题。

而如果去找这个符号存在的 .o 里,是可以找到的:

$ objdump -t /path/to/abcd.o
0000000000000028         *COM*  00000008 _abcd

在合成的静态库 .a 里,也是存在的(一个定义 + 若干个引用):

$ objdump -t /path/to/libabc.a | grep abcd
0000000000000028         *COM*  00000008 _abcd
0000000000000000         *UND* _abcd
0000000000000000         *UND* _abcd
0000000000000000         *UND* _abcd
0000000000000000         *UND* _abcd
0000000000000000         *UND* _abcd

于是觉得很奇怪,就上网搜了一下,找到了一篇 StackOverflow 讲了这个问题。解决方案很简单,就是:

编译的时候打开 -fno-common 设置

而 gcc 10 不会出错的原因是,它默认从 -fcommon 改成了 -fno-common

COMMON 是什么

这时候,肯定不满足于找到一个解决方案,肯定还是会去找背后的原理。

首先,搜索了一下 COMMON 是什么,找到了 Investigating linking with COMMON symbols in ELF 这篇文章。

文章里讲了 COMMON 是做什么的:

Common symbols are a feature that allow a programmer to 'define' several variables of the same name in different source files. This is in contrast with the more popular way of doing, where you define a variable once in a source file, and reference it everywhere else in other source files, using extern. When common symbols are used, the linker will merge all symbols of the same name into a single memory location, the size of which is the largest type of the individual common symbol definitions. For example, if fileA.c defines an uninitialized 32-bit integer myint, and fileB.c defines an 8-bit char myint, then in the final executable, references to myint from both files will point to the same memory location (common location), and the linker will reserve 32 bits for that location.

文章里还讲了具体的实现方法:一个没有初始化的全局变量,在 -fcommon 的情况下,会设为 COMMON;如果有初始化,就按照初始化的值预分配到 .bss 或者 .data。链接的时候,如果有多个同名的 symbol,会有一个规则决定最后的 symbol 放到哪里;如果有冲突的话,就是我们熟悉的 multiple definition 错误了。

为啥会有这种需求,多个 variable 同名,不会冲突而且共享内存?又在别的地方看到说法,COMMON 是给 ancient 代码使用的,还有的提到了 FORTRAN。于是去搜了一下,果然,FORTRAN 是问题的关键

FORTRAN 里面的 COMMON

用关键词很容易可以搜索到讲 COMMON BLOCK in FORTRAN 的文章,FORTRAN 里面的 COMMON 是一种通过全局存储隐式传递参数的方法。拿文章里的例子:

      PROGRAM MAIN
      INTEGER A
      REAL    F,R,X,Y
      COMMON  R,A,F
      A = -14
      R = 99.9
      F = 0.2
      CALL SUB(X,Y)
      END

      SUBROUTINE SUB(P,Q)
      INTEGER I
      REAL    A,B,P,Q
      COMMON  A,I,B
      END

在函数 MAIN 和 SUB 中,都有 COMMON 语句,而 COMMON 后面的变量,就是存储在一个 COMMON 的 symbol 之中,按照顺序映射到 symbol 的内存地址。尝试编译一下上面的代码,然后看一下 symbol:

$ gfortran -g -c test.f -o test.o
$ objdump -t test.o

test.o: file format Mach-O arm64

SYMBOL TABLE:
0000000000000078 g     F __TEXT,__text _main
0000000000000000 g     F __TEXT,__text _sub_
000000000000000c         *COM*  00000010 ___BLNK__

可以看到,出现了一个叫做 ___BLNK__ 的 COMMON symbol,大小是 16 字节。看一下代码中是如何引用的:

$ objdump -S --reloc test.o

test.o: file format Mach-O arm64

Disassembly of section __TEXT,__text:

0000000000000018 _MAIN__:
;         PROGRAM MAIN
      18: fd 7b be a9                   stp x29, x30, [sp, #-32]!
      1c: fd 03 00 91                   mov x29, sp
;         A = -14
      20: 00 00 00 90                   adrp    x0, #0
        0000000000000020:  ARM64_RELOC_GOT_LOAD_PAGE21  ___BLNK__
      24: 00 00 40 f9                   ldr x0, [x0]
        0000000000000024:  ARM64_RELOC_GOT_LOAD_PAGEOFF12   ___BLNK__
      28: a1 01 80 12                   mov w1, #-14
      2c: 01 04 00 b9                   str w1, [x0, #4]
;         R = 99.9
      30: 00 00 00 90                   adrp    x0, #0
        0000000000000030:  ARM64_RELOC_GOT_LOAD_PAGE21  ___BLNK__
      34: 00 00 40 f9                   ldr x0, [x0]
        0000000000000034:  ARM64_RELOC_GOT_LOAD_PAGEOFF12   ___BLNK__
      38: a1 99 99 52                   mov w1, #52429
      3c: e1 58 a8 72                   movk    w1, #17095, lsl #16
      40: 20 00 27 1e                   fmov    s0, w1
      44: 00 00 00 bd                   str s0, [x0]
;         F = 0.2
      48: 00 00 00 90                   adrp    x0, #0
        0000000000000048:  ARM64_RELOC_GOT_LOAD_PAGE21  ___BLNK__
      4c: 00 00 40 f9                   ldr x0, [x0]
        000000000000004c:  ARM64_RELOC_GOT_LOAD_PAGEOFF12   ___BLNK__
      50: a1 99 99 52                   mov w1, #52429
      54: 81 c9 a7 72                   movk    w1, #15948, lsl #16
      58: 20 00 27 1e                   fmov    s0, w1
      5c: 00 08 00 bd                   str s0, [x0, #8]
;         CALL SUB(X,Y)
      60: e1 63 00 91                   add x1, sp, #24
      64: e0 73 00 91                   add x0, sp, #28
      68: 00 00 00 94                   bl  #0 <_MAIN__+0x50>
        0000000000000068:  ARM64_RELOC_BRANCH26 _sub_
;         END
      6c: 1f 20 03 d5                   nop
      70: fd 7b c2 a8                   ldp x29, x30, [sp], #32
      74: c0 03 5f d6                   ret

可以看到,在 MAIN 中引用 A 的时候,取的地址是 ___BLNK__+4R___BLNK__+0F___BLNK__+8。这和代码里的顺序也是一致的。所以在 SUB 中读 A I B 的时候,对应了 MAIN 中的 A R F。通过这种方式,可以在 MAIN 函数里面隐式地给所有函数传递参数。

此外,COMMON 还可以命名,这样就可以区分不同的参数用途:

        PROGRAM MAIN
        INTEGER A
        REAL    F,R,X,Y
        COMMON  R,A,F
        COMMON /test/ X,Y
        A = -14
        R = 99.9
        F = 0.2
        CALL SUB(X,Y)
        END

        SUBROUTINE SUB(P,Q)
        INTEGER I
        REAL    A,B,P,Q
        COMMON  A,I,B
        END

代码添加了一行 COMMON /test/,观察一下 symbol:

$ objdump -t test.o

test.o: file format Mach-O arm64

SYMBOL TABLE:
0000000000000088 g     F __TEXT,__text _main
0000000000000000 g     F __TEXT,__text _sub_
000000000000000c         *COM*  00000010 ___BLNK__
0000000000000008         *COM*  00000010 _test_

和预期的一致:出现了新的 COMMON symbol,对应了 named COMMON Block 里面的变量 X 和 Y。

再看一下汇编里怎么引用的:

;         CALL SUB(X,Y)
      60: 00 00 00 90                   adrp    x0, #0
                0000000000000060:  ARM64_RELOC_GOT_LOAD_PAGE21  _test_
      64: 00 00 40 f9                   ldr     x0, [x0]
                0000000000000064:  ARM64_RELOC_GOT_LOAD_PAGEOFF12       _test_
      68: 01 10 00 91                   add     x1, x0, #4
      6c: 00 00 00 90                   adrp    x0, #0
                000000000000006c:  ARM64_RELOC_GOT_LOAD_PAGE21  _test_
      70: 00 00 40 f9                   ldr     x0, [x0]
                0000000000000070:  ARM64_RELOC_GOT_LOAD_PAGEOFF12       _test_
      74: 00 00 00 94                   bl      #0 <_MAIN__+0x5c>
                0000000000000074:  ARM64_RELOC_BRANCH26 _sub_

可以看到,第一个参数(x0)为 _test_,第二个参数(x1)为 _test_+4,和预期也是一样的。

读到这里,就可以理解为啥有 COMMON symbol 了。可能是为了让 C 代码和 FORTRAN 代码可以互操作 COMMON symbol,就有了这么一出。也可能有的 C 库确实用了类似的方法来实现某些功能。

解决方案

但是,这种用法在现在来看是不推荐的,建议还是该 extern 就 extern,另外,在编译静态库的时候,记得加上 -fno-common

在 M1 上用 QEMU 运行 Debian 虚拟机

背景

看到 @jsteward 在 M1 的 QEMU 中运行了 Windows on ARM,所以我先来试试 Debian on AArch64,这样会简单一些。

参考:https://gist.github.com/niw/e4313b9c14e968764a52375da41b4278#file-readme-md

大约需要 3G 的硬盘空间。

安装 QEMU w/ M1 patches

目前上游的 QEMU 还不支持 M1 的 Hypervisor framework,需要打 patch:

git clone https://mirrors.tuna.tsinghua.edu.cn/git/qemu.git
cd qemu
git checkout master -b wip/hvf
curl 'https://patchwork.kernel.org/series/400619/mbox/'|git am --3way
mkdir build
cd build
../configure --target-list=aarch64-softmmu --enable-cocoa --disable-gnutls
make -j4

编译后,得到 qemu-system-aarch64 的二进制

准备好文件系统

需要下载 EFI 固件Debian 安装镜像,解压前者以后把文件放同一个目录中,并且创建需要的文件:

$ ls *.fd
QEMU_EFI.fd   QEMU_VARS.fd
$ dd if=/dev/zero of=pflash0.img bs=1m count=64
$ dd if=/dev/zero of=pflash1.img bs=1m count=64
$ dd if=QEMU_EFI.fd of=pflash0.img conv=notrunc
$ dd if=QEMU_VARS.fd of=pflash1.img conv=notrunc
$ $QEMU/qemu-img create -f qcow2 disk.qcow2 40G

安装 Debian 系统

接着,执行以下的命令,然后按照提示安装系统:

$ $QEMU/qemu-system-aarch64 \
  -serial mon:stdio \
  -M virt,highmem=off \
  -accel hvf \
  -cpu cortex-a72 \
  -smp 4 \
  -m 4096 \
  -drive file=./pflash0.img,format=raw,if=pflash,readonly=on \
  -drive file=./pflash1.img,format=raw,if=pflash \
  -device virtio-scsi-pci \
  -device virtio-gpu-pci \
  -device qemu-xhci \
  -device usb-kbd \
  -device usb-tablet \
  -drive file=./disk.qcow2,if=none,id=boot,cache=writethrough \
  -device nvme,drive=boot,serial=boot \
  -drive if=none,id=cd,file=debian-10.7.0-arm64-xfce-CD-1.iso,media=cdrom \
  -device scsi-cd,drive=cd \
  -display default,show-cursor=on

需要注意的是,如果用 -cdrom 选项,Debian 会无法识别,所以需要走 SCSI。安装完成后,第一次重启可能会显示失败,不用管。另外,安装界面只在串口处显示,但不会显示在 GUI 中,估计是因为 BUG(感谢 @Harry-Chen 指出)。

启动系统

安装好后,运行下面的命令来启动 Debian 系统:

$ $QEMU/qemu-system-aarch64 \
  -monitor stdio \
  -M virt,highmem=off \
  -accel hvf \
  -cpu cortex-a72 \
  -smp 4 \
  -m 4096 \
  -drive file=./pflash0.img,format=raw,if=pflash,readonly=on \
  -drive file=./pflash1.img,format=raw,if=pflash \
  -device virtio-gpu-pci \
  -device virtio-scsi-pci \
  -device qemu-xhci \
  -device usb-kbd \
  -device usb-tablet \
  -drive file=./disk.qcow2,if=none,id=boot,cache=writethrough \
  -device nvme,drive=boot,serial=boot \
  -display default,show-cursor=on \
  -nic user,model=virtio

注意参数和上面有所不同。启动后就可以在 GUI 上看到 Debian 登录的界面了。

网络

起来以后,可以看到一个网卡 enp0s1 启动并获取 IP 地址:

$ ip l set enp0s1 up
$ dhclient enp0s1

获取到一个 IP 地址后,就可以上网了。

已知问题

在虚拟机内重启以后,可能会启动失败。退出 QEMU 进程重新启动即可。

在 Spack 中用 external 的 Slurm 依赖编译 OpenMPI

最近在一个集群上,需要用一个自己编译的 openmpi,但并没有 root 权限,所以需要自己搞一个 spack,在 spack 里面装 openmpi。但默认的安装选项下,它没有打开 slurm 支持,所以 srun 的话会出问题,只能 sbatch 然后指定 host 去做。于是我研究了一下怎么在 spack 里引入 external 的 slurm,然后用它来编译 openmpi

首先,编译 ~/.spack/packages.yaml

packages:
  slurm:
    buildable: False
    paths:
      "slurm@15-08-7-1%gcc@8.3.0 arch=linux-ubuntu16.04-haswell": /usr

这里 slurm 版本是 15.08.7,我就按照 spack 里面 slurm 的版本号来写了。可以用 spack spec openmpi schedulers=slurm +pmi 来确认一下外部的 slurm 确实出现在了依赖之中。

这一步配好的话,安装的时候就会直接跳过 spack 里面 slurm 的安装。但又出现了 configure 错误,找不到 pmi 的库。于是,先用 external 的 mpirun 看一下配置:

$ module load openmpi-3.0.0
$ ompi_info
...
--with-pmi=/usr
--with-pmi-libdir=/usr/lib/x86_64-linux-gnu
...

可以看到,需要两个 config 参数。然后,在 spack 的 openmpi package.py 中:

if spec.satisfies('schedulers=slurm'):
  config_args.append('--with-pmi={0}'.format(spec['slurm'].prefix))
  if spec.satisfies('@3.1.3:') or spec.satisfies('@3.0.3'):
    if '+static' in spec:
      config_args.append('--enable-static')

所以,需要加一个小 patch:

if spec.satisfies('schedulers=slurm'):
  config_args.append('--with-pmi={0}'.format(spec['slurm'].prefix))
  # patched here
  config_args.append('--with-pmi-libdir={0}/lib/x86_64-linux-gnu'.format(spec['slurm'].prefix))
  if spec.satisfies('@3.1.3:') or spec.satisfies('@3.0.3'):
    if '+static' in spec:
      config_args.append('--enable-static')

然后,就可以编译通过了。

USB/IP 模拟 USB 设备

背景

2018 年的时候发过一篇博客,讲如何用 USB/IP 协议在两台 Linux 电脑之间共享 USB 设备。最近刚好有一个需求,就是针对一个现成的 USB device 的代码,通过 USB/IP 模拟出一个 USB 设备,然后进行调试。

USB/IP 协议

USB/IP 只有一个简略的文档,为数不多的使用 USB/IP 的代码,所以有一些细节没有说的很清楚,只能一点点去尝试。

首先,USB/IP 基于 TCP,端口号 3240。客户端向服务端发送请求,服务端向客户端进行回应。

请求的类型:OP_REQ_DEVLIST OP_REQ_IMPORT USBIP_CMD_SUBMIT 和 USBIP_CMD_UNLINK

回应的类型:OP_REP_DEVLIST OP_REP_IMPORT USBIP_RET_SUBMIT USBIP_RET_UNLINK

工作的过程大概如下:

  1. OP_REQ_DEVLIST 请求获取设备列表
  2. OP_REP_DEVLIST 返回设备列表
  3. OP_REQ_IMPORT 请求 USB 设备
  4. OP_REP_IMPORT 返回 USB 设备
  5. USBIP_CMD_SUBMIT 发送 URB
  6. USBIP_RET_SUBMIT 返回 URB

(先不考虑 CMD_UNLINK 和 RET_UNLINK)

其中前面四个比较简单清晰,所需要的字段也都是 Descriptor 中对应的字段。后面两个就相对复杂一些:URB data 的长度需要根据 endpoint 类型和 direction 共同决定。URB 实际上是 Linux 内核里的一个数据结构。

USB 协议

那么,USB 协议的几种 transfer 怎么对应到 URB 的数据呢?首先看最常见的三种([ref](https://www.beyondlogic.org/usbnutshell/usb4.shtml):

  1. Control Transfer
  2. 第一种是 Control IN,一共有三个阶段,第一个阶段是 Setup,Host 发送给 Device 一个八字节的 Setup Packet;第二个阶段是 Data,Device 发送给 Host 一段数据;第三个阶段是 Status,Host 发送给 Device 一个 Zero Length Packet。此时 Setup Packet 对应 urb 中的 setup,Data 就对应 RET_SUBMIT 里面的 URB data 了,自然 CMD_SUBMIT 中是没有 URB data 的
  3. 第二种是 Control OUT,一共有三个阶段,第一个阶段是 Setup,Host 发送给 Device 一个吧字节的 Setup Packet;第二个阶段是 Data,Host 给 Device 发送一段数据;第三个阶段是 Status,Device 给 Host 发送一个 Zero Length Packet。此时 Setup Packet 对应 urb 中的 setup,Data 对应 CMD_SUBMIT 末尾的 URB data,长度由 transfer_buffer_length 指定。返回的 RET_SUBMIT 不带 URB data,但依然需要有 RET_SUBMIT。
  4. Interrupt/Bulk Transfer
  5. 第一种是 Interrupt/Bulk IN,由 Device 给 Host 发送一段数据,附在 RET_SUBMIT 中。
  6. 第二种是 Interrupt/Bulk OUT,由 Host 给 Device 发送一段数据,中 CMD_SUBMIT 的 URB data 中。返回的 RET_SUBMIT 不带 URB data,但不能不发 RET_SUBMIT。

可见,Interrupt 和 Bulk 是比较简单的,而 Control 和 Isochronous(没有提到)则比较复杂。

回到 USB/IP 协议

其实补充了这些信息以后,就可以实现一个 USB/IP 协议的服务器了。

在 sbt 中 fork 并且并行运行测试

问题

最近在 sbt 使用遇到一个问题,有两个测试,分别用 testOnly 测试的时候没有问题,如果同时测试就会出问题,应该是全局的状态上出现了冲突。一个自然的解决思路是 fork,但是 sbt 默认 fork 之后 test 是顺序执行的,这会很慢。所以搜索了一下,找到了 fork 并且并行测试的方法。

解决方法

解决方法在 sbt 文档中其实就有(原文)。简单来说就是:把每个 test 放到单独的 TestGroup 中,每个 TestGroup 分别用一个 forked JVM 去运行;然后让 sbt 的并行限制设高一些:

// move each test into a group and fork them to avoid race condition
import Tests._
def singleTests(tests: Seq[TestDefinition]) =
  tests map { test =>
    new Group(
      name = test.name,
      tests = Seq(test),
      SubProcess(ForkOptions()))
  }

Test / testGrouping := singleTests( (Test / definedTests).value )
// allow multiple concurrent tests
concurrentRestrictions in Global := Seq(Tags.limitAll(4))

这样就可以了。

在 macOS 上带执行权限 mmap 一个已删除文件遇到的问题和解决方案

背景

实验环境:macOS Catalina 10.15.2

最近在 rcore-rs/zircon-rs 项目中遇到一个比较玄学的问题,首先需求是在 macOS 的用户进程里开辟一段地址空间,然后把这个地址空间多次映射(权限可能不同、同一块内存可能被映射到多个地址),通过 mmap 模拟虚拟地址的映射。采用的是如下的方案:

  1. 在临时目录创建一个文件,把文件大小设为 16M(暂不考虑扩容)
  2. 需要映射一个虚拟地址到物理地址的时候,就对这个文件的物理地址偏移进行 FIXED 映射,虚拟地址就是期望的虚拟地址。

这样的方案在 Linux 下运行地很好,但在 macOS 下总是以一定概率在第二部出现 EPERM。网上搜了很多,但也没搜到相关的信息,于是自己断断续续地研究了一下,现在有一个比较初步的结果。

TL;DR

先说结论:调用一个带 PROT_EXEC 并且映射文件的 mmap 时,macOS 会进行安全检测,如果此时发现文件在文件系统上消失了,它会认为这可能是一个恶意软件行为,进行拦截,返回 EPERM。

而代码实际上在第一步和第二步之间,把临时目录删了:由于进程持有 fd,所以文件并不会真的删掉,当软件退出的时候文件自然会删除,这是临时文件的常见做法(见 tmpfile(3))。

研究过程

查看 Console

在网上一番搜索未果后,就尝试在 Console 里面寻找信息。照着程序名字搜索,可以找到一些信息:

temporarySigning type=1 matchFlags=0x0 path=/path/to/executable

这是编译这个 executable 的时候出现的,好像也没啥问题。然后解除过滤,在这个信息前后按照 syspolicyd 寻找:

initiating malware scan (... info_path: /path/to/temp/file proc_path: /path/to/executable)
Unable (errno: 2) to read file at <private> for process path: <private> library path: <private>
Disallowing load of <private> in 50001, <private>
Library load (/path/to/temp/file) rejected: library load denied by system policy

这几条记录比较可疑,每次运行程序,如果跑挂了,就会出现这几条,如果没跑挂,就不会出现这一条。所以很大概率是被 macOS 拦截了。错误信息的用词是 library,所以大概率是被当成加载动态库了,但既然内容是空的,所以我想的是文件名触碰到了什么奇怪的规则,然后文件名又是随机的,随机导致 EPERM 是概率性出现的,这好像很有道理。于是我把 tmpfile 换成了固定的路径,忽然就好了。但固定的路径只能保证同时只有一个程序在跑,如果路径拼接上 pid,怎么删,谁来删又是一个问题。虽然可以放到 /tmp 下面然后随便搞,但 /tmp 的回收并不是那么积极,在临时目录下丢太多文件也会出现问题。

一丝曙光

这时候,@wangrunji0408 提供了一个方案:在 System Preferences -> Security & Privacy -> Privacy -> Developer Tools 中添加编译该 executable 的程序(如 iTerm、CLion)可以解决这个问题。那么问题应该比较明确了,就是 malware scan 的问题,如果信任了这个 App 为 Developer Tools,它产生的 executable 也是可信的,应该不是恶意软件。但在 tmux 环境下,它哪个 App 也不属于,没法继承,况且把这个权限开放出去也有潜在的安全问题。并且让每个开发者都要这么操作一遍很不方便。

回到 Console

今天刚好看到一个 post,内容是如何在 macOS Catalina 中查看 log 中标记为 private 的内容。如果你注意到的话,上面的 log 中出现了几处 private,这并不是我改的,而是 macOS 自带的隐私机制(当然这种机制似乎并没有采用的很完全,一些消息源没有打上 private 的标签)。

然后按照上面的 post 的方法(另一个 post)开启了一下标记为 private 的内容,正好我的系统没有升级到 10.15.3 所以还能用。此时上面的第二条和第三条就出现了具体内容:

Unable (errno: 2) to read file at /path/to/temp/file for process path: /path/to/executable library path: /path/to/temp/file
Disallowing load of /path/to/temp/file in 61254, /path/to/executable

这个时候问题就很明显了:读取不到文件。这时候回想起 tmpfile 的工作原理,它会删除生成的文件,在删除文件之后,macOS 进行扫描,发现找不到文件,于是 disallow 了,mmap 就会返回 EPERM。

解决方案也很显然了:把删除目录延后,或者放在 /tmp 下等待清理等待。

我也写了一段 C 代码来验证这个现象:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>


int main() {
    int fd = open("mmap", O_RDWR | O_CREAT, 0777);
    uint64_t addr = 0x200000000;
    ftruncate(fd, 16*1024*1024);
    // might not work if unlink is put here (race condition)
    // you can use sleep to reproduce
    unlink("mmap");
    void * res = mmap((void *)addr, 16*1024*1024, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_FIXED, fd, 0);
    // always works if unlink is put here
    // unlink("mmap");
    if (res == MAP_FAILED) {
        perror("mmap");
    } else {
        printf("good");
    }
    return 0;
}

用 Nginx 作为 RTMP 服务器并提供直播服务

Nginx 除了可以做 HTTP 服务器以外,还可以做 RTMP 服务器,同时转成 HLS 再提供给用户,这样可以实现一个直播的服务器,用 OBS 推上来即可。

首先要安装 nginx-rtmp-server 模块,很多的发行版都已经包含了,它的主页是 https://github.com/arut/nginx-rtmp-module,下面很多内容也是来自于它的教程中。

接着,配置 Nginx,在 nginx.conf 的顶层中添加如下的配置:

rtmp {
    server {
            listen 1935;
            chunk_size 4096;

            application live {
                    live on;
                    record off;

                    hls on;
                    hls_path /path/to/save/hls;
                    hls_fragment 1s;
                    hls_playlist_length 10s;
            }

    }
}

这里表示 Nginx 要在 1935 监听一个 RTMP 服务器,然后把 live 下的视频切成片然后存在目录下,提供一个 m3u8 文件以供播放器使用。这里的参数都可以按照实际需求进行调整。这时候应该可以看到 nginx 正确监听 1935 端口,这是 rtmp 的默认端口。

接着,需要在一个 HTTP server 路径下把 HLS serve 出去:

        location /hls {
            # Serve HLS fragments
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /path/to/save/hls;
            add_header Cache-Control no-cache;
        }

这时候,如果你用 rtmp 推一个流(比如用 OBS)到 rtmp://SERVER_IP/live/SOMETHING,那么在对应的目录下会看到 SOMETHING 开头的一系列文件;用播放器打开 http://SERVER_IP/hls/SOMETHING.m3u8 就可以看到直播的视频流了。

如果要直接在浏览器里播放 HLS,需要用 Flowplayer,直接参考官方的例子即可:

<script>
var player = flowplayer("#player", {
        clip: {
                sources: [
                {
                        type: "application/x-mpegurl",
                        src: "https://SERVER_IP/hls/SOMETHING.m3u8"
                }]
        },
        autoplay: true,
        loop: true,
        live: true
});
</script>

上面的各个路径可以按照实际需求改动。

在 macOS 上创建 ESP 镜像文件

最近 rCore 添加了 UEFI 支持,在 QEMU 里跑自然是没有问题,然后尝试放到 VMWare 虚拟机里跑,这时候问题就来了:需要一个带有 ESP 盘的 vmdk 虚拟盘。搜索了一下网络,找到了解决方案:

hdiutil create -fs fat32 -ov -size 60m -volname ESP -format UDTO -srcfolder esp uefi.cdr

其中 60m espuefi.cdr 都可以按照实际情况修改。它会把 esp 目录下的文件放到 ESP 分区中,然后得到一个镜像文件:

uefi.cdr: DOS/MBR boot sector; partition 1 : ID=0xb, start-CHS (0x3ff,254,63), end-CHS (0x3ff,254,63), startsector 1, 122879 sectors, extended partition table (last)

接着转换为 vmdk:

qemu-img convert -O vmdk uefi.cdr uefi.vmdk

这样就可以了。

macOS 下读取并解析 EDID

之前听说了 EDID 的存在,但是一直没有细究里面的格式和内容。今天了解了一下,发现其实非常简单,下面是方法:

首先获取所有显示器输出的 EDID:

ioreg -lw0 | grep IODisplayEDID

输出里会出现 "IODisplayEDID" = <00ffxxxxxxxxxxxxx> 的内容,尖括号内的就是 EDID 的内容。接着,我们采用 edid-decode 进行解析:

git clone git://linuxtv.org/edid-decode.git
cd edid-decode
make
./edid-decode
<Paste EDID here>

就可以看到很详细的 EDID 数据解析了。

ref: https://gist.github.com/OneSadCookie/641549 https://www.avsforum.com/forum/115-htpc-mac-chat/1466910-ability-dump-display-s-edid-mac.html

在 Linux 下捕获 Framebuffer

最近需要在 linux 下抓取 Framebuffer 的内容,在网上找到了两种方法,在我这里只有第二、第三种可以成功,没有细究具体原因,可能与我的 Framebuffer 配置有关。方法如下:

  1. fbgrab :命令就是 fbgrab image.png,直接得到 png 文件,格式是对的,但是用软件打开就是一片空白。用 ImageMagick 转换为 jpg 可以看到一些内容,但是和实际有些不一样。
  2. fbdump :命令就是 fbdump > image.ppm,得到裸的 ppm 文件,图像是正确的,也可以转换为别的格式正常打开。
  3. cat+脚本处理:直接 cat /dev/fb0 > image.rgb,然后用下面的脚本转换为 png。由于 Framebuffer 格式为 RGB,本来 A 所在的 channel 都为 0,所以用一些软件直接打开都是空白,只好写了脚本直接跳过 Alpha Channel。

Framebuffer 配置( fbset 输出):

mode "640x480-0"
        # D: 0.000 MHz, H: 0.000 kHz, V: 0.000 Hz
        geometry 640 480 1024 480 32
        timings 0 0 0 0 0 0 0
        accel false
        rgba 8/16,8/8,8/0,0/0
endmode

转换脚本(参考 [Tips] 擷取 framebuffer 畫面):

#!/usr/bin/perl -w

$w = shift || 240;
$h = shift || 320;
$pixels = $w * $h;

open OUT, "|pnmtopng" or die "Can't pipe pnmtopng: $!\n";

printf OUT "P6%d %d\n255\n", $w, $h;

while ((read STDIN, $raw, 4) and $pixels--) {
   $short = unpack('I', $raw);
   print OUT pack("C3",
      ($short & 0xff0000) >> 16,
      ($short & 0xff00) >> 8,
      ($short & 0xff));
}

close OUT;

用法: cat image.rgb | perl script.pl 1024 480 > console.png

在 Linux 中用 C 代码获取 DNS 服务器列表

最近在做一个作业的时候,发现里面有个步骤是获取 Linux 系统中的 DNS 服务器列表,它的方法很粗暴,直接 cat grep cut 再处理。我就在想有没有完全代码的实现,然后搜了一下,果然有:

#include <resolv.h>
// ...
res_init();
// _res.nsaddr_list is an array of resolvers

用到了全局变量 _res ,虽然很 hacky,但是至少是工作的,不清楚兼容性几何。

rCore 软路由实现

最近在研究软路由在 rCore 上的实现,但限于硬件限制,目前先在虚拟机里测试。软路由大概要做这些东西:

1. 抓包,解析包里的内容
2. 查路由表,找到下一跳在哪
3. 查 ARP,知道下一跳的 MAC 地址
4. 减少 TTL,更新 IP Checksum
5. 把包发出去

第一步直接拿 smoltcp 的 Raw Socket 即可,但是目前只能抓指定 IP Protocol 的包,我用的是 ICMP,但其他的就还抓不了,需要继续改 Smoltcp 源代码。

第二步用的是之前刚修好的 treebitmap 库,它提供了路由表的查询功能,目前路由表还是写死的,之后会用已经部分实现好的 Netlink 接口读取出来。

第三步则是 ioctl 发请求,然后从 smoltcp 内部的 ARP cache 里读取。

第四步很简单,不用多说。

第五步则需要指定出端口,用了一个 index,放在一个特定的 sockaddr 中。

最后的效果就是,能双向转发 ping 通。

网络拓扑:

可以,这很玄学。

后续在想在真机上实验,但是还缺一个网卡驱动,不然就可以用神奇的办法来做这个实验了。

静态编译 sqlite3

最近 rCore 支持了动态链接库,于是想着在测试 sqlite 的时候直接用动态的,不过出现了玄学的问题,它会访问一个不存在的地址,看代码也没看出个所以然来。所以研究了一下 sqlite 的静态编译。首先在 configure 的时候尝试了一下:

$ ./configure CC=x86_64-linux-musl-gcc --disable-shared --enabled-static

发现 libsqlite 确实是静态了,但是 sqlite3 并不是。一番研究以后,发现是 libtool 的原因,只要这样编译:

$ make LTLINK_EXTRAS=-all-static

就可以编译出静态的 sqlite3

sqlite3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

交叉编译 Nginx 1.14.2 到 RISC-V

最近又把一定的精力放到了 RISC-V 64 上的 rCore 用户态程序的支持上,同时也借到了 HiFive Unleashed 板子,所以有真实硬件可以拿来跑了。在这之前先在 QEMU 上把能跑的都跑起来。

由于 rCore 对 glibc 的支持一直有问题,RISC-V 也不例外,所以还是选择用 musl 来做这件事情。一般搜索,终于找到了 Linux 下能用的 musl-riscv-toolchain 。编译好工具链以后,很多需要 libc 的用户态都能跑了,于是想着试一下 nginx 的编译。试着编译了一下,遇到了各种问题,最后搜到了交叉编译 Hi3536 上面使用的 nginx,里面的方法解决了这个问题。最后总结出了这样的 patch :

diff --git a/nginx-1.14.2/auto/cc/name b/nginx-1.14.2/auto/cc/name
index ded93f5..d6ab27a 100644
--- a/nginx-1.14.2/auto/cc/name
+++ b/nginx-1.14.2/auto/cc/name
@@ -7,7 +7,7 @@ if [ "$NGX_PLATFORM" != win32 ]; then

     ngx_feature="C compiler"
     ngx_feature_name=
-    ngx_feature_run=yes
+    ngx_feature_run=no
     ngx_feature_incs=
     ngx_feature_path=
     ngx_feature_libs=
diff --git a/nginx-1.14.2/auto/lib/openssl/make b/nginx-1.14.2/auto/lib/openssl/make
index 126a238..7a0e768 100644
--- a/nginx-1.14.2/auto/lib/openssl/make
+++ b/nginx-1.14.2/auto/lib/openssl/make
@@ -51,7 +51,7 @@ END
 $OPENSSL/.openssl/include/openssl/ssl.h:   $NGX_MAKEFILE
    cd $OPENSSL \\
    && if [ -f Makefile ]; then \$(MAKE) clean; fi \\
-   && ./config --prefix=$ngx_prefix no-shared no-threads $OPENSSL_OPT \\
+   && ./config --prefix=$ngx_prefix no-shared no-threads --cross-compile-prefix=riscv64-linux-musl- $OPENSSL_OPT \\
    && \$(MAKE) \\
    && \$(MAKE) install_sw LIBDIR=lib

diff --git a/nginx-1.14.2/auto/types/sizeof b/nginx-1.14.2/auto/types/sizeof
index 480d8cf..52c7287 100644
--- a/nginx-1.14.2/auto/types/sizeof
+++ b/nginx-1.14.2/auto/types/sizeof
@@ -33,7 +33,7 @@ int main(void) {
 END


-ngx_test="$CC $CC_TEST_FLAGS $CC_AUX_FLAGS \
+ngx_test="gcc $CC_TEST_FLAGS $CC_AUX_FLAGS \
           -o $NGX_AUTOTEST $NGX_AUTOTEST.c $NGX_LD_OPT $ngx_feature_libs"

 eval "$ngx_test >> $NGX_AUTOCONF_ERR 2>&1"

接着,在 ./configure --with-cc=riscv64-linux-musl-gcc --with-cc-opt=-static --with-ld-opt=-static --without-pcre --without-http_rewrite_module --without-http_gzip_module --with-poll_module --without-http_upstream_zone_module 之后,修改 objs/ngx_auto_config.h,加入:

#ifndef NGX_SYS_NERR
#define NGX_SYS_NERR  132
#endif

#ifndef NGX_HAVE_SYSVSHM
#define NGX_HAVE_SYSVSHM 1
#endif

接着就可以正常编译了:

$ file objs/nginx
objs/nginx: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped