前言

上一次装 arch 时我做的一个决定,让我爽到了现在。

我用 linux 已经多久了?我已经记不清了。我在这台 arch 机器上折磨了自己无数次,但这次我遇到了真正的问题。我的 Apollo 项目大量运行比赛进行测试的时候,会产生大量日志数据,而因为此前的双系统安装方式,给 linux 留下的空间很快被吃完了,虽然看上去不过是 140g 的磁盘被日志吃了 70g,但我眼馋本来属于 windows 和老的 manjaro(没错,我还留着这玩意)的另外 300 多 g 磁盘空间呐。

于是,一场波澜壮阔的探索之旅开始了。

初探 BTRFS

无论是现在还是将来,我都会推荐你使用 btrfs 作为你的主力文件系统

btrfs 扩容

我依然记得我选 btrfs 的若干理由:

  1. COW
  2. 快照
  3. 多设备
  4. 软 RAID

说实话,除了一开始那次挂载 subvol 有点印象,后面的功能我都不是很了解。于是这次我决定来体验一下。

我首先清空了 windows 和 manjaro(一点不带手抖的,早该删了),然后将这两个分区重建为一个新的分区(fdisk 无脑冲)。接下来用mkfs.btrfs非常轻松的建立了 btrfs。然后用btrfs device add把新的卷加到/上。这时候用btrfs filesystem show就可以看到整个 btrfs可用的磁盘空间一下变大了好多。当然,这还没完,我还要btrfs filesystem resize来确保btrfs 子卷能分配到足够的空间。最后,要确保数据能被均匀地分配,还要跑一跑btrfs balance

可以说,这样我的问题基本解决了,空间确实够大,但还有几个无关紧要的小问题。

  1. 卷标是 p5,因为此前双系统的缘故,我的 arch 被安装在 nvme01n1p5。而新的卷为 nvme01n1p2,中间的 3 和 4 都是不存在的。

  2. OS 被安装在磁盘中间靠后的位置。如下图所示:

    理想的样子:|=esp=|==OS====================|

    实际的样子:|=esp=|===data===|==OS=========|

虽然但是,这样确实可以用了,于是就这么用吧。

btrfs 快照

接下来我尝试的功能是快照,因为有 COW 的存在,创建快照并没有太大的性能负担(不会全量备份)。一个简单的btrfs subvolume snapshot就足够创建快照了。但是我玩的更花里胡哨一些,我找到了这个:grub-btrfs

这个工具可以在 grub 启动界面把此前对整个系统做的 snapshot 作为启动项,这意味着我可以随时将整个系统回滚至之前的某个状态。这个工具还提供了某种联动,允许定时创建快照并自动更新 grub 启动菜单。我这里想用 systemd.path 提供的功能来完成这种联动,当然除了 systemd,还有 timeshift 和 snapper 等一系列方案。

但是用 systemd 方案有一些 prerequest。systemd.path 会监控某个路径中的文件是否有变化,若有,则执行相关的操作。而为了偷懒,我是打算用默认的配置文件的。默认的配置文件又有一个特殊需求,它要求 snapshot 被挂载到/.snapshots 这个挂载点中。也就是我得创建一个 subvolume,并挂载到这个位置才行。

创建 subvolume 并不是什么难事,但挂载却有大麻烦,先是提示我 mount point didn’t exist,后是辛辛苦苦挂上后会显示一个丑陋的@符号。之前建@var 和@home 子卷的时候就没有这些问题。btrfs 的挂载应该是这样的,首先挂载@到/,这时候查看/的话,会先是有@var 和@home 这两个目录,当你把@var 和@home 挂进去之后,这两个目录就不存在了,而是变成/var 和/home。但是这次挂载@snapshots 不知道为啥就会显示一个丑陋的@snapshots。除此之外,我只能通过指定 subvolid 的方式来在挂载这个子卷,subvol 无法指定。

虽然各种各样奇奇怪怪的问题太多了,不过这样其实也可以用了,至少从@snapshot 和/.snapshots 都可以访问到这个 subvolume。因为没有用 timeshift 这样的软件,我把自动备份的任务交给systemd-cron。这个工具运行我用 cron 的语法创建 systemd-timer。当然,我也写了个简单好用的 alias 来手动做 snapshots:

sudo btrfs subvolume snapshot / /.snapshots/$(date +%F-%T)

systemd.path 监听到这个目录有变化之后就会触发 grub-mkconfig 重新生产 grub 的入口。这时候重启能看到此前的快照,并且可以进入并正常操作。

初探 ESP

一个稍微大了一丢丢的 initramfs 引发的惨案

事情的起因是一些小问题。我想跑一个 ebpf 程序,但是这玩意是拿 python 写的,跑的时候报了个错,结果发现是某个内核参数没开。这下事情就“严重”起来了,因为除了编译内核好像没有别的解决办法。编译内核也许要单开一篇文章,这里先略过了。总之内核编译完了,把内核(bzImage)丢到 efi 分区里去也没啥问题,但是创建 initramfs 这步开始有麻烦了,它提示我的 efi 分区空间不足。也是,efi 分区就 100MB。我当时一开始想的是:“那就给 efi 分区扩容扩容吧”,随即因为扩容太难并且被大多数人说:“没有必要”而作罢。这时候,我有了一个(让我后悔的)惊天大发现:btrfs 可以直接占领整个磁盘!也就是说 btrfs 也可以做引导盘吧,不需要引导分区啦(大误)。在经历一部分尝试后,终于在 arch wiki 上看到了那句:占领整个硬盘之后记得把引导分区装到其他盘上哦。还好没把 esp 分区删了,不然恢复难度就更上一层楼了。

