将 fnOS 从 eMMC 无损迁移至 NVMe SSD 日常用 ARM 设备,总习惯把固件刷进 eMMC 或者 TF 卡。eMMC 读写慢,寿命有限,用久了总觉得差口气。我手头有块 NanoPC-T4,给它刷了 Arm 飞牛固件简单体验了一下,就琢磨:要是能把系统挪到 NVMe 上,应该会爽很多。NanoPC-T4 正好有个 NVMe M.2 插槽,把系统装到外部存储上,就能绕过 eMMC 这个瓶颈。我去翻了友善官方的 eflasher-multiple-os 固件说明,它确实可以把根文件系统写到 NVMe 或 USB 设备里,但我不确定它能不能兼容飞牛的 rootfs。而且官方固件内核太老了(4.19),新特性和 Docker 完整支持都享受不到。好在 ARM 设备的引导思路是相通的,自己研究了一下,把飞牛系统迁移到了 NVMe 硬盘上。下面就是完整的折腾记录。
先搞清楚 ARM 板子是怎么启动的 以 RK3399 为例,芯片内部固化了一段不可更改的启动 ROM(BootROM)。上电后,BootROM 会按顺序扫描可启动设备(一般是 SD 卡 → eMMC → SPI Flash),找到有效的 U-Boot 就加载执行。U-Boot 负责初始化内存、时钟等,然后从某个设备(通常是 eMMC 或 SD 卡的分区)加载内核(kernel)和设备树(dtb)。内核跑起来之后,再根据 root= 参数去挂载根文件系统。
关键点 :RK3399 的 BootROM 不认识 NVMe。U‑Boot 和内核镜像必须放在 eMMC 或 SD 卡上,但根文件系统可以放在任何内核能驱动的地方——当然也包括 NVMe。顺着这个思路,整个方案可以拆成两步:
准备 rootfs :在 NVMe 上建好分区,把 eMMC 上的根文件系统完整复制过去,并处理好 UUID 冲突。
修改 PARTUUID :修改 eMMC 上的内核引导参数,让系统从 NVMe 的 PARTUUID 启动。
逆向分析:飞牛系统的引导逻辑 为了验证上述理论,并确保迁移方案万无一失,我们需要先拆解飞牛官方固件 fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img,看看它的具体的引导逻辑
1. 查看镜像整体布局 在 Linux 主机上,我们首先使用 fdisk 查看镜像的分区表信息:
1 2 3 4 5 6 7 8 9 10 11 root@dev-vm:/home/dev# fdisk -l fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img Disk fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img: 3.74 GiB, 4019191808 bytes, 7849984 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type : gpt Disk identifier: 9CBD79EE-7323-4A43-9CC3-45A0C2FB3636 Device Start End Sectors Size Type fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img1 65536 598015 532480 260M Linux filesystem fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img2 630784 7833599 7202816 3.4G Linux filesystem
我们发现:
**P1 (Boot)**:起始于第 65536 扇区(即 32MB 处),大小 260MB。这说明前 32MB 存放了 U-Boot、ATF 等底层引导代码,不属于 GPT 分区管理
**P2 (Rootfs)**:起始于第 630784 扇区,大小 3.4GB。
2. 挂载并检查文件系统类型 为了确认分区的具体文件系统类型,我们使用 losetup -P 自动扫描分区并挂载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 root@dev-vm:/home/dev# losetup -f --show -P fnos_Mainland-PE_arm_1.0.0_nanopc-t4_251.img /dev/loop0 root@dev-vm:/home/dev# lsblk /dev/loop0 NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 3.7G 0 loop ├─loop0p1 259:2 0 260M 0 part └─loop0p2 259:3 0 3.4G 0 part root@dev-vm:/home/dev# mkdir -p /mnt/img_p1 /mnt/img_p2 root@dev-vm:/home/dev# mount /dev/loop0p1 /mnt/img_p1 root@dev-vm:/home/dev# mount /dev/loop0p2 /mnt/img_p2 root@dev-vm:/home/dev# df -T /mnt/img_p1 /mnt/img_p2 Filesystem Type 1K-blocks Used Available Use% Mounted on /dev/loop0p1 ext4 247469 60368 169693 27% /mnt/img_p1 /dev/loop0p2 btrfs 3601408 2331256 932424 72% /mnt/img_p2
Boot 分区 (p1) 是 ext4 格式 。这意味着我们在 NVMe 上格式化 Boot 分区时,必须使用 mkfs.ext4,而不能使用 FAT32。
Root 分区 (p2) 是 btrfs 格式 。这验证了迁移时必须处理 Btrfs 的 UUID 冲突问题。
3. 查看 Boot 分区内容 挂载后,我们查看 /boot 分区里到底有什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@dev-vm:/home/dev# ls -l /mnt/img_p1 total 24560 -rwxr-xr-x 1 root root 1234 Mar 22 10:00 boot.cmd -rwxr-xr-x 1 root root 1344 Mar 22 10:00 boot.scr -rw-r--r-- 1 root root 234567 Mar 22 10:00 config-6.12.41-trim drwxr-xr-x 2 root root 4096 Mar 22 10:00 dtb drwxr-xr-x 2 root root 4096 Mar 22 10:00 efi -rw-r--r-- 1 root root 85 Mar 22 10:00 fnEnv.txt drwxr-xr-x 2 root root 4096 Mar 22 10:00 grub -rw-r--r-- 1 root root 8901234 Mar 22 10:00 initrd.img-6.12.41-trim drwx------ 2 root root 16384 Mar 22 10:00 lost+found -rw-r--r-- 1 root root 4567890 Mar 22 10:00 System.map-6.12.41-trim -rwxr-xr-x 1 root root 11234567 Mar 22 10:00 vmlinuz -rwxr-xr-x 1 root root 11234567 Mar 22 10:00 vmlinuz-6.12.41-trim
这里包含了内核 (vmlinuz)、设备树 (dtb)、引导脚本 (boot.scr) 以及我们要修改的关键文件 fnEnv.txt。
4. 深度解析:引导脚本 (boot.cmd) 的逻辑 通过查看 boot.cmd,我们发现了飞牛引导逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 root@dev-vm:/home/dev# cat /mnt/img_p1/boot.cmd setenv load_addr "0x9000000" setenv overlay_error "false" setenv verbosity "1" setenv console "both" setenv bootlogo "false" setenv rootfstype "btrfs,ext4" setenv docker_optimizations "on" setenv earlycon "off" test -n "${distro_bootpart} " || distro_bootpart=1test -n "${distro_rootpart} " || distro_rootpart=2echo "Boot script loaded from ${devtype} ${devnum} :${distro_bootpart} " if test -e ${devtype} ${devnum} :${distro_bootpart} ${prefix} fnEnv.txt; then load ${devtype} ${devnum} :${distro_bootpart} ${load_addr} ${prefix} fnEnv.txt env import -t ${load_addr} ${filesize} fi part uuid ${devtype} ${devnum} :${distro_bootpart} bootuuid part uuid ${devtype} ${devnum} :${distro_rootpart} rootuuid setenv bootargs "root=PARTUUID=${rootuuid} rootwait rw rootfstype=${rootfstype} ${consoleargs} consoleblank=0 loglevel=${verbosity} ubootpart=${bootuuid} usb-storage.quirks=${usbstoragequirks} ${extraargs} ${extraboardargs} " if test "${docker_optimizations} " = "on" ; then setenv bootargs "${bootargs} cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory" ; fi load ${devtype} ${devnum} :${distro_bootpart} ${kernel_addr_r} ${prefix} vmlinuz load ${devtype} ${devnum} :${distro_bootpart} ${fdt_addr_r} ${prefix} dtb/${fdtfile} fdt addr ${fdt_addr_r} fdt resize 65536 booti ${kernel_addr_r} - ${fdt_addr_r}
逻辑链条分析:
默认行为 :脚本通过 part uuid 获取当前启动设备 (eMMC)第 2 分区的 PARTUUID,并设为默认的 root=PARTUUID=${rootuuid}。
环境变量注入 :脚本检测并加载 /boot/fnEnv.txt,将其中的变量(如 extraargs)导入环境。
参数优先级 :${extraargs} 被显式拼接在 bootargs 字符串的末尾 。Linux 内核在解析启动参数时,遵循“后出现者覆盖先出现者” 的规则。
再看 fnEnv.txt 的初始内容:
1 2 3 4 5 6 root@dev-vm:/home/dev# cat /mnt/img_p1/fnEnv.txt verbosity=1 bootlogo=false console=both extraargs=cma=256M fdtfile=rockchip/rk3399-nanopc-t4.dtb
结论: 当我们在 fnEnv.txt 的 extraargs 中追加 root=PARTUUID=<NVMe_ID> 时,最终的启动命令行变成了:... root=PARTUUID=<eMMC_ID> ... root=PARTUUID=<NVMe_ID>
内核读取到第二个 root 参数,从而忽略第一个,成功从 NVMe 启动。这种设计非常巧妙,它允许用户在不重新编译 boot.scr 二进制文件的情况下,通过简单的文本编辑即可灵活切换启动盘。
基于上述分析,我们决定采用 “eMMC 引导 + NVMe 系统” 的分离架构:
eMMC :只放引导文件(U‑Boot、内核、DTB),将挂载NVME硬盘的根文件系统
NVMe :放完整的根文件系统,所有读写都在这儿,享受高速低延迟。
这个方案既能绕过 RK3399 的引导限制,又能把 eMMC 的寿命省下来,同时把 NVMe 的性能用满。而且,这种“引导介质与 rootfs 分离”的思路并不局限于 NVMe——如果 eMMC 损坏了,或者板子根本没有 eMMC,甚至你想把系统挪到 USB 3.0 硬盘、SATA 盘、另一张 TF 卡上,本质都是一样的:只要 BootROM 能从某个介质(比如 TF 卡)把 U‑Boot 和内核拉起来,内核就能从任何它认得到的设备上挂载 rootfs。后面你会看到,我们只改了一个 root=PARTUUID= 参数,这个参数指向哪里,系统就从哪里启动。
我手里的东西
开发板:NanoPC-T4(RK3399)
引导盘:板载 eMMC,14.6 GB
系统盘:Intel Optane M10 16GB NVMe,实际可用只有 13.4 GB
系统:fnOS 1.1.24
内核:Linux 6.12.41
文件系统:Btrfs
引导方式:U-Boot 脚本(boot.scr + fnEnv.txt)
注意 :本文所有操作都假设你的硬盘设备名为 nvme0n1。你的可能是 nvme1n1 或别的,请用 lsblk 确认后替换。
一上来就碰了个硬钉子 量了一下,傲腾 M10 虽然标称 16GB,实际可用只有 13.4 GB,而 eMMC 上的根分区占了 14.1 GB。用 dd 直接克隆肯定不行,目标盘比源盘还小,得换个思路——不能用块设备克隆,只能用文件级复制,挑着有用的文件搬过去。
一步步开干 前提 :你的设备已经刷好飞牛系统并从 eMMC/TF 正常启动,本文所有操作均在该系统下执行。
1. 先看看设备
我的输出:
1 2 3 4 5 6 7 8 9 10 11 12 root@NanoPC-T4:/# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS mmcblk2 179:0 0 14.6G 0 disk ├─mmcblk2p1 179:1 0 285M 0 part /boot └─mmcblk2p2 179:2 0 14.1G 0 part / mmcblk2boot0 179:32 0 4M 1 disk mmcblk2boot1 179:64 0 4M 1 disk zram0 252:0 0 1.9G 0 disk [SWAP] nvme0n1 259:0 0 13.4G 0 disk └─nvme0n1p1 259:1 0 13.4G 0 part └─md0 9:0 0 13.4G 0 raid1 └─trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 253:0 0 13.4G 0 lvm
注意,我的傲腾 M10 之前在飞牛上挂载过,残留了 LVM 和 RAID 的配置,得先清理干净。
2. 清理 NVMe 上的旧配置 如果不清理干净,后面分区格式化会出各种奇怪问题——比如明明已经分区了,但 lsblk 还是能看到旧的分区结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 umount -l /vol1 2>/dev/null || true umount -l /dev/md0 2>/dev/null || true umount -l /dev/nvme0n1p1 2>/dev/null || true lvchange -an -ff trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 2>/dev/null || true mdadm --stop /dev/md0 2>/dev/null || true dmsetup remove trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 2>/dev/null || true dd if =/dev/zero of=/dev/nvme0n1 bs=512 count=100 conv=fsync partprobe /dev/nvme0n1
执行完后,lsblk 应该只看到一个光秃秃的 nvme0n1:
1 2 3 4 5 6 7 8 9 root@NanoPC-T4:/# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS mmcblk2 179:0 0 14.6G 0 disk ├─mmcblk2p1 179:1 0 285M 0 part /boot └─mmcblk2p2 179:2 0 14.1G 0 part / mmcblk2boot0 179:32 0 4M 1 disk mmcblk2boot1 179:64 0 4M 1 disk zram0 252:0 0 1.9G 0 disk [SWAP] nvme0n1 259:0 0 13.4G 0 disk
3. 确认 eMMC 上的文件系统类型
我得到:
1 2 root@NanoPC-T4:/# blkid /dev/mmcblk2p2 /dev/mmcblk2p2: LABEL="rootfs" UUID="48b0a7cb-68bc-4337-b9ad-fc605dbb31bf" UUID_SUB="f29b6a03-37ca-4e05-987e-a2b5d5e05154" BLOCK_SIZE="4096" TYPE="btrfs" PARTUUID="afe747ae-13c5-4540-8cd0-94fb975662e5"
是 Btrfs,记下这个 UUID,后面要用。
4. 给 NVMe 分区(第一步:准备 rootfs 的开始) 在给 NVMe 分区时,我特意创建了两个分区:p1 (Boot) 和 p2 (Root),这与 eMMC 上的原始布局完全一致。这样做主要基于两点考虑:
保持拓扑一致性 : 保留相同的分区结构(Boot + Root),使得 NVMe 硬盘在逻辑上成为 eMMC 的完整副本。如果未来需要更换引导介质,或者使用工具对整块 NVMe 进行镜像备份/恢复时,这种一致性可以避免很多路径映射和挂载点的麻烦。
隐性备份 /boot 分区 : 虽然 RK3399 的 BootROM 无法直接从 NVMe 启动,导致 NVMe 上的 p1 分区在正常启动流程中不会被加载,但它完整地保留了内核、设备树和引导脚本。
灾难恢复价值 :如果 eMMC 上的 /boot 分区因误操作或文件系统损坏而丢失,我们可以轻松地从 NVMe 的 p1 分区中恢复这些关键文件。
版本对照 :当我们需要升级内核或调试引导问题时,NVMe 上的 /boot 可以作为上一个稳定版本的参考基准。
因此,尽管 p1 不参与启动,但将其复制过去是一种低成本、高收益的保险策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 wipefs -a /dev/nvme0n1 partprobe /dev/nvme0n1 parted /dev/nvme0n1 mklabel gpt parted /dev/nvme0n1 mkpart primary ext4 1MiB 286MiB parted /dev/nvme0n1 mkpart primary ext4 286MiB 100% partprobe /dev/nvme0n1 lsblk /dev/nvme0n1
5. 格式化 1 2 3 4 5 mkfs.ext4 /dev/nvme0n1p1 mkfs.btrfs -f -L rootfs_nvme /dev/nvme0n1p2
格式化完,顺手记一下新分区的 UUID 和 PARTUUID:
6. 挂载并复制数据(继续准备 rootfs) 1 2 3 4 5 6 7 8 9 10 11 mkdir -p /mnt/dst_boot /mnt/dst_root mount /dev/nvme0n1p1 /mnt/dst_boot mount /dev/nvme0n1p2 /mnt/dst_rootdf -h / df -h /mnt/dst_root
确认空间足够后,开始复制。因为目标盘比源盘小,不能用 dd,只能用 rsync 做文件级复制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rsync -avHAX /boot/ /mnt/dst_boot/ rsync -avHAX \ --exclude='/proc/*' \ --exclude='/sys/*' \ --exclude='/dev/*' \ --exclude='/run/*' \ --exclude='/mnt/*' \ --exclude='/tmp/*' \ --exclude='/lost+found/*' \ --exclude='/vol*/' \ --exclude='/media/*' \ / /mnt/dst_root/
等几分钟,看到类似这样的输出就成功了:
1 2 sent 5,244,244,907 bytes received 1,572,046 bytes 62,080,674.00 bytes/sec total size is 5,241,423,994 speedup is 1.00
7. 修改 NVMe 分区的 UUID(关键步骤,属于准备 rootfs 的收尾) 现在遇到一个新问题:我们用 rsync 把文件原样复制过去了,但 Btrfs 文件系统的 UUID 也被原样复制了。也就是说,NVMe 上的新 rootfs 和 eMMC 上的旧 rootfs 有完全相同的 UUID。内核在挂载时看到两个相同 UUID 的 Btrfs 卷,就会混乱——它不知道该用哪一个。所以必须给 NVMe 上的 rootfs 生成一个新的 UUID。
先卸载 root 分区(btrfstune 要求分区不能处于挂载状态):
然后生成新 UUID:
1 btrfstune -u /dev/nvme0n1p2
工具会问你是否确认,输入 y:
1 2 3 4 5 6 New fsid: ff743428-987f-4649-b87e-3a94a65c94c6 Set superblock flag CHANGING_FSID Change fsid in extent tree Change fsid in chunk tree Clear superblock flag CHANGING_FSID Fsid change finished
记录下新旧 UUID 和 PARTUUID:
1 2 3 4 5 6 7 OLD_UUID=$(blkid -s UUID -o value /dev/mmcblk2p2) NEW_UUID=$(blkid -s UUID -o value /dev/nvme0n1p2) NEW_PARTUUID=$(blkid -s PARTUUID -o value /dev/nvme0n1p2)echo "旧 UUID (eMMC): $OLD_UUID " echo "新 UUID (NVMe): $NEW_UUID " echo "新 PARTUUID (NVMe): $NEW_PARTUUID "
我的输出:
1 2 3 旧 UUID (EMMC): 48b0a7cb-68bc-4337-b9ad-fc605dbb31bf 新 UUID (NVMe): ff743428-987f-4649-b87e-3a94a65c94c6 新 PARTUUID (NVMe): 871c986b-5385-45ae-a241-2e875a3ecc43
8. 修改 NVMe 里的 /etc/fstab(让新 rootfs 引用自己的 UUID) NVMe 上的 rootfs 里还有一个 /etc/fstab 文件,它里面写的是旧的 UUID(指向 eMMC)。如果不改,即使内核从 NVMe 启动了,后续挂载 / 时又会因为 UUID 不匹配而出错。所以得把它也换成新的 UUID。
重新挂载 NVMe 的 root 分区:
1 mount /dev/nvme0n1p2 /mnt/dst_root
备份并替换 UUID:
1 2 cp /mnt/dst_root/etc/fstab /mnt/dst_root/etc/fstab.bak sed -i "s/$OLD_UUID /$NEW_UUID /g" /mnt/dst_root/etc/fstab
验证一下:
1 cat /mnt/dst_root/etc/fstab
应该看到 / 分区已经指向新的 UUID:
1 2 3 4 5 root@NanoPC-T4:/# cat /mnt/dst_root/etc/fstab ....... UUID=ff743428-987f-4649-b87e-3a94a65c94c6 / btrfs defaults,noatime,errors=remount-ro 0 1 UUID=cf2ecdac-946c-4790-a894-19c10b526a1a /boot ext4 defaults,noatime,errors=remount-ro 0 2 tmpfs /tmp tmpfs defaults,nosuid 0 0
确认无误后,卸载:
1 2 umount /mnt/dst_root umount /mnt/dst_boot
至此,第一步“准备 rootfs” 完成:NVMe 上已经有了一个独立、可用的根文件系统,UUID 也改好了。
9. 修改 eMMC 上的引导配置(第二步:修改 PARTUUID) 现在,NVMe 上的系统已经准备好了,但内核还不知道要去 NVMe 上找 root。因为 RK3399 的 BootROM 只认识 eMMC/SD 卡,所以我们还是从 eMMC 启动内核,但要让内核把根文件系统挂载到 NVMe 上。这就需要修改内核启动参数。
另外,如果直接使用文件系统的 UUID 来指定 root,可能会遇到问题:在早期引导阶段,内核不一定能正确解析 Btrfs 的 UUID(尤其当有多个相同类型的文件系统时)。更可靠的办法是用 PARTUUID——这是分区表的属性,在分区创建时就固定了,不会因为文件系统格式化而改变,而且内核在很早期的阶段就能识别它。
编辑 eMMC 上的 /boot/fnEnv.txt:
原来的内容大概是这样:
1 2 3 4 5 6 verbosity=1 bootlogo=false console=both extraargs=cma=256M fdtfile=rockchip/rk3399-nanopc-t4.dtb kernelfile=vmlinuz-6.12.41-trim
我们需要在 extraargs=cma=256M 后面追加 root=PARTUUID=xxxx-xxxx-xxxx-xxxx。注意 PARTUUID= 后面不要加双引号 ,cma=256M 和 root=... 之间必须有一个空格 。
修改后:
1 2 3 4 5 6 verbosity=1 bootlogo=false console=both extraargs=cma=256M root=PARTUUID=871c986b-5385-45ae-a241-2e875a3ecc43 fdtfile=rockchip/rk3399-nanopc-t4.dtb kernelfile=vmlinuz-6.12.41-trim
为什么用 PARTUUID 而不是 UUID? PARTUUID 是分区表的属性,不会因为文件系统重新格式化而改变,在早期引导阶段更可靠。而且,这个 root=PARTUUID= 参数是通用的——如果你的 eMMC 坏了,你可以把同样的参数指向另一张 TF 卡上的根分区;如果你想用 USB 3.0 硬盘启动,只要内核能识别该硬盘,改一下 PARTUUID 就行。整个方案的精髓就在这一行。
10. 重启验证 建议再检查一遍 /boot/fnEnv.txt 里的 PARTUUID 是否和 blkid /dev/nvme0n1p2 输出一致。
验证一下是否真的跑在 NVMe 上 重启后,登录系统,执行:
1 2 3 df -hT / lsblk mount | grep " / "
可以看到 / 已经挂载在 /dev/nvme0n1p2 上了,eMMC 的 mmcblk2p2 没有被使用。
性能测试:傲腾到底有多快 Intel Optane M10 是一块很有意思的小硬盘。它的容量不大(16GB),但用的是 3D XPoint 介质,延迟极低,读写磨损均衡做得特别好。普通 TLC 固态的 4K 随机读写通常只有几万 IOPS,而傲腾可以轻松跑到六位数。在 NanoPC-T4 上,虽然 PCIe 2.0 x4 的带宽限制了顺序读写(理论上限约 1.6 GB/s,实际更少),但傲腾真正的杀手锏——4K 随机读写 和超低延迟 ——几乎不受影响。跑数据库、Docker 容器、频繁读写小文件的场景,这块小傲腾能带来脱胎换骨的体验。
先装 fio:
1 sudo apt update && sudo apt install fio -y
然后跑几个测试:
1 2 3 4 5 6 7 8 9 10 11 fio --name=randread --ioengine=libaio --iodepth=1 --rw=randread --bs=4k --direct=1 --size=256M --numjobs=1 --runtime=60 --group_reporting --filename=/home/fio_test_read fio --name=randwrite --ioengine=libaio --iodepth=1 --rw=randwrite --bs=4k --direct=1 --size=256M --numjobs=1 --runtime=60 --group_reporting --filename=/home/fio_test_write fio --name=mixed --ioengine=libaio --iodepth=4 --rw=randrw --rwmixread=70 --bs=4k --direct=1 --size=256M --numjobs=2 --runtime=60 --group_reporting --filename=/home/fio_mixedrm /home/fio_test_* /home/fio_mixed_* 2>/dev/null
我的测试结果:
测试项目
速度/性能
延迟
评价
顺序写入
611 MB/s
-
吃满 PCIe 2.0 带宽
顺序读取
800 MB/s
-
含内存缓存加速
4K 随机读
137,000 IOPS
6.16 μs
企业级性能
4K 随机写
135,000 IOPS
6.13 μs
读写几乎一致
混合读写
读 181k / 写 77.8k IOPS
29 μs
低负载依然极低延迟
傲腾在 RK3399 上跑出了 13 万 IOPS,延迟只有 6 微秒——系统响应飞快,Docker 容器启动也明显流畅了。
这个方案能走多远 回过头看,这次迁移的核心其实就两步:先在目标盘上准备好一个独立的根文件系统,再改一下 eMMC 上 fnEnv.txt 里的 root=PARTUUID= 参数。这个思路并不绑定 NVMe,也不依赖 eMMC 必须完好。
引导介质可以是 eMMC,也可以是 TF 卡。万一哪天 eMMC 彻底写坏了,只要把同样的引导文件(U‑Boot、内核、dtb)和 fnEnv.txt 拷到一张 TF 卡上,插上去就能照样启动。而 root=PARTUUID= 后面那个参数,你想指向哪里就指向哪里——NVMe 可以,USB 3.0 硬盘可以,SATA 盘可以,甚至另一张 TF 卡也可以。只要内核能驱动那个设备,系统就能从那儿跑起来。
所以这套方法不仅救活了我这块小容量傲腾,也适用于任何想把瑞芯微 ARM 设备的系统从慢速存储迁到高速外部介质的场景。如果你的 eMMC 还没坏,它可以作为提速方案;如果已经坏了,它更是一条复活路径。
如果你使用的是大容量 SSD ,社区中常见的 dd 克隆法可能更快。但如果你像我一样使用小容量傲腾硬盘,亦或是想深入理解 ARM 引导机制,那么本文的 rsync + PARTUUID 方案是唯一且最安全的选择。
LECREATE
2026 年 3 月 22 日