x86_64 Linux A/B 系统设计:从 GRUB 选槽到 OverlayFS 根文件系统

1. 设计目标与启动全貌 设备在线升级时,写入镜像只是其中一步。更新可能在断电时中止,新内核可能无法挂载根目录,业务也可能在系统启动后才暴露问题。A/B 设计的目标,是在候选版本尚未被确认时保留一套可启动的旧系统,并为失败后的重启指定明确的去向。 本文面向熟悉 Linux 分区、挂载与 systemd 的读者,讨论 Legacy BIOS + GPT 的 x86_64 设备:GRUB 2 选择启动槽位,自定义 BusyBox initramfs 组装根文件系统,systemd 启动主系统并组织健康检查。RootFS 使用只读 SquashFS,运行时写入由持久化的 OverlayFS 可写层(upper)承接。 这是一篇架构设计与关键机制说明:给出分区约定、状态转换、挂载顺序和验收条件。GRUB 选槽使用伪代码,/init 展示核心片段;完整部署还需实现错误分支、升级程序、超时监督和恢复环境。文中没有提供目标硬件实测记录,因此不据此宣称已经达到生产级可靠性。 设计遵循三个约定: 成套更新:一个槽位包含匹配的 kernel、initramfs 和 RootFS,始终一起选择、一起升级。 保留回退对象:当前运行槽与已确认的稳定槽一致时,才允许更新另一槽;候选版本经过健康检查后才成为默认版本。 分离系统与数据:系统基线保存在 SquashFS;系统写入进入槽位独立的可写层;需要跨版本保留的业务数据单独存放。 选择 SquashFS,是为了把系统基线做成可整体校验、替换的只读镜像;选择 OverlayFS,是为了兼容需要可写根目录的软件。代价是 upper 会保存偏离基线的内容,必须与镜像版本一起管理。普通重启不会恢复出厂状态;本文也不覆盖纯 UEFI 启动、磁盘冗余或完整的可信启动链。 设备上电 → BIOS 自检、初始化启动所需硬件,选择磁盘 → 执行磁盘 LBA 0(保护性 MBR)中的 GRUB 引导代码 → 加载 p1 内嵌的 GRUB core.img,访问 p2 的模块与 grub.cfg → grub.cfg 读取启动状态,选择 A/B,将该槽 kernel 和 initramfs 装入内存 → GRUB 按 Linux 启动协议传参并跳转到内核入口 → kernel 解压、初始化内存和驱动,将 initramfs 解包到初始根文件系统 → 执行 initramfs 中的 /init(PID 1,本文为 BusyBox shell 脚本) → /init 挂载 SquashFS 和 DATA,再挂载 OverlayFS 到 /newroot → exec switch_root /newroot /sbin/init → systemd 接管 PID 1,按依赖启动服务,开放登录或业务接口 → 独立健康检查通过后,才确认试启动版本 initramfs 是文件系统归档,不是独立执行的程序;OverlayFS 是内核文件系统,由早期用户态请求挂载。 /init 与 systemd 属于同一个内核下的前后两个用户态阶段,切根不会重新启动内核。若改用 systemd 构建 initramfs,它会更早成为 PID 1,并通过 systemctl switch-root 交接,不能原样套用本文的 BusyBox 脚本。两种方式见 systemd initrd 接口。 ...

September 14, 2026 · 9 min · zhumouren

将Ubuntu20.04变为一个路由器

前言 本文记录的是作者手头上的设备搭建情况,不同的芯片架构、系统版本搭建情况可能有所不同。以下是设备情况介绍 芯片架构: aarch64 Linux内核版本: 4.19.232 发行版: Ubuntu20.04 设备路由设置 修改内核参数 vi /etc/sysctl.conf net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 net.ipv6.conf.all.accept_ra = 2 修改物理网口名称 在ubuntu中用ifconfig查看网口信息有可能有enP1p17s0这种网口,想要修改为规律的ethx可以按下面的方式做 udevadm info -a -p /sys/class/net/eth0 # 可以通过这个命令查看网口属性以编写下面的配置 vi /etc/udev/rules.d/70-persistent-net.rules SUBSYSTEM=="net", ACTION=="add", SUBSYSTEMS=="platform", KERNELS=="fe2a0000.ethernet", NAME="eth0" SUBSYSTEM=="net", ACTION=="add", SUBSYSTEMS=="platform", KERNELS=="fe010000.ethernet", NAME="eth1" SUBSYSTEM=="net", ACTION=="add", SUBSYSTEMS=="pci", KERNELS=="0001:11:00.0", NAME="eth2" SUBSYSTEM=="net", ACTION=="add", SUBSYSTEMS=="pci", KERNELS=="0002:21:00.0", NAME="eth3" SUBSYSTEM=="net", ACTION=="add", SUBSYSTEMS=="pci", KERNELS=="0000:01:00.0", NAME="eth4" 这里可以关注SUBSYSTEMS有platform、pci两种值但实际还有更多,看具体以太网口的属性。KERNELS代表设备硬件位置,可以通过udevadm命令来查看。 设置lan口、wan口 以下设置为eth0为wan0口,其他为lan口。 vi /etc/network/interfaces.d/wan auto wan0 iface wan0 inet dhcp bridge_ports eth0 metric 30 vi /etc/network/interfaces.d/lan auto lan iface lan inet static bridge_ports eth1 eth2 eth3 eth4 address 192.168.1.1 netmask 255.255.255.0 监听wan口拔插 在wan口插入网线时需要自动启动dhcp clinet获取ip地址并设置route,在wan口拔出网线时需要清空wan口的ip并更新route。默认情况下ubuntu并不会做这些事情 ...

