<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>喵's StackHarbor</title>
  <subtitle>Whisper to the World</subtitle>
  <icon>https://sh.alynx.one/images/Mikoto-Karon-White.webp</icon>
  <link rel="self" type="application/atom+xml" href="https://sh.alynx.one/atom.xml" />
  <link rel="alternate" type="text/html" href="https://sh.alynx.one/" />
  <updated>2026-03-03T11:29:49.355Z</updated>
  <id>https://sh.alynx.one/</id>
  <author>
    <name>Alynx Zhou</name>
    <email>alynx.zhou@gmail.com</email>
  </author>
  <generator uri="https://github.com/AlynxZhou/hikaru-generator-feed/" version="v2.5.2">Hikaru Generator Feed</generator>
  <entry>
    <title>Arch Linux ARM 在树莓派上的内核选择</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/RPi-Arch-Linux-ARM-Kernel/" />
    <id>https://sh.alynx.one/posts/RPi-Arch-Linux-ARM-Kernel/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2026-02-06T09:03:00.000Z</published>
    <updated>2026-02-06T09:03:00.000Z</updated>
    <summary type="html">
      对于 64 位的树莓派 3 和树莓派 4，安装 Arch Linux ARM 的话有两种内核可以选择，一种是来自博通的 rpi 分支，包含一些树莓派特定的补丁，另一种是来自主线的 mainline 分支，这个和其它系统的内核来源没什么区别。如果你选择安装 64 位的系统，那默认附…
    </summary>
    <content type="html">
      &lt;p&gt;对于 64 位的树莓派 3 和树莓派 4，安装 Arch Linux ARM 的话有两种内核可以选择，一种是来自博通的 rpi 分支，包含一些树莓派特定的补丁，另一种是来自主线的 mainline 分支，这个和其它系统的内核来源没什么区别。如果你选择安装 64 位的系统，那默认附带的是 mainline 内核，使用上没有什么问题，包括 GPU 驱动也是可用的，但是常见的那些操作树莓派 GPIO 的库和代码都假设你使用的 rpi 内核，会通过 rpi 内核特定的接口操作，因此如果你需要使用 GPIO 但又不想自己从头操作 GPIO 的话就需要使用 rpi 内核。但是默认只有 32 位的 Arch Linux ARM 使用的才是 rpi 内核，在 64 位机器上运行 32 位系统感觉怪怪的。&lt;/p&gt;
&lt;p&gt;在我刚拿到 64 位的树莓派的时候就是这么个情况，也许是历史遗留问题，那时候似乎没有运行 rpi 内核的 64 位系统可以选择。但是我最近把吃灰的树莓派 4 拿出来的时候搜索了一下包列表，发现其实已经有 rpi 内核可以用了，这里简单记录一下怎么切换成 rpi 内核。&lt;/p&gt;
&lt;p&gt;具体安装只需要安装下面两个包：&lt;/p&gt;
&lt;figure data-raw=&quot;# pacman -S linux-rpi raspberrypi-bootloader
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# pacman -S linux-rpi raspberrypi-bootloader
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Mainline 内核使用 u-boot 作为 bootloader，而 rpi 内核需要使用对应的 bootloader，安装的时候会提示你要移除原本的内核和 u-boot，确认就可以了。然后如果你改过 mainline 内核的启动参数，那需要也对 rpi 内核修改一下，因为两者修改启动参数的方式不一样，rpi 内核和其它的树莓派系统一样使用 &lt;code&gt;cmdline.txt&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后在安装 mainline 内核的系统时你应该已经按照安装说明把 fstab 里面的 &lt;code&gt;mmcblk0&lt;/code&gt; 换成了 &lt;code&gt;mmcblk1&lt;/code&gt;，这时候需要换回 rpi 内核使用的 &lt;code&gt;mmcblk0&lt;/code&gt;。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="树莓派" label="树莓派" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E6%A0%91%E8%8E%93%E6%B4%BE/" />
    <category term="树莓派" label="树莓派" scheme="https://sh.alynx.one/tags/%E6%A0%91%E8%8E%93%E6%B4%BE/" />
  </entry>
  <entry>
    <title>Emacs 在终端里大喊 OI</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Emacs-Says-OI-in-Terminal/" />
    <id>https://sh.alynx.one/posts/Emacs-Says-OI-in-Terminal/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2026-02-01T08:22:00.000Z</published>
    <updated>2026-02-01T08:22:00.000Z</updated>
    <summary type="html">
      Emacs 和大部分现代的代码编辑器不同，它除了是 GUI 编辑器还是一个 TUI 编辑器（或者说 GUI 的部分只是实现了一个特别的终端模拟器更合适）。所以当我出门使用 MacBook 但又要写 Linux 项目的时候，我发现直接 ssh 到 Linux 机器上调用 TUI 的…
    </summary>
    <content type="html">
      &lt;p&gt;Emacs 和大部分现代的代码编辑器不同，它除了是 GUI 编辑器还是一个 TUI 编辑器（或者说 GUI 的部分只是实现了一个特别的终端模拟器更合适）。所以当我出门使用 MacBook 但又要写 Linux 项目的时候，我发现直接 ssh 到 Linux 机器上调用 TUI 的 Emacs 比把代码同步过来再同步回去更加方便。但是很快我遇到一个奇怪的问题，只要我在 iTerm2 上移动鼠标，Emacs 里面就插入出现一串奇怪的编码一样的内容，然后只要切到别的窗口再切回 iTerm2，Emacs 就会插入一次 &lt;code&gt;OI&lt;/code&gt;。如果是在 tmux 里面运行 Emacs 则不会有这个问题，让我百思不得其解。&lt;/p&gt;
