在 MacOS 上做 Linux 内核开发的一些有趣经验和坑

近一年多一直在用 MacOS 做 Linux 内核开发,因为是 POSIX 兼容的环境所以把一些坑旅顺之后,竟然感觉还挺顺手的,而且因为基本不用其他特性也没感受到几个 MacOS 的 Bug 问题,体验甚佳。不过也有一些奇怪的地方,花了一些精力去适配,还是挺值得的:

UNIX 兼容

MacOS 上还算友好的就这点,默认的用户目录就是 UNIX 环境中的 $HOME 变量,文件路径分隔符是 “ / ”,有 / 根目录,软连接,chmod 权限等,一些常见的命令基本都一样,不过是更偏 BSD 风格。对于不太依赖 Linux/GNU 特性的 POSIX 兼容程序,脚本之类的,基本都可以直接跑起来。所以整体对没有复杂依赖的内核开发,Web 开发等,还是很友好的。

直接在 Launchpad 上面搜索框里搜 Terminal / 终端,然后固定一个底端快捷方式),一些常用工具,比如 SUDO, SSH,都有,比如可以直接在终端中运行:

ssh-keygen -t rsa -b 4096 -o -a 100

生成的 SSH key 默认就放在 MacOS 用户目录,~/.ssh/id_rsa.pub。

当然也有很多不一样的,毕竟是苹果的 OS。桌面环境就是基于此进行了很多封装,更偏向于以 Application 为对象进行管理和交互,文件管理就不怎么方便了… 比较方便的是终端里可以用 open <filename> 的方式正常打开一个文件或者跳到 Finder 中。

当然默认并没有包管理器(下面会用 Homebrew 进行软件包管理)。

Homebrew 包管理

这个基本是 MacOS 上想做任何和代码沾边的事情的标配了,安装也很简单,跟着主页的操作就一两步:https://brew.sh/

之后就可以用 Brew 安装各种软件,Terminal 里执行 brew install <name> 来安装,brew update 升级,brew uninstall <name> 卸载,没什么特别的。

或许值得注意的是开发细节是,在 M1 平台上,Brew 默认把所有文件都安装到了 /opt/homebrew 中而不是我们常见的 /usr,然后通过更改 $PATH 的方式使其可用。

有时在系统更新后会出现一些 binary 不可用的情况,可以用 xcode-select –install 来解决。

终端和字体

MacOS 的字体渲染和配置是真的好,默认终端对我来说也足够用了,只是为了支持特殊符号,我一般会安装 Nerd Font 来支持一些奇怪的 TUI 应用符号。

MacOS 默认终端设置中可以更改默认 Profile 来改变样式,我改为了暗色主题 + 字体选择了 Nerd Font Hack,这里也有很多其他不错的字体可用:

https://www.nerdfonts.com/

适应 MacOS 的快捷键

MacOS 上似乎很多快捷键都和操作方式都是完全钉死的… 因此我也不打算去 Hack 相关内容,尽量去适应 MacOS 的操作逻辑,实际用下来竟然意外的不错。

工作流上,每次基本只打开一个终端实例,然后按 Ctrl + Command + F 设为全屏,这样这个桌面我就只用来访问终端了。Ctrl + ⬅️ 和 Ctrl + ➡️ 可以只用键盘在终端和其他 App 之间切换,在终端页面 Command + T 创建新 Tab,每个 Tab 里用开一个 Tmux,这样,Command + 数字 1 – 9 切换 Tab,每个 Tab 里 Ctrl + B 后按 1 – 9 切换 Tmux 窗口,一个 Tab 可以专注一个 Task。

Shell 选择和配置

MacOS 自带的 Shell 很老旧,Bash 3.x,我切换到了 Fish。

Fish Shell:https://fishshell.com/,官方整合了 Brew 安装,即直接执行 brew install fish。安装后在 Terminal 中按 Command + “,” 打开设置,把 General 中的 “Shells open with” 改成 “/opt/homebrew/bin/fish”,新打开的 Terminal 就默认使用 Fish 了。

在 Shell 上进行更一步的定制化就和其他 OS 差异不大,比如我一直在用的 Starship,一个可以提供非常好看的 Shell Prompt:https://starship.rs/,按照 Installation 中的 macOS 以及 Fish 对应的方法即可配置安装。

我自己的 Shell 中各种 Helper 和 配置都比较繁多,整理了一个 Git Repo:https://github.com/ryncsn/.home

大小写敏感的 Volume

MacOS 上我遇到的第一个重大问题就是文件系统是默认大小写不敏感的,我的解决方法也还好,在 Launchpad 里面的 Disk Utility 里可以很方便的创建一个新 Volume ,创建的时候格式可以选择带 “Case-Sensitive” 的即可。这个 Volume 类似 ZFS / BTRFS 中的抽象 Volume,不需要预设大小等。

点击图上右上角加号就可以添加 Volume 了。

新建的 Volume 会出现在 /Volume 路径中,为了方便使用我用 ln -s /Volume/<vol name>/<dir> ~/<dir> 的形式,将需要区分大小写的项目等放在软连接的目录中。

虚拟机