June 6, 2025 · 1 min · zhumouren

UNIX环境高级编程-进程操作

1. 进程环境 #include <stdlib.h> void exit(int status); exit(0) 与 return 0 是等价的。 2. 进程标识和创建子进程 每个进程都有一个非负整型表示的唯一进程ID,但进程ID是可以被复用的。当一个进程终止后进程ID会进入候选池,并且一般新的进程不会使用最近终止进程的进程ID。 #include <unistd.h> pid_t getpid(void); // 返回值: 调用进程的进程ID pid_t getppid(void); // 返回值: 调用进程的父进程ID fork() 函数可以创建一个新进程,子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。并且在修改时使用了写时复制(Copy-On-Write,COW)技术。 #include <unistd.h> pid_t fork(void); // 返回值: 子进程返回0, 父进程返回子进程ID,若出错返回-1 以下是一个示例代码,用于创建一个子进程并展示父进程和子进程的ID与父进程ID,以及如何通过信号避免僵尸进程。 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> void sigchld_handler(int sig) { int status; pid_t id = waitpid(-1, NULL, WNOHANG); if (WIFEXITED(status)) { printf("remove proc id: %d \n", id); printf("Child send: %d \n", WEXITSTATUS(status)); } } int main(int argc, char *argv[]) { struct sigaction sa; sa.sa_handler = sigchld_handler; sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 初始化信号屏蔽集,确保不阻塞任何额外信号 sigemptyset(&sa.sa_mask); // 注册 SIGCHLD 信号处理 sigaction(SIGCHLD, &sa, NULL); pid_t pid; int val = 24; if ((pid = fork()) < 0) { fprintf(stderr, "fork err: %d\n", pid); exit(1); } else if (pid == 0) { val++; printf("child: pid = %d, ppid = %d\n", getpid(), getppid()); printf("child: val = %d\n", val); _exit(0); } else { sleep(5); printf("root: pid = %d, ppid = %d, child_pid = %d\n", getpid(), getppid(), pid); printf("root: val = %d\n", val); sleep(5); } exit(0); } 以下是执行程序后的结果,一个有意思点是程序的父进程ID是bash,子进程修改的数据并不会影响到父进程的数据。 ...

November 27, 2024 · 3 min · zhumouren

OpenWrt开发入门实战(一)Docker环境搭建

1. 环境准备 一台带有Windows10(64位)及以上专业版的电脑(其他操作系统也行,只要能用Docker就好了 CPU为X86_64 Docker(用作OpenWrt的编译环境和测试环境) 2. 使用Docker搭建OpenWrt编译环境 本文使用docker-compse构建,构建脚本目录环境为 docker-linux-env/ | docker-compose.yml | ----ubuntu-compile-openwrt/ | | Dockerfile | | sources.list | - 以下为各文件的具体内容 docker-compose.yml version: '3' services: ubuntu-compile-openwrt: build: ./ubuntu-compile-openwrt environment: TZ: Asia/Shanghai volumes: - compile-openwrt:/root # compile-openwrt 是数据卷 - compile-openwrt-home:/home ports: - "2211:22" volumes: compile-openwrt: compile-openwrt-home: Dockerfile # 以最新的Ubuntu镜像为模板 FROM ubuntu:24.04 # 将本目录下的sources.list作为容器的一个文件 ADD sources.list /root/sources.list # 使用国内Ubuntu源,更新快 RUN mv /etc/apt/sources.list /etc/apt/sources.list_bak RUN cp /root/sources.list /etc/apt/sources.list RUN apt update # 安装常用工具 RUN apt install -y vim git nano # 安装编译OpenWrt官方实例相关工具 RUN apt install -y build-essential clang flex bison g++ gawk \ gcc-multilib g++-multilib gettext git libncurses5-dev libssl-dev \ python3-setuptools rsync swig unzip zlib1g-dev file wget # 安装当前镜像对当前OpenWrt编译所需要库 RUN apt install -y libelf-dev locales # 设置LOCALE RUN locale-gen en_US.UTF-8 # 修改root密码 RUN echo 'root:pw' | chpasswd # 添加自定义用户 RUN adduser buildbot \ && echo 'buildbot:pw' | chpasswd # 安装ssh RUN apt install -y openssh-server RUN mkdir -p /var/run/sshd # 开放22端口 EXPOSE 22 #设置自启动命令 CMD ["/usr/sbin/sshd", "-D"] sources.list ...

October 10, 2024 · 2 min · zhumouren