33 - 不可变系统与系统扩展
不可变(Immutable)操作系统是 Linux 发行版的新趋势。通过将根文件系统设为只读、采用原子更新和声明式配置,不可变系统在安全性、可复现性和可回滚性方面带来了革命性改进。本章将全面探讨不可变系统的理念、各发行版的实现,以及如何在 Arch Linux 上实现类似特性。
33.1 什么是不可变系统
核心理念
不可变系统的基本原则:
graph LR A["只读根文件系统<br/>/ 为 read-only<br/>防止意外修改"] --- B["声明式配置<br/>配置即代码<br/>可复现环境"] --- C["原子更新<br/>更新要么全成功<br/>要么全失败回滚"] style A fill:#e1f5fe,stroke:#333 style B fill:#fff9c4,stroke:#333 style C fill:#c8e6c9,stroke:#333
只读根文件系统:
/usr为只读,包含所有系统文件- 运行时不可修改系统文件
- 软件安装通过镜像层或事务机制完成
声明式配置:
- 系统状态由配置文件完整描述
- 相同配置产生相同系统
- 版本控制友好
原子更新:
- 更新过程中系统始终处于一致状态
- 更新失败可自动回滚
- 不存在”半更新”状态
为什么要不可变
| 优势 | 说明 |
|---|---|
| 安全性 | 恶意软件无法修改系统文件 |
| 可复现性 | 完全相同的配置产生完全相同的系统 |
| 可回滚性 | 更新失败可一键回滚到之前的版本 |
| 稳定性 | 系统文件不会被意外修改 |
| 可审计性 | 可以验证系统文件的完整性 |
| 简化运维 | 系统状态可预测,减少”配置漂移” |
传统 vs 不可变
flowchart LR subgraph trad["传统系统"] direction LR T1["用户"] -->|"pacman -S pkg"| T2["直接修改 /usr"] T3["用户"] -->|"编辑 /etc/xxx"| T4["直接修改配置"] end subgraph immut["不可变系统"] direction LR I1["用户"] -->|"声明需要 pkg"| I2["生成新的系统镜像"] I3["用户"] -->|"声明配置"| I4["生成新的系统层"] end style trad fill:#ffcdd2,stroke:#333 style immut fill:#c8e6c9,stroke:#333
33.2 不可变系统全景
Fedora Silverblue / Kinoite
基于 rpm-ostree 的不可变桌面系统:
# 系统更新(原子操作)
rpm-ostree upgrade
# 安装/卸载软件包(需要重启生效)
rpm-ostree install vim
rpm-ostree uninstall firefox
# 回滚到上一版本
rpm-ostree rollback
# 查看状态
rpm-ostree status
# Pin 特定部署防止被清理
rpm-ostree pin 0
# 覆盖替换包
rpm-ostree override replace ./my-package.rpm
rpm-ostree override remove package-name特点:
- 基于 OSTree 进行系统镜像管理
/usr只读,/etc和/var可写- 应用程序通过 Flatpak 安装
- 容器中使用 toolbox/distrobox 进行开发
openSUSE MicroOS
面向容器和服务器的不可变系统:
# 事务更新
transactional-update pkg install vim
transactional-update dup
# 回滚
transactional-update rollback
# 清理旧快照
transactional-update cleanup
# 在事务中运行 shell
transactional-update shell特点:
- 基于 Btrfs 快照的事务更新
- 每次更新创建新快照,重启切换
- GRUB 引导菜单显示快照列表
- 可选桌面版本(MicroOS Desktop)
NixOS
完全声明式的操作系统:
# /etc/nixos/configuration.nix
{ config, pkgs, ... }:
{
boot.loader.systemd-boot.enable = true;
networking.hostName = "my-nixos";
environment.systemPackages = with pkgs; [
vim
git
firefox
];
services.openssh.enable = true;
users.users.myuser = {
isNormalUser = true;
extraGroups = [ "wheel" ];
};
system.stateVersion = "24.05";
}# 重建系统
sudo nixos-rebuild switch
# 回滚
sudo nixos-rebuild switch --rollback
# 列出代系
nix-env --list-generations
# 垃圾回收
nix-collect-garbage -d特点:
- Nix store(
/nix/store)中所有包按哈希存储 - 完全声明式:整个系统由一个配置文件描述
- 多代系管理,任意回滚
- 函数式包管理器
- 可复现构建(reproducible builds)
GNOME OS
GNOME 项目的参考操作系统:
- 基于 OSTree
- 专注于 GNOME 桌面体验
- 主要用于 GNOME 开发和测试
- 使用 Flatpak 安装应用
Vanilla OS
用户友好的不可变发行版:
# Vanilla OS 2.0 使用 Vib(Vanilla Image Builder)
# 基于 OCI 容器镜像构建系统
# apx:在容器中管理包
apx install vim # 在默认容器中安装
apx --dnf install vim # 在 Fedora 容器中安装
apx --apt install vim # 在 Debian 容器中安装
# 使用 Distrobox 进行开发
distrobox create -n dev -i archlinux
distrobox enter devblendOS
多发行版融合:
- 支持同时运行 Arch、Fedora、Ubuntu 等的包管理器
- 基于容器技术无缝集成
- 声明式配置 (
/system.yaml) - 原子更新
# /system.yaml
repo: 'https://pkg-repo.blendos.co'
impl: 'https://github.com/user/track.git'
packages:
- firefox
- vim
services:
- bluetooth
- NetworkManager
user:
shell: /bin/zshcarbonOS
专注于安全和简洁的不可变系统:
- 完全不可变的
/usr - Flatpak 唯一的应用安装方式
- 基于 OSTree
- GNOME 桌面
Universal Blue
基于 Fedora Silverblue 的定制镜像:
# 切换到 Universal Blue 镜像
rpm-ostree rebase \
ostree-unverified-registry:ghcr.io/ublue-os/silverblue-main:latest
# 提供开箱即用的驱动和编解码器
# Bazzite(游戏向)、Bluefin(开发向)等变体33.3 在 Arch 上实现不可变特性
Btrfs 只读快照 + 回滚
# 前提:使用 Btrfs 文件系统,推荐子卷布局
# @ → /
# @home → /home
# @log → /var/log
# @cache → /var/cache
# @pkgs → /var/cache/pacman/pkg
# 创建只读快照(更新前)
btrfs subvolume snapshot -r / /.snapshots/$(date +%Y%m%d-%H%M%S)
# 查看快照
btrfs subvolume list -s /
# 回滚到快照
# 1. 启动到 Live USB
# 2. 挂载 Btrfs 分区
mount /dev/sda2 /mnt
# 3. 移除当前根子卷
mv /mnt/@ /mnt/@.broken
# 4. 从快照创建可写副本
btrfs subvolume snapshot /mnt/.snapshots/20240101-120000 /mnt/@
# 5. 重启
reboot使用 snapper 自动管理快照:
pacman -S snapper snap-pac
# 创建配置
snapper -c root create-config /
# 配置 snapper
cat /etc/snapper/configs/root# /etc/snapper/configs/root
SUBVOLUME="/"
FSTYPE="btrfs"
ALLOW_GROUPS=""
ALLOW_USERS=""
TIMELINE_CREATE="yes"
TIMELINE_CLEANUP="yes"
TIMELINE_MIN_AGE="1800"
TIMELINE_LIMIT_HOURLY="5"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="4"
TIMELINE_LIMIT_MONTHLY="3"
TIMELINE_LIMIT_YEARLY="1"
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="1800"
NUMBER_LIMIT="50"# snap-pac:pacman 操作前后自动创建快照
# 安装 snap-pac 后自动生效
# 查看快照
snapper list
# 比较快照
snapper diff 1..2
# 回滚
snapper rollback
# 手动创建快照
snapper create -d "before risky change"
# 删除快照
snapper delete 5overlayfs 根文件系统
将根文件系统设为只读,使用 overlayfs 叠加可写层:
# 方案:只读根 + overlayfs 可写层
# 1. 在 initramfs 中配置 overlayfs
# /etc/mkinitcpio.conf 中添加 hook
# 自定义 initcpio hook:
# /etc/initcpio/hooks/overlay-root#!/usr/bin/ash
run_hook() {
# 挂载只读根
mount -o ro /dev/sda2 /new_root
# 创建 overlayfs 所需目录(使用 tmpfs 或独立分区)
mount -t tmpfs tmpfs /overlay
mkdir -p /overlay/upper /overlay/work
# 挂载 overlayfs
mount -t overlay overlay \
-o lowerdir=/new_root,upperdir=/overlay/upper,workdir=/overlay/work \
/new_root
}# 验证
mount | grep overlay
# overlay on / type overlay (rw,lowerdir=/mnt/readonly,upperdir=/mnt/rw/upper,workdir=/mnt/rw/work)systemd-sysext 系统扩展
在只读 /usr 基础上叠加扩展:
# 创建系统扩展
mkdir -p /tmp/myext/usr/bin
mkdir -p /tmp/myext/usr/lib/extension-release.d
# 放入需要扩展的文件
cp /path/to/custom-tool /tmp/myext/usr/bin/
# 创建扩展元数据
cat > /tmp/myext/usr/lib/extension-release.d/extension-release.myext <<EOF
ID=arch
ARCHITECTURE=x86-64
EOF
# 方式 1:使用目录
cp -r /tmp/myext /var/lib/extensions/myext
# 方式 2:创建 raw 镜像
mkfs.erofs /var/lib/extensions/myext.raw /tmp/myext
# 方式 3:创建 squashfs 镜像
mksquashfs /tmp/myext /var/lib/extensions/myext.raw
# 合并扩展到系统
systemd-sysext merge
# 检查状态
systemd-sysext status
# 取消合并
systemd-sysext unmerge
# 刷新(取消 + 重新合并)
systemd-sysext refresh
# 自动合并(开机时)
systemctl enable systemd-sysext扩展叠加原理:
graph TD A["合并后的 /usr<br/>(用户看到的)"] --- B["扩展层 extension.raw<br/>(overlayfs upper)"] B --- C["原始只读 /usr<br/>(overlayfs lower)"] style A fill:#c8e6c9,stroke:#333,stroke-width:2px style B fill:#fff9c4,stroke:#333 style C fill:#e1f5fe,stroke:#333
systemd-confext 配置扩展
类似 sysext,但用于 /etc:
# 创建配置扩展
mkdir -p /tmp/myconfext/etc/myapp
mkdir -p /tmp/myconfext/etc/extension-release.d
cat > /tmp/myconfext/etc/extension-release.d/extension-release.myconfext <<EOF
ID=arch
EOF
cp /path/to/myapp.conf /tmp/myconfext/etc/myapp/
# 放到配置扩展目录
cp -r /tmp/myconfext /var/lib/confexts/myconfext
# 合并
systemd-confext merge
systemd-confext status
systemd-confext unmergedm-verity 完整性验证
详见下一节。
A/B 分区方案
graph LR A["ESP<br/>FAT32<br/>512M"] --- B["Root A<br/>只读 ext4<br/>20G"] --- C["Root B<br/>只读 ext4<br/>20G"] --- D["/var<br/>ext4<br/>10G"] --- E["/home<br/>ext4<br/>剩余"] style A fill:#f3e5f5,stroke:#333 style B fill:#c8e6c9,stroke:#333 style C fill:#fff9c4,stroke:#333 style D fill:#e1f5fe,stroke:#333 style E fill:#bbdefb,stroke:#333
# A/B 方案工作流程:
# 1. 当前从 Root A 启动
# 2. 更新时将新系统写入 Root B
# 3. 更新引导加载程序指向 Root B
# 4. 重启到 Root B
# 5. 如果失败,回退到 Root A
# 使用 systemd-repart 管理 A/B 分区
cat > /etc/repart.d/10-root-a.conf <<EOF
[Partition]
Type=root
Label=root-a
SizeMinBytes=20G
SizeMaxBytes=20G
EOF
cat > /etc/repart.d/11-root-b.conf <<EOF
[Partition]
Type=root-secondary
Label=root-b
SizeMinBytes=20G
SizeMaxBytes=20G
EOF
# 使用 systemd-boot 的自动发现功能(Discoverable Partitions Spec)
# systemd-boot 可以自动识别 A/B 分区并选择正确的根分区33.4 dm-verity 深入
工作原理
dm-verity 使用 Merkle hash tree(默克尔哈希树)验证块设备数据的完整性:
graph TD ROOT["Root Hash<br/>(唯一标识整个设备内容)"] ROOT --> H0["Hash 0"] ROOT --> H1["Hash 1"] H0 --> H00["H 00"] H0 --> H01["H 01"] H1 --> H10["H 10"] H1 --> H11["H 11"] H00 --> B0["Block 0"] H01 --> B1["Block 1"] H10 --> B2["Block 2"] H11 --> B3["Block 3"] style ROOT fill:#ffcdd2,stroke:#333,stroke-width:2px style H0 fill:#fff9c4,stroke:#333 style H1 fill:#fff9c4,stroke:#333 style H00 fill:#e1f5fe,stroke:#333 style H01 fill:#e1f5fe,stroke:#333 style H10 fill:#e1f5fe,stroke:#333 style H11 fill:#e1f5fe,stroke:#333 style B0 fill:#c8e6c9,stroke:#333 style B1 fill:#c8e6c9,stroke:#333 style B2 fill:#c8e6c9,stroke:#333 style B3 fill:#c8e6c9,stroke:#333
原理说明:
- 将设备分成固定大小的数据块(默认 4096 字节)
- 计算每个数据块的哈希值
- 将哈希值逐层向上合并,形成树状结构
- 最终得到一个根哈希(root hash)
- 读取数据时,计算数据块哈希并沿树验证到根哈希
- 任何数据篡改都会导致哈希不匹配
创建 verity 设备
# 安装工具
pacman -S device-mapper
# 准备数据设备(只读根文件系统)
# 假设 /dev/sda3 是只读根分区
# 1. 创建 verity 哈希
veritysetup format /dev/sda3 /dev/sda4
# 输出:
# VERITY header information for /dev/sda4
# UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Hash type: 1
# Data blocks: 262144
# Data block size: 4096
# Hash block size: 4096
# Hash algorithm: sha256
# Salt: xxxxxxxxxx...
# Root hash: abcdef1234567890... ← 记下这个值!
# 2. 打开 verity 设备
veritysetup open /dev/sda3 verified-root /dev/sda4 abcdef1234567890...
# 3. 挂载验证后的设备
mount -o ro /dev/mapper/verified-root /mnt
# 4. 验证完整性(读取时自动验证)
# 如果任何块被篡改,读取会返回 I/O 错误# 使用文件而非分区
# 创建数据镜像
dd if=/dev/zero of=rootfs.img bs=1M count=1024
mkfs.ext4 rootfs.img
# ... 填充文件系统内容 ...
# 创建哈希镜像
veritysetup format rootfs.img hash.img
# 记录 root hash
# 使用 loop 设备
losetup /dev/loop0 rootfs.img
losetup /dev/loop1 hash.img
veritysetup open /dev/loop0 verified-root /dev/loop1 <root-hash>
mount -o ro /dev/mapper/verified-root /mnt与 initramfs 集成
# 在 initramfs 中设置 dm-verity
# 需要在 mkinitcpio 中添加 dm-verity 支持
# /etc/mkinitcpio.conf
MODULES=(dm-verity)
HOOKS=(base udev autodetect modconf block filesystems)
# 内核命令行参数
# root=/dev/mapper/verified-root
# systemd.verity=yes
# systemd.verity_root_data=/dev/sda3
# systemd.verity_root_hash=/dev/sda4
# systemd.verity_root_hash=<root-hash-value>在 systemd 中使用 dm-verity
systemd 内置了 dm-verity 支持:
# 内核命令行选项
systemd.verity=yes
systemd.verity_root_data=PARTUUID=xxxx
systemd.verity_root_hash=PARTUUID=yyyy
systemd.verity_root_hash=<root-hash>
# 或使用 Discoverable Partitions Specification
# GPT 分区类型 ID 自动识别数据分区和哈希分区# /etc/veritytab(类似 crypttab)
# name data_device hash_device root_hash options
verified-root /dev/sda3 /dev/sda4 abcdef1234567890... root-hash-signature=/path/to/sig# 使用 systemd-veritysetup
systemd-veritysetup attach verified-root /dev/sda3 /dev/sda4 abcdef1234567890...
systemd-veritysetup detach verified-root
# systemd-veritysetup-generator 可以从内核命令行自动生成 Unit保护根文件系统完整性
完整的 dm-verity 保护方案:
flowchart TD A["UEFI Secure Boot"] --> B["签名的 UKI<br/>(systemd-stub + 内核 + initramfs)"] B --> C["initramfs 中包含 root hash"] C --> D["dm-verity 验证根文件系统"] D --> E["只读根 + sysext 扩展"] E --> F["/etc overlay + /var 可写"] style A fill:#ffcdd2,stroke:#333 style B fill:#fff9c4,stroke:#333 style C fill:#ffecb3,stroke:#333 style D fill:#e1f5fe,stroke:#333 style E fill:#bbdefb,stroke:#333 style F fill:#c8e6c9,stroke:#333
# 将 root hash 嵌入 UKI
ukify build \
--linux=/boot/vmlinuz-linux \
--initrd=/boot/initramfs-linux.img \
--cmdline="root=/dev/mapper/verified-root ro \
systemd.verity=yes \
systemd.verity_root_data=PARTUUID=xxxx \
systemd.verity_root_hash=PARTUUID=yyyy \
systemd.verity_root_hash=abcdef1234..." \
--output=/boot/EFI/Linux/arch-verity.efi \
--sign-kernel \
--secureboot-private-key=db.key \
--secureboot-certificate=db.crt33.5 用户状态文件(三层分离模型)
文件系统分层
不可变系统将文件系统分为三层:
graph TD subgraph readonly["只读层 /usr"] U1["操作系统核心文件"] U2["所有软件包的文件"] U3["由 sysext 扩展"] U4["dm-verity 保护完整性"] end subgraph config["配置层 /etc"] E1["系统配置"] E2["管理员自定义"] E3["overlay (默认值 + 自定义)"] E4["systemd-confext 扩展"] end subgraph data["数据层 /var + /home"] V1["/var/lib — 服务状态数据"] V2["/var/log — 日志"] V3["/var/cache — 缓存"] V4["/home — 用户数据"] end style readonly fill:#e1f5fe,stroke:#333 style config fill:#fff9c4,stroke:#333 style data fill:#c8e6c9,stroke:#333
/etc 作为配置状态
# 传统方式:/etc 中的文件是软件包安装 + 管理员修改的混合
# 问题:难以区分哪些是默认值,哪些是管理员自定义
# 不可变方式:
# 1. 默认配置存放在 /usr/lib/ 或 /usr/share/factory/etc/
# 2. /etc 只包含管理员的覆盖/自定义
# 3. 系统优先读取 /etc,找不到则回退到 /usr/lib/
# systemd 的配置搜索顺序
# /etc/systemd/system.conf ← 管理员自定义
# /run/systemd/system.conf ← 运行时覆盖
# /usr/lib/systemd/system.conf ← 包默认值/var 作为可变数据
# /var 子目录约定
/var/lib/ # 持久化状态数据(数据库、包管理器状态等)
/var/log/ # 日志文件
/var/cache/ # 可重建的缓存
/var/spool/ # 待处理的队列数据
/var/tmp/ # 重启后保留的临时文件
/var/run/ → /run # 运行时数据(符号链接)systemd-tmpfiles 管理 /etc 默认值
# /usr/lib/tmpfiles.d/etc-defaults.conf
# 当 /etc 中的文件不存在时,从 /usr/share/factory/etc/ 复制
# 复制文件(如果目标不存在)
C /etc/hostname - - - - /usr/share/factory/etc/hostname
C /etc/locale.conf - - - - /usr/share/factory/etc/locale.conf
C /etc/vconsole.conf - - - - /usr/share/factory/etc/vconsole.conf
# 创建符号链接(如果目标不存在)
L /etc/os-release - - - - ../usr/lib/os-release
L /etc/localtime - - - - ../usr/share/zoneinfo/UTC
# 创建目录
d /etc/sysctl.d 0755 root root -
d /etc/tmpfiles.d 0755 root root -/usr 只读 + /etc overlay 模式
# 实现方案:/usr 只读挂载,/etc 使用 overlayfs
# 1. /usr 直接只读挂载(dm-verity 保护)
# 2. /etc 的 lower 层为 /usr/share/factory/etc/(默认配置)
# 3. /etc 的 upper 层为 /var/lib/etc-overlay/(管理员修改)
mount -t overlay overlay \
-o lowerdir=/usr/share/factory/etc,upperdir=/var/lib/etc-overlay/upper,workdir=/var/lib/etc-overlay/work \
/etc
# 这样:
# - 未修改的文件来自 /usr/share/factory/etc/(只读)
# - 管理员修改的文件存储在 /var/lib/etc-overlay/upper/
# - 可以清楚看到管理员做了哪些自定义# 查看管理员自定义了哪些文件
ls /var/lib/etc-overlay/upper/
# 重置某个配置到默认值
rm /var/lib/etc-overlay/upper/hostname
# 下次读取 /etc/hostname 时会回退到 /usr/share/factory/etc/hostname33.6 OSTree 原理
OSTree 是一个基于内容寻址的文件系统版本管理工具:
graph TD subgraph store["对象存储 (类似 Git)"] S1["文件内容按 SHA256 哈希存储"] S2["目录树作为树对象存储"] S3["Commit 记录完整系统状态"] S4["Branch 指向最新提交"] end subgraph deploy["部署"] D1["从对象存储硬链接到部署目录"] D2["多个部署可共存 (/ostree/deploy/)"] D3["引导加载程序选择部署"] D4["相同文件不重复存储 (去重)"] end store --> deploy style store fill:#e1f5fe,stroke:#333 style deploy fill:#c8e6c9,stroke:#333
# OSTree 基本操作
ostree --repo=/ostree/repo summary -u
ostree --repo=/ostree/repo log <ref>
ostree --repo=/ostree/repo ls <commit>
ostree --repo=/ostree/repo diff <commit1> <commit2>
# 目录结构
/ostree/
├── repo/ # 对象存储
│ ├── objects/ # SHA256 哈希的文件/目录对象
│ ├── refs/ # 分支引用
│ └── config # 仓库配置
└── deploy/
└── arch/ # 部署
├── deploy/
│ ├── <checksum1>/ # 部署 1
│ └── <checksum2>/ # 部署 2
└── var/ # 共享的 /varOSTree vs Git:
| 特性 | Git | OSTree |
|---|---|---|
| 设计目标 | 源代码 | 操作系统文件 |
| 对象类型 | blob, tree, commit, tag | file, dirtree, dirmeta, commit |
| 文件权限 | 有限(仅可执行位) | 完整(uid, gid, mode, xattr) |
| 大文件 | 性能差 | 设计考虑 |
| 硬链接 | 不支持 | 支持(用于部署) |
| 增量下载 | 有限 | 支持静态 delta |
33.7 容器化 vs 不可变系统
graph TD subgraph container["容器化"] C1["隔离应用"] C2["每个应用独立环境"] C3["运行时隔离"] C4["Docker/Podman"] C5["OCI 镜像 / 微服务"] end subgraph immutable["不可变系统"] I1["保护整个系统"] I2["系统本身不可变"] I3["存储层不可变"] I4["OSTree/Btrfs snapshots"] I5["系统镜像 / 桌面&服务器"] end container --> BEST["最佳实践<br/>不可变系统 + 容器化应用<br/>例: Fedora CoreOS + Podman"] immutable --> BEST style container fill:#e1f5fe,stroke:#333 style immutable fill:#c8e6c9,stroke:#333 style BEST fill:#fff9c4,stroke:#333,stroke-width:2px
两者是互补关系:
- 不可变系统保护宿主操作系统
- 容器化隔离应用程序
- 组合使用实现最大安全性和灵活性
33.8 Flatpak/Snap 在不可变系统中的角色
在只读系统上安装 GUI 应用:
# Flatpak(不可变系统的首选)
pacman -S flatpak
# 添加 Flathub
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# 安装应用
flatpak install flathub org.mozilla.firefox
flatpak install flathub com.visualstudio.code
# 运行
flatpak run org.mozilla.firefox
# 更新
flatpak update
# 查看已安装
flatpak list
# 权限管理
flatpak override --user --filesystem=home org.example.App
flatpak permission-listFlatpak 在不可变系统中的优势:
| 特性 | 说明 |
|---|---|
| 沙箱隔离 | 使用 Bubblewrap 沙箱 |
| 独立运行时 | 自带依赖,不依赖系统库 |
| Portal | 通过 Portal 安全访问系统资源 |
| 去重 | 共享运行时减少空间 |
| 不触碰 /usr | 安装在 /var/lib/flatpak 或 ~/.local/share/flatpak |
| 独立更新 | 应用更新不需要系统更新 |
Distrobox / Toolbox 用于开发:
# 在不可变系统上进行开发
pacman -S distrobox
# 创建开发环境
distrobox create -n arch-dev -i archlinux:latest
distrobox create -n ubuntu-dev -i ubuntu:24.04
# 进入开发环境
distrobox enter arch-dev
# 在容器中安装任何东西
sudo pacman -S gcc cmake python nodejs
# 导出应用到宿主
distrobox-export --app code
distrobox-export --bin /usr/bin/gcc --export-path ~/.local/bin33.9 不可变系统的局限性与折中
局限性
| 局限 | 说明 |
|---|---|
| 学习曲线 | 需要改变传统的系统管理习惯 |
| 灵活性降低 | 无法随意安装系统级软件 |
| 调试困难 | 只读系统上排错需要额外步骤 |
| 兼容性 | 某些需要修改系统文件的软件不兼容 |
| 磁盘空间 | 快照/多版本占用更多空间 |
| 更新延迟 | 更新通常需要重启才能生效 |
| 驱动问题 | 内核模块和驱动管理更复杂 |
折中方案
# 在 Arch 上渐进式采用不可变特性:
# 级别 1:Btrfs 快照(最简单)
# - 使用 snapper + snap-pac
# - 更新前自动快照
# - 保持传统使用方式
# 级别 2:只读 /usr + overlay /etc
# - /usr 挂载为只读
# - /etc 使用 overlayfs
# - 需要更新时临时 remount
# 级别 3:dm-verity + sysext
# - 完全验证的只读根
# - 软件通过 sysext 扩展
# - 最高安全性,灵活性最低
# 级别 4:A/B 分区 + 原子更新
# - 类似 ChromeOS/Android 的方案
# - 更新写入备用分区
# - 失败自动回退在 Arch 上的实践建议
# 推荐的渐进式方案:
# 1. 使用 Btrfs 子卷布局
mkfs.btrfs /dev/sda2
mount /dev/sda2 /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@var
btrfs subvolume create /mnt/@snapshots
# 2. 安装 snapper + snap-pac
pacman -S snapper snap-pac grub-btrfs
# 3. 配置自动快照
snapper -c root create-config /
# 4. 使用 systemd-sysext 管理额外软件
# 5. 使用 Flatpak 安装桌面应用
# 6. 使用 Distrobox 进行开发
# 这样在保持 Arch 灵活性的同时获得不可变系统的部分优势33.10 构建自定义不可变 Arch 系统
完整的构建流程:
#!/usr/bin/env bash
# build-immutable-arch.sh
set -euo pipefail
WORK_DIR="/tmp/immutable-build"
OUTPUT_DIR="/srv/images"
ROOT_IMG="$OUTPUT_DIR/rootfs.img"
HASH_IMG="$OUTPUT_DIR/hash.img"
# 1. 创建根文件系统镜像
truncate -s 4G "$ROOT_IMG"
mkfs.ext4 "$ROOT_IMG"
mkdir -p "$WORK_DIR/rootfs"
mount "$ROOT_IMG" "$WORK_DIR/rootfs"
# 2. 安装基础系统
pacstrap "$WORK_DIR/rootfs" base linux linux-firmware systemd
# 3. 基本配置
arch-chroot "$WORK_DIR/rootfs" bash -c "
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
echo 'LANG=en_US.UTF-8' > /etc/locale.conf
echo 'immutable-arch' > /etc/hostname
systemctl enable systemd-networkd systemd-resolved
"
# 4. 将 /etc 默认值复制到 /usr/share/factory/etc
cp -a "$WORK_DIR/rootfs/etc" "$WORK_DIR/rootfs/usr/share/factory/etc"
# 5. 卸载并创建 dm-verity 哈希
umount "$WORK_DIR/rootfs"
veritysetup format "$ROOT_IMG" "$HASH_IMG" | tee "$OUTPUT_DIR/verity-info.txt"
ROOT_HASH=$(grep "Root hash:" "$OUTPUT_DIR/verity-info.txt" | awk '{print $3}')
echo "Root hash: $ROOT_HASH"
# 6. 创建 UKI
ukify build \
--linux="$WORK_DIR/rootfs/boot/vmlinuz-linux" \
--initrd="$WORK_DIR/rootfs/boot/initramfs-linux.img" \
--cmdline="root=/dev/mapper/verified-root ro systemd.verity=yes \
systemd.verity_root_data=/dev/sda3 \
systemd.verity_root_hash=$ROOT_HASH" \
--output="$OUTPUT_DIR/arch-immutable.efi"
echo "Build complete!"
echo "Root hash: $ROOT_HASH"33.11 未来展望
不可变系统的发展趋势:
现在 未来
─────────────────────────────────────────────
传统包管理器 → 镜像化系统 + 容器化应用
就地更新 → 原子更新 + A/B 切换
手动配置 → 声明式配置 + GitOps
信任执行 → dm-verity + Secure Boot
单一系统 → 系统 + 扩展(sysext)
关键技术:
- composefs: EROFS + overlayfs 组合,用于高效的只读文件系统叠加
- systemd-sysupdate: 原子系统更新工具
- mkosi: 构建系统镜像的声明式工具
- UKI: 统一内核镜像简化安全启动
- Confidential Computing: 保护运行时数据
不可变系统代表了 Linux 系统管理的未来方向。即使你选择继续使用传统的 Arch Linux,理解这些概念也将帮助你更好地理解现代基础设施和容器技术。