目前 HVF + Libvirt 可用度还不错,只需要用 brew install libvirt,然后 brew service start libvirt,就可以使用熟悉的 virsh 操作了。我的虚拟机 XML 参考如下,注意把里面 CREATE_YOUR_OWN_QCOW2_IMAGE_HERE 换成你自己的 Qcow2 image。

<domain type='hvf'>
  <name>VM1</name>
  <memory unit='MiB'>4096</memory>
  <vcpu placement='static'>8</vcpu>
  <os firmware='efi'>
    <type arch='aarch64' machine='virt-6.2'>hvm</type>
    <firmware>
      <feature enabled='no' name='enrolled-keys'/>
      <feature enabled='no' name='secure-boot'/>
    </firmware>
    <loader readonly='yes' type='pflash'>/opt/homebrew/Cellar/qemu/8.0.2/share/qemu/edk2-aarch64-code.fd</loader>
    <nvram template='/opt/homebrew/Cellar/qemu/8.0.2/share/qemu/edk2-arm-vars.fd'></nvram>
    <boot dev='cdrom'/>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <gic version='2'/>
  </features>
  <cpu mode='custom' match='exact' check='none'>
    <model fallback='forbid'>host</model>
  </cpu>
  <clock offset='utc'>
    <timer name='rtc' tickpolicy='catchup'/>
    <timer name='hpet' present='no'/>
    <timer name='pit' tickpolicy='delay'/>
  </clock>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <devices>
    <emulator>/opt/homebrew/bin/qemu-system-aarch64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='CREATE_YOUR_OWN_QCOW2_IMAGE_HERE'/>
      <target dev='vda' bus='virtio'/>
    </disk>
    <interface type='user'>
      <mac address='52:54:00:1f:51:94'/>
      <model type='virtio'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </interface>
    <serial type='pty'>
      <target type='system-serial' port='0'>
        <model name='pl011'/>
      </target>
    </serial>
    <console type='pty'>
      <target type='serial' port='0'/>
    </console>
    <audio id='1' type='none'/>
  </devices>
  <seclabel type='none' model='none'/>
</domain>

因为经常需要对虚拟机的细节进行调整和串口访问,又不想操作太多 Qemu 命令,所以使用了 Libvirt 来管理。这个虚拟机使用的是 HVF,硬件虚拟化没有什么开销,不过用的是 SLIRP 网络栈,网络性能略有损失,虚拟机看到的宿主机 IP 是 10.0.2.2。

NFS 文件共享

文件共享我用的是 MacOS 自带的 NFSv3,配置 /etc/exports 即可,不过 exports 里面格式比较怪,如下(详细可在 Terminal 中运行 man exports 来查看):

/Volumes/<YOUR_NFS_SHARE_PATH> -alldirs -maproot=root:wheel 127.0.0.1

直接挂载可能会看到类似如下错误:

mount -v -t nfs 10.0.2.2:/Volumes/XXX /mnt/Volume/XXX
mount.nfs: timeout set for Mon Oct 30 17:41:01 2023
mount.nfs: trying text-based options 'vers=4.2,addr=10.0.2.2,clientaddr=10.0.2.15'
mount.nfs: mount(2): Protocol not supported
mount.nfs: trying text-based options 'vers=4,minorversion=1,addr=10.0.2.2,clientaddr=10.0.2.15'
mount.nfs: mount(2): Protocol not supported
mount.nfs: trying text-based options 'vers=4,addr=10.0.2.2,clientaddr=10.0.2.15'
mount.nfs: mount(2): Protocol not supported
mount.nfs: trying text-based options 'addr=10.0.2.2'
mount.nfs: prog 100003, trying vers=3, prot=6
mount.nfs: trying 10.0.2.2 prog 100003 vers 3 prot TCP port 2049
mount.nfs: prog 100005, trying vers=3, prot=17
mount.nfs: trying 10.0.2.2 prog 100005 vers 3 prot UDP port 835
[933.642116] RPC: server 10.0.2.2 requires stronger authentication.
mount.nfs: mount(2): Permission denied
mount.nfs: access denied by server while mounting 10.0.2.2:/Volumes/XXX

原因是 MacOS 的 NFS server 默认要求源端口需要是 reserved 范围内,这个并不能保证而且并不能提升安全性。在 MacOS 侧的 /etc/nfs.conf 中,加入如下内容即可修复:

nfs.server.require_resv_port = 0
nfs.server.mount.require_resv_port = 0

另外值得一提的是,不需要串口的,需要更好的网络和文件共享性能,只想用 Docker 等的话,podman 是个非常好的选择,brew 里的 podman 自带虚拟机管理,会启动一个虚拟机并通过其来。

内核代码补全

在 Mac OS 上进行原生代码编辑和补全比想象中容易得多,我使用 VIM + YCM,配合这个 .ycm_extra_conf.py:

https://github.com/ryncsn/.home/blob/master/misc/linux.ycm_extra_conf.py

在配置好 Vim YCM 插件后,只需要拷贝到 Linux 内核目录下就可以进行补全了,错误检查会有一些 False Positive,一些特殊的文件格式,比如 sched 中一些 C 语言原文件是 Concat 而成的,这个还暂时没法很好的处理。

(持续更新中)