&lt;p&gt;我甚至关掉了 Emacs 对于终端鼠标事件的支持，但是 &lt;code&gt;OIOI&lt;/code&gt; 还是存在，我开始怀疑这是 iTerm2 的鼠标支持的问题，于是我去 Emacs 中文群里面问了一下。群友 @Kana 建议我使用 &lt;code&gt;emacs -Q -nw&lt;/code&gt; 测试一下会不会是我配置文件的问题，因为他之前遇到过同样的问题，原因是绑定了 &lt;code&gt;M-[&lt;/code&gt; 的快捷键，结果我试了一下还真是！我把 &lt;code&gt;M-[&lt;/code&gt; 绑定到了一个减少 tab stop 的函数上，注释掉这一行就好了。&lt;/p&gt;
&lt;p&gt;既然知道问题出在哪里，原因也就很好分析了。终端里的复杂功能是通过使用 &lt;code&gt;\e[&lt;/code&gt; 开头的转义控制序列实现的，比如最常见的颜色输出就是通过 &lt;code&gt;\e[&lt;/code&gt; 开头后面跟上颜色编码实现，鼠标事件和窗口焦点切换也是。而 Emacs 有一个特性是如果你的键盘没有 &lt;code&gt;Alt&lt;/code&gt; 键，那么你可以先按下 &lt;code&gt;Esc&lt;/code&gt; 键再松开，然后再按下另一个键，就等价于按下 &lt;code&gt;Alt&lt;/code&gt; 键和这个按键的组合键。恰好 &lt;code&gt;\e&lt;/code&gt; 是 &lt;code&gt;Esc&lt;/code&gt;，于是转义序列就被 Emacs 识别为一个 &lt;code&gt;M-[&lt;/code&gt; 的组合键然后执行了我绑定的函数，后面的编码就会被认为是普通字符插入到 buffer 里面。所以对于 &lt;code&gt;\e[I&lt;/code&gt; 和 &lt;code&gt;\e[O&lt;/code&gt;，看起来就好像 Emacs 在不停大喊 OIOIOI 一样。以前我是完全不考虑在终端里运行 Emacs 的，所以就忽略了这个问题。&lt;/p&gt;
&lt;p&gt;PS 每年跨年之后我都会忘记要写新一年的年份，比如刚刚我就把这篇文章的日期写成了 2025 年 2 月 1 号然后思考为什么它没有出现在首页上。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Emacs" label="Emacs" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Emacs/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Emacs" label="Emacs" scheme="https://sh.alynx.one/tags/Emacs/" />
  </entry>
  <entry>
    <title>我始终搞不懂多光标编辑和查找替换的区别在哪</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/No-Idea-about-Multiple-Cursors/" />
    <id>https://sh.alynx.one/posts/No-Idea-about-Multiple-Cursors/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-12-21T03:16:29.000Z</published>
    <updated>2025-12-21T03:16:29.000Z</updated>
    <summary type="html">
      许多代码编辑器的官网都会展示自己的多光标编辑功能，乍一看这似乎是个效率很高很花哨的功能，但是实际上我从来没能真正的在实际写代码时利用它。 最早推出多光标编辑的可能是 Sublime Text，但是我没有使用过。我在使用 Atom 的时候第一次接触到这个，Atom 激活多光标的方法…
    </summary>
    <content type="html">
      &lt;p&gt;许多代码编辑器的官网都会展示自己的多光标编辑功能，乍一看这似乎是个效率很高很花哨的功能，但是实际上我从来没能真正的在实际写代码时利用它。&lt;/p&gt;
&lt;p&gt;最早推出多光标编辑的可能是 Sublime Text，但是我没有使用过。我在使用 Atom 的时候第一次接触到这个，Atom 激活多光标的方法之一是使用 Ctrl + 鼠标点击来产生多个光标，这可以让你快速的定位到你需要编辑的位置，唯一的缺点是你的手需要先离开键盘去拿鼠标。如果你不想让手离开键盘再回来（或者你的场景不适合用鼠标），那剩下的插入光标的方法无非是在上一行或者下一行的相同列插入光标，或者选中什么文本然后在所有相同的文本处插入光标。&lt;/p&gt;
&lt;p&gt;这就导致一个关键问题：每当我意识到“这个地方是否可以用多光标编辑一次解决”的时候，最后都会变成用查找替换解决。不信？当你在所有相同文本处插入光标的时候，不就是对这个文本进行了一次查找吗？然后你对所有这些光标进行的编辑也是相同的，那不就是替换吗？&lt;/p&gt;
&lt;p&gt;或者换一种说法，其实多光标编辑最大的问题是：除了用鼠标，没有什么简单的办法定位到许多不同的需要操作的文本。这个问题也不是多光标能解决的，多光标只能解决定位之后的事。我唯一能想到的可以绕开定位问题的场景是模版补全，编辑器自动给模版里多个需要用户起变量名的地方插入多光标，用户可以一次输入这些变量名。但是这又要求模版补全的插件考虑到是否有多光标功能。&lt;/p&gt;
&lt;p&gt;如果你能想到什么非常适合多光标编辑的场景和更加简单的用法，希望你能在评论区告诉我，这样也许我能意识到怎么用多光标更合适然后成功的把它加入我的工作流。&lt;/p&gt;
&lt;p&gt;与此类似的是，我经常意识到在编辑代码的时候自己的操作能力是有限的。如果有一些看起来效率很高的操作，但是由于各种原因，比如使用频率太低了记不住，或者操作起来需要思考很久，那它带来的思维负担超出了进行一系列下意识简单操作的难度，我会倾向于干脆放弃。一个不太恰当的例子可能是我在 Emacs 里面实现了类似 Vim 的按 &lt;code&gt;C-o&lt;/code&gt; 在当前行下面添加一行空行，但实际上我根本没有用过它，因为我发现我按 &lt;code&gt;C-e Enter&lt;/code&gt; 并不会慢多少，而且我根本不需要思考就可以做出这个操作。再比如虽然几乎所有 Emacs 用户都在用 magit，但我的习惯还是回到终端里面操作 git。总之我常常羡慕别人编辑代码时花哨的操作，但同时我又会意识到这也许真的不适合我。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Emacs" label="Emacs" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Emacs/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Emacs" label="Emacs" scheme="https://sh.alynx.one/tags/Emacs/" />
  </entry>
  <entry>
    <title>通过 Intel 无线网卡远程唤醒台式机</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Wake-on-WLAN/" />
    <id>https://sh.alynx.one/posts/Wake-on-WLAN/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-10-06T04:37:56.000Z</published>
    <updated>2025-10-11T03:01:01.000Z</updated>
    <summary type="html">
      有些时候需要开启我的台式机处理一些任务，特别是出门在外的时候，但是台式机里有两块 NVIDIA GPU，如果一直开机也太不节能环保了。所以就想研究一些能远程开机/唤醒的方案。 最简单的方案肯定是 BIOS 里设置来电就开机，然后购买一个智能插座。但是在智能插座到货之前我就要出门，…
    </summary>
    <content type="html">
      &lt;p&gt;有些时候需要开启我的台式机处理一些任务，特别是出门在外的时候，但是台式机里有两块 NVIDIA GPU，如果一直开机也太不节能环保了。所以就想研究一些能远程开机/唤醒的方案。&lt;/p&gt;
&lt;p&gt;最简单的方案肯定是 BIOS 里设置来电就开机，然后购买一个智能插座。但是在智能插座到货之前我就要出门，所以决定排列组合一下手头的方案。&lt;/p&gt;
&lt;p&gt;BIOS 里通常都会有 Wake on LAN 的支持，这个是网卡收到特定格式的数据包，检查和自己 MAC 地址一致之后就会通过 PCI-E 唤醒电脑。所以一开始我打算尝试这个，反正 Mac mini 待机功耗非常低，我可以一直开着 Mac mini，然后需要的时候 ssh 到 Mac mini 上再给台式机发包唤醒它。&lt;/p&gt;
&lt;p&gt;第一个障碍是虽然我的 Mac mini 和 NAS 和台式机之间是有网线连接的，但是 Intel X550 这个万兆网卡是不支持 Wake on LAN 的。所以我不得不再拿一根网线把主板的板载网卡和 Mac mini 连接起来。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&quot;https://wiki.archlinux.org/title/Wake-on-LAN&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;Arch Wiki 的 Wake-on-LAN 页面&lt;/a&gt;，需要在 BIOS 的高级电源管理里打开由 PCI-E 设备唤醒，但是只打开这个不行，因为这样关机时候网卡还是没有启用的，所以需要打开 UEFI 网络堆栈，让网卡即使是在关机的状态下也可以启用。&lt;del&gt;然后根据网上的说法还需要关闭 BIOS 的快速自检&lt;/del&gt; 经过验证并不需要关闭 BIOS 的快速自检，也许大家把这个和 Windows 的快速启动搞混了。然后在系统里用 &lt;code&gt;ethtool -s eth0 wol g&lt;/code&gt; 开启网卡的 Wake on LAN 支持。但是即使以上都打开了，还是不能保证能用。至少我主板上的两个网卡都没法唤醒机器。开机状态下用 netcat 可以看到 Wake on LAN 的 magic packet，关机/睡眠状态下可以看到网卡灯亮着，但是就是没有办法唤醒机器。按道理说 Wake on LAN 只和连接层有关，所以并不需要配置 IP，我也不清楚中间哪里有问题导致不能用，也许是 macOS 或者是什么的限制导致必须指定 IP？但 BIOS 里又没有办法配置 UEFI IPXE 的 IP 地址。&lt;/p&gt;
&lt;p&gt;就在我觉得没有希望的时候，@lilydjwg 跟我说其实无线网卡也是可以唤醒电脑的，Intel AX200 确实支持这个功能。只要使用 &lt;code&gt;iw phy phy0 wowlan enable magic-packet&lt;/code&gt; 启用就可以了。也许需要用 &lt;code&gt;nmcli con modify CONNECTION 802-11-wireless.wake-on-wlan 0x8&lt;/code&gt; 给 Network Manager 也启用一下。&lt;/p&gt;
&lt;p&gt;唤醒的方式和 Wake on LAN 一样，Linux 下面可以使用 &lt;code&gt;wol&lt;/code&gt;，macOS 下面可以使用 &lt;code&gt;wakeonlan&lt;/code&gt; 这个脚本。需要注意的是无线和有线一样需要有连接才能收到包，有线只要插着网线就行了，但无线需要保持你的 WiFi 连接，而 UEFI 是没有无线网络支持的，所以 Wake on WLAN 只有睡眠状态能唤醒，不像 Wake on LAN （理论上）支持关机状态开机，但是对我的省电需求来说，休眠到内存也足够了。&lt;/p&gt;
&lt;p&gt;因为需要一直保持 WiFi 连接才能用，如果你的网络管理器/无线网卡开启了什么省电模式，它可能会在睡眠一定时间后断掉连接，然后就没办法唤醒了。这自然不是我们希望的，所以可以使用 &lt;code&gt;nmcli con modify CONNECTION wifi.powersave 2&lt;/code&gt; 关掉 Network Manager 的省电模式，然后给 &lt;code&gt;iwlwifi&lt;/code&gt; 内核模块设置 &lt;code&gt;power_save=0&lt;/code&gt; 关掉驱动的省电模式。&lt;/p&gt;
&lt;p&gt;使用流程就是 &lt;code&gt;systemctl suspend&lt;/code&gt; 让电脑休眠。然后用同一个网段的 Mac mini 执行 &lt;code&gt;wakeonlan -i 目标 IP 地址 目标无线网卡 MAC 地址&lt;/code&gt;，就可以发出 magic packet，台式机收到后就会唤醒了。理论上来说这个和 IP 无关所以可以不指定 IP 地址以发送 magic packet 到广播地址，但是不知道为什么这样唤醒不了。&lt;/p&gt;
&lt;p&gt;需要注意的是以上各种依赖的都是 PCI-E 设备可以唤醒系统的功能，因此只有板载的无线网卡或者 PCI-E 的无线网卡可能支持唤醒，USB 无线网卡是不行的。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;更新（2025-10-11）：我似乎摸索出了有线网卡唤醒的正确方案，默认 &lt;code&gt;wakeonlan&lt;/code&gt; 使用的 &lt;code&gt;255.255.255.255&lt;/code&gt; 这个广播地址不起作用。似乎你应该使用的不是目标网卡的 IP 地址而是 &lt;strong&gt;要发包的网卡所在网段的广播地址&lt;/strong&gt;：比如我把 Mac mini 和台式机用网线直接连接起来，然后给 Mac mini 的有线网卡设置 IP 地址为 &lt;code&gt;10.10.11.9/24&lt;/code&gt;，那么只要我使用 &lt;code&gt;wakeonlan -i 10.10.11.255 目标有线网卡 MAC 地址&lt;/code&gt; 就可以正确唤醒台式机了。即使台式机处在关机状态，只要打开了 BIOS 里的由 PCI-E 唤醒和 UEFI 网络堆栈也可以开机。对于 IP 是 &lt;code&gt;192.168.1.9/24&lt;/code&gt; 的 Mac mini 无线网卡，也可以使用 &lt;code&gt;wakeonlan -i 192.168.1.255 目标无线网卡 MAC 地址&lt;/code&gt;。我不知道为什么使用目标无线网卡的 IP 地址也可以唤醒，但也许是路由器替我做了什么工作，没有路由器的情况下就必须使用要发包的网卡所在网段的广播地址了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/tags/Linux/" />
  </entry>
  <entry>
    <title>ReFrame：又一个 Linux 远程桌面</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/ReFrame-Yet-Another-Remote-Desktop-for-Linux/" />
    <id>https://sh.alynx.one/posts/ReFrame-Yet-Another-Remote-Desktop-for-Linux/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-09-17T01:48:39.000Z</published>
    <updated>2025-09-17T01:48:39.000Z</updated>
    <summary type="html">
      虽然大部分时间有 SSH 就足够方便高效的控制一台 Linux 设备，但是难免会遇到一些需要在某台机器的桌面环境下才能处理的需求。Linux 下面有各种各样的远程桌面实现方案，但是也各有各的问题。 传统基于 X server 的远程桌面方案就不用说了，2025 年连 NVIDIA…
    </summary>
    <content type="html">
      &lt;p&gt;虽然大部分时间有 SSH 就足够方便高效的控制一台 Linux 设备，但是难免会遇到一些需要在某台机器的桌面环境下才能处理的需求。Linux 下面有各种各样的远程桌面实现方案，但是也各有各的问题。&lt;/p&gt;
&lt;p&gt;传统基于 X server 的远程桌面方案就不用说了，2025 年连 NVIDIA 都支持 Wayland 了，当然不能要求用户使用远程桌面之前再注销登录进 X11 会话。RustDesk 是个流行的远程桌面方案，但是它不支持 Wayland 下面的远程登录。如果不考虑其它桌面，只考虑我使用的 GNOME 的话，GNOME Remote Desktop 确实号称自己支持远程登录，但是当初我们讨论这个的实现方式的时候，由于 GNOME 的远程桌面接口是每个用户会话独立的，所以必然涉及不同的服务之间的切换，然后他们选择用 RDP 协议的 ServerRedirection 来实现，结果就是只能使用 RDP，而且不是所有的 RDP 客户端都支持 ServerRedirection，比如 macOS 下的 Windows App 就不支持，于是对我来说也几乎不能用。而由于我不用 wlroots，我也不确定 wayvnc 是否支持远程登录。&lt;/p&gt;
&lt;p&gt;解决问题的希望出现在 &lt;a href=&quot;https://github.com/isjerryxiao/kmsvnc/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;kmsvnc&lt;/a&gt;，这个项目使用 DRM 的 API 获取显卡输出的 frame buffer，因此和使用什么桌面完全无关，然后使用 VA-API 将 frame buffer 解码成像素数据交给 VNC 服务器，使用 uinput 处理 VNC 服务器的输入数据。而对于没有连接显示器的设备，可以使用 Linux 的功能强制开启一个显示输出。虽然这个项目还有一些问题，比如在 NVIDIA 显卡上 VA-API 没法解码，但是和作者 &lt;a href=&quot;https://github.com/isjerryxiao/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@isjerryxiao&lt;/a&gt; 聊了一下之后，他觉得应该使用 OpenGL 的 API 来解码 frame buffer，因为显卡的渲染部分总是要能处理自己的格式的。然后还有些其它的问题，比如 kmsvnc 只在启动时获取一次 buffer ID，导致从登录界面切换到用户会话时不会更新显示内容，以及由于 DRM API 需要权限，整个程序都以 root 权限运行，这对一个网络服务来说不是很安全，我把这些作为改进的目标。&lt;/p&gt;
&lt;p&gt;为了解决权限问题，我决定将程序分成两部分，高权限的进程仅仅负责从 DRM 抓取 frame buffer 和向 uinput 写入输入事件，低权限的进程负责启动一个 VNC 服务器，将 frame buffer 渲染成像素数据传给客户端和处理客户端的输入事件。DRM 导出和 OpenGL 导入交换的是内核的 DMA-BUF 文件描述符，而 UNIX socket 是可以传送文件描述符的，所以两个进程使用 UNIX socket 通信。为了避免重复造轮子，直接使用了 GLib 的各种工具和事件循环。&lt;/p&gt;
&lt;p&gt;抓取 frame buffer 的部分其实没有什么困难，而输入处理的部分值得注意的是正常的坐标输入设备分为相对和绝对两种类型，常见的鼠标和触摸板属于相对设备，而触摸屏属于绝对设备，当然像 VNC 传过来的坐标自然也是绝对坐标，所以这里需要模拟成绝对设备，不然很难处理客户端和服务端初始的光标位置不一致的情况。像 Qemu 这种虚拟机，默认虚拟的指针设备也是触摸屏而非鼠标。唯一一个需要注意的点是你需要给 uinput 的指针设备指定 X 轴和 Y 轴的长度，但是这里不要求你比例和屏幕尺寸一致。因为一般桌面环境会将指针设备的 X 轴和 Y 轴映射到整个虚拟桌面的大小，所以你需要的其实是鼠标指针在整个虚拟桌面中位置的比例，然后直接乘上你设置的长度值。&lt;/p&gt;
&lt;p&gt;OpenGL 的部分和普通的程序不一样的地方在于普通的程序一般是跑在窗口系统里的，使用 EGL 建立 OpenGL Context 的时候建立的是基于窗口系统的，但是这个程序是独立在窗口系统外面的，所以使用的是 EGL 的 surfaceless 平台，需要设置 &lt;code&gt;EGL_PLATFORM=surfaceless&lt;/code&gt; 进行离屏渲染，然后对于大部分双显卡的系统，默认的 surfaceless 设备并不是实际用于输出的设备，但是只有显卡自己才能解码自己的输出格式，因此还要选择用哪个显卡渲染才行。至于将 DMA-BUF 的文件描述符导入成 OpenGL 纹理的 &lt;code&gt;EGL_LINUX_DMA_BUF_EXT&lt;/code&gt; 虽然是个扩展，但是因为 0 拷贝的渲染需求实在是太常见了，一般的显卡应该都实现了，所以不算什么特别过分的要求。&lt;/p&gt;
&lt;p&gt;输入事件处理最难的地方在于把客户端的坐标映射成相对整个虚拟桌面的全局坐标，因为虚拟桌面其实是桌面环境的概念，我们没什么特别好的办法拿到整个虚拟桌面的尺寸，和当前选中的显示器在虚拟桌面中的相对位置，只能把这个问题留给用户，让用户手动设置在配置文件里面。&lt;/p&gt;
&lt;p&gt;整个进程完全是单线程的，不管是 socket 通信还是 VNC 服务器都跑在 GLib 的事件循环上，既然单线程的性能完全足够，就没有必要搞多线程增加额外的复杂度了。我觉得最妙的地方是我利用 systemd 的 socket 来启动高权限的进程，这样就做到只有当有 VNC 客户端连接进来时，高权限的部分才会运行，而所有 VNC 客户端都退出之后，高权限的部分也会退出。按需启动高权限进程增加了安全性，也减少了不必要的资源占用。&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/AlynxZhou/reframe/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/tags/Linux/" />
    <category term="远程桌面" label="远程桌面" scheme="https://sh.alynx.one/tags/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2/" />
  </entry>
  <entry>
    <title>在 NAS 上部署更多服务</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Deploy-More-Services-on-NAS/" />
    <id>https://sh.alynx.one/posts/Deploy-More-Services-on-NAS/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-08-08T11:24:38.000Z</published>
    <updated>2025-08-08T11:24:38.000Z</updated>
    <summary type="html">
      &lt;div class=&quot;alert-blue&quot;&gt;由于篇幅限制就不写详细的配置教程了，只会记录一下文档里没有的需求和解决方案。&lt;/div&gt;

&lt;p&gt;基本上我在 NAS 上部署服务的时候相当克制，虽然我装了一个完整版的 Linux，但其实也就跑了 samba 共享、headscale 和 transmission 下载这几个。我也没有什么出门在外远程看家里的蓝光电影的需求，再说公网网速也很难满足这个需求，所以除非确实需要什么新功能，我会尽量少在 NAS 上运行服务。不过最近我还是发现了一些需求需要部署其它服务来解决。&lt;/p&gt;

    </summary>
    <content type="html">
      &lt;div class=&quot;alert-blue&quot;&gt;由于篇幅限制就不写详细的配置教程了，只会记录一下文档里没有的需求和解决方案。&lt;/div&gt;

&lt;p&gt;基本上我在 NAS 上部署服务的时候相当克制，虽然我装了一个完整版的 Linux，但其实也就跑了 samba 共享、headscale 和 transmission 下载这几个。我也没有什么出门在外远程看家里的蓝光电影的需求，再说公网网速也很难满足这个需求，所以除非确实需要什么新功能，我会尽量少在 NAS 上运行服务。不过最近我还是发现了一些需求需要部署其它服务来解决。&lt;/p&gt;
&lt;a id=&quot;more&quot;&gt;&lt;/a&gt;

&lt;h1 id=&quot;PhotoPrism&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Deploy-More-Services-on-NAS/#PhotoPrism&quot;&gt;&lt;/a&gt;PhotoPrism&lt;/h1&gt;
&lt;p&gt;一个我早就想解决的需求是按时间线快速查看照片，我发现翻看以前的照片非常有意思，有些已经忘掉的事情看到照片就会想起当时的心情，所以要是能快速的点开过去某个时间的照片就好了。&lt;/p&gt;
&lt;p&gt;为了不把自己绑死在某个后期软件上，我没有使用诸如 Lightroom 这类软件内置的照片库管理，而是直接将照片从存储卡里复制到磁盘阵列里。因为不想手动管理重复项，所以我就保持相机的文件夹逻辑。索尼相机虽然在拍照时可以直接按照日期建立文件夹，但是它是前四位序号加后四位月份日期，因此按名称排序并不是时间顺序。即使忽略掉这个问题，也没有办法快速的查看什么时候拍了什么照片，因为文件管理器通常并不能让你隔着文件夹看到里面都有什么照片。再考虑到拍了四年照片之后我已经积攒了相当大的照片库，直接打开一个个目录查看源文件对于磁盘 IO 速度是个相当大的考验。所以是时候部署一个能自动给照片分时分类创建缩略图的服务了。&lt;/p&gt;
&lt;p&gt;我选择的方案是 PhotoPrism，当然有好几个类似的项目可选，我就是随便选了一个，没什么特殊理由。PhotoPrism 的官网建议用户以 docker 的方式部署，但我不太喜欢容器化，而且我只有一台服务器也享受不到容器化带来的什么方便，所以我选择通过 AUR 的 &lt;code&gt;photoprism&lt;/code&gt; 包安装。&lt;/p&gt;
&lt;p&gt;官方同时支持 sqlite 和 mysql 两个数据库，但是对于实际的项目使用 mysql 性能要好很多。如果你按照官方的文档使用 docker 应该会要你再部署一个 mariadb 的容器并配置它，但是却没有提如果不用 docker 要怎么办。其实也很简单，总之先装一个 mariadb 配置好并启动，这一部分直接参照 Arch Wiki 就好了。然后我创建了一个叫做 &lt;code&gt;photoprism&lt;/code&gt; 的数据库和一个 &lt;code&gt;photoprism&lt;/code&gt; 的数据库用户，再给这个用户设置好密码，给它操作这个数据库的权限，数据库准备工作就算完成了。&lt;/p&gt;
&lt;p&gt;使用 docker 的话通常是编辑 compose 文件里面的环境变量来配置 PhotoPrism，不使用 docker 的话则是通过配置文件。AUR 包里面的 PhotoPrism 会加载 &lt;code&gt;/etc/photoprism/defaults.yml&lt;/code&gt;，里面只有几个路径要设置，当然其实也可以不改路径，毕竟我不打算把照片导入到 PhotoPrism 管理的目录，而是把我的照片目录链接过去，所以这几个目录只会存 PhotoPrism 的配置文件和缓存文件。然后按照官方文档的建议，不要在 &lt;code&gt;defaults.yml&lt;/code&gt; 里面加更多内容，而是在你设置的 &lt;code&gt;ConfigPath&lt;/code&gt; 下面创建一个 &lt;code&gt;options.yml&lt;/code&gt; 在里面进行设置。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;options.yml&lt;/code&gt; 里面的内容和 compose 文件里设置的环境变量的内容其实是等价的，只是写法不同，必须设置的大概也就如下几项，其它的当你启动 PhotoPrism 在设置里修改之后它会自动更新这个文件：&lt;/p&gt;
&lt;figure data-raw=&quot;AdminPassword: admin
AdminUser: admin
AuthMode: password
DatabaseDriver: mysql
DatabaseName: photoprism
DatabasePassword: your_database_user_password
DatabaseServer: /run/mysqld/mysqld.sock
DatabaseUser: photoprism
DisableTLS: true
HttpPort: 2342
SiteUrl: https://photos.example.com/
&quot; data-info=&quot;language-yaml&quot; data-lang=&quot;yaml&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;AdminPassword: admin
AdminUser: admin
AuthMode: password
DatabaseDriver: mysql
DatabaseName: photoprism
DatabasePassword: your_database_user_password
DatabaseServer: /run/mysqld/mysqld.sock
DatabaseUser: photoprism
DisableTLS: true
HttpPort: 2342
SiteUrl: https://photos.example.com/
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;如果你和我一样 mariadb 和 PhotoPrism 运行在同一台机器上，那可以通过 UNIX socket 指定数据库连接，否则可以使用 IP 和端口。如果你想给别人分享相册，就把 &lt;code&gt;SiteUrl&lt;/code&gt; 设置成外网的网址。&lt;/p&gt;
&lt;p&gt;我不打算使用 PhotoPrism 的导入功能，这个功能会将你的照片复制到它管理的目录并按规则重命名。我打算只让它索引我的照片，所以要将我的照片目录挂载到
 &lt;code&gt;OriginalsPath&lt;/code&gt; 而不是 &lt;code&gt;ImportPath&lt;/code&gt;。使用 docker compose 的话会在 compose 文件里指定如何挂载，而我使用 fstab 进行 dir bind mount。以上这些基本上和使用 docker compose 的逻辑一样，只是配置的方式不同。&lt;/p&gt;
&lt;p&gt;然后如果修改路径到别处的话，记得保证 &lt;code&gt;photoprism&lt;/code&gt; 用户对这几个路径是可读写的，因为 systemd 会以 &lt;code&gt;photoprism&lt;/code&gt; 用户运行 PhotoPrism，&lt;code&gt;systemctl start photoprism&lt;/code&gt; 之后，内网应该就可以通过 &lt;code&gt;2342&lt;/code&gt; 端口访问它的前端了。点击资料库 -&amp;gt; 索引，勾选完全重新扫描然后点击开始，应该就可以看到它开始索引目录里的照片并创建缩略图了。&lt;/p&gt;
&lt;p&gt;我大概有 24 万张照片，其中一半是 RAW 一半是对应的 JPG，PhotoPrism 运行了一天一夜给我所有的照片建立了索引和缩略图。如果你的 GPU 可以被它用来加速或者你的磁盘阵列速度比我快，应该不需要这么久。&lt;/p&gt;
&lt;h1 id=&quot;Jellyfin&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Deploy-More-Services-on-NAS/#Jellyfin&quot;&gt;&lt;/a&gt;Jellyfin&lt;/h1&gt;
&lt;p&gt;我当然是没有出门在外看 NAS 上的视频的需求，但是在家里看的需求还是有的。之前我一直使用文件管理器通过 samba 直接播放，在电脑上当然不成问题，但是不知道是由于无线带宽有限还是 Android 上的 samba 客户端的问题，用手机或者平板看文件体积比较大的电影就加载不动了。Android 版 VLC 虽然有内置的 samba 支持，但是那个文件管理器超级难用啊，没有按日期排序，从一个目录返回上一个目录还不能保持之前的位置。所以我觉得部署一个 Jellyfin 还是有必要的。&lt;/p&gt;
&lt;p&gt;Jellyfin 不使用系统的数据库，也不需要修改很多配置文件，大部分都在启动之后的设置向导里设置就可以了。所以安装就是直接在 Arch 官方源里安装 &lt;code&gt;jellyfin-server&lt;/code&gt; 和 &lt;code&gt;jellyfin-web&lt;/code&gt;，然后打开 &lt;code&gt;/etc/jellyfin/jellyfin.env&lt;/code&gt; 看看你需要不需要修改默认的 data 和 cache 目录，然后把存放视频的目录挂载到 data 目录下面就行了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;systemctl start jellyfin&lt;/code&gt; 之后内网访问 &lt;code&gt;8096&lt;/code&gt; 端口，按照设置向导设置，之后让它扫描你的视频目录。由于它要生成缩略图，所以也会比较吃资源。至于其它的使用和配置方法应该网上有大把资料，就不啰嗦了。&lt;/p&gt;
&lt;h1 id=&quot;Caddy&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Deploy-More-Services-on-NAS/#Caddy&quot;&gt;&lt;/a&gt;Caddy&lt;/h1&gt;
&lt;p&gt;我一般是不需要在外网访问 NAS 上的服务的，因为我可以通过 tailscale 的 VPN 进行访问。但偶尔我会有通过 web 服务给朋友发文件的需求和给被拍的朋友通过 PhotoPrism 返图的需求，这种情况下就有必要进行 HTTPS 反代了。&lt;/p&gt;
&lt;p&gt;要做 HTTPS 反代首先域名我早就有了，北京联通的 IP 虽然不是固定的，但是这个可以通过 DDNS 解决，我自己使用的是 CloudFlare 的 API 和 ddclient，证书的话可以通过 Let&#039;s Encrypt 解决，虽然现在流行的 HTTPS 反代服务器都有自动给配置文件里的域名申请证书的功能，但我还是不得不使用 certbot 提供的 DNS-01 验证。因为 HTTP-01 和 TLS-01 验证要求你的 80 或者 443 端口要能被外网访问，但是在国内运营商的网络里你暴露这种常见端口，怕不是觉得自己家水表还是查的不够勤。&lt;/p&gt;
&lt;p&gt;至于反代服务器，虽然 NGINX 自己就是个高性能反代服务器，但是它的配置文件还是太过复杂了，我不知道怎么写才能让它反向代理 websocket，PhotoPrism 的文档也说不建议使用 NGINX 反向代理，于是我决定挑一个现代又傻瓜的反代服务器。一开始我本来想尝试 Traefik，但是我看了很久它的文档，感觉全都在写它如何如何支持 docker 服务自动发现，但是我根本没用 docker，也不需要服务发现，我想找如何配置它让它代理我指定的端口，翻来覆去也没看明白。然后我去看 Caddy，发现 Caddy 的文档非常简单易懂，而且核心就是通过简单的手动配置实现各种功能，所以我就用 Caddy 了。&lt;/p&gt;
&lt;p&gt;Caddy 和 NGINX 一样既支持文件服务器又支持反向代理，所以我把之前用 NGINX 提供的文件服务也转由 Caddy 负责了。我的需求是在内网我会直接通过 IP 和端口访问对应的服务，不需要 Caddy 转发，然后外网则通过 Caddy 进行 HTTPS 反代。&lt;/p&gt;
&lt;p&gt;在官方源里安装 &lt;code&gt;caddy&lt;/code&gt; 之后打开 &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;，最下面应该有一行 &lt;code&gt;import /etc/caddy/conf.d/*&lt;/code&gt;，这一行可以不动，我打算用它实现分离各个不同服务的配置的功能。然后首先实现端口 &lt;code&gt;2345&lt;/code&gt; 上的文件服务器，非常简单：&lt;/p&gt;
&lt;figure data-raw=&quot;:2345 {
	file_server browse {
		root /mnt/hdd0/http/fileshare
	}
}
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;:2345 {
	file_server browse {
		root /mnt/hdd0/http/fileshare
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;只要这么几行就够了，比起写 NGINX 的配置文件还是简单太多了。想要添加 HTTP 的用户密码验证的话就去阅读 Caddy 文档里关于 &lt;code&gt;basic_auth&lt;/code&gt; 的部分。&lt;/p&gt;
&lt;p&gt;然后就是添加外网反向代理的配置，我给 NAS 分配了一个域名，但我并不想让文件服务器作为这个域名的唯一功能，所以我通过 Caddy 给它重定向到一个子目录，这里可以创建一个 &lt;code&gt;/etc/caddy/conf.d/fileshare&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;https://nas_domain.example.com {
	tls path_to_fullchain.pem path_to_privkey.pem

	redir /fileshare /fileshare/
	handle_path /fileshare/* {
		reverse_proxy :2345

	}
}
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;https://nas_domain.example.com {
	tls path_to_fullchain.pem path_to_privkey.pem

	redir /fileshare /fileshare/
	handle_path /fileshare/* {
		reverse_proxy :2345

	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;相信这个配置已经简单到不需要我介绍也可以看懂了，至于 PhotoPrism 和 Jellyfin 这样的服务它们不推荐重定向到子目录（会影响 web 资源加载的路径），所以我不得不再分配一个域名给它并且设置 ddclient 和 certbot。然后同样创建一个 &lt;code&gt;/etc/caddy/conf.d/photoprism&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;https://photo_domain.example.com {
	tls path_to_fullchain.pem path_to_privkey.pem

	reverse_proxy :2342
}
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;https://photo_domain.example.com {
	tls path_to_fullchain.pem path_to_privkey.pem

	reverse_proxy :2342
}
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这看起来比上一个还简单了。启动 Caddy 的话就 &lt;code&gt;systemctl start caddy&lt;/code&gt;，改了配置文件就 &lt;code&gt;systemctl reload caddy&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最后由于我太懒了不想记住每一个 web 服务的端口号，我简单写了个网页列出所有这些服务，这样我在内网访问就可以先打开这个网页再点我想用的。但由于我的 NAS 有好几个网卡，我希望通过万兆直连访问时候就使用万兆网卡的 IP，通过路由器访问的时候就使用板载网卡的 IP，但是 HTML 的链接不能只写端口号，而普通的静态页面显然是不支持按需修改链接的。但是 Caddy 是使用 go 编写的，所以作为 web 服务器的时候支持 go 的模版语法（好吧我并不喜欢 go 的模版语法，但是有总比没有强）。所以可以在 &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; 里面加入下面一段开启一个支持模版的 web 服务器：&lt;/p&gt;
&lt;figure data-raw=&quot;http:// {
	root * /mnt/ssd0/http/caddy
	templates
	file_server browse
}
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;http:// {
	root * /mnt/ssd0/http/caddy
	templates
	file_server browse
}
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后只要编辑 &lt;code&gt;/mnt/ssd0/http/caddy/index.html&lt;/code&gt;，在里面用 &lt;code&gt;{{ .Host }}&lt;/code&gt; 代替本机的 IP 就可以了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/tags/Linux/" />
    <category term="Caddy" label="Caddy" scheme="https://sh.alynx.one/tags/Caddy/" />
    <category term="PhotoPrism" label="PhotoPrism" scheme="https://sh.alynx.one/tags/PhotoPrism/" />
  </entry>
  <entry>
    <title>升级万兆网络</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Move-to-10GbE/" />
    <id>https://sh.alynx.one/posts/Move-to-10GbE/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-08-03T10:01:50.000Z</published>
    <updated>2025-08-03T10:01:50.000Z</updated>
    <summary type="html">
      其实我一开始决定升级到万兆的原因不是觉得 2.5G 网络不够用，毕竟 btrfs 并没有内建 SSD 缓存的功能，甚至都没有 RAID1 的读取速度优化，所以机械硬盘顶天也就 300M/s 而已（何况我之前的 NAS 硬件也没有添加 SSD 的空间了，更何况建立 bcache 缓…
    </summary>
    <content type="html">
      &lt;p&gt;其实我一开始决定升级到万兆的原因不是觉得 2.5G 网络不够用，毕竟 btrfs 并没有内建 SSD 缓存的功能，甚至都没有 RAID1 的读取速度优化，所以机械硬盘顶天也就 300M/s 而已（何况我之前的 NAS 硬件也没有添加 SSD 的空间了，更何况建立 bcache 缓存需要清空当前的阵列从头开始，考虑到已有的数据量这显然不现实）。原因其实只是因为我以比较低的价格捡漏了一台 Mac mini 开始日常使用，而 Finder 在使用无线网络的时候访问 Samba 的性能实在不敢恭维（即使是有线网，Finder 在访问内容比较多的目录的时候加载速度也显著慢于 Nautilus，但至少比无线网好很多）。所以我考虑要不再从 NAS 拉一根网线到 Mac mini。但是我的 NAS 上已经没有多余的网口了，如果只是把原来接台式机的单口 2.5G PCIe 网卡换成双口 2.5G PCIe 网卡未免有些太过无聊，何况 Mac mini 本身也只有千兆网口，无论如何都得配个外接网卡，索性全部战未来换成万兆网卡吧。&lt;/p&gt;
&lt;p&gt;万兆网卡分成光口和电口两种。光口网卡温度比较低，但是较长距离需要使用光纤，布线的时候就得非常小心，较短距离可以使用某种封装好的铜线，但是为了不影响睡眠我把 NAS 放在卧室门外，又要绕过门缝又要贴墙，总距离有个小十米，所以感觉不是很适合我。电口网卡温度比较高，但可以使用普通的网线（虽然有人说需要什么六类七类线，但实际上家庭使用不超过 30 米的距离只要普通的五类线就完全可以跑满万兆了），而且考虑到如果以后升级新的 Mac mini 可以直接选配万兆网卡，那个也是电口的，所以我还是买了电口的万兆网卡。&lt;/p&gt;
&lt;p&gt;Mac mini 这边其实没什么好选的，雷电口的万兆网卡一共也就那么几款，因为本身这个品类针对的就是各种 Mac，需要使用 Mac 能支持的芯片。我选择了一款 AQC113 芯片的，这款也是带万兆网卡的 Mac mini 内置的型号，因为市场比较小所以价格相比 PCIe 的服务器网卡要贵一些。如果你胆子大你也可以考虑自己购买雷电转 PCIe 的转接卡和 PCIe 万兆网卡拼一个，但可能需要专门处理散热。AQC113 已经是电口网卡里面据说“不那么热”的型号了，但我买的这款运行时候摸上去还是会感觉有点烫手，不过也可能是因为这款使用铝制外壳把热量传递出来的原因，反正雷电设备本身就很热，我的雷电 hub 平时摸上去也是会觉得很热的程度，还要考虑到 hub 使用的是塑料外壳，热量并不是完全传递出来。不过使用起来都一切正常，没有出现过那些发热比较严重的网卡因为不装风扇就过热卡死的问题。&lt;/p&gt;
&lt;p&gt;台式机和 NAS 这边可选的就很多了，因为是 PCIe 可以购买服务器网卡，大部分都是半高和双口的。我只有三台设备，主要的需求是把 Mac 和 PC 分别连接到 NAS，就省了买万兆交换机的钱。最便宜的电口万兆网卡是 Intel 的 X540，但是这款出来的比较早，是 PCIe 2.0 x8，制程比较老发热也比较严重，很多人建议在散热片上加一个小风扇，不然会死机。但是小风扇是高频噪音重灾区，而且考虑到以后升级主板的话大部分主板 PCIe x8 是和第一条 x16 共享的，反而一般都有独立的 x4 插槽，所以我觉得为了这两个加点成本是可以接受的。最后我买了两张 PCIe 3.0 x4 的 Intel X550，不加独立风扇使用起来也没什么问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Move-to-10GbE/1.jpg&quot; alt=&quot;1.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;软件方面的话我在 NAS 上配了个桥接，倒不是因为我特别需要 Mac 访问 PC，只是我想让 NAS 的两个网口都用同一个 IP，而不是 Mac 和 PC 用不同的 IP 访问 NAS。因为我的 NAS 上面也用 Network Manager 所以建桥接也很简单。以及通过网络使用 &lt;code&gt;rsync&lt;/code&gt; 复制文件的时候不要使用 &lt;code&gt;--sparse&lt;/code&gt; 参数，尤其是在 macOS 上，这个参数会导致速度下降 100~200M/s。使用 &lt;code&gt;cp&lt;/code&gt; 或者各种文件管理器的时候倒是没什么要注意的。&lt;/p&gt;
&lt;p&gt;后面我还是把万兆网络利用了起来，因为我忍受不了 ITX 的 NAS 机箱完全没有一点扩展空间的现状很久了，同时我买了一块 2T 的 NVMe SSD，本来是格式化成 exFAT 格式装进 USB 硬盘盒打算剪辑的时候随插随用，后面发现 macOS 的 exFAT 驱动性能比 Linux 下差很多，所以一不做二不休给 NAS 买了新的 MATX 主板和机箱，然后把这块 SSD 装在 NAS 的第二条 NVMe 槽用作剪辑盘，结果 macOS 通过万兆网和 Samba 访问比通过 USB 和 exFAT 快很多……&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BD%91%E7%BB%9C/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/tags/%E7%BD%91%E7%BB%9C/" />
  </entry>
  <entry>
    <title>macOS 微调技巧合集</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Collection-of-macOS-Tweaks/" />
    <id>https://sh.alynx.one/posts/Collection-of-macOS-Tweaks/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-07-31T08:33:55.000Z</published>
    <updated>2025-07-31T08:33:55.000Z</updated>
    <summary type="html">
      以下记录了一些常见或者不常见的 macOS 微调技巧，其中有些是因为 macOS 的默认设置过于难用，然后又很难想到用什么关键词才能搜索到的选项，有些则是作为开发环境和 Linux 不一样的地方，可以进行调整。 关闭拖拽到屏幕边缘并等待切换工作区 作为一个桌面程序员，我认为最令用…
    </summary>
    <content type="html">
      &lt;p&gt;以下记录了一些常见或者不常见的 macOS 微调技巧，其中有些是因为 macOS 的默认设置过于难用，然后又很难想到用什么关键词才能搜索到的选项，有些则是作为开发环境和 Linux 不一样的地方，可以进行调整。&lt;/p&gt;
&lt;h1 id=&quot;%E5%85%B3%E9%97%AD%E6%8B%96%E6%8B%BD%E5%88%B0%E5%B1%8F%E5%B9%95%E8%BE%B9%E7%BC%98%E5%B9%B6%E7%AD%89%E5%BE%85%E5%88%87%E6%8D%A2%E5%B7%A5%E4%BD%9C%E5%8C%BA&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Collection-of-macOS-Tweaks/#%E5%85%B3%E9%97%AD%E6%8B%96%E6%8B%BD%E5%88%B0%E5%B1%8F%E5%B9%95%E8%BE%B9%E7%BC%98%E5%B9%B6%E7%AD%89%E5%BE%85%E5%88%87%E6%8D%A2%E5%B7%A5%E4%BD%9C%E5%8C%BA&quot;&gt;&lt;/a&gt;关闭拖拽到屏幕边缘并等待切换工作区&lt;/h1&gt;
&lt;p&gt;作为一个桌面程序员，我认为最令用户恼火的使用体验就是按住并等待了，你不知道要等待多久才会触发，往往你以为已经触发了结果还没有，极大地降低了日常操作的效率。但是似乎苹果的设计师并没有意识到这种设计在电脑上有多难用，并且在多个地方使用了按住并等待触发的逻辑。其中甚至包含互相冲突的功能：比如 macOS 15 新增的拖动窗口到屏幕边缘分屏，这个和拖动窗口到屏幕边缘切换工作区完全冲突。你必须小心翼翼的拖动窗口到边缘，但是又不能完全碰到边缘，才可以完美的触发分屏而不是切换工作区。这在日常使用中几乎是不可能的，考虑到分屏更常用并且有多种方法切换工作区，关掉这个愚蠢的功能才是合理的。但是苹果并没有提供直接关掉这个功能的选项，只能通过把触发的等待时间加大到几乎不可能误触发来近似实现了。&lt;/p&gt;
&lt;figure data-raw=&quot;$ defaults write com.apple.dock workspaces-edge-delay -float 60 &amp;amp;&amp;amp; killall Dock
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ defaults write com.apple.dock workspaces-edge-delay -float 60 &amp;amp;&amp;amp; killall Dock
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这个指令会将等待时间加大到 60 秒，我相信没有人会在边缘按住鼠标 60 秒吧。&lt;/p&gt;
&lt;h1 id=&quot;%E5%87%8F%E5%B0%8F%E5%8F%B3%E4%B8%8A%E8%A7%92%E6%89%98%E7%9B%98%E5%9B%BE%E6%A0%87%E9%97%B4%E8%B7%9D&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Collection-of-macOS-Tweaks/#%E5%87%8F%E5%B0%8F%E5%8F%B3%E4%B8%8A%E8%A7%92%E6%89%98%E7%9B%98%E5%9B%BE%E6%A0%87%E9%97%B4%E8%B7%9D&quot;&gt;&lt;/a&gt;减小右上角托盘图标间距&lt;/h1&gt;
&lt;p&gt;macOS 右上角托盘图标之间的间距简直大到离谱，特别是在有刘海的屏幕上，本来就不宽裕的长度根本容纳不下几个图标。很多用户被迫购买一些付费的软件来折叠显示托盘图标，实际上这个间距是可以调小的，只是一下子很难想到。&lt;/p&gt;
&lt;figure data-raw=&quot;$ defaults -currentHost write -globalDomain NSStatusItemSpacing -int 5
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ defaults -currentHost write -globalDomain NSStatusItemSpacing -int 5
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这个会将状态栏图标的间隔缩小为 5。&lt;/p&gt;
&lt;figure data-raw=&quot;$ defaults -currentHost write -globalDomain NSStatusItemSelectionPadding -int 3
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;$ defaults -currentHost write -globalDomain NSStatusItemSelectionPadding -int 3
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这个会将状态栏图标点击和悬浮的显示特效宽度缩小为 3。&lt;/p&gt;
&lt;p&gt;也许需要注销并重新登录才会生效。在这个间距下只要再关掉一些不需要一直显示的图标，应该就可以显示所有日常需要的托盘图标了。&lt;/p&gt;
&lt;h1 id=&quot;%E5%85%BC%E5%AE%B9%E4%BD%BF%E7%94%A8-GNU-%E7%89%88%E6%9C%AC%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7%E7%9A%84%E8%84%9A%E6%9C%AC&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Collection-of-macOS-Tweaks/#%E5%85%BC%E5%AE%B9%E4%BD%BF%E7%94%A8-GNU-%E7%89%88%E6%9C%AC%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7%E7%9A%84%E8%84%9A%E6%9C%AC&quot;&gt;&lt;/a&gt;兼容使用 GNU 版本命令行工具的脚本&lt;/h1&gt;
&lt;p&gt;绝大多数 Linux 发行版附带的命令行工具都是 GNU 版本，而 macOS 附带的则是 BSD 版本，这两者各自包含一些不同的命令行选项。你可以通过 Homebrew 安装各种 GNU 版本的工具，但 Homebrew 默认会给这些命令加上 &lt;code&gt;g&lt;/code&gt; 前缀以和系统自带的 BSD 版本区分开，并且会把不带 &lt;code&gt;g&lt;/code&gt; 的文件安装到单独的目录里而不是加入到 &lt;code&gt;PATH&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;如果只是顺应个人的使用习惯，那可以通过以下的方式设置 &lt;code&gt;PATH&lt;/code&gt; 来默认使用 GNU 版本的工具：&lt;/p&gt;
&lt;figure data-raw=&quot;$ export PATH=&amp;quot;/opt/homebrew/opt/coreutils/libexec/gnubin:${PATH}&amp;quot;
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ export PATH=&quot;/opt/homebrew/opt/coreutils/libexec/gnubin:${PATH}&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;但是对于另一些命令行选项区别更大而又常用于脚本的工具（比如 &lt;code&gt;sed&lt;/code&gt;，&lt;code&gt;find&lt;/code&gt;，&lt;code&gt;grep&lt;/code&gt;），直接设置 &lt;code&gt;PATH&lt;/code&gt; 虽然可以兼容一些在 Linux 下开发的脚本，但是可能会不支持在 macOS 下开发的脚本，因为作者可能使用的是 BSD 版本的工具。我的解决方案是设置一个定义 &lt;code&gt;PATH&lt;/code&gt; 的 alias：&lt;/p&gt;
&lt;figure data-raw=&quot;$ alias gnubin=&amp;quot;PATH=\&amp;quot;\
/opt/homebrew/opt/coreutils/libexec/gnubin:\
/opt/homebrew/opt/findutils/libexec/gnubin:\
/opt/homebrew/opt/grep/libexec/gnubin:\
/opt/homebrew/opt/gnu-sed/libexec/gnubin:\
/opt/homebrew/opt/gnu-tar/libexec/gnubin:\
${PATH}\
\&amp;quot;&amp;quot;
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ alias gnubin=&quot;PATH=\&quot;\
/opt/homebrew/opt/coreutils/libexec/gnubin:\
/opt/homebrew/opt/findutils/libexec/gnubin:\
/opt/homebrew/opt/grep/libexec/gnubin:\
/opt/homebrew/opt/gnu-sed/libexec/gnubin:\
/opt/homebrew/opt/gnu-tar/libexec/gnubin:\
${PATH}\
\&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这样当执行 GNU 版本的命令或者脚本时，只要在前面加上 &lt;code&gt;gnubin&lt;/code&gt; 再执行即可。&lt;/p&gt;
&lt;h1 id=&quot;%E8%B7%A8%E8%AE%BE%E5%A4%87%E6%8E%A5%E6%94%B6%E6%96%87%E4%BB%B6%E6%97%B6%E4%BD%BF%E7%94%A8%E8%BE%83%E6%96%B0%E7%9A%84-rsync&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Collection-of-macOS-Tweaks/#%E8%B7%A8%E8%AE%BE%E5%A4%87%E6%8E%A5%E6%94%B6%E6%96%87%E4%BB%B6%E6%97%B6%E4%BD%BF%E7%94%A8%E8%BE%83%E6%96%B0%E7%9A%84-rsync&quot;&gt;&lt;/a&gt;跨设备接收文件时使用较新的 rsync&lt;/h1&gt;
&lt;p&gt;macOS 自带了一个版本比较旧的 rsync，你可以使用 Homebrew 自行安装比较新的 rsync，但是当你从其他 Linux 机器通过 rsync 向 macOS 机器传文件时可能会报错，提示你使用的命令行选项不支持。这是因为 rsync 默认通过 SSH 进行跨设备文件传输，而 SSH 默认不会读取用户设置的 &lt;code&gt;PATH&lt;/code&gt; 环境变量，直接去 &lt;code&gt;/usr/bin&lt;/code&gt; 下面调用使用的是 macOS 自带的老版本 rsync，也就无法支持比较新的版本才有的选项了。&lt;/p&gt;
&lt;p&gt;解决方法是先编辑 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，设置 &lt;code&gt;PermitUserEnvironment PATH&lt;/code&gt; 让它读取用户设置的 &lt;code&gt;PATH&lt;/code&gt; 环境变量。&lt;/p&gt;
&lt;p&gt;然后编辑 &lt;code&gt;~/.ssh/environment&lt;/code&gt; 给 &lt;code&gt;PATH&lt;/code&gt; 前面添加 Homebrew 的目录：&lt;/p&gt;
&lt;figure data-raw=&quot;PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;重新启动 SSH 服务，应该就可以调用 Homebrew 安装的 rsync 了。&lt;/p&gt;
&lt;h1 id=&quot;Open-Build-Service&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Collection-of-macOS-Tweaks/#Open-Build-Service&quot;&gt;&lt;/a&gt;Open Build Service&lt;/h1&gt;
&lt;p&gt;这一部分也许只有我自己工作会用到，或者你也使用 Open Build Service，需要在 macOS 上运行 &lt;code&gt;osc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Homebrew 安装的 &lt;code&gt;osc&lt;/code&gt; 在 macOS 里读不到系统的证书，所以即使将 SUSE 的证书导入到系统，访问 &lt;code&gt;api.suse.de&lt;/code&gt; 时候也会报错。&lt;/p&gt;
&lt;p&gt;解决方法是在 &lt;code&gt;~/.config/osc/oscrc&lt;/code&gt; 里给 &lt;code&gt;[https://api.suse.de]&lt;/code&gt; 设置 &lt;code&gt;cafile=/Users/alynx/openvpn/SUSE/SUSE_Trust_Root.crt&lt;/code&gt; 让它能自己找到 SUSE 证书。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;osc vc&lt;/code&gt; 调用的是 &lt;code&gt;obs-build&lt;/code&gt; 里面的 &lt;code&gt;vc&lt;/code&gt; 脚本，但是由于 macOS 默认 APFS 设置了不区分大小写，而 &lt;code&gt;obs-build&lt;/code&gt; 源码里包含 &lt;code&gt;Build&lt;/code&gt; 和 &lt;code&gt;PBuild&lt;/code&gt; 目录，以及 &lt;code&gt;build&lt;/code&gt; 和 &lt;code&gt;pbuild&lt;/code&gt; 脚本，macOS 会直接报文件名冲突，根本没办法解包 &lt;code&gt;obs-build&lt;/code&gt; 的源码，也就没办法打包了。虽然重新格式化 APFS 可以设置区分大小写，但一些屎山软件比如 Adobe 家的垃圾只支持不区分大小写的文件系统，所以这么做得不偿失。2025 年还不支持区分大小写简直是犯罪啊。&lt;/p&gt;
&lt;p&gt;解决方法是直接去 &lt;a href=&quot;https://github.com/openSUSE/obs-build/raw/refs/heads/master/vc&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;https://github.com/openSUSE/obs-build/raw/refs/heads/master/vc&lt;/a&gt; 下载这个脚本到 &lt;code&gt;~/bin/osc-vc&lt;/code&gt; 之类的地方，然后在 &lt;code&gt;~/.config/osc/oscrc&lt;/code&gt; 给 &lt;code&gt;[general]&lt;/code&gt; 设置 &lt;code&gt;vc-cmd=/Users/alynx/bin/osc-vc&lt;/code&gt; 让它找到这个脚本就行了。&lt;/p&gt;
&lt;p&gt;默认 &lt;code&gt;osc&lt;/code&gt; 会去 &lt;code&gt;/usr/lib/obs/service&lt;/code&gt; 下面寻找各种 &lt;code&gt;obs-service&lt;/code&gt; 插件，但自然你在 macOS 下面是不能安装到这个目录的，而且这些插件都没有打包成 Homebrew 的 Formula。我自己创建了一个 Tap 在 &lt;a href=&quot;https://github.com/AlynxZhou/homebrew-openbuildservice/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;https://github.com/AlynxZhou/homebrew-openbuildservice/&lt;/a&gt;，里面的 &lt;code&gt;osc&lt;/code&gt; 将插件目录修改成了 &lt;code&gt;${HOMEBREW_PREFIX}/lib/obs/service&lt;/code&gt;，并且将其他的插件的安装位置也修改了，所以建议你安装我的版本而不是 Homebrew core 里面的。不过这些插件大部分都依赖 &lt;code&gt;obs-build&lt;/code&gt; 里面的各种脚本才能运行，所以直到 &lt;code&gt;obs-build&lt;/code&gt; 同意修改目录名字来兼容不区分大小写的文件系统之前，都只能通过 &lt;code&gt;osc commit --noservice&lt;/code&gt; 的方式凑合用了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="macOS" label="macOS" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/macOS/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="macOS" label="macOS" scheme="https://sh.alynx.one/tags/macOS/" />
  </entry>
  <entry>
    <title>东京之旅</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Journey-to-Tokyo/" />
    <id>https://sh.alynx.one/posts/Journey-to-Tokyo/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2025-04-16T04:47:00.000Z</published>
    <updated>2025-04-16T04:47:00.000Z</updated>
    <summary type="html">
      题外话：本来应该去年九月从东京回来就写的，但由于一些不是很众所周知的原因我很久没更新了。昨天回家看到影视飓风发了去东京拍的样片，又想起来之前拍的照片，所以决定还是写一下。 我其实是特别懒得出门的人，不是不想，只是嫌麻烦，而且其实我也没有特别想去的地方，除了日本。毕竟如果你也像我一…
    </summary>
    <content type="html">
      &lt;p&gt;题外话：本来应该去年九月从东京回来就写的，但由于一些不是很众所周知的原因我很久没更新了。昨天回家看到影视飓风发了去东京拍的样片，又想起来之前拍的照片，所以决定还是写一下。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我其实是特别懒得出门的人，不是不想，只是嫌麻烦，而且其实我也没有特别想去的地方，除了日本。毕竟如果你也像我一样是个动漫爱好者的话你就很难不想去日本看看。所以董老师问我要不要一起去东京玩的时候，我做了足够久的心理建设。搞定签证机票住宿等等一系列麻烦的东西之后，我终于要进行人生第一次出国旅行了。在北京待了太久，早就想拍点不一样的东西，所以这篇主要是贴一些我出门玩拍的照片。&lt;/p&gt;
&lt;p&gt;买机票的时候没有买直飞，因为我也没有去过香港，所以买了中午到香港然后午夜飞东京的转机，到了香港凭护照和离开香港的机票就可以办进入香港的手续，并不需要港澳通行证。然后把在深圳的高中同学喊了过来一起吃了晚饭，我还是第一次见依山而建的城市。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/1.jpg&quot; alt=&quot;1.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;晚上两个人随便走走于是去拍了维多利亚港。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/2.jpg&quot; alt=&quot;2.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于一些众所周知的原因，在香港坐地铁的时候感觉非常熟悉好像回到了北京地铁一样（或许应该说北京某些地铁像香港地铁才对）。&lt;/p&gt;
&lt;p&gt;然后午夜飞东京第二天早上落地成田机场，经过一系列入境手续之后坐电车去酒店，虽然我的日语远没到能日常交流的水平，但靠着比划和英语大概也没什么问题。而且很大概率你能碰到会中文的工作人员。由于前一天很早出发然后午夜才睡觉所以这一天根本没拍什么照片，下午到酒店能入住之后直接就躺了。董老师来东京的目的是去看魔法未来，所以我们前几天住了一个很偏的酒店，与其说离东京近不如说是离千叶近，而且主要是房间真的小啊！&lt;/p&gt;
&lt;p&gt;落地东京的第一天除了吃饭睡觉什么也没干，第二天起来我坐电车去了御茶水的乐器街（说起来知道这边是乐器街还是因为听丸之内虐待狂），虽然距离很远但感觉并没有花很久，也许是人在北京住久了就会对通勤变得宽容？虽然这边大部分卖的都是我不会弹的吉他贝斯类的，但还是看得眼花缭乱。这边有一家很出名的 ESP 店，我走到二楼的时候应该是有个店员看出来我是中国游客所以用中文问我“你想找什么？”，但我一下子就听出来他应该是会说中文的日本人，因为母语者发音不会这么刻意。同学想要一个国内还没上市的效果器，问了一下价格，比闲鱼代购便宜太多了，于是我非常乐意的帮他代购了一个。但我拍的不是乐器而是那个好像很著名的电车机位，铃芽之旅里面出现过的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/3.jpg&quot; alt=&quot;3.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于台风要来，第一天和第二天都在下雨，虽然我是很喜欢下雨，但是鞋子湿透了还是很痛苦。&lt;/p&gt;
&lt;p&gt;第三天白天去了魔法未来的展览，因为很喜欢赛车初音于是买了一把 24 款赛车初音的雨伞，结果临走的时候人在去机场的电车上想起来把伞落在酒店了，于是赶紧给酒店发邮件，他们对于处理外国游客遗落的物品有成熟的流程，所以后面我付了邮费提交了地址他们把伞给我寄到了家里。以及因为发现酒店楼下的罗森有赛车初音的一番赏所以每天晚上都去抽两发还和便利店打工的中东小哥聊了几次。晚上我意识到这个酒店离海边很近于是就跑去海边，然后拍到了海边散步的情侣 T_T 作为内陆长大而又很少去海边的人，晚上走到海边的时候感觉非常空旷，如果有幽闭恐惧症的反义词的话，应该可以更好的描述我当时的体验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/4.jpg&quot; alt=&quot;4.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;第四天我决定把来拍照的目的贯彻到底，中午先去了川崎站进行了一个 Girls Band Cry 的打卡，结果在丸福咖啡店遇到了另外两个说中文的游客，全都是看了 GBC 来打卡的！可惜我在川崎站外面没有遇到什么弹吉他的大姐姐。买了下午天空树的门票，直接冲到最高进行一个俯瞰风景。天空树上面有个许愿台，可以买扭蛋然后把愿望写在里面的彩带上，于是我一边想着 &lt;em&gt;“把愿望写在离天空更近的地方实现的会不会更快一点”&lt;/em&gt; 一边买了两个扭蛋，不过现在想想感受还是很复杂。我这个时候还没看过龙族，看完了觉得还好当时临时决定去天空树，下次一定还会再去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/5.jpg&quot; alt=&quot;5.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/6.jpg&quot; alt=&quot;6.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;出来的时候又开始下小雨！这就是临海城市的感觉吗？但是我抬头向上看的时候发现云雾围绕的天空树于是拍下了绝佳的照片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/7.jpg&quot; alt=&quot;7.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/8.jpg&quot; alt=&quot;8.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于这里是地标性建筑，所以下面有一个购物中心有很多专门的周边店，方便来玩的游客，我在里面转了很久，但由于一些原因不是很开心。但是接下来在商场里吃了一份寿喜锅，很好吃！&lt;/p&gt;
&lt;p&gt;第五天我们换到了市区里的酒店，住在浅草桥附近去哪里都很方便！而且房间面积竟然比第一家酒店要大。这一天我去了东京塔！我这次来其实没有制定什么旅游计划，单纯是给出国玩做一次可行性验证，所以其实落地东京就算是阶段性胜利了。但如果有什么一定要拍的，那就是东京塔了，因为我喜欢红色！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/9.jpg&quot; alt=&quot;9.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;转来转去找了好久附近哪里的楼上有东京塔的倒影，结果我低头发现东京塔的倒影就在东京塔的底座上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/10.jpg&quot; alt=&quot;10.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;第六天我好像没有拍什么照片，因为酒店离秋叶原只有一站路所以我就早起去了秋叶原，二次元怎么能不去秋叶原电器街呢！但是这里甚至走路都能听到人说中文，“你买的这个东西 90% 是 Made in China！&quot;。&lt;/p&gt;
&lt;p&gt;没记错的话中午吃了一家叫做 468 的寿司，这家的老板人很好很有意思，见到我们是外国人于是拿出一张纸在上面画一盘寿司和一碗汤然后写上价格，还问我们是哪里来的。汤是一种粘稠的淀粉汤，尝起来大概是用胡椒调味，中间有一颗炸过的土豆泥丸子，是他家的招牌菜“芋吸”，感觉就是明明是普通的食材普通的调味，但能想到这样做确实是别出心裁，从来没有在别的地方尝过类似的，非常喜欢于是跟老板说每人再点一份。&lt;/p&gt;
&lt;p&gt;晚上我们根据排行榜去吃了和牛烧肉！来日本怎么能不吃和牛烧肉呢！店很小所以排队花了一些时间，但是吃到的时候感觉确实是非常值得，缺点的话……大概只有因为是炭火烤肉所以如果有顾客没搞好肉被点着了屋里就会很大浓烟吧。&lt;/p&gt;
&lt;p&gt;第七天我们去了涩谷附近，先去了明治神宫但这里没有人举行婚礼。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/11.jpg&quot; alt=&quot;11.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在原宿买了一件我以前根本不会买的衣服，白色的衣服上面是各种蓝色的带子，当时莫名其妙的想到明日方舟的服装风格，然后店员小哥又很喜欢初音所以和我们聊了一会于是就买了（但是衣服很贵 T_T）。接着去了附近的稳田神社，好像设定上是平安名堇住在这里所以甚至有上面画着她的御守，所以我给列表里推平安名堇的 coser 带了一个。原宿这边有 Fender 的旗舰店，进去转了一圈，但我又不会弹这个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/12.jpg&quot; alt=&quot;12.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;晚上去了涩谷，这里可能是这几天见过人最多的地方了。董老师跟我说有什么稀奇的感觉和西直门差不多，我说西直门哪有这么多人，董老师想了一会说那三里屯，三里屯倒是稍微有点这个意思。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/13.jpg&quot; alt=&quot;13.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/14.jpg&quot; alt=&quot;14.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/15.jpg&quot; alt=&quot;15.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;第八天去了之前在上野的时候去了一次的矢先稻荷神社，当时只是在地图上看到附近有神社就想去看看，结果一路上很想去厕所又找不到，走到神社门口发现有牌子说最近的厕所在哪里，感觉非常有缘分，但是第一次去身上没有多少硬币，所以又去了一次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/16.jpg&quot; alt=&quot;16.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;中午本来想吃排行榜靠前的拉面店，结果到了门口发现老板给自己放了十天假，于是我们去了另一家高分的 らーめん 改，我以前吃过的日式拉面都是酱油或者猪骨汤的，这家的面是类似手擀面的粗面，汤是一种海鲜加盐的清汤，和我之前吃过的全都不一样，非常喜欢。&lt;/p&gt;
&lt;p&gt;下午又去了秋叶原，拍到一张很好看的逆光照片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/17.jpg&quot; alt=&quot;17.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;傍晚的时候从酒店窗户看出去云很好看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/18.jpg&quot; alt=&quot;18.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;这次其实没安排去富士山的计划，但想去东京的一个理由是因为富士山下这首歌，回程的飞机特地选了能看见富士山的一侧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Journey-to-Tokyo/19.jpg&quot; alt=&quot;19.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;最奇妙的感受其实是以前只在动漫里听过的地名突然有了实感，去过之后再看到这些地名，就会想起当时的感觉。它们不再是几个音节的排列组合，而是变成了无数个真实的细节。&lt;/p&gt;
&lt;p&gt;我其实很想多录一些视频，但是一个人暴走的结果就是没有什么体力拿出相机了……最后还是用拍到的一些片段剪了个 vlog 出来。&lt;/p&gt;
&lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113162573585735&amp;amp;bvid=BV1VCtneGE3g&amp;amp;cid=25918505203&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;还会再去的。&lt;/p&gt;

    </content>
    <category term="摄影" label="摄影" scheme="https://sh.alynx.one/categories/%E6%91%84%E5%BD%B1/" />
    <category term="生活" label="生活" scheme="https://sh.alynx.one/tags/%E7%94%9F%E6%B4%BB/" />
    <category term="摄影" label="摄影" scheme="https://sh.alynx.one/tags/%E6%91%84%E5%BD%B1/" />
    <category term="东京" label="东京" scheme="https://sh.alynx.one/tags/%E4%B8%9C%E4%BA%AC/" />
  </entry>
  <entry>
    <title>GNOME 和 IBus 和 Wayland 输入法</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/GNOME-IBus-Wayland-IME/" />
    <id>https://sh.alynx.one/posts/GNOME-IBus-Wayland-IME/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-06-20T09:19:55.000Z</published>
    <updated>2024-06-25T06:52:35.000Z</updated>
    <summary type="html">
      长久以来，用户对于 GNOME 的输入法是怎么实现的有着各种各样的误解，比如误以为 IBus 掌控从界面到引擎的全部逻辑。而我最近尝试给 GNOME 添加了 text-input-v1 支持，以便用户可以在运行于 Wayland 下的 Chromium/Electron 程序中使…
    </summary>
    <content type="html">
      &lt;p&gt;长久以来，用户对于 GNOME 的输入法是怎么实现的有着各种各样的误解，比如误以为 IBus 掌控从界面到引擎的全部逻辑。而我最近尝试给 GNOME 添加了 text-input-v1 支持，以便用户可以在运行于 Wayland 下的 Chromium/Electron 程序中使用输入法（Chromium 只支持 text-input-v1，而不是大部分其它程序都在用的 text-input-v3），这个过程中我阅读了大量和输入法相关的代码，打算把具体的结构写下来，这样如果有人想尝试修改，可以从我这里参考，而不是阅读错误的资料，找不到应该修改的位置，同时也防止自己某天想重新改的时候想不起来是怎么回事（实际上在我打算写这篇文章的时候发现自己已经开始忘记了，好恐怖）。&lt;/p&gt;
&lt;p&gt;Wayland 下面标准的输入法结构分为三个部分，应用程序和混成器和输入法，其中应用程序通过 text-input 协议和混成器交换数据，混成器通过 input-method 协议和输入法交换数据。但假如你简单地把混成器对应成 GNOME Shell，输入法对应成 IBus，那就大错特错了。实际上 GNOME Shell 和 IBus 共同组成了这个结构里的“输入法”，而 Mutter 则实现了 text-input 和应用程序通过 Wayland 交互，也就是“混成器”，GNOME Shell 和 Mutter 则通过 ClutterInputMethod 这个接口交换数据，而不是 input-method 协议。下面我逐个解释具体的细节。&lt;/p&gt;
&lt;p&gt;首先是“输入法”部分，IBus 本身是可以当作完整的输入法使用，也就是既包含界面又包含引擎，但实际上的运行方式是 IBus 只负责引擎部分，和界面有关比如候选词提示框和切换输入法的逻辑全部是在 GNOME Shell 里面实现的。这样做的原因是在 Overview 模式下所有客户端的窗口都会被重定向，GNOME Shell 抢占了输入焦点，而 IBus 自己绘制界面时也被认为是客户端窗口之一，目前的设计不会对输入法客户端窗口做什么特殊处理，所以这里是 GNOME Shell 负责界面逻辑，然后通过 D-Bus 和 IBus 通信，获取到输入法的数据填充到输入法的界面里。题外话就是经常有人吐槽为什么 GNOME 要使用 IBus 提供的 D-Bus 协议而不是使用 Wayland 标准的 input-method 协议，但实际上 GNOME Shell 和 IBus 之间的 D-Bus 连接传输的是“输入法内部的数据”，而 input-method 协议传递的是“输入法和混成器之间的数据”，这两者显然不是一个东西。&lt;/p&gt;
&lt;p&gt;因此，如果你想修改 GNOME 下面输入法的界面，那需要修改的可能不是 IBus，而是 GNOME Shell 下面的 &lt;a href=&quot;https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/ibusCandidatePopup.js&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;&lt;code&gt;js/ui/ibusCandidatePopup.js&lt;/code&gt;&lt;/a&gt;，这个文件包含候选框界面，或者 &lt;a href=&quot;https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/status/keyboard.js&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;&lt;code&gt;js/ui/status/keyboard.js&lt;/code&gt;&lt;/a&gt;，这个文件包含输入源选择框。&lt;/p&gt;
&lt;p&gt;然后 GNOME Shell 需要和 Mutter 交换数据，这样才能通过 text-input 协议把数据从 Mutter 传送到应用程序里。如果打算实现 input-method 协议，你应该整个替换掉 GNOME Shell 和 IBus 组合起来实现的“输入法”，而非拿着输入法内部的 D-Bus 连接大做文章。实现起来并不困难，Mutter 本身使用 Clutter 作为图形库，Clutter 有 ClutterInputMethod 这个接口管理输入法，无论你是什么来头，最后都要回到这个接口，只有这样才能保证对 GNOME Shell 自己的 UI 也能实现文本输入（因为它们最后都是使用 Clutter），因此主要工作是把 input-method 的接口和 ClutterInputMethod 的接口连接起来。但这是理想情况，实际为什么没人这么做呢……因为 Wayland 实际上提供的是进程间通信，input-method 协议说到底只是给输入法和混成器两个不同进程提供一个通过 Wayland 进行进程间通信的方案而已……但 Mutter 是个库而不是单独的进程，GNOME Shell 和 Mutter 直接链接，属于同一个进程，因此完全没必要再绕一圈进程间通信啊……所以你可以看到 GNOME Shell 直接继承了 ClutterInputMethod 接口然后在里面对接 IBus 的 D-Bus 协议传过来的数据……&lt;/p&gt;
&lt;p&gt;如果你对这一部分有兴趣，可以直接查看 GNOME Shell 下面的 &lt;a href=&quot;https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/misc/inputMethod.js&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;&lt;code&gt;js/misc/inputMethod.js&lt;/code&gt;&lt;/a&gt;。如果你仍然坚持应当实现 input-method 协议的话，你可以尝试按我说的去改，但我仍然认为这样收益不大，因为只是解决了输入法从混成器获取数据的问题，也就是说是让 GNOME 支持 IBus 以外的输入法的方案之一，为什么说是之一呢，因为实际上目前 Linux 的输入法也只剩 IBus 和 Fcitx 两个，而 &lt;a href=&quot;https://www.csslayer.info/wordpress/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@CSSlayer&lt;/a&gt; 已经通过在 Fcitx 里实现 IBus 和 GNOME Shell 之间的 D-Bus 协议的方式解决了这个问题……因此我选择对他心怀感激然后接受现状。&lt;/p&gt;
&lt;p&gt;但除此之外你还需要解决输入法自己绘制界面在 Overview 下面会被 GNOME Shell 忽略的问题，如果你仔细阅读过 &lt;a href=&quot;https://www.csslayer.info/wordpress/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@CSSlayer&lt;/a&gt; 关于 Fcitx 开发的博客文章，你就会意识到最大的限制是在 Wayland 下面一个客户端程序没办法决定自己的全局坐标，而输入法也只是个普通的客户端窗口，解决这个需要混成器对输入法的窗口进行特殊处理，因此你需要在 Mutter 里面给 input-method 专门打洞来实现单独处理输入法窗口的 &lt;code&gt;wl_surface&lt;/code&gt; 这部分。但对于 GNOME 来说，输入法界面已经是在混成器进程里实现的了，所以你可能需要从头写一大堆代码来处理这个并且和现有的输入法界面代码没有任何地方能复用，上游也自然没有很强烈的意愿合并这个，而对于 Fcitx 来说也可以通过 kimpanel 的 GNOME 扩展把 Fcitx 的界面嵌入到混成器进程里，这实际上和 GNOME Shell 对于输入法界面的处理方式是类似的……已经有可用的解决方案，实现 input-method 我觉得是有点吃力不讨好，至少你不能要求我去做这个……&lt;/p&gt;
&lt;p&gt;至于程序和混成器之间就没那么多事情了，这里就是通过标准的 text-input 协议进行，对于 Mutter 来说，仍然是把 ClutterInputMethod 的接口和 text-input 协议的接口对接起来，我做的主要是这一部分的工作。&lt;/p&gt;
&lt;p&gt;当然还有一种办法是 IM Module，因为说到底 text-input 和 input-method 只是为了想办法在程序和输入法之间交换数据，而你也可以绕过 Wayland，绕过混成器，IM Module 就是这样的方式，应用程序的图形库使用 IM Module 通过 D-Bus 直接和输入法交换数据，告诉输入法应该在哪里显示界面，有哪些按键事件，输入法再把候选词传回来。对于 GNOME，这个情况下只是用到了 GNOME Shell 里面关于输入法界面的部分，和 Mutter 以及 Wayland 就关系不大了。&lt;/p&gt;
&lt;p&gt;最后如果你还是看不懂，那我画了几个框图：&lt;/p&gt;
&lt;p&gt;你想象中的唯一的 Wayland 输入法架构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/GNOME-IBus-Wayland-IME/1.png&quot; alt=&quot;1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可能实际正在运行的架构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/GNOME-IBus-Wayland-IME/2.png&quot; alt=&quot;2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;GNOME 只是在上面的基础上进行了一点点点点点变化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/GNOME-IBus-Wayland-IME/3.png&quot; alt=&quot;3.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;至于你问我 GNOME 为什么这么设计输入法结构？这不是我能回答你的问题，也许是历史原因，也许是特殊结构（毕竟 Clutter 这个图形库一开始是个客户端的图形库），我这篇文章只是讲述现状，希望能减少一些以讹传讹。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="GNOME" label="GNOME" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/GNOME/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="GNOME" label="GNOME" scheme="https://sh.alynx.one/tags/GNOME/" />
    <category term="输入法" label="输入法" scheme="https://sh.alynx.one/tags/%E8%BE%93%E5%85%A5%E6%B3%95/" />
  </entry>
  <entry>
    <title>十年过去了，我买了台 MacBook</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/10-Years-Passed-and-I-Bought-MacBook/" />
    <id>https://sh.alynx.one/posts/10-Years-Passed-and-I-Bought-MacBook/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-06-10T10:46:35.000Z</published>
    <updated>2024-06-10T10:46:35.000Z</updated>
    <summary type="html">
      我其实已经很久没有属于自己的笔记本了。毕业之后公司发了一台 Precision 5530 作为工作电脑，我就把我自己的 Precision 5510 留给家里人用了，再加上为了打游戏我分别在自己住处和家里装了台式机，用笔记本的场景就更少了。想来想去，需要笔记本的场景只有一个，就是…
    </summary>
    <content type="html">
      &lt;p&gt;我其实已经很久没有属于自己的笔记本了。毕业之后公司发了一台 Precision 5530 作为工作电脑，我就把我自己的 Precision 5510 留给家里人用了，再加上为了打游戏我分别在自己住处和家里装了台式机，用笔记本的场景就更少了。想来想去，需要笔记本的场景只有一个，就是出远门拍照的时候，可能需要把相机存储卡里的数据复制到移动硬盘里，然后可能顺便快速修图发朋友圈。虽然我之前尝试过平板电脑也能满足这些需求，但平板电脑供电的能力有限，连接移动硬盘还需要外接供电，而且 Android 版的 Lightroom 导入的速度也太慢了，只能说勉强能用。&lt;/p&gt;
&lt;p&gt;当然我可以直接带着公司的 Precision 5530 出门的时候用，但是实在是太重了，而且在 Linux 下面电池顶多能用两小时。有了台式机之后我对笔记本的需求产生了一些变化，比如笔记本可以不需要处理所有的任务，复杂的工作可以在台式机上解决，所以我对笔记本的需求变成了轻便和长续航。这样一看苹果的 M1 变成了最合适的选项：续航可以和平板电脑相比，但是又是实打实的电脑，同时对于剪视频修图这种媒体编辑非常合适。当然这都是马后炮，我一开始只是因为在二手频道刷到有人出售 M1 的 MacBook Air 只要 3000 块出头，感觉这个价格就算是买一个回来玩玩也很合适。不过这个价格是丐版，8G 内存对我来说等于不能用。但我看了一下 16G M1 的 MacBook Air，价格要贵 1500 左右，又有点下不去手。&lt;/p&gt;
&lt;p&gt;我恰好有一个朋友在苹果实习，她告诉我苹果有员工优惠计划，可以以 85 折给家人或者朋友购买 MacBook，不过仅限于官网有卖的产品，于是我开始思考要不要干脆加钱正经买一台笔记本给自己用，这中间反复的纠结过程就略过不提，总之我最后两千块钱卖掉了一千块钱购入的万世权杖（当时工作之后打算买一个玩玩，没想到涨了一倍），又两千块钱卖掉了很久没用过的尼康大 F，然后本着买新不买旧不行挂闲鱼的逻辑买了 M3 的 MacBook Air，毕竟有个在苹果工作的朋友可不容易！&lt;/p&gt;
&lt;p&gt;一开始我是想买 16G + 256G 的版本的，但是发现 16G + 512G 可以直接去 Apple Store 提货，思考再三还是买了 512G，甚至还加了 AppleCare+。然后看到午夜黑的配色虽然我知道肯定会粘指纹但我还是义无反顾的选了，然后直奔朝阳大悦城的 Apple Store 提了电脑本体和苹果购物袋。&lt;/p&gt;
&lt;p&gt;这个过程中当然有朋友质疑我一个 Linux 桌面程序员为什么会买 MacBook 自己用，除了上面那些比较理性的原因之外，还有个原因是就算是 &lt;strong&gt;Linux 桌面程序员也不是一出生就是 Linux 桌面程序员&lt;/strong&gt; 的吧！大概十年以前我还不是 Linux 桌面程序员（甚至不是程序员）的时候，我对 Retina MacBook Pro 的宣传片印象深刻：&lt;/p&gt;
&lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=799187626&amp;amp;bvid=BV19y4y1Y7rc&amp;amp;cid=296995197&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;我买电脑确实是看脸的，因此不管别的 Linux 爱好者有多喜欢 ThinkPad，我都对那玩意嗤之以鼻：您能先让自己看起来像是一整块东西吗？不过我看到这个宣传片的时候可能还在上初中，距离我能买得起五位数电脑看起来不能说是遥遥无期，也可以说是希望渺茫。至于上大学之前花了两万块买了 Precision 5510 则是后话了，原因也很简单，单纯是 XPS 15 那个时候看脸比 MacBook 更酷，而且我那个时候已经开始用 Linux 了。所以如果没有 XPS 15，我可能就会买 MacBook Pro，不过再回想一下 2016 年到 2020 年苹果在 MacBook 产品线上做了多少愚蠢的决策（我会把我这辈子见过最蠢的笔记本电脑的称号颁给 New MacBook……真是只有外观设计完全不考虑能不能用的顶峰啊）……可能没有买苹果反而是幸运。&lt;/p&gt;
&lt;p&gt;但把时间拨回到 2012 年，Retina MacBook Pro 确实看起来很棒，用起来也很棒。考虑到使用 M 系列芯片的 MacBook Air 确实在续航和性能之间找到了平衡点，并且苹果确实逐渐从当年制造愚蠢笔记本的顶峰退下来，所以我觉得是时候买一台了，就算是为了延迟满足十年前的自己。就像选择午夜黑的颜色也很简单，它很像十年前我用的蓝黑墨水，不像碳素墨水那么无聊，也不像蓝墨水那么肤浅。我也没打算考虑买 Windows 笔记本装 Linux 的选项，因为显然续航上难以和 MacBook 相比，而且按我看脸的标准能买的 Windows 笔记本也奔着五位数去了……&lt;/p&gt;
&lt;p&gt;拿到手之后确实也是比较满意吧，除了内存和硬盘卖金子价格还是让我很不爽，以及刘海……虽然一个写插件恶搞过这个刘海的人买了刘海笔记本这件事有点讽刺吧，但我想问既然因为刘海加高了顶栏，为什么要特意把顶栏做的比刘海高一点点！就算是为了告诉用户刘海两侧是联通的，这个设计也足够逼死强迫症了。&lt;/p&gt;
&lt;p&gt;我自己的博客生成器因为是 Node.js 写的所以直接就可以在 macOS 上运行，Emacs 则一直以来对 macOS 支持都好得过分，简单修改了 Ansel 的脚本之后我也成功在 macOS 上运行了，DaVinci Resolve 也是跨平台的，所以我期望在笔记本上做的任务都还比较方便，我甚至也跑了 Tailscale 方便我随时远程连接到 Linux 设备上。虽然别人可能觉得我打肿脸充胖子但我还是要说我觉得 GNOME 桌面的使用体验比 macOS 桌面要好，我在自己的博客里写自己的主观看法不是很过分吧！其它的当然可以适应（比如我在 GNOME 里把关闭按钮改到左边了，我不是很在乎这个，然后在 macOS 里面用 Loop 实现了窗口靠边贴放），但为什么 macOS 不能自己调整输入法顺序！我想要一个固定的自己设置的输入法列表是什么大逆不道的事情吗？&lt;/p&gt;
&lt;p&gt;当然少不了去星巴克当气氛组，比如这篇博客就是在星巴克写的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/10-Years-Passed-and-I-Bought-MacBook/starbucks.jpg&quot; alt=&quot;starbucks&quot;&gt;&lt;/p&gt;
&lt;p&gt;关于买这台笔记本还有个很有意思的插曲是一开始和在苹果的朋友对需求对了半小时结果她以为我要买的是 iPad Pro，特别是我还说了我要跑 Node.js 要写博客 iPad Pro 太蠢了之后，我不知道这个对话是怎么进行下去的，我们跨频道聊天一直聊到她以为我要花两千块钱买妙控键盘——我曾经无数次的批评过这个破玩意，毕竟 HHKB 这种顶级的静电容键盘我也才花了两千块……我真的有思考要是最后她给我买了 iPad Pro 和妙控键盘我是掏钱还是不掏钱……&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="MacBook" label="MacBook" scheme="https://sh.alynx.one/tags/MacBook/" />
  </entry>
  <entry>
    <title>Module 的意思不是 Module，Module 的意思是 Config</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Module-Does-Not-Mean-Module-Module-Means-Config/" />
    <id>https://sh.alynx.one/posts/Module-Does-Not-Mean-Module-Module-Means-Config/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-06-05T12:02:37.000Z</published>
    <updated>2024-06-05T12:02:37.000Z</updated>
    <summary type="html">
      如果有得选，在开始一个新项目之前我是无论如何也不会选择 CMake 做构建系统的，我不信有任何人觉得 CMake 的语法很友好很容易读，Meson 相比之下更容易读写，而且按我的经验也更好用。但最大的问题是 Meson 出来的很晚，于是有大量的项目在 CMake 已经流行而 Me…
    </summary>
    <content type="html">
      &lt;p&gt;如果有得选，在开始一个新项目之前我是无论如何也不会选择 CMake 做构建系统的，我不信有任何人觉得 CMake 的语法很友好很容易读，Meson 相比之下更容易读写，而且按我的经验也更好用。但最大的问题是 Meson 出来的很晚，于是有大量的项目在 CMake 已经流行而 Meson 还没出现的时间里把基于 Autotools 的构建系统换成了 CMake（剩下没有更换的那些多数在 Meson 稳定之后更换到了 Meson）。显然把所有 CMake 项目重写成 Meson 是不现实的，最大的困难其实是 CMake 虽然难用，但在“跨平台的构建系统”这一点上已经足够用了，所以对于已有的 CMake 项目，我选择还是凑合用。&lt;/p&gt;
&lt;p&gt;实际需要我本人亲手调整 CMake 的项目很少，Ansel 是其中一个，因为当初从 Darktable fork 出来的时候我几乎是完全地整理了一遍它的构建系统，而我今天打开 Element 发现法国老哥在群里 @ 我说是 MSYS2 的 cURL 更新到 8.8.0 导致我们的 CI 构建不了了让我帮他看看，虽然我自己也不是很想写 CMake，但为了避免他一言不合就把依赖复制到项目源码里面我早就跟他说过这种事情我替他处理，所以还是说到做到。&lt;/p&gt;
&lt;p&gt;报错就是一堆 undefined reference，我直接去 Google 搜了一下没有搜到什么有用的资料。去 MSYS2 的 issue 搜索 curl 倒是搜到了 &lt;a href=&quot;https://github.com/msys2/MINGW-packages/issues/21028&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;https://github.com/msys2/MINGW-packages/issues/21028&lt;/a&gt;，下面有人说把 &lt;code&gt;CURL_LIBRARIES&lt;/code&gt; 这个变量换成 &lt;code&gt;CURL::libcurl&lt;/code&gt; 这个 target 就可以了。我倒是理解这是什么意思，但我想知道这是怎么回事。这中间的过程可以说是曲折离奇。&lt;/p&gt;
&lt;p&gt;首先是在这里看到有维护者说这个可能是因为他们把 cURL 从 Autotools 构建换成 CMake 构建导致的，但我其实不是很理解，因为我印象里 CMake 查找 package 是调用 &lt;code&gt;Find&amp;lt;PackageName&amp;gt;.cmake&lt;/code&gt;，我去看了我们项目本身没有提供这个文件，那应该调用的就是 &lt;code&gt;/usr/share/cmake/Modules/FindCURL.cmake&lt;/code&gt; 这个由 CMake 本身提供的文件了，但抛开 CMake 这种 Find 模块谜一样的语法不谈，这个文件明确设置了 &lt;code&gt;CURL_LIBRARIES&lt;/code&gt; 变量，那也就是说这个文件根本没有生效？这时候我去翻了 CMake &lt;code&gt;find_package()&lt;/code&gt; 的文档，好家伙，这玩意除了有基础和扩展两种语法，还有 Module 和 Config 两种查找模式（内心一万匹草泥马飞奔而过）……如果是 Module 模式，就会用 &lt;code&gt;FindCURL.cmake&lt;/code&gt; 这个模块，但如果是 Config 模式，就会去调用 &lt;code&gt;CURLConfig.cmake&lt;/code&gt; 这个配置……而后面这个配置里面只写了 CMake 的 target，没有定义 &lt;code&gt;CURL_LIBRARIES&lt;/code&gt; 这个变量。&lt;/p&gt;
&lt;p&gt;但这样又有两个疑问，首先是为什么会有后面这个 Config 文件呢？毕竟以前一直用的是 Module，我比较了 Arch 的 curl PKGBUILD 和 MSYS2 的 curl PKGBUILD 得出结论：这个文件是 CMake 构建项目的时候自动基于当前所有的 target 生成的，因为 Arch 使用 Autotools 构建的包没有这个文件。但为什么默认会使用这个 Config 呢？按照 CMake 的文档，Config 似乎是 Module 的 fallback，也就是说只有没有 Module 的时候才会使用 Config，我百思不得其解。但总之解决方案很简单，对于 Ansel 自己，我使用了下面的代码：&lt;/p&gt;
&lt;figure data-raw=&quot;find_package(CURL REQUIRED)
if(TARGET CURL::libcurl)
  set(CURL_LIBRARIES CURL::libcurl)
endif()
&quot; data-info=&quot;language-cmake&quot; data-lang=&quot;cmake&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(CURL REQUIRED)
if(TARGET CURL::libcurl)
  set(CURL_LIBRARIES CURL::libcurl)
endif()
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这样可以保证对不同版本的兼容性，而对于我们自己构建的 exiv2 依赖，我一开始打算直接 &lt;code&gt;find_package(CURL MODULE REQUIRED)&lt;/code&gt;，毕竟按照文档这样可以强制它忽略 Config 嘛，但结果我推到 CI 上仍然报错。我不是很理解，直到我看了一眼 log 发现它仍然是用的 Config，我当时真的骂人了，我不是写的很清楚要强制 Module 吗？这到底是怎么回事？&lt;/p&gt;
&lt;p&gt;当然实际上解决方法是有的，只要使用前一种方法就可以了。但我咽不下这口气，不按照文档说的顺序先尝试 Module 就算了，我强制 Module 了还是给我用 Config 是什么意思？你猜我最后在哪找到了解释，是在 &lt;a href=&quot;https://cmake.org/cmake/help/latest/module/FindCURL.html#curl-cmake&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;&lt;code&gt;FindCURL.cmake&lt;/code&gt; 的文档页面&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If CURL was built using the CMake buildsystem then it provides its own CURLConfig.cmake file for use with the find_package() command&#039;s config mode. This module looks for this file and, if found, returns its results with no further action.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我觉得任何一个脑子正常的程序员都该骂做出这个改动的人，首先你改变了默认的加载顺序也就算了，我可以理解成这是你们 CMake 用户的某种隐性约定我新来的我不知道。你用 Config 的时候不遵守 &lt;code&gt;Find&amp;lt;PackageName&amp;gt;.cmake&lt;/code&gt; Module 约定的导出变量我也忍了。但是请问为什么我在 &lt;strong&gt;显式指定&lt;/strong&gt; 了 &lt;code&gt;MODULE&lt;/code&gt; 关键字的情况下，仍然会被不知不觉的改变行为？这个说明藏在这么小的专题页面里，是生怕用户搞清楚为什么代码和实际行为完全相反吗？写这个逻辑的人有没有意识到自己是在好心办坏事？&lt;/p&gt;
&lt;p&gt;这个 &lt;code&gt;FindCURL.cmake&lt;/code&gt; 是 CMake 官方提供的 module 之一，所以也没必要试图甩锅给其他人。总之我在使用 CMake 的过程中除了迷惑到难以阅读的拼写习惯，反人类且可能混杂不同时期或者不同风味（比如 &lt;code&gt;find_package()&lt;/code&gt; 就有两种语法）的 API，写起来弯弯绕绕的约定习惯（比如 &lt;code&gt;Find&amp;lt;PackageName&amp;gt;.cmake&lt;/code&gt; 约定导出 &lt;code&gt;&amp;lt;PackageName&amp;gt;_LIBRARIES&lt;/code&gt; 然后经常会看到什么 &lt;code&gt;set(&amp;lt;PackageName&amp;gt;_LIBRARIES &amp;lt;PackageName&amp;gt;_LIBRARY)&lt;/code&gt;）之外，现在又多了个就算你显式指定参数还是会偷偷做出相反行为的体验。我不反对 CMake 确实能用，但和 Meson 的使用体验比起来，写 CMake 确实不能算是什么令人享受的事情。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="编程" label="编程" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BC%96%E7%A8%8B/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="编程" label="编程" scheme="https://sh.alynx.one/tags/%E7%BC%96%E7%A8%8B/" />
    <category term="CMake" label="CMake" scheme="https://sh.alynx.one/tags/CMake/" />
  </entry>
  <entry>
    <title>Pango 中的 Ink 和 Logical 矩形</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Ink-and-Logical-Rectangles-of-Pango/" />
    <id>https://sh.alynx.one/posts/Ink-and-Logical-Rectangles-of-Pango/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-05-27T03:50:22.000Z</published>
    <updated>2024-05-27T03:50:22.000Z</updated>
    <summary type="html">
      如果你尝试过自己在程序里绘制一段字符串，你就会发现你得到的 buffer 的高度并不总是你设置的字号大小，特别是英文。这个现象的原因是英文在书写的时候并不像汉字一样高矮都一样，而是有上有下，比如 g 这个字符的尾巴总是向下伸出来一点，这些字符通常是基线对齐的，也就是说主体部分会躺…
    </summary>
    <content type="html">
      &lt;p&gt;如果你尝试过自己在程序里绘制一段字符串，你就会发现你得到的 buffer 的高度并不总是你设置的字号大小，特别是英文。这个现象的原因是英文在书写的时候并不像汉字一样高矮都一样，而是有上有下，比如 g 这个字符的尾巴总是向下伸出来一点，这些字符通常是基线对齐的，也就是说主体部分会躺在一条基线上，尾巴和头发则可能会伸出来。而就算是方块造型的汉字，字体也可能会在上下留下一定的空间，导致实际排版的尺寸比设置的字号要大。（我不是专业的排版工作者或者字体设计师，如果说错了希望大家指正。）&lt;/p&gt;
&lt;p&gt;通常来说这种多占用一点空间的行为不会有什么问题，因为只有这样渲染一排字符的时候看起来才会比较自然。但假如你像我一样希望在字符串周围绘制边框的话，事情就变得麻烦起来了，如果你只是按照你设置的字号大小去绘制边框，你会发现这个字号大小被 Pango 这样的排版库当作基线以上的高度，所以字母的尾巴会出现在边框外面，这是绘制下划线的方式而不是绘制边框的方式。当然你可以从 Pango 获取一些有用的数据，有两个矩形尺寸可以获得，一个是 logical，这个是 Pango 用来排版的矩形，它是按照基线安排的，所以你使用这个绘制会保证字符主体都在一条直线上，多个字符串绘制出来的效果比较接近书写的习惯，也因此它的 x 和 y 坐标总是 0，是绘制的基准点。另一个是 ink，这个是“着墨”区域，也就是说把所有字符包进去的最小矩形，由于字符书写的时候是错落有致的，x 和 y 坐标并不总是 0，而是相对 logical 基准点的 offset，比如一个斜体的字符的 x 坐标很可能是负数，因为书写的时候它是倾斜到左边的字符下面的。&lt;/p&gt;
&lt;p&gt;对于这两个矩形，官方文档基本没有解释，而网上查到的很多解释是错的，实际上可以用下面的程序把它们绘制出来：&lt;/p&gt;
&lt;figure data-raw=&quot;#include &lt;gtk/gtk.h&gt;

static void draw(GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data)
{
	PangoLayout *layout = pango_cairo_create_layout(cr);
	pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
	PangoFontDescription *font = pango_font_description_new();
	pango_font_description_set_family(font, &amp;quot;serif&amp;quot;);
	pango_font_description_set_style(font, PANGO_STYLE_ITALIC);
	pango_font_description_set_absolute_size(font, 200 * PANGO_SCALE);
	pango_layout_set_font_description(layout, font);

	PangoRectangle string_ink;
	PangoRectangle string_logical;
	pango_layout_set_text(layout, &amp;quot;g&amp;quot;, -1);
	pango_layout_get_pixel_extents(layout, &amp;amp;string_ink, &amp;amp;string_logical);
	g_print(&amp;quot;Logical: x is %d, y is %d, width is %d, height is %d.\n&amp;quot;, string_logical.x, string_logical.y, string_logical.width, string_logical.height);
	g_print(&amp;quot;Ink: x is %d, y is %d, width is %d, height is %d.\n&amp;quot;, string_ink.x, string_ink.y, string_ink.width, string_ink.height);

	cairo_set_source_rgb(cr, 0.3, 0.3, 0.3);
	cairo_paint(cr);

	const int x = 100;
	const int y = 100;
	cairo_move_to(cr, x, y);
	cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
	pango_cairo_show_layout(cr, layout);

	cairo_set_line_width(cr, 5);
	cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, 1.0);
	cairo_rectangle(cr, x + string_logical.x, y + string_logical.y, string_logical.width, string_logical.height);
	cairo_stroke(cr);

	cairo_set_source_rgba(cr, 0.0, 0.0, 1.0, 1.0);
	cairo_rectangle(cr, x + string_ink.x, y + string_ink.y, string_ink.width, string_ink.height);
	cairo_stroke(cr);
}

static void on_activate(GtkApplication *app, gpointer user_data)
{
	GtkWidget *window = gtk_application_window_new (app);
	gtk_window_set_title(GTK_WINDOW(window), &amp;quot;Test Pango Extents&amp;quot;);
	gtk_window_set_default_size(GTK_WINDOW(window), 500, 500);
	GtkWidget *area = gtk_drawing_area_new();
	gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw, NULL, NULL);
	gtk_window_set_child(GTK_WINDOW(window), area);
	gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char *argv[])
{
	GtkApplication *app = gtk_application_new(&amp;quot;one.alynx.test-pango-extents&amp;quot;, G_APPLICATION_DEFAULT_FLAGS);
	g_signal_connect(app, &amp;quot;activate&amp;quot;, G_CALLBACK(on_activate), NULL);
	int status = g_application_run(G_APPLICATION(app), argc, argv);
	g_object_unref(app);
	return status;
}
&quot; data-info=&quot;language-c&quot; data-lang=&quot;c&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;gtk/gtk.h&amp;gt;

static void draw(GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data)
{
	PangoLayout *layout = pango_cairo_create_layout(cr);
	pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
	PangoFontDescription *font = pango_font_description_new();
	pango_font_description_set_family(font, &quot;serif&quot;);
	pango_font_description_set_style(font, PANGO_STYLE_ITALIC);
	pango_font_description_set_absolute_size(font, 200 * PANGO_SCALE);
	pango_layout_set_font_description(layout, font);

	PangoRectangle string_ink;
	PangoRectangle string_logical;
	pango_layout_set_text(layout, &quot;g&quot;, -1);
	pango_layout_get_pixel_extents(layout, &amp;amp;string_ink, &amp;amp;string_logical);
	g_print(&quot;Logical: x is %d, y is %d, width is %d, height is %d.\n&quot;, string_logical.x, string_logical.y, string_logical.width, string_logical.height);
	g_print(&quot;Ink: x is %d, y is %d, width is %d, height is %d.\n&quot;, string_ink.x, string_ink.y, string_ink.width, string_ink.height);

	cairo_set_source_rgb(cr, 0.3, 0.3, 0.3);
	cairo_paint(cr);

	const int x = 100;
	const int y = 100;
	cairo_move_to(cr, x, y);
	cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
	pango_cairo_show_layout(cr, layout);

	cairo_set_line_width(cr, 5);
	cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, 1.0);
	cairo_rectangle(cr, x + string_logical.x, y + string_logical.y, string_logical.width, string_logical.height);
	cairo_stroke(cr);

	cairo_set_source_rgba(cr, 0.0, 0.0, 1.0, 1.0);
	cairo_rectangle(cr, x + string_ink.x, y + string_ink.y, string_ink.width, string_ink.height);
	cairo_stroke(cr);
}

static void on_activate(GtkApplication *app, gpointer user_data)
{
	GtkWidget *window = gtk_application_window_new (app);
	gtk_window_set_title(GTK_WINDOW(window), &quot;Test Pango Extents&quot;);
	gtk_window_set_default_size(GTK_WINDOW(window), 500, 500);
	GtkWidget *area = gtk_drawing_area_new();
	gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw, NULL, NULL);
	gtk_window_set_child(GTK_WINDOW(window), area);
	gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char *argv[])
{
	GtkApplication *app = gtk_application_new(&quot;one.alynx.test-pango-extents&quot;, G_APPLICATION_DEFAULT_FLAGS);
	g_signal_connect(app, &quot;activate&quot;, G_CALLBACK(on_activate), NULL);
	int status = g_application_run(G_APPLICATION(app), argc, argv);
	g_object_unref(app);
	return status;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;下面是编译命令：&lt;/p&gt;
&lt;figure data-raw=&quot;$ gcc -o test-pango-extents `pkg-config --cflags --libs gtk4` test-pango-extents.c
$ ./test-pango-extents
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ gcc -o test-pango-extents `pkg-config --cflags --libs gtk4` test-pango-extents.c
$ ./test-pango-extents
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;输出的尺寸是这样：&lt;/p&gt;
&lt;figure data-raw=&quot;Logical: x is 0, y is 0, width is 113, height is 265.
Ink: x is -1, y is 102, width is 122, height is 151.
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;Logical: x is 0, y is 0, width is 113, height is 265.
Ink: x is -1, y is 102, width is 122, height is 151.
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;得到的结果是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Ink-and-Logical-Rectangles-of-Pango/screenshot.png&quot; alt=&quot;test-pango-extents&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这两个都不符合我的需求，因为我希望字符串按照边框对齐而不是按照基线对齐，同时边框看起来都一样高，直接绘制 logical 矩形实际仍然是基线对齐，比我想要的大了很多。而按照 ink 绘制不能保证边框一样高。最后的做法是使用 ink 的宽度绘制边框的宽度，然后利用 ink 的高度重新计算绘制位置将字符放在底边上，这里要注意的是因为默认绘制是以 logical 矩形左上角作为基准点，需要减去 ink 矩形的左上角 offset 才能从 ink 的左上角开始绘制。具体的修改在 &lt;a href=&quot;https://github.com/AlynxZhou/showmethekey/commit/f1a2a5a995d6c224700b376b287c7aaff512d365#diff-295efe2fde895e5fedc8e4194f1be7ba064dc9787643139520f7913541aee1f6&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;https://github.com/AlynxZhou/showmethekey/commit/f1a2a5a995d6c224700b376b287c7aaff512d365#diff-295efe2fde895e5fedc8e4194f1be7ba064dc9787643139520f7913541aee1f6&lt;/a&gt;，这里就不赘述了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="编程" label="编程" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BC%96%E7%A8%8B/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="编程" label="编程" scheme="https://sh.alynx.one/tags/%E7%BC%96%E7%A8%8B/" />
    <category term="字体" label="字体" scheme="https://sh.alynx.one/tags/%E5%AD%97%E4%BD%93/" />
  </entry>
  <entry>
    <title>构建和运行 Xen HVM 和 PV</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Xen-HVM-PV/" />
    <id>https://sh.alynx.one/posts/Xen-HVM-PV/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-03-07T15:19:19.000Z</published>
    <updated>2024-03-12T09:59:48.000Z</updated>
    <summary type="html">
      &lt;div class=&quot;alert-red&quot;&gt;请注意本文并不是推荐读者使用 Xen 作为虚拟化方案，相反，KVM 才是目前更合适大部分读者的方案。&lt;/div&gt;

&lt;h1 id=&quot;%E7%AE%80%E4%BB%8B&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E7%AE%80%E4%BB%8B&quot;&gt;&lt;/a&gt;简介&lt;/h1&gt;
&lt;p&gt;由于工作需要，最近我需要搞一个 Xen PV 来进行测试，在此之前我一直使用 qemu/KVM，只是听说 Xen 是 KVM 之前流行过的虚拟化方案。比起几乎什么都不需要做交给 libvirt 包办就可以的 KVM，Xen 的设置相对要复杂一点。&lt;/p&gt;

    </summary>
    <content type="html">
      &lt;div class=&quot;alert-red&quot;&gt;请注意本文并不是推荐读者使用 Xen 作为虚拟化方案，相反，KVM 才是目前更合适大部分读者的方案。&lt;/div&gt;

&lt;h1 id=&quot;%E7%AE%80%E4%BB%8B&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E7%AE%80%E4%BB%8B&quot;&gt;&lt;/a&gt;简介&lt;/h1&gt;
&lt;p&gt;由于工作需要，最近我需要搞一个 Xen PV 来进行测试，在此之前我一直使用 qemu/KVM，只是听说 Xen 是 KVM 之前流行过的虚拟化方案。比起几乎什么都不需要做交给 libvirt 包办就可以的 KVM，Xen 的设置相对要复杂一点。&lt;/p&gt;
&lt;a id=&quot;more&quot;&gt;&lt;/a&gt;

&lt;p&gt;首先 Xen 分为 HVM 和 PV 两种常见的虚拟方案（PVH 我也没用过），HVM 依赖于硬件虚拟化，和常见的虚拟机没什么区别，而 PV 并不依赖硬件虚拟化，是通过虚拟机 Linux 内核中特殊的驱动，将请求转给宿主机的内核代为操作，但配置起来也更加复杂。目前 PV 所需的代码已经并入 Linux 内核上游，你能安装的发行版大部分都可以直接作为 PV 的虚拟机运行。&lt;/p&gt;
&lt;p&gt;无论是 HVM 还是 PV，Xen 都用 domU（domain 的缩写，U 可以是 1、2、3……）代表虚拟机，然后用 dom0 代表宿主机，当然文档里也会叫宿主机 Hypervisor，这里我也可能直接将虚拟机叫做 VM。&lt;/p&gt;
&lt;h1 id=&quot;%E6%9E%84%E5%BB%BA%E5%92%8C%E8%BF%90%E8%A1%8C-dom0&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E6%9E%84%E5%BB%BA%E5%92%8C%E8%BF%90%E8%A1%8C-dom0&quot;&gt;&lt;/a&gt;构建和运行 dom0&lt;/h1&gt;
&lt;div class=&quot;alert-blue&quot;&gt;温馨提示：根据我的经验，在 Xen Hypervsior 上运行的 GNOME 桌面会由于未知原因在未知操作时卡住，建议不要用你平时使用的开发机作为 Xen Hypervisor，而是另找一台机器作为服务器运行 Xen Hypervisor，然后远程连接上去操作。&lt;/div&gt;

&lt;p&gt;和 KVM 不需要什么操作就能用不一样，Xen 需要你构建一个单独的 boot loader，在加载 Linux 内核之前先加载它，从而实现 Xen 的支持。我这里用的 Arch Linux，需要构建 &lt;code&gt;xen&lt;/code&gt; 这个 AUR 包，根据 wiki 所说推荐用下面的指令构建：&lt;/p&gt;
&lt;figure data-raw=&quot;$ build_stubdom=true efi_dir=&amp;quot;/boot&amp;quot; makepkg -si
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ build_stubdom=true efi_dir=&quot;/boot&quot; makepkg -si
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;我这里直接把 ESP 挂载到 &lt;code&gt;/boot&lt;/code&gt;，你可能需要按情况修改。&lt;/p&gt;
&lt;p&gt;你还需要 &lt;code&gt;xen-qemu&lt;/code&gt; 这个 AUR 包提供 qemu 前端对 Xen 的支持，但和一般的 AUR 包不同的是如果你直接构建这个包，得到的包很可能和你已经安装的 qemu 文件冲突。这是我遇到的唯一一个必须在 clean chroot 才能构建的包。然后由于它依赖 &lt;code&gt;xen&lt;/code&gt; 这个 AUR 包，你必须手动操作。&lt;/p&gt;
&lt;p&gt;首先安装 &lt;code&gt;devtools&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;# pacman -S devtools
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# pacman -S devtools
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后创建一个用于构建 clean chroot 的目录：&lt;/p&gt;
&lt;figure data-raw=&quot;$ mkdir ~/chroot
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mkdir ~/chroot
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后在里面安装基础依赖：&lt;/p&gt;
&lt;figure data-raw=&quot;$ mkarchroot ~/chroot/root base-devel
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mkarchroot ~/chroot/root base-devel
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;一般这时候 chroot 里面应该已经是最新的了，但也可以用下面的命令更新：&lt;/p&gt;
&lt;figure data-raw=&quot;$ arch-nspawn ~/chroot/root pacman -Syu
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ arch-nspawn ~/chroot/root pacman -Syu
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后切换到你包含 &lt;code&gt;xen-qemu&lt;/code&gt; 的 &lt;code&gt;PKGBUILD&lt;/code&gt; 的目录，你可能需要的两个依赖是 &lt;code&gt;xen&lt;/code&gt; 和 &lt;code&gt;numactl&lt;/code&gt;，前者我们刚刚构建过，后者我们可以直接从官方仓库通过 &lt;code&gt;pacman -S numactl&lt;/code&gt; 安装，然后在命令行参数里指定这两个文件的位置：&lt;/p&gt;
&lt;figure data-raw=&quot;$ makechrootpkg -c -r ~/chroot -I ../xen/xen-4.18.1pre-1-x86_64.pkg.tar.zst -I /var/cache/pacman/pkg/numactl-2.0.18-1-x86_64.pkg.tar.zst
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ makechrootpkg -c -r ~/chroot -I ../xen/xen-4.18.1pre-1-x86_64.pkg.tar.zst -I /var/cache/pacman/pkg/numactl-2.0.18-1-x86_64.pkg.tar.zst
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后用 &lt;code&gt;pacman -U xen-qemu-*.tar.pkg.zst&lt;/code&gt; 安装你刚刚构建好的包，此时应该没有文件冲突了。&lt;/p&gt;
&lt;p&gt;你还需要安装下面的包提供虚拟机内的 BIOS 和 UEFI 引导支持：&lt;/p&gt;
&lt;figure data-raw=&quot;# pacman -S seabios edk2-ovmf
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# pacman -S seabios edk2-ovmf
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后你需要加载构建好的 Xen boot loader，让它在 Linux 内核之前启动，我使用的 systemd-boot，所以下面就简单写 systemd-boot 的配置方式，逻辑上是完全一致的，如果你使用 GRUB，建议参考 Arch Wiki。&lt;/p&gt;
&lt;p&gt;首先添加一个 systemd-boot 启动项文件，我这里使用 &lt;code&gt;/boot/loader/entries/xen.conf&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;title Xen Hypervisor
sort-key xen
efi /xen.efi
&quot; data-info=&quot;language-conf&quot; data-lang=&quot;conf&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-conf&quot;&gt;title Xen Hypervisor
sort-key xen
efi /xen.efi
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;如果你也用 systemd-boot，这个文件对你来说应该非常简单，构建 &lt;code&gt;xen&lt;/code&gt; 包的时候已经将支持 EFI 的 Xen boot loader 也就是 &lt;code&gt;xen.efi&lt;/code&gt; 这个文件安装到了 ESP，只要引导它就可以。&lt;/p&gt;
&lt;p&gt;接下来我们编写 Xen 的配置文件让它可以正确找到你的 initramfs 和内核，并传递内核参数，配置文件 &lt;code&gt;xen.cfg&lt;/code&gt; 需要和 &lt;code&gt;xen.efi&lt;/code&gt; 位于同一个目录，这里就是 &lt;code&gt;/boot/xen.cfg&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;[global]
default=xen

[xen]
options=console=vga loglvl=all noreboot
kernel=vmlinuz-linux root=&amp;quot;UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX&amp;quot; rootfstype=&amp;quot;btrfs&amp;quot; rootflags=&amp;quot;rw,defaults,noatime,compress=zstd:3,ssd,space_cache,subvolid=257,subvol=/@&amp;quot; rw add_efi_memmap threadirqs nvidia_drm.modeset=1
ramdisk=initramfs-linux.img
ucode=amd-ucode.img
&quot; data-info=&quot;language-conf&quot; data-lang=&quot;conf&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-conf&quot;&gt;[global]
default=xen

[xen]
options=console=vga loglvl=all noreboot
kernel=vmlinuz-linux root=&quot;UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX&quot; rootfstype=&quot;btrfs&quot; rootflags=&quot;rw,defaults,noatime,compress=zstd:3,ssd,space_cache,subvolid=257,subvol=/@&quot; rw add_efi_memmap threadirqs nvidia_drm.modeset=1
ramdisk=initramfs-linux.img
ucode=amd-ucode.img
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;以上的内核参数是我所用的，你可以从你当前的 boot loader 启动项文件里复制出你正在用的内核参数。如果你复制了 &lt;code&gt;xen&lt;/code&gt; 包自带的示例文件而不是我的，需要注意里面包含限制宿主机可用内存的参数，这是为了避免在创建虚拟机时再限制宿主机内存，影响宿主机各种缓存的策略，默认的值给的很小，可能导致无法正常启动桌面，你可能需要改大一点。不过对于我们这种简单 debug 用，可以直接忽略此参数，影响不大。&lt;/p&gt;
&lt;p&gt;然后你需要让 Xen 所需的一些守护进程开机启动：&lt;/p&gt;
&lt;figure data-raw=&quot;# systemctl enable xenconsoled.service xen-init-dom0.service xen-qemu-dom0-disk-backend.service xendomains.service
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# systemctl enable xenconsoled.service xen-init-dom0.service xen-qemu-dom0-disk-backend.service xendomains.service
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后重启系统，选择名字是 &lt;code&gt;Xen Hypervisor&lt;/code&gt; 的启动项，你应该就有一个可以运行 Xen 的环境了。&lt;/p&gt;
&lt;h1 id=&quot;%E9%85%8D%E7%BD%AE%E5%92%8C%E8%BF%90%E8%A1%8C-domU&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E9%85%8D%E7%BD%AE%E5%92%8C%E8%BF%90%E8%A1%8C-domU&quot;&gt;&lt;/a&gt;配置和运行 domU&lt;/h1&gt;
&lt;p&gt;然后按道理说既然 Xen 和 KVM 都是用 qemu 作为前端，那完全可以交给我常用的 virt-manager 操办一切，但我尝试构建了带 Xen 支持的 libvirt，结果运行起来发现由于某个 bug，它并不能真的支持 Xen，所以只能全手动操作了。&lt;/p&gt;
&lt;h2 id=&quot;%E6%9E%84%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%94%A8%E7%9A%84-NAT-%E7%BD%91%E7%BB%9C&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E6%9E%84%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%94%A8%E7%9A%84-NAT-%E7%BD%91%E7%BB%9C&quot;&gt;&lt;/a&gt;构建虚拟机用的 NAT 网络&lt;/h2&gt;
&lt;p&gt;virt-manager 会帮我自动创建一个 NAT 网络使得虚拟机之间可以互相联系并且通过宿主机访问外网，我相信大部分人都需要让虚拟机联网，但只能我自己解决这件事了。&lt;/p&gt;
&lt;p&gt;具体的操作包含以下几步：添加一个桥接接口；然后给它分配一个 IP；再开启系统的 NAT 转发（包含 &lt;code&gt;sysctl&lt;/code&gt; 和 &lt;code&gt;iptables&lt;/code&gt; 两部分，我自己也不是 &lt;code&gt;iptables&lt;/code&gt; 高手所以我也不能给你解释）；然后在这个接口上启动 DHCP 服务器，给虚拟机提供 IP 地址和 DNS 服务器。这样就完成了宿主机的部分，Xen 可以根据虚拟机的配置文件自动把虚拟机添加到你刚才创建的桥接接口上。&lt;/p&gt;
&lt;p&gt;为了简化这个操作我编写了一个脚本：&lt;/p&gt;
&lt;figure data-raw=&quot;#!/bin/bash

set -x

OUT_IF=&amp;quot;${1}&amp;quot;
BR_IF=&amp;quot;${2}&amp;quot;
BR_IP=&amp;quot;192.168.123.1&amp;quot;
BR_IP_RANGE=&amp;quot;192.168.123.100,192.168.123.200&amp;quot;
BR_DNS=&amp;quot;1.1.1.1,8.8.8.8&amp;quot;

[[ -z &amp;quot;${OUT_IF}&amp;quot; ]] &amp;amp;&amp;amp; exit 1
[[ -z &amp;quot;${BR_IF}&amp;quot; ]] &amp;amp;&amp;amp; BR_IF=&amp;quot;vmbr0&amp;quot;

ip link add name &amp;quot;${BR_IF}&amp;quot; type bridge
ip link set dev &amp;quot;${BR_IF}&amp;quot; up

ip address add dev &amp;quot;${BR_IF}&amp;quot; &amp;quot;${BR_IP}/24&amp;quot;

# Enable NAT, so VMs can accept Internet.
FORWARD=$(sysctl --values net.ipv4.ip_forward)
# See &lt;https://www.karlrupp.net/en/computer/nat_tutorial&gt;.
sysctl net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o &amp;quot;${OUT_IF}&amp;quot; -j MASQUERADE

# Start DHCP server so we don&#039;t need to manually assign IP addresses for VMs.
# `--no-daemon` starts dnsmasq in debug mode, so it won&#039;t overload SIGINT.
# It is hard to prevent `dnsmasq` from listening `127.0.0.1:53` as a DNS server,
# so just disable its DNS server, and send DNS server to VMs via DHCP.
# It seems you need to set `--bind-interfaces` with `--interface` to make it
# bind to a interface only, otherwise it will try to bind to other interfaces
# and conflict with other dnsmasq instance (if exists).
dnsmasq --no-daemon \
	--port=0 \
	--interface=&amp;quot;${BR_IF}&amp;quot; \
	--bind-interfaces \
	--dhcp-range=&amp;quot;${BR_IP_RANGE}&amp;quot; \
	--dhcp-option=&amp;quot;option:dns-server,${BR_DNS}&amp;quot;

iptables -t nat -D POSTROUTING -o &amp;quot;${OUT_IF}&amp;quot; -j MASQUERADE
sysctl &amp;quot;net.ipv4.ip_forward=${FORWARD}&amp;quot;

ip link set dev &amp;quot;${BR_IF}&amp;quot; down
ip link delete &amp;quot;${BR_IF}&amp;quot;
&quot; data-info=&quot;language-bash&quot; data-lang=&quot;bash&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

set -x

OUT_IF=&quot;${1}&quot;
BR_IF=&quot;${2}&quot;
BR_IP=&quot;192.168.123.1&quot;
BR_IP_RANGE=&quot;192.168.123.100,192.168.123.200&quot;
BR_DNS=&quot;1.1.1.1,8.8.8.8&quot;

[[ -z &quot;${OUT_IF}&quot; ]] &amp;amp;&amp;amp; exit 1
[[ -z &quot;${BR_IF}&quot; ]] &amp;amp;&amp;amp; BR_IF=&quot;vmbr0&quot;

ip link add name &quot;${BR_IF}&quot; type bridge
ip link set dev &quot;${BR_IF}&quot; up

ip address add dev &quot;${BR_IF}&quot; &quot;${BR_IP}/24&quot;

# Enable NAT, so VMs can accept Internet.
FORWARD=$(sysctl --values net.ipv4.ip_forward)
# See &amp;lt;https://www.karlrupp.net/en/computer/nat_tutorial&amp;gt;.
sysctl net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o &quot;${OUT_IF}&quot; -j MASQUERADE

# Start DHCP server so we don&#039;t need to manually assign IP addresses for VMs.
# `--no-daemon` starts dnsmasq in debug mode, so it won&#039;t overload SIGINT.
# It is hard to prevent `dnsmasq` from listening `127.0.0.1:53` as a DNS server,
# so just disable its DNS server, and send DNS server to VMs via DHCP.
# It seems you need to set `--bind-interfaces` with `--interface` to make it
# bind to a interface only, otherwise it will try to bind to other interfaces
# and conflict with other dnsmasq instance (if exists).
dnsmasq --no-daemon \
	--port=0 \
	--interface=&quot;${BR_IF}&quot; \
	--bind-interfaces \
	--dhcp-range=&quot;${BR_IP_RANGE}&quot; \
	--dhcp-option=&quot;option:dns-server,${BR_DNS}&quot;

iptables -t nat -D POSTROUTING -o &quot;${OUT_IF}&quot; -j MASQUERADE
sysctl &quot;net.ipv4.ip_forward=${FORWARD}&quot;

ip link set dev &quot;${BR_IF}&quot; down
ip link delete &quot;${BR_IF}&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;你可以把这个脚本保存成 &lt;code&gt;mkvmbr0.sh&lt;/code&gt;，然后 &lt;code&gt;chmod +x mkvmbr0.sh&lt;/code&gt;，然后用 &lt;code&gt;ip a&lt;/code&gt; 查看你当前联网所用的端口名，比如我的是 &lt;code&gt;wlp5s0&lt;/code&gt;，就可以 &lt;code&gt;sudo ./mkvmbr0.sh wlp5s0&lt;/code&gt; 创建一个运行在 &lt;code&gt;vmbr0&lt;/code&gt; 端口上的 NAT 网络，脚本会启动 &lt;code&gt;dnsmasq&lt;/code&gt; 作为 DHCP 服务器并通过 DHCP 服务器给虚拟机下发 DNS 服务器地址，如果你已经用完了虚拟机，&lt;code&gt;Ctrl+C&lt;/code&gt; 打断 &lt;code&gt;dnsmasq&lt;/code&gt; 它就会进行后续的清理工作并退出。&lt;/p&gt;
&lt;h2 id=&quot;%E7%BC%96%E5%86%99-HVM-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E7%BC%96%E5%86%99-HVM-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6&quot;&gt;&lt;/a&gt;编写 HVM 配置文件&lt;/h2&gt;
&lt;p&gt;HVM 实在是没什么复杂的，这么说是相对于 PV 而言，比如说构建一个 openSUSE Tumbleweed 的 HVM 可以写一个叫做 &lt;code&gt;hvm-tumbleweed.cfg&lt;/code&gt; 的文件：&lt;/p&gt;
&lt;figure data-raw=&quot;name = &#039;hvm-tumbleweed&#039;
builder = &#039;hvm&#039;
memory = 2048
vcpus = 4
disk = [ &#039;file:/home/alynx/xen/disk-tumbleweed.img,xvda,rw&#039;, &#039;file:/home/alynx/xen/openSUSE-Tumbleweed-DVD-x86_64-Current.iso,sdb:cdrom,r&#039; ]
vif = [ &#039;mac=00:16:3e:00:00:02,bridge=vmbr0&#039; ]
vnc = 1
vnclisten = &#039;0.0.0.0&#039;
vncdisplay = 1
&quot; data-info=&quot;language-conf&quot; data-lang=&quot;conf&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-conf&quot;&gt;name = &#039;hvm-tumbleweed&#039;
builder = &#039;hvm&#039;
memory = 2048
vcpus = 4
disk = [ &#039;file:/home/alynx/xen/disk-tumbleweed.img,xvda,rw&#039;, &#039;file:/home/alynx/xen/openSUSE-Tumbleweed-DVD-x86_64-Current.iso,sdb:cdrom,r&#039; ]
vif = [ &#039;mac=00:16:3e:00:00:02,bridge=vmbr0&#039; ]
vnc = 1
vnclisten = &#039;0.0.0.0&#039;
vncdisplay = 1
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这里没什么要注意的，无非是 &lt;code&gt;bridge=&lt;/code&gt; 后面接你 NAT 网络的桥接端口。以及你在配置文件里写 &lt;code&gt;xvda&lt;/code&gt; 在虚拟机里会变成 &lt;code&gt;sda&lt;/code&gt;，所以不要再写另一个叫做 &lt;code&gt;sda&lt;/code&gt; 的设备了。如果你有多个虚拟机，记得修改 MAC 地址和 VNC 端口。&lt;/p&gt;
&lt;p&gt;创建磁盘文件可以用下面的命令：&lt;/p&gt;
&lt;figure data-raw=&quot;$ truncate -s 20G disk-tumbleweed.img
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ truncate -s 20G disk-tumbleweed.img
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后用下面的命令就可以启动这个虚拟机：&lt;/p&gt;
&lt;figure data-raw=&quot;# xl create hvm-tumblweed.cfg
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# xl create hvm-tumblweed.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;其他的命令可以直接 &lt;code&gt;man xl&lt;/code&gt; 查看手册。可以使用 &lt;code&gt;vncviewer YOUR_HOST_IP:1&lt;/code&gt; 连接虚拟机。关于这个配置文件的具体语法可以参考 &lt;a href=&quot;https://xenbits.xen.org/docs/unstable/man/xl.cfg.5.html&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;%E7%BC%96%E5%86%99-PV-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E7%BC%96%E5%86%99-PV-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6&quot;&gt;&lt;/a&gt;编写 PV 配置文件&lt;/h2&gt;
&lt;p&gt;PV 比起 HVM 可就复杂太多了，最痛苦的一个问题是由于它不是硬件虚拟化，所以你没有办法运行虚拟机磁盘上的 boot loader！解决方案有两个，要么是直接在配置文件里写好内核和 initramfs 的路径（这样你就得想办法把虚拟机的内核和 initramfs 搞到宿主机磁盘上），要么是在宿主机上构建一个 GRUB 镜像，然后让这个 GRUB 去找虚拟机磁盘里的 &lt;code&gt;grub.cfg&lt;/code&gt; 并执行，然后引导虚拟机里面的内核（这都哪跟哪啊）。&lt;/p&gt;
&lt;p&gt;当然，如果你虚拟机的系统并不使用 GRUB 作为 boot loader，那你就只能使用第一种方案了。对于 openSUSE Tumbleweed，我摸索通了后面的方案，因此我在这里介绍这个方案如何操作。&lt;/p&gt;
&lt;p&gt;首先你需要克隆 GRUB 的源码，因为你得专门构建一个能在 Xen 虚拟机里运行的 GRUB：&lt;/p&gt;
&lt;figure data-raw=&quot;$ git clone git://git.savannah.gnu.org/grub.git
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git clone git://git.savannah.gnu.org/grub.git
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后构建（如果你的 Xen 是 32 位机器上的，把 &lt;code&gt;amd64&lt;/code&gt; 换成 &lt;code&gt;i386&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;$ ./autogen.sh
$ ./configure --prefix=/opt/grub-xen --target=amd64 --with-platform=xen
$ make
# make install
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ./autogen.sh
$ ./configure --prefix=/opt/grub-xen --target=amd64 --with-platform=xen
$ make
# make install
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这会将 Xen 版本的 GRUB 安装到 &lt;code&gt;/opt/grub-xen&lt;/code&gt;，接下来我们需要利用 GRUB 可以将一个 tar 作为 memdisk 的特性，在里面写一个 &lt;code&gt;grub.cfg&lt;/code&gt; 让 GRUB 去按我们指定的路径搜索实际的 &lt;code&gt;grub.cfg&lt;/code&gt; 并加载它（别问我为什么，这鬼东西简直太邪门了）。&lt;/p&gt;
&lt;p&gt;首先写一个 &lt;code&gt;grub-bootstrap.cfg&lt;/code&gt;，这个文件的唯一作用就是让 GRUB 加载 memdisk 里面的 &lt;code&gt;grub.cfg&lt;/code&gt;：&lt;/p&gt;
&lt;figure data-raw=&quot;normal (memdisk)/grub.cfg
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;normal (memdisk)/grub.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后写一个 &lt;code&gt;grub.cfg&lt;/code&gt;，下面关键的问题来了，你怎么知道要搜索的真正的 &lt;code&gt;grub.cfg&lt;/code&gt; 的路径呢，当然是想办法挂载出来自己看了，我这个文件里写了常见的安装好的系统的 &lt;code&gt;grub.cfg&lt;/code&gt; 的位置和我自己看到的 openSUSE 安装 iso 里面的 &lt;code&gt;grub.cfg&lt;/code&gt; 的位置，所以可以同时支持安装和启动系统：&lt;/p&gt;
&lt;figure data-raw=&quot;if search -s -f /boot/grub/grub.cfg ; then
    echo &amp;quot;Reading (${root})/boot/grub/grub.cfg&amp;quot;
    configfile /boot/grub/grub.cfg
fi

if search -s -f /boot/grub2/grub.cfg ; then
    echo &amp;quot;Reading (${root})/boot/grub2/grub.cfg&amp;quot;
    configfile /boot/grub2/grub.cfg
fi

if search -s -f /grub/grub.cfg ; then
    echo &amp;quot;Reading (${root})/grub/grub.cfg&amp;quot;
    configfile /grub/grub.cfg
fi

if search -s -f /grub2/grub.cfg ; then
    echo &amp;quot;Reading (${root})/grub2/grub.cfg&amp;quot;
    configfile /grub2/grub.cfg
fi

if search -s -f /EFI/BOOT/grub.cfg ; then
    echo &amp;quot;Reading (${root})/EFI/BOOT/grub.cfg&amp;quot;
    configfile /EFI/BOOT/grub.cfg
fi
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;if search -s -f /boot/grub/grub.cfg ; then
    echo &quot;Reading (${root})/boot/grub/grub.cfg&quot;
    configfile /boot/grub/grub.cfg
fi

if search -s -f /boot/grub2/grub.cfg ; then
    echo &quot;Reading (${root})/boot/grub2/grub.cfg&quot;
    configfile /boot/grub2/grub.cfg
fi

if search -s -f /grub/grub.cfg ; then
    echo &quot;Reading (${root})/grub/grub.cfg&quot;
    configfile /grub/grub.cfg
fi

if search -s -f /grub2/grub.cfg ; then
    echo &quot;Reading (${root})/grub2/grub.cfg&quot;
    configfile /grub2/grub.cfg
fi

if search -s -f /EFI/BOOT/grub.cfg ; then
    echo &quot;Reading (${root})/EFI/BOOT/grub.cfg&quot;
    configfile /EFI/BOOT/grub.cfg
fi
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后把它打包成 tar：&lt;/p&gt;
&lt;figure data-raw=&quot;$ tar -cf memdisk.tar grub.cfg
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ tar -cf memdisk.tar grub.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后创建一个支持 Xen 的包含所有 GRUB 模块的 GRUB 镜像，我们将把它当作真正的虚拟机的 boot loader 运行，使用如下命令：&lt;/p&gt;
&lt;figure data-raw=&quot;$ /opt/grub-xen/bin/grub-mkimage -O x86_64-xen \
       -c grub-bootstrap.cfg \ 
       -m memdisk.tar \
       -o grub-x86_64-xen.bin \
       /opt/grub-xen/lib/grub/x86_64-xen/*.mod
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ /opt/grub-xen/bin/grub-mkimage -O x86_64-xen \
       -c grub-bootstrap.cfg \ 
       -m memdisk.tar \
       -o grub-x86_64-xen.bin \
       /opt/grub-xen/lib/grub/x86_64-xen/*.mod
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后你就可以编写一个 &lt;code&gt;pv-tumbleweed.cfg&lt;/code&gt; 的配置文件：&lt;/p&gt;
&lt;figure data-raw=&quot;name = &#039;pv-tumbleweed&#039;
memory = 2048
vcpus = 4
kernel = &amp;quot;grub-x86_64-xen.bin&amp;quot;
disk = [ &#039;file:/home/alynx/xen/disk-tumbleweed.img,sda,rw&#039;, &#039;file:/home/alynx/xen/openSUSE-Tumbleweed-DVD-x86_64-Current.iso,sdb:cdrom,r&#039; ]
vif = [ &#039;mac=00:16:3e:00:00:01,bridge=vmbr0&#039; ]
vnc = 1
vnclisten = &#039;0.0.0.0&#039;
vncdisplay = 1
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;name = &#039;pv-tumbleweed&#039;
memory = 2048
vcpus = 4
kernel = &quot;grub-x86_64-xen.bin&quot;
disk = [ &#039;file:/home/alynx/xen/disk-tumbleweed.img,sda,rw&#039;, &#039;file:/home/alynx/xen/openSUSE-Tumbleweed-DVD-x86_64-Current.iso,sdb:cdrom,r&#039; ]
vif = [ &#039;mac=00:16:3e:00:00:01,bridge=vmbr0&#039; ]
vnc = 1
vnclisten = &#039;0.0.0.0&#039;
vncdisplay = 1
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;没错我们这里把我们刚刚生成的 boot loader 作为内核首先拉起来，然后不出意外你应该就能看到安装程序启动了，但是如果你火急火燎的一路下一步安装，你就会掉进下一个坑：openSUSE 默认使用 snapper 管理 btrfs 快照，默认的配置方案把 &lt;code&gt;/boot&lt;/code&gt; 也放在 btrfs 子卷上。而上游的 GRUB 会从 btrfs 的根子卷而不是默认子卷开始访问，我是没能搞清楚该如何简单直接的访问 snapper 最新的快照所在的子卷，openSUSE 的 GRUB 则是打了一大堆 patch 让 GRUB 支持查找和加载默认的 btrfs 子卷。总之你直接安装之后我们的 &lt;code&gt;grub.cfg&lt;/code&gt; 是查找不到真正的 &lt;code&gt;grub.cfg&lt;/code&gt; 的，最简单的方案就是安装时干脆不要用 btrfs 从而不用 snapper，或者把 &lt;code&gt;/boot&lt;/code&gt; 单独分区单独格式化单独挂载。&lt;/p&gt;
&lt;p&gt;然后你应该可以用下面的命令启动并链接到虚拟机的终端了：&lt;/p&gt;
&lt;figure data-raw=&quot;# xl create -c pv-tumblweed.cfg
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# xl create -c pv-tumblweed.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;关于这个配置文件的具体语法可以参考 &lt;a href=&quot;https://xenbits.xen.org/docs/unstable/man/xl.cfg.5.html&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h1 id=&quot;%E8%BF%99%E4%B8%80%E5%88%87%E5%80%BC%E5%BE%97%E5%90%97%EF%BC%9F&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Xen-HVM-PV/#%E8%BF%99%E4%B8%80%E5%88%87%E5%80%BC%E5%BE%97%E5%90%97%EF%BC%9F&quot;&gt;&lt;/a&gt;这一切值得吗？&lt;/h1&gt;
&lt;p&gt;在经历这一系列不知道是什么鬼东西的操作之后你终于有了一个可以用的 Xen PV，也许它唯一的优势就是可以在没有硬件虚拟化的机器上跑虚拟机，为此你付出的代价是一个没法用桌面的宿主机，一个说不定哪个新系统就没法引导的虚拟机 boot loader。但现在的设备有几个没有硬件虚拟化支持呢？这就意味着对于大多数人你可以简单地使用 qemu/KVM 几乎不需要任何额外的配置，并且还有 virt-manager 这样的程序全程帮你图形化配置虚拟机和 NAT 网络。&lt;/p&gt;
&lt;p&gt;而比如你想用上面的办法手动构建网络并启动一些 SLES Minimal OS 或者 openSUSE JeOS，这个过程也更简单，首先在 &lt;code&gt;/etc/qemu/bridge.conf&lt;/code&gt; 里加入一行 &lt;code&gt;allow vmbr0&lt;/code&gt;，然后用之前的 NAT 网络脚本 &lt;code&gt;sudo ./mkvmbr0.sh wlp5s0&lt;/code&gt;，再用下面的 qemu 命令：&lt;/p&gt;
&lt;figure data-raw=&quot;$ qemu-system-x86_64 \
    -enable-kvm \
    -m 1G \
    -smp 1 \
    -drive if=virtio,format=qcow2,file=SLES15-SP5-Minimal-VM.x86_64-kvm-and-xen-GM.qcow2 \
    -nographic \
    -netdev bridge,id=eth0,br=vmbr0 \
    -device virtio-net,netdev=eth0
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ qemu-system-x86_64 \
    -enable-kvm \
    -m 1G \
    -smp 1 \
    -drive if=virtio,format=qcow2,file=SLES15-SP5-Minimal-VM.x86_64-kvm-and-xen-GM.qcow2 \
    -nographic \
    -netdev bridge,id=eth0,br=vmbr0 \
    -device virtio-net,netdev=eth0
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;它会调用 &lt;code&gt;qemu-bridge-helper&lt;/code&gt; 自动将虚拟机加入你构建的桥接 NAT 网络，和 Xen 比起来简单很多，也不需要额外的配置。因此如果你只是需要一个自己的虚拟化平台，完全没有必要使用已经不再流行且可能存在更多问题的 Xen，使用 KVM 就足够了。这篇文章仅仅是为了在读者不得不需要构建一个 Xen 环境 debug 时作为参考。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="虚拟化" label="虚拟化" scheme="https://sh.alynx.one/tags/%E8%99%9A%E6%8B%9F%E5%8C%96/" />
    <category term="Xen" label="Xen" scheme="https://sh.alynx.one/tags/Xen/" />
  </entry>
  <entry>
    <title>kmsvnc 但是无头</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Headless-kmsvnc/" />
    <id>https://sh.alynx.one/posts/Headless-kmsvnc/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-03-06T09:38:48.000Z</published>
    <updated>2024-03-06T09:38:48.000Z</updated>
    <summary type="html">
      由于 Wayland 并不存在像 X11 一样一个单独管理用于最终显示的 framebuffer 的进程，因此远程桌面需要各个桌面做自己的实现。而因为登录界面和用户会话一般是两个会话，远程登录过程中的会话切换就变得非常复杂，虽然 GNOME Remote Desktop 已经有了…
    </summary>
    <content type="html">
      &lt;p&gt;由于 Wayland 并不存在像 X11 一样一个单独管理用于最终显示的 framebuffer 的进程，因此远程桌面需要各个桌面做自己的实现。而因为登录界面和用户会话一般是两个会话，远程登录过程中的会话切换就变得非常复杂，虽然 GNOME Remote Desktop 已经有了一个初步实现，但我对那个的逻辑不是很满意，而且他们目前依赖 RDP 的 server rediection，所以并不支持 VNC。&lt;/p&gt;
&lt;p&gt;按照当初我做 Show Me The Key 的逻辑，如果一样东西在混成器层面上不好实现，那应该考虑在下一层更统一的层面上实现，&lt;a href=&quot;https://github.com/isjerryxiao/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@Jerry Xiao&lt;/a&gt; 老师做了一个叫做 &lt;a href=&quot;https://github.com/isjerryxiao/kmsvnc/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;kmsvnc&lt;/a&gt; 的项目，通过 DRM/KMS 获取 framebuffer 并转给 VNC 服务器，虽然它也有一些自己的小问题，但我很喜欢这个方案。&lt;/p&gt;
&lt;p&gt;实际上我需要用到远程桌面的场景不多，毕竟 ssh 大部分时候更可靠也更高效，不过有时候需要修改 NAS 上一些桌面设置的时候会用到。但此时就有第一个问题，我的 NAS 是无头（就是没接显示器）的，没有显示器的情况下桌面没有输出，也就获取不到 framebuffer。请教了 &lt;a href=&quot;https://github.com/isjerryxiao/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@Jerry Xiao&lt;/a&gt; 老师之后得知可以通过内核参数实现强制让内核以为我们有个显示器，就可以解决这个问题。&lt;/p&gt;
&lt;p&gt;这个目的需要两个内核参数，分别是 &lt;code&gt;video=&lt;/code&gt; 指定强制开启哪个显卡输出端口，和 &lt;code&gt;drm.edid_firmware=&lt;/code&gt; 给这个端口指定一个 EDID 从而指定分辨率，正常的显示器会通过端口汇报自己的 EDID，而我们这里没有，所以需要手动指定。&lt;/p&gt;
&lt;p&gt;具体这两个参数都支持什么我就不细说了，因为很复杂，建议自己看文档。首先我们得选择一个显卡已经有的输出端口用来做这个，可以用下面的命令：&lt;/p&gt;
&lt;figure data-raw=&quot;$ for p in /sys/class/drm/*/status; do con=${p%/status}; echo -n &amp;quot;${con#*/card?-}: &amp;quot;; cat $p; done
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ for p in /sys/class/drm/*/status; do con=${p%/status}; echo -n &quot;${con#*/card?-}: &quot;; cat $p; done
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;它会列出端口和它们的连接状态，比如我这里有三个：&lt;/p&gt;
&lt;figure data-raw=&quot;DP-1: disconnected
HDMI-A-1: connected
HDMI-A-2: disconnected
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;DP-1: disconnected
HDMI-A-1: connected
HDMI-A-2: disconnected
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;很神奇的是我的主板上实际只有一个 DP 和一个 HDMI（我的是核芯显卡），经过我的测试主板的 HDMI 输出是 &lt;code&gt;HDMI-A-2&lt;/code&gt;，至于 &lt;code&gt;HDMI-A-1&lt;/code&gt; 到底在哪我不清楚，可能是 USB-C，但如果它并没有物理输出，那就更好了，所以我这里选择它。另外我建议不要选择 DP 输出，因为内核内置的 EDID 固件似乎并不支持 DP，导致无法指定分辨率。&lt;/p&gt;
&lt;p&gt;然后我们通过 &lt;code&gt;video=&quot;HDMI-A-1:D&quot;&lt;/code&gt; 强制开启这个 HDMI 端口，一般你可能会查到用 &lt;code&gt;e&lt;/code&gt; 表示强制启用一个端口，不过 &lt;code&gt;D&lt;/code&gt; 表示强制启用一个数字输出端口，应该用哪个都无所谓。然后我们用 &lt;code&gt;drm.edid_firmware=&quot;HDMI-A-1:edid/1920x1080.bin&quot;&lt;/code&gt; 给这个端口指定一个 EDID 固件，这里我们使用内核内置的 &lt;code&gt;1920x1080&lt;/code&gt; 分辨率的固件，你也可以选择其它内置的，可以在内核源码 &lt;code&gt;drivers/gpu/drm/drm_edid_load.c&lt;/code&gt; 文件中找到它们的名字和内容。&lt;/p&gt;
&lt;p&gt;然后重新启动系统，再用上面的命令，应该就像我一样有一个 &lt;code&gt;connected&lt;/code&gt; 的输出了。然后我们启动 kmsvnc：&lt;/p&gt;
&lt;figure data-raw=&quot;# kmsvnc -p 5901 -b 0.0.0.0 -4 -d /dev/dri/card0
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;# kmsvnc -p 5901 -b 0.0.0.0 -4 -d /dev/dri/card0
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;再从另一台机器上访问：&lt;/p&gt;
&lt;figure data-raw=&quot;$ vncviewer YOUR_SERVER_IP:5901
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;$ vncviewer YOUR_SERVER_IP:5901
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;应该就能看到登录界面了。不过如果你也用 GDM，你可能会发现登录之后屏幕一直是 GDM 的背景色没有切换到用户会话，这个其实是因为 GDM 会选择另一个空闲的 tty 启动 GNOME，而 &lt;a href=&quot;https://github.com/isjerryxiao/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;@Jerry Xiao&lt;/a&gt; 老师表示还没有找到一个普适的方案检测 tty 切换，所以这时候需要你手动 &lt;code&gt;Ctrl+C&lt;/code&gt; 打断 kmsvnc，然后再重新运行上面的命令，应该就会选择 GNOME 桌面所在的 plane 了。&lt;/p&gt;
&lt;p&gt;如果你不是刚开机就启动 kmsvnc，可能会发现它报告找不到 plane 就退出了，这个目测是因为 GDM 检测不到用户输入就会自动停止显示器输出，只要 &lt;code&gt;systemctl restart gdm&lt;/code&gt; 重新启动一下 GDM 就可以了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="远程桌面" label="远程桌面" scheme="https://sh.alynx.one/tags/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2/" />
    <category term="VNC" label="VNC" scheme="https://sh.alynx.one/tags/VNC/" />
    <category term="kmsvnc" label="kmsvnc" scheme="https://sh.alynx.one/tags/kmsvnc/" />
    <category term="NAS" label="NAS" scheme="https://sh.alynx.one/tags/NAS/" />
  </entry>
  <entry>
    <title>谁动了我的 DNS 解析？（重制版）</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Who-Moved-My-DNS-Resolving-Remastered/" />
    <id>https://sh.alynx.one/posts/Who-Moved-My-DNS-Resolving-Remastered/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-02-06T03:53:30.000Z</published>
    <updated>2024-02-06T03:53:30.000Z</updated>
    <summary type="html">
      &lt;p&gt;这一篇是之前 &lt;a href=&quot;/posts/Who-Moved-My-DNS-Resolving/&quot;&gt;谁动了我的 DNS 解析？&lt;/a&gt; 的重制版，因为那一篇杂糅了关于设置 Zeroconf 的 mDNS 的需求和关于 Linux 下面 DNS 解析到底是怎么工作的描述，我怀疑大部分读者对前者不感兴趣（因为我自己后来也发现这玩意不是很可靠），而更想了解后者，所以打算拉出来单写一篇。&lt;/p&gt;
&lt;p&gt;标题显然是化用自《谁动了我的奶酪？》，即使我并没有读过这本书。&lt;/p&gt;

    </summary>
    <content type="html">
      &lt;p&gt;这一篇是之前 &lt;a href=&quot;/posts/Who-Moved-My-DNS-Resolving/&quot;&gt;谁动了我的 DNS 解析？&lt;/a&gt; 的重制版，因为那一篇杂糅了关于设置 Zeroconf 的 mDNS 的需求和关于 Linux 下面 DNS 解析到底是怎么工作的描述，我怀疑大部分读者对前者不感兴趣（因为我自己后来也发现这玩意不是很可靠），而更想了解后者，所以打算拉出来单写一篇。&lt;/p&gt;
&lt;p&gt;标题显然是化用自《谁动了我的奶酪？》，即使我并没有读过这本书。&lt;/p&gt;
&lt;a id=&quot;more&quot;&gt;&lt;/a&gt;

&lt;h1 id=&quot;long-long-ago&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#long-long-ago&quot;&gt;&lt;/a&gt;long long ago&lt;/h1&gt;
&lt;p&gt;一般要讲故事，开头都是“很久很久以前……”，不过计算机领域也没什么太古老的故事可讲，毕竟公认的互联网前身 ARPANET 也就是二十世纪的事情。那个时候能互联的机器一共也就那么几个，所以解决的办法简单粗暴：我们每个机器都保存一个文件，里面记录所有人对应的域名和 IP 不就行了？这个优良传统一直留了下来，也就是现在所有系统里都有的 hosts 文件——不管你写的对不对，它的优先级都比后来出现的 DNS 查询要高。&lt;/p&gt;
&lt;p&gt;然后随着加入网络的机器越来越多，这个办法不好用了，毕竟每来一个新人就要所有人更新自己的文件，这复杂度也太高了。所以干脆我们搞一个集中的服务器专门放这个列表，其它机器都向它查询就好了，这就是 DNS 服务器的原理。然后在局域网里，一般路由器和 DNS 服务器以及 DHCP 服务器都是同一台机器，因为很自然的所有设备都会连到路由器上，而 DHCP 服务器恰好知道它分配出去的 IP 地址，所以如果你输入内网设备的主机名恰好能解析，那通常是你的路由器做了这些工作。&lt;/p&gt;
&lt;p&gt;但既然有了 DNS 服务器，那问题就变得复杂起来，比如我该将我的 DNS 服务器设置成哪一个？特别地，你可能会发现有很多不同的程序在试图修改你的 DNS 服务器设置，导致你打开某些网站本该秒开却不停地转圈圈，事情为什么会这么复杂？&lt;/p&gt;
&lt;h1 id=&quot;chattr-+i--etc-resolv-conf&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#chattr-+i--etc-resolv-conf&quot;&gt;&lt;/a&gt;chattr +i /etc/resolv.conf&lt;/h1&gt;
&lt;p&gt;很多 Linux 用户都知道修改 DNS 服务器可以通过编辑 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 实现，很多 Linux 用户也被 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 困扰，一些人发现自己的这个文件是个软链接，而另一些人发现这个文件总被 Network Manager 覆盖，还有些人的发行版让他们用一个叫 &lt;code&gt;resolvconf&lt;/code&gt; 的工具处理，然后现在 systemd 又搞了个叫 systemd-resolved 的东西来插一脚……我说的这些已经足够让一些不想学新东西同时又神经紧张的人开始大喊“fuck systemd, fuck network manager, fuck desktop environment and fuck the whole modern world”然后执行 &lt;code&gt;chattr +i /etc/resolv.conf&lt;/code&gt; 了。不过别着急小炸药包们，也许这个世界上新出现的各种东西目的并不只是惹恼你们这群大笨蛋，哦是的，没错，我说，大笨蛋，恐龙勇士（停停停不要翻译腔了），你不需要的功能并不意味着别人也不需要。总之，不要觉得世界都围着你转，至少读一下这些东西的文档，会告诉你怎么阻止它们修改你的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;在 DNS 服务器设置这件事上并不是一个 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 搞定所有，有关这个的故事也是 long long ago，但毕竟是 UNIX 纪元之后的事情，没有太久，大概确实上古时代的程序都是直接读这个获取 DNS 服务器然后再做 DNS 解析的，但实际上这也不一定 OK，比如像之前说的 hosts 文件也需要考虑。所以就有了更复杂的解决方案，大部分程序做 DNS 解析实际上是调用 glibc 里面 &lt;code&gt;getaddrinfo&lt;/code&gt; 这个 API，所以在它后面我们就可以做一些工作。一个叫做 Name Service Switch 的东西发明出来就是干这个的，它是基于插件的，我们可以通过阅读 &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; 里面的 &lt;code&gt;hosts&lt;/code&gt; 这一行来理解，比如我这里默认是这样的：&lt;/p&gt;
&lt;figure data-raw=&quot;hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;简单翻译一下的话意思就是查询一个域名的时候首先看看是不是 systemd-machined 的容器（&lt;code&gt;mymachines&lt;/code&gt; 模块），不是的话再问问 systemd-resolved 能不能解析（&lt;code&gt;resolve&lt;/code&gt; 模块），如果 systemd-resolved 可用，那到这也就完事了，后面的就不管了（&lt;code&gt;[!UNAVAIL=return]&lt;/code&gt;），至于为什么我一会解释，然后 &lt;code&gt;files&lt;/code&gt; 模块会读 hosts 文件，所以它优先级总是高于 DNS 服务器，然后看看是不是本机（&lt;code&gt;myhostname&lt;/code&gt; 模块），然后再读 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 里面的 DNS 服务器进行查询。&lt;/p&gt;
&lt;p&gt;按照这个顺序，如果你处在一个极其简单的网络环境：只有一个网络连接（这里包含各种有线无线 VPN 隧道在内都只能有一个）并且完全不会移动到其它网络连接下使用，那确实只要在 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 里面写死一个公开的 DNS 服务器就可以满足你的所有查询需求。但可惜并不是所有人的使用环境都这么简单，所以每个工具都有额外的策略并试图修改 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;。&lt;/p&gt;
&lt;h1 id=&quot;%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%E2%80%A6%E2%80%A6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%E2%80%A6%E2%80%A6&quot;&gt;&lt;/a&gt;当你是个需要来回跑的笔记本用户……&lt;/h1&gt;
&lt;p&gt;下面让我们考虑一个比那些觉得自己手搓一个 DHCP 客户端就能联网的大脑皮层极其光滑的只要 &lt;code&gt;chattr +i /etc/resolv.conf&lt;/code&gt; 就能解决问题的小笨蛋们的场景稍微复杂一点的场景：你是一个背着笔记本来回跑的上班族，公司 WiFi 和家里 WiFi 的网段并不一样，而你需要在公司的时候将 DNS 服务器设置为公司的路由器，在家的时候将 DNS 服务器设置为家的路由器（什么水晶室女），以便在两地都可以通过内网设备的主机名访问对应的内网设备，显然你不可能靠 &lt;code&gt;chattr +i /etc/resolv.conf&lt;/code&gt; 解决问题。&lt;/p&gt;
&lt;p&gt;这就是为什么 Network Manager 需要修改你的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;（其它网络管理器我就不考虑了因为我没用过，而且对于所有这种可以帮你自动连接 WiFi 的网络管理器而言，设置 DNS 服务器的逻辑都应该是相同的），对于每个不同的网络连接，它都会记录或者自动获取该局域网的 DNS 服务器，然后根据你当前激活的连接把这个 DNS 服务器写入 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，保证无论是使用 &lt;code&gt;getaddrinfo&lt;/code&gt; 的程序还是自己读取 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 的老古董程序都可以获取到正确的局域网 DNS 服务器从而访问内网里的设备。&lt;/p&gt;
&lt;h1 id=&quot;%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%E2%80%A6%E2%80%A6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%E2%80%A6%E2%80%A6&quot;&gt;&lt;/a&gt;当你是个需要来回跑的笔记本用户，同时你还需要通过 VPN 远程办公……&lt;/h1&gt;
&lt;p&gt;如果你完全理解了上面那一段，恭喜你已经脱离了草履虫进化成了脊椎动物了！但你又遇到了更复杂的场景：你的工作要求你连接公司的 VPN，从而通过内部的 DNS 服务器解析一些内网才能访问的网站，当你打开了 VPN 下载一些内网才能下载的工具时你想同时放一些音乐打发时间，但你最爱的音乐网站现在要转 3 秒的圈才能访问！Network Manager 可以帮你解决这个问题吗？答案是在某些情况下可以！&lt;/p&gt;
&lt;p&gt;尝试分析这个新的需求，你会发现问题在于 VPN 服务也要设置一个 DNS 服务器，如果你的 VPN 服务尝试自己覆盖 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，那么 Network Manager 之前按照你的连接帮你设置的“正常的”的 DNS 服务器就会消失，你所有的 DNS 解析就会全部绕到公司内网的 DNS 服务器上跑一圈，导致所有网站的解析都变得很慢。这个时候你可能又打算大喊 &lt;code&gt;chattr +i /etc/resolv.conf&lt;/code&gt; 退化成草履虫，但这样你就没办法解析公司内网的域名了，也许更好的办法是只让 Network Manager 管理 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;。因为大部分的 VPN 都已经有 Network Manager 的插件了，所以你只要在 Network Manager 里添加你的 VPN 连接，它就会像管理 WiFi 一样管理这个连接，此时你会发现你的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 里已经同时有了家里路由器的 DNS 服务器和 VPN 的 DNS 服务器。&lt;/p&gt;
&lt;p&gt;理想状态下，你的 VPN 应该会自动通知客户端对于哪些域名使用这个 VPN 的 DNS 服务器查询，如果你勾选了“仅对此网络上的资源使用此连接”的话。这样访问不在内网上的网站就不会到这个 DNS 服务器上转一圈，而是直接跑到“正常”的 DNS 服务器上查询。但如果不幸这个自动配置的过程出了问题，你可以通过 &lt;code&gt;nmcli&lt;/code&gt; 修改这个 VPN 连接的 &lt;code&gt;ipv4.dns-search&lt;/code&gt; 项，把需要在内网查询的域名列表手动设置好。&lt;/p&gt;
&lt;p&gt;对于绝大部分用户，这样的配置应该已经可以满足他们了！但实际情况永远只有更复杂，所以我们还要继续！&lt;/p&gt;
&lt;h1 id=&quot;%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%EF%BC%8C%E4%BD%86%E4%BD%A0%E7%9A%84-VPN-%E5%B9%B6%E6%B2%A1%E6%9C%89-Network-Manager-%E7%9A%84%E6%8F%92%E4%BB%B6%E2%80%A6%E2%80%A6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%EF%BC%8C%E4%BD%86%E4%BD%A0%E7%9A%84-VPN-%E5%B9%B6%E6%B2%A1%E6%9C%89-Network-Manager-%E7%9A%84%E6%8F%92%E4%BB%B6%E2%80%A6%E2%80%A6&quot;&gt;&lt;/a&gt;当你是个需要来回跑的笔记本用户，同时你还需要通过 VPN 远程办公，但你的 VPN 并没有 Network Manager 的插件……&lt;/h1&gt;
&lt;p&gt;欢迎刚刚进化完成的灵长类！坐稳了！我们要向现代人的方向冲刺了！现在我有一个更加复杂的需求：我给自己的各种内网设备搭建了一个 VPN，这样我即使身在外面也能访问我家里的服务器，但这个 VPN 使用 Tailscale，Network Manager 并没有相关的插件，于是 Tailscale 也来试图覆盖我的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，听个音乐又需要转 3 秒的圈，怎么办？&lt;/p&gt;
&lt;p&gt;最好在那边手握 &lt;code&gt;chattr +i /etc/resolv.conf&lt;/code&gt; 的草履虫嘲笑之前堵上他的嘴，因为这同样会导致我失去解析 VPN 内网域名的能力，我们是现代人，我们要用 systemd-resolved 解决这个问题。&lt;/p&gt;
&lt;p&gt;systemd-resolved 并不仅仅是一个管理 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 的工具，实际上它本身是一个自带缓存的 DNS 服务器，然后向上接管各种不同的 DNS 查询逻辑，向下为各种需要设置 DNS 服务器的工具提供接口。因此如果你的各种网络连接工具都支持 systemd-resolved 的接口，那它们就不需要自己修改 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，而是改为配合 systemd-resolved 工作，恰好 Network Manager 和 Tailscale 都支持 systemd-resolved。&lt;/p&gt;
&lt;p&gt;在接管 DNS 查询这个目的上 systemd-resolved 提供了三种不同的接口：首先是自己实现了一个 D-Bus 接口，其它程序可以通过这个接口来实现。然后是在 Name Service Switch 里添加了属于自己的模块以接管 &lt;code&gt;getaddrinfo&lt;/code&gt;，如果检测到 systemd-resolved 已启用，那它的缓存 DNS 服务器就会接管所有的处理，包括 hosts 和 hostname，以及如果没有缓存到，就会主动向上级 DNS 服务器查询，因此在 &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; 里面写了如果检测到 systemd-resolved 就直接返回，跳过后面的模块。最后对于那些自己读取 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 的老古董，它也会修改这个文件接管这类程序，这时这个文件只是个软链接，里面只有一句就是把 DNS 服务器设置为 systemd-resolved 自己的 DNS 缓存服务器。&lt;/p&gt;
&lt;p&gt;如果你要启用 systemd-resolved，务必保证你的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 是指向 systemd-resolved 管理的文件的软链接：&lt;/p&gt;
&lt;figure data-raw=&quot;# ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这样如果 Network Manager 和 Tailscale 启动时检测到 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 是软链接，就会知道自己需要配合 systemd-resolved 工作（如果你正在配置这个，那就手动重启它们！）。此时执行 &lt;code&gt;resolvectl status&lt;/code&gt;，应该能看到对于不同的网络接口，都配置了不同的 DNS 服务器，以及需要通过这个服务器查询的域名（如果你配置过的话！）。&lt;/p&gt;
&lt;p&gt;当然，你也可以通过 systemd-resolved 的配置添加一个全局的 DNS 服务器。systemd-resolved 支持 unicast，也就是说如果你查询的域名不符合任何一个网络接口设置的要通过这个接口的 DNS 服务器查询的域名的话，它就会通过所有网络接口的 DNS 服务器查询（也包含你设置的全局 DNS 服务器），然后取最快返回的结果。&lt;/p&gt;
&lt;h1 id=&quot;%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%EF%BC%8C%E4%BD%86%E4%BD%A0%E7%9A%84-VPN-%E5%B9%B6%E6%B2%A1%E6%9C%89-Network-Manager-%E7%9A%84%E6%8F%92%E4%BB%B6%EF%BC%8C%E5%AE%83%E4%B9%9F%E4%B8%8D%E6%94%AF%E6%8C%81-systemd-resolved%E2%80%A6%E2%80%A6&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#%E5%BD%93%E4%BD%A0%E6%98%AF%E4%B8%AA%E9%9C%80%E8%A6%81%E6%9D%A5%E5%9B%9E%E8%B7%91%E7%9A%84%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%A8%E6%88%B7%EF%BC%8C%E5%90%8C%E6%97%B6%E4%BD%A0%E8%BF%98%E9%9C%80%E8%A6%81%E9%80%9A%E8%BF%87-VPN-%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC%EF%BC%8C%E4%BD%86%E4%BD%A0%E7%9A%84-VPN-%E5%B9%B6%E6%B2%A1%E6%9C%89-Network-Manager-%E7%9A%84%E6%8F%92%E4%BB%B6%EF%BC%8C%E5%AE%83%E4%B9%9F%E4%B8%8D%E6%94%AF%E6%8C%81-systemd-resolved%E2%80%A6%E2%80%A6&quot;&gt;&lt;/a&gt;当你是个需要来回跑的笔记本用户，同时你还需要通过 VPN 远程办公，但你的 VPN 并没有 Network Manager 的插件，它也不支持 systemd-resolved……&lt;/h1&gt;
&lt;p&gt;我们的口号是不做草履虫！实际上这种场景已经非常非常少见了，绝大多数的场景都能被 Network Manager + systemd-resolved 覆盖，这也就是为什么越来越多的通用发行版都使用这一套进行网络管理和 DNS 解析的原因。但如果你真的遇到了，也许你听说过一个叫做 &lt;code&gt;resolvconf&lt;/code&gt; 的工具，在以前某些发行版会预装它。它的逻辑似乎就是有各种程序都打算自己修改 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，那你们干脆都别管了，我来管，听我的（现在有 N + 1 种解决方案了）。至于它到底是怎么工作的，如何配置，这些都别问我！因为我也没有用过！我觉得你不如去建议不支持 systemd-resolved 的项目支持 systemd-resolved 比较实际……&lt;/p&gt;
&lt;p&gt;总之无论如何如果你搜到一篇老旧的教程告诉你设置这个 &lt;code&gt;resolvconf&lt;/code&gt;，那你都该留个心眼查一下，万一你这个需求已经可以用 Network Manager 的插件或者它已经支持 systemd-resolved 了呢？&lt;/p&gt;
&lt;h1 id=&quot;%E5%A4%AA%E9%95%BF%E4%B8%8D%E7%9C%8B%EF%BC%81&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/#%E5%A4%AA%E9%95%BF%E4%B8%8D%E7%9C%8B%EF%BC%81&quot;&gt;&lt;/a&gt;太长不看！&lt;/h1&gt;
&lt;p&gt;我准备了一张图，让你知道 DNS 解析在 Linux 上都发生了什么，以及 Network Manager 和 systemd-resolved 各自都扮演什么角色……&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/Who-Moved-My-DNS-Resolving-Remastered/dns.png&quot; alt=&quot;dns.png&quot;&gt;&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/%E7%BD%91%E7%BB%9C/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/tags/Linux/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/tags/%E7%BD%91%E7%BB%9C/" />
    <category term="DNS" label="DNS" scheme="https://sh.alynx.one/tags/DNS/" />
    <category term="resolv.conf" label="resolv.conf" scheme="https://sh.alynx.one/tags/resolv.conf/" />
    <category term="Network Manager" label="Network Manager" scheme="https://sh.alynx.one/tags/Network%20Manager/" />
    <category term="systemd-resolved" label="systemd-resolved" scheme="https://sh.alynx.one/tags/systemd-resolved/" />
  </entry>
  <entry>
    <title>使用 Headscale 和 Tailscale 构建虚拟专用网</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Build-VPN-with-Headscale-and-Tailscale/" />
    <id>https://sh.alynx.one/posts/Build-VPN-with-Headscale-and-Tailscale/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-02-04T11:39:26.000Z</published>
    <updated>2024-02-04T11:39:26.000Z</updated>
    <summary type="html">
      需求 很多在家里装了 NAS 的人都有一个相似的需求，那就是出门在外如何访问内网的 NAS 上运行的服务。很多人会选择公网 IP + 端口映射把需要的服务直接暴露到公网上，或者通过公网的 VPS 进行反向代理。但这些我都不放心，首先我的目的只是自己访问，而不是给别人访问，其次对于…
    </summary>
    <content type="html">
      &lt;h1 id=&quot;%E9%9C%80%E6%B1%82&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Build-VPN-with-Headscale-and-Tailscale/#%E9%9C%80%E6%B1%82&quot;&gt;&lt;/a&gt;需求&lt;/h1&gt;
&lt;p&gt;很多在家里装了 NAS 的人都有一个相似的需求，那就是出门在外如何访问内网的 NAS 上运行的服务。很多人会选择公网 IP + 端口映射把需要的服务直接暴露到公网上，或者通过公网的 VPS 进行反向代理。但这些我都不放心，首先我的目的只是自己访问，而不是给别人访问，其次对于一些简易的 WebUI，暴露在公网上也容易被无聊的人扫端口并尝试入侵。实际上这个需求更倾向于 VPN（这里指的是它本来的意思也就是虚拟专用网，而不是佛跳墙），我曾经尝试过使用 WireGuard 和公网 VPS 构建一个简单的 VPN，但效果并不好，首先是我的 VPS 并不在国内，作为所有流量的中继实在是太不合适，实际使用起来几乎卡到不能自理，其次是 WireGuard 用作 VPN 服务器的话需要把其它所有 peers 都添加到服务器里，实在是太过麻烦。&lt;/p&gt;
&lt;p&gt;第一点我其实没想到什么好的解决办法，能想到最好的也就是利用家里有公网 IP 的特点把 VPN 服务器改到家里。而第二点我差点就想改成自建 OpenVPN 了，但这时我偶然找到一些资料，说不应该手动组建 WireGuard 网络，而是使用一些基于 WireGuard 的工具帮你自动组网。比较之后我决定使用 Tailscale。&lt;/p&gt;
&lt;p&gt;Tailscale 能做的并不仅仅是帮你建立一个 VPN 服务器然后自动添加客户端，在此之上它有一些更妙的功能，比如 WireGuard 实际上并不是服务端/客户端架构，peers 之间是对等的，于是 Tailscale 可以尝试通过 NAT 穿透建立点对点的 WireGuard 连接，如果无法穿透才会通过服务端中继（Tailscale 官网有一篇关于如何实现较为可靠的 NAT 穿透的文章，至少我是没怎么看懂），这听起来很适合我的需求并且在实际使用中极大的提升了我的体验。但 Tailscale 本身只是客户端，它们通过销售自己的服务提供服务器供用户连接，客户端是开源的但服务端是闭源的。而我显然更希望自己搭建服务端，幸好有 Headscale 这个开源项目自己实现了一个 Tailscale 服务端，可以自己搭建。但 Headscale 自己的文档非常的简陋，所以我决定写篇博客记录一下具体配置的过程。&lt;/p&gt;
&lt;h1 id=&quot;Headscale&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Build-VPN-with-Headscale-and-Tailscale/#Headscale&quot;&gt;&lt;/a&gt;Headscale&lt;/h1&gt;
&lt;p&gt;首先在公网能访问到的服务器上安装 Headscale，Arch Linux 的官方仓库里已经打包了：&lt;/p&gt;
&lt;figure data-raw=&quot;# pacman -S headscale
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# pacman -S headscale
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后需要修改配置文件 &lt;code&gt;/etc/headscale/config.yaml&lt;/code&gt;，里面需要修改的只有几处，我这里简单介绍一下：&lt;/p&gt;
&lt;p&gt;首先第一个要修改的是 &lt;code&gt;server_url&lt;/code&gt;，这个就是客户端连接服务器时使用的地址和端口，Headscale 使用的是 HTTP 协议，如果你不想明文在公网上裸奔，那可以在后面添加 HTTPS 证书和密钥使它支持 HTTPS。&lt;/p&gt;
&lt;p&gt;同样还需要修改 &lt;code&gt;listen_addr&lt;/code&gt;，控制 Headscale 监听的网段和端口，这里端口要和上面的一致。&lt;/p&gt;
&lt;p&gt;下面其它的控制数据库和 gRPC 都保持默认即可，然后你可以修改想要给子网设备分配 IP 的网段，只要修改 &lt;code&gt;ip_prefixes&lt;/code&gt; 就可以，要注意的是并不是所有网段都可以用，Tailscale 本身已经限制了一部分，你只能选择这个网段的子网段。我这里注释掉了 IPv6 因为我不需要。&lt;/p&gt;
&lt;p&gt;如果你想设置 HTTPS，Headscale 本身支持通过 ACME 帮你自动申请证书，这当然是最好的，但它并不支持通过 DNS 的方式验证域名所有权，也就意味着需要你能够监听 80 或者 443 端口，如果你是公网 IP 的家宽，这基本等于被 ISP 查水表，而如果是 VPS，你也大概率可能在这些端口上运行了其它的 HTTP 服务，所以我没有用这个功能。但它下面还提供了手动指定证书和密钥的选项，你可以使用 certbot 或者 acme.sh 之类的功能帮你处理好证书（和 certbot 搏斗实在是太痛苦了所以我省略了），然后将 &lt;code&gt;tls_cert_path&lt;/code&gt; 设置为 &lt;code&gt;fullchain.pem&lt;/code&gt; 所在的路径，&lt;code&gt;tls_key_path&lt;/code&gt; 设置为 &lt;code&gt;privkey.pem&lt;/code&gt; 所在的路径就可以。（需要注意 certbot 放置证书的路径只有 root 能读写，而 Headscale 并不是以 root 用户运行的，所以你还需要写 hooks 把文件复制出来并修改权限……）&lt;/p&gt;
&lt;p&gt;然后还有一个关于 DNS 的部分需要修改，Tailscale 提供了一个叫做 MagicDNS 的机制，当你连接上这个网络之后，就可以像在家用路由器后面一样通过主机名直接访问对应的设备，或者使用主机名 + 你定义的域名后缀，MagicDNS 会帮你解析到正确的 IP。但这里有一个问题，Headscale 默认的配置会让你运行 Tailscale 的设备将自己的 MagicDNS 服务器设置为 systemd-resolved 对所有域名使用的默认服务器（对没错 Tailscale 客户端上的 DNS 逻辑是被 Headscale 服务端控制的，什么奇怪的脑回路），这其实很不方便，特别是对于国内的一些网站比如 B 站会解析很慢并解析到离你比较远的 CDN 上，所以需要关闭这个功能，只优先对 Tailscale 的域名使用 MagicDNS 服务器。只要将 &lt;code&gt;dns_config&lt;/code&gt; 下面 &lt;code&gt;override_local_dns&lt;/code&gt; 设置为 &lt;code&gt;false&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;然后你还需要修改 &lt;code&gt;dns_config&lt;/code&gt; 下面 &lt;code&gt;base_domain&lt;/code&gt; 这一项，这个是 MagicDNS 里内部域名的后缀。&lt;/p&gt;
&lt;p&gt;解决了这些之后你就可以启动守护进程：&lt;/p&gt;
&lt;figure data-raw=&quot;# systemctl enable --now headscale
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# systemctl enable --now headscale
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Headscale 的进程和相关配置都属于 &lt;code&gt;headscale&lt;/code&gt; 用户和 &lt;code&gt;headscale&lt;/code&gt; 组，因此如果你想直接修改相关配置，可以将自己加入 &lt;code&gt;headscale&lt;/code&gt; 组：&lt;/p&gt;
&lt;figure data-raw=&quot;# gpasswd -a alynx headscale
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# gpasswd -a alynx headscale
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后你需要创建一个 Headscale 的 user，说是 user 其实更像是 namespace：&lt;/p&gt;
&lt;figure data-raw=&quot;$ headscale users create azvpn
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ headscale users create azvpn
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;上面提到的内部域名的逻辑就是 &lt;code&gt;主机名.用户名.内部域名后缀&lt;/code&gt;，比如我设置的 &lt;code&gt;base_domain&lt;/code&gt; 是 &lt;code&gt;alynx.one&lt;/code&gt;，那 timbersaw 这台主机的内部域名就是 &lt;code&gt;timbersaw.azvpn.alynx.one&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;后面我们会把设备添加到这个 namespaces，添加的时候自然需要验证权限，一般是 Tailscale 发起请求，Headscale 返回一个链接，打开链接之后是一条指令，你需要将里面的 USERNAME 换成你想要的，然后在 Headscale 所在的机器上运行这个指令。当然如果你不方便 ssh 连到 Headscale 所在的服务器，你也可以创建 preauthkey，直接在 Tailscale 连接时提供即可：&lt;/p&gt;
&lt;figure data-raw=&quot;$ headscale preauthkeys create --user azvpn --reusable --expiration 12h
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ headscale preauthkeys create --user azvpn --reusable --expiration 12h
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h1 id=&quot;Tailscale--Linux-&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Build-VPN-with-Headscale-and-Tailscale/#Tailscale--Linux-&quot;&gt;&lt;/a&gt;Tailscale (Linux)&lt;/h1&gt;
&lt;p&gt;这个同样也在 Arch Linux 的官方仓库，直接安装即可：&lt;/p&gt;
&lt;figure data-raw=&quot;# pacman -S tailscale
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# pacman -S tailscale
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;稍微复杂的一个部分是 DNS，显然 MagicDNS 会修改你的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 设置为自己的 DNS
服务器，但如果你和我的配置相同，那应该这个文件也是由 NetworkManager 管理的。如果你已经理解了 Linux 下面 DNS 解析的逻辑，你应该清楚无论何时都只应该有一个进程管理这个文件。解决方法要么是使用 NetworkManager 的插件来运行 Tailscale 从而只让 NetworkManager 管理 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;（并没有这样的插件），要么是两者全部放弃自己管理 DNS，交给第三者管理。&lt;/p&gt;
&lt;p&gt;无论是 Tailscale 还是 NetworkManager 都能自动检测 systemd-resolved 并配合它工作，所以我们启用这个代替 NetworkManager 管理 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，过程很简单也很好理解。&lt;/p&gt;
&lt;p&gt;首先把 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 链接到 systemd-resolved 的 stub 文件，这个文件的作用只有一个就是把 DNS 服务器设置成 systemd-resolved 运行的 DNS 服务器，这样所有的 DNS 查询就都被传给 systemd-resolved 进行处理：&lt;/p&gt;
&lt;figure data-raw=&quot;# ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后启动 systemd-resolved：&lt;/p&gt;
&lt;figure data-raw=&quot;# systemctl enable --now systemd-resolved
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# systemctl enable --now systemd-resolved
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;接下来重启 NetworkManager，当它启动时检测到 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 是指向 systemd-resolved 的 stub 文件的软链接，就不会尝试修改该文件而是自动配合 systemd-resolved 工作：&lt;/p&gt;
&lt;figure data-raw=&quot;# systemctl restart NetworkManager
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# systemctl restart NetworkManager
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后启动 Tailscale 的守护进程：&lt;/p&gt;
&lt;figure data-raw=&quot;# systemctl enable --now tailscaled
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# systemctl enable --now tailscaled
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;接下来就可以尝试连接到 Headscale 服务器：&lt;/p&gt;
&lt;figure data-raw=&quot;# tailscale up --login-server https://YOURSERVER:YOURPORT
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# tailscale up --login-server https://YOURSERVER:YOURPORT
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;如果你不想进行上面的手动验证流程，这一步可以直接附加上刚才创建的 preauthkey：&lt;/p&gt;
&lt;figure data-raw=&quot;# tailscale up --login-server https://YOURSERVER:YOURPORT --auth-key YOURPREAUTHKEY
&quot; data-info=&quot;language-shell&quot; data-lang=&quot;shell&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;# tailscale up --login-server https://YOURSERVER:YOURPORT --auth-key YOURPREAUTHKEY
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;此时运行 &lt;code&gt;ip a&lt;/code&gt;，应该可以看到多了一个叫 &lt;code&gt;tailscale0&lt;/code&gt; 的网络接口。使用 &lt;code&gt;resolvectl status&lt;/code&gt; 则可以看到这个接口有自己的 DNS 服务器，并且对 &lt;code&gt;azvpn.alynx.one&lt;/code&gt; 的域名使用此服务器查询。此时已经可以使用 Tailscale 内网分配的 IP 或者 MagicDNS 提供的域名像在物理路由器后面一样访问内网的各种设备。&lt;/p&gt;
&lt;h1 id=&quot;Tailscale--Android-&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Build-VPN-with-Headscale-and-Tailscale/#Tailscale--Android-&quot;&gt;&lt;/a&gt;Tailscale (Android)&lt;/h1&gt;
&lt;p&gt;Tailscale 也有开源的 Android 客户端并且已经上架了 Google Play Store，但你安装之后可能会发现没有自定义服务器的选项，你需要点开并关闭右上角三个点菜单多次，然后菜单里就会多出一项 Change Server，设置成你自建的 Headscale 服务器，然后就可以使用主界面第二个登录选项进行交互登录了。目前似乎 Android 客户端还不支持使用 preauthkey 登录。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BD%91%E7%BB%9C/" />
    <category term="网络" label="网络" scheme="https://sh.alynx.one/tags/%E7%BD%91%E7%BB%9C/" />
    <category term="Tailscale" label="Tailscale" scheme="https://sh.alynx.one/tags/Tailscale/" />
  </entry>
  <entry>
    <title>Node 的 http.request() 需要对 response 进行错误处理</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Node-HTTP-Request-Needs-to-Handle-Response-Error/" />
    <id>https://sh.alynx.one/posts/Node-HTTP-Request-Needs-to-Handle-Response-Error/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2024-01-03T07:21:05.000Z</published>
    <updated>2024-01-03T07:21:05.000Z</updated>
    <summary type="html">
      &lt;p&gt;我发现有些时候 Telegram bot 很适合用来 host 一些我自己要用的服务，因为只要通过手机上的聊天框就可以控制了，不需要我自己写一些什么后台页面。为了让构建和安装一个新 bot 的过程尽量简单，我自己用 Node 写了一个 &lt;a href=&quot;https://github.com/AlynxZhou/azbot-telegram/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;没有外部依赖的 Telegram bot 框架&lt;/a&gt;。完全使用 Node 自带的模块比较麻烦的一点就是你需要自己基于 &lt;code&gt;http.request()&lt;/code&gt; 进行封装，因为原版基于 &lt;code&gt;EventEmitter&lt;/code&gt; 的接口写起来实在是太复杂了。&lt;/p&gt;

    </summary>
    <content type="html">
      &lt;p&gt;我发现有些时候 Telegram bot 很适合用来 host 一些我自己要用的服务，因为只要通过手机上的聊天框就可以控制了，不需要我自己写一些什么后台页面。为了让构建和安装一个新 bot 的过程尽量简单，我自己用 Node 写了一个 &lt;a href=&quot;https://github.com/AlynxZhou/azbot-telegram/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;没有外部依赖的 Telegram bot 框架&lt;/a&gt;。完全使用 Node 自带的模块比较麻烦的一点就是你需要自己基于 &lt;code&gt;http.request()&lt;/code&gt; 进行封装，因为原版基于 &lt;code&gt;EventEmitter&lt;/code&gt; 的接口写起来实在是太复杂了。&lt;/p&gt;
&lt;a id=&quot;more&quot;&gt;&lt;/a&gt;

&lt;p&gt;把 &lt;code&gt;http.request()&lt;/code&gt; 封装成 Promise 比一般的 API 要难一点，但也不是完全做不到，比如 &lt;a href=&quot;https://nodejs.org/api/http.html#httprequestoptions-callback&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;官方文档上的示例代码&lt;/a&gt; 是这样写的（复制这么长一段不是我要占字数而是我真的被它坑了）：&lt;/p&gt;
&lt;figure data-raw=&quot;import http from &#039;node:http&#039;;
import { Buffer } from &#039;node:buffer&#039;;

const postData = JSON.stringify({
  &#039;msg&#039;: &#039;Hello World!&#039;,
});

const options = {
  hostname: &#039;www.google.com&#039;,
  port: 80,
  path: &#039;/upload&#039;,
  method: &#039;POST&#039;,
  headers: {
    &#039;Content-Type&#039;: &#039;application/json&#039;,
    &#039;Content-Length&#039;: Buffer.byteLength(postData),
  },
};

const req = http.request(options, (res) =&gt; {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding(&#039;utf8&#039;);
  res.on(&#039;data&#039;, (chunk) =&gt; {
    console.log(`BODY: ${chunk}`);
  });
  res.on(&#039;end&#039;, () =&gt; {
    console.log(&#039;No more data in response.&#039;);
  });
});

req.on(&#039;error&#039;, (e) =&gt; {
  console.error(`problem with request: ${e.message}`);
});

// Write data to request body
req.write(postData);
req.end();
&quot; data-info=&quot;language-JavaScript&quot; data-lang=&quot;JavaScript&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-JavaScript&quot;&gt;import http from &#039;node:http&#039;;
import { Buffer } from &#039;node:buffer&#039;;

const postData = JSON.stringify({
  &#039;msg&#039;: &#039;Hello World!&#039;,
});

const options = {
  hostname: &#039;www.google.com&#039;,
  port: 80,
  path: &#039;/upload&#039;,
  method: &#039;POST&#039;,
  headers: {
    &#039;Content-Type&#039;: &#039;application/json&#039;,
    &#039;Content-Length&#039;: Buffer.byteLength(postData),
  },
};

const req = http.request(options, (res) =&amp;gt; {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding(&#039;utf8&#039;);
  res.on(&#039;data&#039;, (chunk) =&amp;gt; {
    console.log(`BODY: ${chunk}`);
  });
  res.on(&#039;end&#039;, () =&amp;gt; {
    console.log(&#039;No more data in response.&#039;);
  });
});

req.on(&#039;error&#039;, (e) =&amp;gt; {
  console.error(`problem with request: ${e.message}`);
});

// Write data to request body
req.write(postData);
req.end();
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;那对于一个 POST 请求，我就可以这样封装：&lt;/p&gt;
&lt;figure data-raw=&quot;import * as http from &amp;quot;node:http&amp;quot;;
import {Buffer} from &amp;quot;node:buffer&amp;quot;;
const post = (url, body, headers = {}) =&gt; {
  const opts = {
    &amp;quot;method&amp;quot;: &amp;quot;POST&amp;quot;,
    &amp;quot;timeout&amp;quot;: 1500,
    &amp;quot;headers&amp;quot;: {}
  };
  for (const [k, v] of Object.entries(headers)) {
    opts[&amp;quot;headers&amp;quot;][k.toLowerCase()] = v;
  }
  if (!(isBuffer(body) || isString(body))) {
    body = JSON.stringify(body);
    opts[&amp;quot;headers&amp;quot;][&amp;quot;content-type&amp;quot;] = &amp;quot;application/json&amp;quot;;
    opts[&amp;quot;headers&amp;quot;][&amp;quot;content-length&amp;quot;] = `${Buffer.byteLength(body)}`;
  }
  return new Promise((resolve, reject) =&gt; {
    const req = http.request(url, opts, (res) =&gt; {
      const chunks = [];
      res.on(&amp;quot;data&amp;quot;, (chunk) =&gt; {
	chunks.push(chunk);
      });
      res.on(&amp;quot;end&amp;quot;, () =&gt; {
	resolve(Buffer.concat(chunks));
      });
    });
    req.on(&amp;quot;error&amp;quot;, reject);
    req.write(body);
    req.end();
  });
};
&quot; data-info=&quot;language-JavaScript&quot; data-lang=&quot;JavaScript&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-JavaScript&quot;&gt;import * as http from &quot;node:http&quot;;
import {Buffer} from &quot;node:buffer&quot;;
const post = (url, body, headers = {}) =&amp;gt; {
  const opts = {
    &quot;method&quot;: &quot;POST&quot;,
    &quot;timeout&quot;: 1500,
    &quot;headers&quot;: {}
  };
  for (const [k, v] of Object.entries(headers)) {
    opts[&quot;headers&quot;][k.toLowerCase()] = v;
  }
  if (!(isBuffer(body) || isString(body))) {
    body = JSON.stringify(body);
    opts[&quot;headers&quot;][&quot;content-type&quot;] = &quot;application/json&quot;;
    opts[&quot;headers&quot;][&quot;content-length&quot;] = `${Buffer.byteLength(body)}`;
  }
  return new Promise((resolve, reject) =&amp;gt; {
    const req = http.request(url, opts, (res) =&amp;gt; {
      const chunks = [];
      res.on(&quot;data&quot;, (chunk) =&amp;gt; {
	chunks.push(chunk);
      });
      res.on(&quot;end&quot;, () =&amp;gt; {
	resolve(Buffer.concat(chunks));
      });
    });
    req.on(&quot;error&quot;, reject);
    req.write(body);
    req.end();
  });
};
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;反正流程无非是创建 request，然后在 response 里面收集 data 到 buffer，然后处理 request 的 error，再把 body 写到 request 里面。看起来很简单毕竟官方文档也这么写的对吧！然后就掉进坑里了。&lt;/p&gt;
&lt;p&gt;我的 Telegram bot 设置是要不停通过 HTTP 轮询获取更新，为了保证能一直轮询下去，就要在遇到错误的时候 catch 住简单处理，然后继续进行下次轮询。但明明我已经在可能出现错误的时候都处理了，bot 还是会在跑了几天以后遇到错误（通常是 &lt;code&gt;read ETIMEOUT&lt;/code&gt;）然后完全停住，只能手动重启。我对此绞尽脑汁，但是想不出哪里有问题，同时因为这个要 bot 跑一段时间才能复现，也很难 debug，我甚至手动打了 log 来看是轮询停住了还是轮询没有停但却一直得到空的结果，实际证明是遇到错误停住了，但我不是已经进行错误处理了吗？&lt;/p&gt;
&lt;p&gt;这个问题实在是找不到什么参考，我尝试了一些没有意义的办法，最后差点去翻什么 axios 之类的代码看他们如何解决的了。不过我在此之前想了一下，会不会是因为不仅要写 &lt;code&gt;req.on(&quot;error&quot;, reject);&lt;/code&gt;，还要写 &lt;code&gt;res.on(&quot;error&quot;, reject);&lt;/code&gt; 来处理 response 的错误，否则 Node 就会直接把相关的错误抛出来停掉？其实我心里觉得不太可能，毕竟 &lt;strong&gt;示例代码里根本都没有写这句&lt;/strong&gt;，但我还是本着没办法的办法写上去了：&lt;/p&gt;
&lt;figure data-raw=&quot;diff --git a/azbot-telegram/bot-utils.js b/azbot-telegram/bot-utils.js
index 42e002e..f90a8eb 100644
--- a/azbot-telegram/bot-utils.js
+++ b/azbot-telegram/bot-utils.js
@@ -360,13 +360,13 @@ const post = (url, body, headers = {}) =&gt; {
   return new Promise((resolve, reject) =&gt; {
     const req = https.request(url, opts, (res) =&gt; {
       const chunks = [];
+      res.on(&amp;quot;error&amp;quot;, reject);
       res.on(&amp;quot;data&amp;quot;, (chunk) =&gt; {
         chunks.push(chunk);
       });
       res.on(&amp;quot;end&amp;quot;, () =&gt; {
         resolve(Buffer.concat(chunks));
       });
     });
     req.on(&amp;quot;error&amp;quot;, reject);
     req.write(body);
&quot; data-info=&quot;language-patch&quot; data-lang=&quot;patch&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code class=&quot;language-patch&quot;&gt;diff --git a/azbot-telegram/bot-utils.js b/azbot-telegram/bot-utils.js
index 42e002e..f90a8eb 100644
--- a/azbot-telegram/bot-utils.js
+++ b/azbot-telegram/bot-utils.js
@@ -360,13 +360,13 @@ const post = (url, body, headers = {}) =&amp;gt; {
   return new Promise((resolve, reject) =&amp;gt; {
     const req = https.request(url, opts, (res) =&amp;gt; {
       const chunks = [];
+      res.on(&quot;error&quot;, reject);
       res.on(&quot;data&quot;, (chunk) =&amp;gt; {
         chunks.push(chunk);
       });
       res.on(&quot;end&quot;, () =&amp;gt; {
         resolve(Buffer.concat(chunks));
       });
     });
     req.on(&quot;error&quot;, reject);
     req.write(body);
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后问题就神奇的解！决！了！我的 bot 连续跑了十天半个月也没有挂，我心里这个气啊，为什么官方文档里的示例一点没提到要对 response 的 error 事件进行处理呢？甚至在网上也很难找到相关的信息，我推测是大部分人并不从头自己封装 HTTP 模块而是直接使用现成的库比如 axios，然后可能有人发现过这问题就简单地给 axios 提了这么一个 fix，就再也没人提起过这件事了。总之还是希望官方文档能更新一下示例代码吧。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="编程" label="编程" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BC%96%E7%A8%8B/" />
    <category term="JavaScript" label="JavaScript" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BC%96%E7%A8%8B/JavaScript/" />
    <category term="JavaScript" label="JavaScript" scheme="https://sh.alynx.one/tags/JavaScript/" />
  </entry>
  <entry>
    <title>2023 年的 Arch Linux 安装指南</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/2023-Arch-Install/" />
    <id>https://sh.alynx.one/posts/2023-Arch-Install/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2023-07-28T12:42:53.000Z</published>
    <updated>2023-07-28T12:42:53.000Z</updated>
    <summary type="html">
      在安装 Arch Linux 之前，首先要准备 Arch Linux 的安装媒介。如果你打算安装在虚拟机里，那你并不需要一个实体的存储介质，因为虚拟机可以直接加载 iso 文件。但不管你在哪里安装，你都需要获取这个 iso 文件，引导进入其中的临时系统才能继续安装。 Arch L…
    </summary>
    <content type="html">
      &lt;p&gt;在安装 Arch Linux 之前，首先要准备 Arch Linux 的安装媒介。如果你打算安装在虚拟机里，那你并不需要一个实体的存储介质，因为虚拟机可以直接加载 iso 文件。但不管你在哪里安装，你都需要获取这个 iso 文件，引导进入其中的临时系统才能继续安装。&lt;/p&gt;
&lt;p&gt;Arch Linux 的安装镜像每月更新一次，如果你点开官网的下载页面，你会发现没有直接的下载链接，而是推荐你使用种子下载或者镜像站下载。这是一个非常有必要的要求，因为官方的服务器不能承受世界各地所有的请求流量，以上两种方法通过将单一的下载来源转换为多个下载来源有效的减轻了官方服务器的压力。&lt;/p&gt;
&lt;p&gt;考虑到当今种子下载并不是一个流行的下载方式，对于部分读者而言可能难以掌握，我们这里就选择镜像站下载。所谓的镜像站就是将官方服务器上的文件原样下载到自己的服务器上，然后给别人提供下载服务的服务器。有了镜像站，世界各地的用户就不必连接相对较远较慢的官方服务器，而可以就近选择镜像站，获取到完全一样的文件。&lt;/p&gt;
&lt;p&gt;我们可以打开某个镜像站同步 &lt;code&gt;archlinux&lt;/code&gt; 的目录，然后找到 &lt;code&gt;iso/latest&lt;/code&gt; 目录，里面的 &lt;code&gt;archlinux-x86_64.iso&lt;/code&gt; 就是我们需要的，以清华大学的镜像站为例，链接就是 &lt;a href=&quot;https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/latest/archlinux-x86_64.iso&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/latest/archlinux-x86_64.iso&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;注意！Arch Linux 官方只对 x86-64 架构提供支持，如果你的设备不是该架构（可能性很低，如果你不是该架构，你应该已经有足够的经验自己解决问题了），可能需要使用其它分支项目并参阅相关的文档。&lt;/p&gt;
&lt;p&gt;当下载好 iso 文件之后，需要准备对应的安装媒介，这需要一个实体的存储介质，光盘是最传统的安装媒介，这也导致了各种系统的安装程序都以光盘镜像（iso）的格式打包。但想必当今的用户寻找光盘和光驱可能有些难度，所以 U 盘成为了更流行的安装媒介，找出一个你没有使用的 U 盘，备份好原本的数据，然后连接到你下载了安装镜像的电脑上。&lt;/p&gt;
&lt;p&gt;如果你的电脑上已经运行了 Linux，那你可以通过 &lt;code&gt;dd&lt;/code&gt; 命令将 iso 文件写入到 U 盘里，Arch Linux 的安装镜像经过特殊处理，可以支持这样的 U 盘引导。首先通过 &lt;code&gt;lsblk -f&lt;/code&gt; 查看你的 U 盘对应的设备文件是什么，然后使用 &lt;code&gt;dd if=/PATH/TO/archlinux-x86_64.iso of=/dev/sdX&lt;/code&gt;，记得把 iso 和设备文件的路径改成你的实际路径，并且不要使用 U 盘分区的设备文件，而是使用代表整个 U 盘的设备文件。然后执行 &lt;code&gt;sync&lt;/code&gt;，让内核把内存里缓冲的数据写回磁盘，保证安装镜像完全写进 U 盘里面。&lt;/p&gt;
&lt;p&gt;但你也可能会说如果我有 Linux，我为什么要装 Linux？这种情况下我们推荐 Windows 用户使用 Rufus 创建安装 U 盘，这个软件下载即可运行，不需要安装，然后在软件里分别选择 iso 文件的位置和 U 盘设备，点击写入即可获得一个安装 U 盘。&lt;/p&gt;
&lt;p&gt;无论你使用哪种方式，接下来弹出 U 盘，准备重启电脑。不过要保证重启的时候 U 盘仍然在你的电脑上。现在的电脑应该都支持 UEFI 引导，你需要搜索你的主板型号得知你的电脑应该按什么按键进入启动设备选单，反正无非是 F8、DEL、Enter 中的一个，在显示主板 logo 的时候狂按，直到出现一个让你选择的菜单，使用键盘上的方向键选择你刚刚做好的安装 U 盘，然后按下 Enter 选择。等屏幕上走完启动流程，你应该就会自动登录进一个 Arch Linux 的环境。如果你开启了安全启动，那你需要关掉，因为 Arch Linux 的安装镜像并没有进行安全启动需要的签名，这里就不介绍具体如何关闭了，因为各家主板的界面都不一样，建议搜索引擎搜索自己的主板型号+关闭安全启动。&lt;/p&gt;
&lt;p&gt;Arch Linux 的安装环境是没有桌面的，你需要在命令行里自己调用各种命令完成一系列安装相关的操作，这样看起来比较难，但是也很灵活，可以根据自己的需要调整。首先你要做的是确定自己已经联网了，最简单的就是从路由器插一根网线到你的电脑上，这样应该就能上网了。如果你没有网线——那现在就该去买一根，比起现在给你讲清楚怎么在命令行下面连接无线网络，买网线更简单，真的。&lt;/p&gt;
&lt;p&gt;然后你应该使用 &lt;code&gt;date&lt;/code&gt; 命令查看系统时间是否重要，许多加密方式依赖时间正确，比如 https，因此如果它不正确，你应该改正它，不过大部分情况都是正确的。&lt;/p&gt;
&lt;p&gt;接下来你应该准备安装系统的磁盘分区，首先你得通过 &lt;code&gt;lsblk -f&lt;/code&gt; 找到要安装的硬盘，如果是 SATA 硬盘，它可能是 &lt;code&gt;/dev/sda&lt;/code&gt; 或者 &lt;code&gt;/dev/sdb&lt;/code&gt; 之类，如果是 NVMe 硬盘，那可能是 &lt;code&gt;/dev/nvme0n1&lt;/code&gt;，一般来说根据容量判断是不会错的。你的目标磁盘上应当留有一定的未分配空间给新系统使用。注意如果分区和格式化时操作错误，可能会让你丢失已有的重要数据，因此在进行操作前务必仔细确认。&lt;/p&gt;
&lt;p&gt;UEFI 引导的机器大部分都使用 GPT 分区表，当然这其实主要是 Windows 的限制，因此我们使用 &lt;code&gt;gdisk&lt;/code&gt; 进行分区，如果你使用的不是 GPT 分区表，那你可能需要自行了解一些相关知识。当你不知道该做什么的时候，输入 &lt;code&gt;?&lt;/code&gt; 可以显示帮助，输入 &lt;code&gt;p&lt;/code&gt; 可以打印当前的分区表，输入 &lt;code&gt;q&lt;/code&gt; 可以退出 &lt;code&gt;gdisk&lt;/code&gt;，只有输入 &lt;code&gt;w&lt;/code&gt; 才会真正修改硬盘上的分区表，所以如果你不确定就不要输入 &lt;code&gt;w&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;输入 &lt;code&gt;p&lt;/code&gt; 打出当前的分区表之后，你应该首先找到一个小的 FAT32 分区，一般会在磁盘的开头，容量不会超过 1G，这是你的 ESP 系统分区，UEFI 要求把引导文件放在这里。然后你应该按 &lt;code&gt;n&lt;/code&gt; 新建一个分区，一般它会自动计算未分配空间的开头，不过你也可以手动输入来纠正，然后输入新分区的结尾位置，也可以用 &lt;code&gt;+100G&lt;/code&gt; 的方式表示从开头位置创建一个 100G 的分区。一般只要分一个分区做 Linux 的根分区就好了，不过你有需要也可以创建更多的分区，比如你可能需要一个 swap 分区，那就用相同的办法创建一个。创建完你需要的分区之后，输入 &lt;code&gt;p&lt;/code&gt; 确认一下新的分区表，然后按 &lt;code&gt;w&lt;/code&gt; 写入分区表。&lt;/p&gt;
&lt;p&gt;接下来你需要在分好的分区上创建文件系统也就是格式化，因为分区表只是标记“从哪里到哪里属于哪个分区”，并没有在对应的位置创建实际的结构。比如你可以用 &lt;code&gt;mkfs.ext4 /dev/sdXY&lt;/code&gt; 格式化你刚创建的根分区，然后用 &lt;code&gt;mkswap /dev/sdXY&lt;/code&gt; 格式化你刚才创建的 swap 分区。记得在执行命令之前确认你使用的设备文件正确。然后你需要挂载你创建的分区到 &lt;code&gt;/mnt&lt;/code&gt;，稍候会向里面写入系统文件。首先用 &lt;code&gt;mount /dev/sdXY /mnt&lt;/code&gt;，把根分区挂载上，然后你需要创建其它分区的挂载点，比如 EFI 系统分区，对于这个如何挂载有很多种说法，不过我一般直接把它当作 boot 分区挂载，这样内核也会被安装到这个分区，有些预装 Windows 系统的电脑可能会分一个极小的 EFI 系统分区以至于放不下内核，那你可能需要查找更多资料，这不在这篇文章的讨论范围之内。总之先 &lt;code&gt;mkdir /mnt/boot&lt;/code&gt; 然后 &lt;code&gt;mount /dev/sdXY /mnt/boot&lt;/code&gt;。最后可以用 &lt;code&gt;swapon /dev/sdXY&lt;/code&gt; 启用你刚才创建的 swap 分区，这样记录新系统挂载点的时候就会记录这个 swap 分区。&lt;/p&gt;
&lt;p&gt;然后需要修改镜像站列表，和之前下载 iso 一样，系统需要的各种软件包也依靠镜像站提高分发效率。你需要用一个编辑器编辑 &lt;code&gt;/etc/pacman.d/mirrorlist&lt;/code&gt;，如果你没有熟悉的编辑器，那 &lt;code&gt;nano&lt;/code&gt; 应该是个适合新手的选择，因为各种操作需要的快捷键都会显示在屏幕底部，&lt;code&gt;^&lt;/code&gt; 代表 Ctrl，&lt;code&gt;M&lt;/code&gt; 代表 Alt，在列表里找到离你地理位置比较近的几个镜像站，然后删除对应的 &lt;code&gt;Server = &lt;/code&gt; 前面的 &lt;code&gt;#&lt;/code&gt; 来启用这个镜像站，一般启用两三个就足够了。&lt;/p&gt;
&lt;p&gt;然后就可以正式安装软件包到创建的分区了！使用 &lt;code&gt;pacstrap -K /mnt base base-devel linux linux-firmware&lt;/code&gt; 安装软件包到 &lt;code&gt;/mnt&lt;/code&gt;，你可以在后面附加更多你需要的软件包以便一并安装，甚至如果你不想第一次启动新系统还是命令行的话，也可以在这一步直接附加桌面环境进去。这里我贴一个基于我常用软件总结的列表作为参考：&lt;/p&gt;
&lt;figure data-raw=&quot;base base-devel linux linux-firmware man-db man-pages btrfs-progs vim nano git rsync gnome gdm networkmanager firefox meson ninja efibootmgr haveged ibus-rime ffmpeg noto-fonts noto-fonts-cjk noto-fonts-emoji ntfs-3g btop p7zip parallel tree ttf-roboto unrar unarchiver wget usbutils bind
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;base base-devel linux linux-firmware man-db man-pages btrfs-progs vim nano git rsync gnome gdm networkmanager firefox meson ninja efibootmgr haveged ibus-rime ffmpeg noto-fonts noto-fonts-cjk noto-fonts-emoji ntfs-3g btop p7zip parallel tree ttf-roboto unrar unarchiver wget usbutils bind
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;如果你和我有不同的偏好，你应该已经清楚如何安装你需要的软件，我在这里只以我自己使用的软件作为例子。为了方便使用，我在这一步直接安装了桌面环境，但桌面环境需要有可用的显卡驱动，对于 Intel 和 AMD，它们的开源驱动已经足够好用，应该会自动引入 &lt;code&gt;mesa&lt;/code&gt; 所以没什么需要额外操作的，但是对于 NVIDIA，你还需要安装 &lt;code&gt;nvidia&lt;/code&gt; 这个包来引入 NVIDIA 的闭源驱动。&lt;/p&gt;
&lt;p&gt;然后等待下载安装即可，现在大家的带宽都很高，如果确实选了离自己近的镜像站，这个步骤应该花不了多少时间。&lt;/p&gt;
&lt;p&gt;然后读取你对新系统的挂载信息并写入到新系统里，以便新系统基于这个数据挂载硬盘，使用 &lt;code&gt;genfstab -U /mnt &amp;gt;&amp;gt; /mnt/etc/fstab&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;现在你的新分区里应该有一个新系统需要的各种文件了，但是你还需要对它进行各种设置，首先需要 chroot 到新的系统，这是一个 Linux 内核的功能，可以让你以另一个文件系统作为根目录从而操作其中的各种文件，这里使用 &lt;code&gt;arch-chroot /mnt&lt;/code&gt; 进入新系统的根目录。&lt;/p&gt;
&lt;p&gt;然后你要指定自己新系统的时区，比如你使用的时区是 &lt;code&gt;Asia/Shanghai&lt;/code&gt;，那可以执行 &lt;code&gt;ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime&lt;/code&gt;，你也可以将 &lt;code&gt;Asia/Shanghai&lt;/code&gt; 修改为其它的时区，所有可用的时区都以目录和文件的形式列在 &lt;code&gt;/usr/share/zoneinfo/&lt;/code&gt; 下面。&lt;/p&gt;
&lt;p&gt;然后你需要执行 &lt;code&gt;hwclock --systohc&lt;/code&gt;，这会假设你的 BIOS 时间是 UTC，这和 Windows 默认的假设不一致，Windows 认为你的 BIOS 时间就是本地时间。可以让 Linux 认为 BIOS 时间是本地时间，但是可能会导致各种问题，同样也有办法让 Windows 认为 BIOS 时间是 UTC 时间，只需要随便新建一个文本文档，写入如下内容：&lt;/p&gt;
&lt;figure data-raw=&quot;Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation]
&amp;quot;RealTimeIsUniversal&amp;quot;=dword:00000001
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation]
&quot;RealTimeIsUniversal&quot;=dword:00000001
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;然后保存并修改扩展名为 &lt;code&gt;.reg&lt;/code&gt;，然后双击导入注册表项并重启。&lt;/p&gt;
&lt;p&gt;接下来修改本地化相关内容，首先是 glibc 需要对不同的语言生成不同的配置，需要用编辑器编辑 &lt;code&gt;/etc/locale.gen&lt;/code&gt; 文件，必须要启用的是 &lt;code&gt;en_US.UTF-8 UTF-8&lt;/code&gt;，别的可以按需求启用你需要的，只要删掉开头的 &lt;code&gt;#&lt;/code&gt;，注意只要启用带 &lt;code&gt;UTF-8&lt;/code&gt; 的就可以。不过这一步也可以略过其他的只启用英语，然后在桌面环境里修改语言的话桌面环境应该会自动处理相关的文件。然后运行 &lt;code&gt;locale-gen&lt;/code&gt;，它会根据上述文件的内容具体生成对应文件。&lt;/p&gt;
&lt;p&gt;然后创建 &lt;code&gt;/etc/locale.conf&lt;/code&gt; 文件，写入你当前使用的 &lt;code&gt;LANG&lt;/code&gt; 变量，不过其实 tty 不能显示中文，所以这一步推荐设置成英文，等到桌面起来了再改也来得及，因此推荐写入 &lt;code&gt;LANG=en_US.UTF-8&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;网络相关的配置首先需要设置 hostname，这一步只要打开 &lt;code&gt;/etc/hostname&lt;/code&gt; 文件写入你想要的主机名就行了。我的习惯是使用 NetworkManager 管理网络连接，因此需要设置让系统下次启动时启用 NetworkManager，只要 &lt;code&gt;systemctl enable NetworkManager&lt;/code&gt; 就可以。NetworkManager 会自动管理你的网卡，比如有线网卡就会自动尝试 DHCP，同时也提供和桌面环境的集成，方便使用无线网卡。&lt;/p&gt;
&lt;p&gt;一些无线网卡需要的固件可能被单独划分在别的包里，此时你可以安装这些包，比如高通的网卡就是 &lt;code&gt;pacman -S linux-firmware-qcom&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后你需要进行启动相关的设置，首先你得生成 initramfs，这个东西解决的问题是“需要加载模块才能读取对应的文件系统，但模块就存在那个文件系统上”这种问题，为了打破鸡生蛋还是蛋生鸡的循环，解决方法就是创建一个非常小的包含必要模块的文件，和内核放在一起，保证启动时可以加载这个文件。生成这个文件很简单，因为我们没有什么特殊的配置，只要执行 &lt;code&gt;mkinitcpio -P&lt;/code&gt; 就行了。&lt;/p&gt;
&lt;p&gt;同时现代的 CPU 都支持加载微码来热修复 CPU 的 bug，这也是通过在启动时加载相关的文件实现，首先需要安装对应的微码包，如果是 Intel 就 &lt;code&gt;pacman -S intel-ucode&lt;/code&gt;，AMD 就 &lt;code&gt;pacman -S amd-ucode&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后你还需要一个 bootloader 加载你的内核和 initramfs，最流行功能最全的是 GRUB，但我觉得 systemd-boot 也完全够用了，所以我选择 systemd-boot。因为已经安装了 systemd 所以就不需要额外安装什么了，只要 &lt;code&gt;bootctl install&lt;/code&gt;，就可以安装引导需要的文件。&lt;/p&gt;
&lt;p&gt;但我们仍然需要告诉 loader 去哪里加载内核，加载哪个内核。systemd-boot 需要我们手动编写配置文件记录这些内容。首先需要 &lt;code&gt;mkdir /boot/loader/entries&lt;/code&gt; 建立用于放置不同内核启动项的文件，然后再编辑 &lt;code&gt;/boot/loader/entries/arch.conf&lt;/code&gt; 给默认的内核编写一个文件。&lt;/p&gt;
&lt;p&gt;一个配置文件推荐包含以下内容：&lt;/p&gt;
&lt;figure data-raw=&quot;title   Arch Linux
linux   /vmlinuz-linux
initrd	/amd-ucode.img
initrd  /initramfs-linux.img
options root=&amp;quot;UUID=xxxx-xxxx-xxxx&amp;quot; rw add_efi_memmap
&quot; class=&quot;code-block&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;code&gt;title   Arch Linux
linux   /vmlinuz-linux
initrd	/amd-ucode.img
initrd  /initramfs-linux.img
options root=&quot;UUID=xxxx-xxxx-xxxx&quot; rw add_efi_memmap
&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;基本上你需要改的有两处，一个是如果是 Intel 就把 &lt;code&gt;amd-ucode&lt;/code&gt; 改成 &lt;code&gt;intel-ucode&lt;/code&gt;，另一个是要把内核参数里 &lt;code&gt;root=&lt;/code&gt; 的值设置为你的根分区，以便内核找到你真正的根分区。这个可以通过打开 &lt;code&gt;/etc/fstab&lt;/code&gt; 找到里面挂载到 &lt;code&gt;/&lt;/code&gt; 的设备得到需要的值。&lt;/p&gt;
&lt;p&gt;然后你需要编辑 &lt;code&gt;/boot/loader/loader.conf&lt;/code&gt;，这是给 loader 的配置，其实只需要一行 &lt;code&gt;default arch.conf&lt;/code&gt;，告诉 loader 默认加载 arch 内核的配置就可以了。&lt;/p&gt;
&lt;p&gt;最后需要进行密码配置，首先执行 &lt;code&gt;passwd&lt;/code&gt; 设置 root 密码。由于 root 权限太高，平时不建议使用 root 操作，所以我们可以通过 &lt;code&gt;useradd -m newuser&lt;/code&gt; 创建一个普通用户，&lt;code&gt;-m&lt;/code&gt; 的意思是会自动给用户创建同名的 home 目录存储用户相关的文件，你也可以把 &lt;code&gt;newuser&lt;/code&gt; 改成任何你想要的用户名。然后执行 &lt;code&gt;passwd newuser&lt;/code&gt; 给这个新用户设置密码。同时为了方便进行高权限操作，我们需要允许新用户执行 &lt;code&gt;sudo&lt;/code&gt;，首先执行 &lt;code&gt;EDITOR=nano visudo&lt;/code&gt; 编辑 sudoers 文件，找到 &lt;code&gt;root ALL=(ALL) ALL&lt;/code&gt; 一行，然后在下面插入 &lt;code&gt;newuser ALL=(ALL) ALL&lt;/code&gt;（记得用你想要的用户名），保存即可。&lt;/p&gt;
&lt;p&gt;然后运行 &lt;code&gt;systemctl enable gdm&lt;/code&gt;，这会告诉系统启动时启用 GNOME 显示管理器，下次启动时你就会得到图形界面并可以直接登录进桌面。此时关于新系统的设置都已基本完成，执行 &lt;code&gt;exit&lt;/code&gt; 退出 chroot，然后执行 &lt;code&gt;reboot&lt;/code&gt; 重启电脑，你应该可以在 UEFI 启动选单里找到 Linux Boot Manager，选择就会启动新安装的 Arch Linux。&lt;/p&gt;
&lt;p&gt;由于此时应该已经启动图形界面了，对于桌面的各种设置只要在图形界面的设置程序里设置即可，就不需要专门讲述怎么用了。&lt;/p&gt;

    </content>
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Linux" label="Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/" />
    <category term="Arch Linux" label="Arch Linux" scheme="https://sh.alynx.one/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/Linux/Arch%20Linux/" />
    <category term="计算机" label="计算机" scheme="https://sh.alynx.one/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/" />
    <category term="Arch Linux" label="Arch Linux" scheme="https://sh.alynx.one/tags/Arch%20Linux/" />
  </entry>
  <entry>
    <title>DaVinci Resolve 奇怪的素材位置计算逻辑</title>
    <link rel="alternate" type="text/html" href="https://sh.alynx.one/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/" />
    <id>https://sh.alynx.one/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/</id>
    <author>
      <name>Alynx Zhou</name>
      <email>alynx.zhou@gmail.com</email>
    </author>
    <published>2023-07-28T07:20:02.000Z</published>
    <updated>2023-07-28T07:20:02.000Z</updated>
    <summary type="html">
      &lt;p&gt;上一篇文章提到了 DaVinci Resolve 对于素材位置的计算逻辑非常奇怪并且不肯修改，这篇我试图概括一下具体的计算逻辑方便自己使用。如果你也遇到了同样的问题并且希望他们改进，可以去支持 &lt;a href=&quot;https://forum.blackmagicdesign.com/viewtopic.php?f=33&amp;amp;t=179153&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;我发的帖子（英语）&lt;/a&gt;。&lt;/p&gt;

    </summary>
    <content type="html">
      &lt;p&gt;上一篇文章提到了 DaVinci Resolve 对于素材位置的计算逻辑非常奇怪并且不肯修改，这篇我试图概括一下具体的计算逻辑方便自己使用。如果你也遇到了同样的问题并且希望他们改进，可以去支持 &lt;a href=&quot;https://forum.blackmagicdesign.com/viewtopic.php?f=33&amp;amp;t=179153&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noreferrer noopener&quot;&gt;我发的帖子（英语）&lt;/a&gt;。&lt;/p&gt;
&lt;a id=&quot;more&quot;&gt;&lt;/a&gt;

&lt;h1 id=&quot;%E8%AE%A1%E7%AE%97%E5%9F%BA%E5%87%86&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/#%E8%AE%A1%E7%AE%97%E5%9F%BA%E5%87%86&quot;&gt;&lt;/a&gt;计算基准&lt;/h1&gt;
&lt;p&gt;缩放/裁切/位置永远以原图大小作为基准，不会互相影响。&lt;/p&gt;
&lt;p&gt;项目设置里有“缩放原图至适配大小且不出现裁切”和“不调整原图大小并裁切超出部分”两个比较合理的选项，“缩放原图至适配大小且不出现裁切”可以理解为插入时间线之前就改变了原图大小。例如画布尺寸 1920x1080，素材尺寸 512x512，选择“缩放原图至适配大小且不出现裁切”，相当于用外部命令把素材缩放到 1080x1080 然后再插入时间线，后续缩放/裁切/位置均以 1080x1080 作为基准。&lt;/p&gt;
&lt;h1 id=&quot;%E7%BC%A9%E6%94%BE&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/#%E7%BC%A9%E6%94%BE&quot;&gt;&lt;/a&gt;缩放&lt;/h1&gt;
&lt;p&gt;缩放变换只计算原图大小。&lt;/p&gt;
&lt;p&gt;缩放变换默认以素材中心作为基准。由于位置变换计算太复杂了，不考虑改变锚点参数的情况。&lt;/p&gt;
&lt;h1 id=&quot;%E8%A3%81%E5%88%87&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/#%E8%A3%81%E5%88%87&quot;&gt;&lt;/a&gt;裁切&lt;/h1&gt;
&lt;p&gt;裁切只计算原图大小。无论素材的缩放变换参数是多少，都使用原图大小计算结果。&lt;/p&gt;
&lt;p&gt;例：画布 1920x1080，素材 512x512。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择“缩放原图至适配大小且不出现裁切”，此时原图大小是 1080x1080，左侧裁切 50% 应输入 &lt;code&gt;1080 * 50% = 512&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;选择“不调整原图大小并裁切超出部分”，此时原图大小是 512x512，左侧裁切 50% 应输入 &lt;code&gt;512 * 50% = 256&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于位置变换计算太复杂了，不考虑勾选“保留图片位置”的情况。&lt;/p&gt;
&lt;h1 id=&quot;%E4%BD%8D%E7%BD%AE&quot;&gt;&lt;a class=&quot;heading-link header-link&quot; href=&quot;/posts/Strange-Logic-of-DaVinci-Resolve-on-Calculating-Clip-Position/#%E4%BD%8D%E7%BD%AE&quot;&gt;&lt;/a&gt;位置&lt;/h1&gt;
&lt;p&gt;位置变换只计算原图大小。无论素材的缩放变换参数和裁切参数是多少，都使用原图大小计算结果。&lt;/p&gt;
&lt;p&gt;DaVinci Resolve 对于素材位置输入框使用特殊的计算逻辑（怀疑是 bug），假设画布宽度为 &lt;code&gt;canvas_width&lt;/code&gt;，画布高度为 &lt;code&gt;canvas_height&lt;/code&gt;，原图宽度为 &lt;code&gt;clip_width&lt;/code&gt;, 原图高度为 &lt;code&gt;clip_height&lt;/code&gt;，如果想将素材水平方向移动 &lt;code&gt;x&lt;/code&gt; 像素，垂直方向移动 &lt;code&gt;y&lt;/code&gt; 像素，则需要填入的数据需要按照 &lt;code&gt;f(x) = x / clip_width * canvas_width&lt;/code&gt; 和 &lt;code&gt;f(y) = y / clip_height * canvas_height&lt;/code&gt; 进行计算。注意，按此计算逻辑，填入的两个数据的比例显然和实际移动的像素比例不同。&lt;/p&gt;
&lt;p&gt;例：画布 1920x1080，素材 512x512。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择“缩放原图至适配大小且不出现裁切”，此时原图大小是 1080x1080，向左移动 540 像素应输入 &lt;code&gt;540 / 1080 * 1920 = 960&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;选择“不调整原图大小并裁切超出部分”，此时原图大小是 512x512，向左移动 540 像素应输入 &lt;code&gt;540 / 512 * 1920 = 2025&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

    </content>
    <category term="摄影" label="摄影" scheme="https://sh.alynx.one/categories/%E6%91%84%E5%BD%B1/" />
    <category term="摄影" label="摄影" scheme="https://sh.alynx.one/tags/%E6%91%84%E5%BD%B1/" />
  </entry>
</feed>