在经历了一些混乱的思考后,我靠着官方文档和一些论坛理清了思路:efi 分区中只需要有 grub 来做引导就 ok 了。但是我的 esp 挂载到了/boot,而 linux 内核以及 initramfs 都会默认生成在这个位置。因此我做了一个重大的决定:把 esp 重新挂载到/boot/efi 这个位置上。接下来重新生成 initramfs,重新写入 grub.cfg。这样内核的存储位置就不必受到限制了,同时在这个过程中,我对 initramfs 是用来干啥的,fallback 内核又是用来干啥的有了进一步了解。

再探 BTRFS

这是一项浩瀚的工程。我决定让整块硬盘只留下两个分区:一个 esp,一个 btrfs。为了达成这个目标,一开始我做了个有点创意的尝试,我首先建立了一个新的 btrfs 分区,然后加入到原有的 btrfs 分区,让数据分布在两个分区中。然后我删除了原来的分区,根据 btrfs 的说明,删除某个设备后,数据会被移动到仍然存在的设备,也就是另一个分区中。然后我发现原来的分区确实被 umount 了,lsblk 显示所有数据都存在于新的分区中。但打开 fdisk 后发现,这些数据在磁盘中的位置并没有发生变化,仅仅是卷标变了而已。看样子,还是逃不了一次巨大无比的 mv 或 cp 来把数据手动移动到磁盘靠前的位置了。

所以我重新做了计划:

  1. 建立一个跟随在 esp 后面的分区
  2. 把原来的 btrfs 从磁盘的中间部分移动到上述分区
  3. 回收原来的 btrfs,并分配给上述分区

重新规划分区

在分出来一块足够大的分区之后,我们先把它变成 btrfs,然后规划一下子卷。子卷有相当多的好处,比如可以只针对 VFS 中的某一部分做快照,或是做磁盘配额。我这里还是沿用以前的规划:

  1. @ -> /
  2. @var -> /var
  3. @home -> /home
  4. @snapshots -> /.snapshots

还记得初探 btrfs 中,我说过不管我怎样操作,系统都会多一个丑陋的/@snaphsots 吗,这其实跟挂载的方式有关。当我们创建 btrfs 子卷的时候,我们的挂载方式是直接mount /dev/nvme01n1p2 /mnt,而我们使用的时候,实质上是将@子卷挂载到了/位置。也就是说,想要解决这个问题,就不应该在挂载好@子卷的操作系统中进行子卷的创建和挂载,而是将整个分区挂载至某个位置后再进行子卷的创建和挂载。

迁移的小技巧

迁移也是一件有点小技巧的事,我们要解决一些小问题。

首先是哪些目录需要迁移?众所周知,linux 中的一些目录其实是虚拟出来的 API。我记得我的第一次尝试就卡在了 cp /proc 下的某个目录。这里尝试列出一些不用迁移的 api 目录。

  • /boot/efi (这是我的 efi 分区,有的人可能是/boot,这些目录到时候会被挂载,所以自然不用复制一遍)
  • /dev (linux 会负责挂载,实质上好像是 systemd 在干这个)
  • /mnt (这个目录理论上应当是空的,把它加进来是因为我们操作的时候往往会把源目录挂载到这里)
  • /proc(应该没人会想着对这个目录下手吧)
  • /run (同上)
  • /sys (同上)
  • /tmp (这个会自动挂载 tmpfs)

在所有数据都移动后,我发现我没法 chroot 到这个目录下,因为它没有挂载/proc 之类的目录。这个地方手动处理感觉比较麻烦,所以我选择了简单但高效的pacstrap /mnt base。在能够 chroot(我更推荐 arch-chroot)进入这个迁移完的目录后,我们得重新生成 initramfs 来让 linux 引导进入我们这个新的文件系统。在此之前还要改一改/etc/fstab,保证挂载的是新的子卷,而不是原来的,注意 subvol 和 subvolid 都需要修改。其实也可以简单一点pacstrap /mnt base linux就完事了。

重启之后能进入到新的文件系统,并且查看后能看到所有文件都在,那就 ok 了。

回收原来的分区

回收本身并没有什么难度,fdisk 之后敲个 d 就好了,有点点挑战性的是给新建立的分区扩容,注意,是分区扩容,而不是像之前一样建立一个新的 btrfs 并加入到这个分区中来。

查了下资料之后,步骤大致如下:

  1. fdisk 进去
  2. 首先 d 删除那个新建好的分区,注意,这里只要不敲 w,删除这个操作并不会真实执行,所以可以放心地敲。
  3. 再 n 新建分区,选择范围时保证开头与原来分区的开头一致,结尾可以扩大到比原来的结尾更远的位置,我这里是直接选到了磁盘的末尾。
  4. 敲个 w 保存,注意它提示你是否要删除原来分区上已有的 btrfs 标记时,要选择 n。不然你就得到了一个全新的磁盘,所有的数据都会丢失。
  5. 这个过程中的任何一步都可以执行 p 来查看当前的分区状态。

我觉得最精彩的操作就是最后的不选择删除 btrfs 标记,这样保证了分区的扩容,又不影响已经存在的文件系统。

小结

在经历了长达两天的折腾之后,我得到了什么?

  1. 一个干净的 esp,里面只有 grub 的相关配置。
  2. 一个干净的 btrfs 大分区,并且这个分区紧跟在 esp 后面,没有浪费一点空间。
  3. 一个快捷的 btrfs 快照创建与恢复机制。
  4. 一个多内核的 linux,我的 kernel 和 initramfs 都存储在 btrfs 中,不会被 esp 分区大小限制。
  5. 成就感

当然,除了上面这些,这次也算是搞清楚了 how arch linux is boot。下一步的话,我希望下次能用一套脚本来构建这一整套系统,而不是靠我手动 Operation。