NanoPC-T4 实战:将 fnOS 从 eMMC 无损迁移至 NVMe SSD 的完整方案(含 Btrfs 与小容量盘适配)
一、背景与目标
1.1 理解 ARM 开发板的引导过程
**在开始之前,有必要先简单梳理一下 ARM 开发板的引导流程。以 RK3399 为例,芯片内部固化了一段不可更改的启动 ROM(BootROM)。上电后,BootROM 会按照预设顺序扫描可启动设备(通常是 SD 卡 → eMMC → SPI Flash),找到有效的引导程序(U-Boot)后加载执行。U-Boot 负责初始化硬件(DDR、时钟等),然后从指定设备(通常是 eMMC 或 SD 卡的分区)加载内核(kernel)和设备树(dtb)。内核启动后,再根据 **root= 参数挂载根文件系统。
关键点在于:RK3399 的 BootROM 无法直接识别 NVMe 硬盘。引导程序(U-Boot)和内核镜像必须存放在 eMMC 或 SD 卡上。但根文件系统可以放在任何内核能识别的地方——包括 NVMe。
1.2 硬件限制
RK3399 芯片有一个众所周知的限制:不支持直接从 NVMe/M.2 硬盘启动。引导程序必须位于 eMMC 或 SD 卡上。友善官方给出的 eflasher-multiple-os 固件虽然可以将根文件系统写入 NVMe 或 USB 设备,但我无法确定它对 Arm 飞牛的兼容性。而且官方固件内核过老(4.19),无法享受新特性和 Docker 完整支持。eMMC 读写慢且寿命有限,不适合作为长期运行的系统盘。
1.3 解决方案
**采用 **“eMMC 引导 + NVMe 系统” 分离架构:
- eMMC:仅存放引导文件(U-Boot、Kernel、DTB),几乎只读,寿命无限
- NVMe:存放完整根文件系统,承担所有读写,享受高速低延迟
该方案既能突破 RK3399 的引导限制,又能充分发挥 NVMe 的性能优势,同时延长 eMMC 的使用寿命。这套方案基本适用于常见的 Linux 固件。
1.4 本次案例的特殊性
| 项目 |
源盘 (eMMC) |
目标盘 (NVMe) |
| 总容量 |
14.6 GB |
13.4 GB |
| 根分区大小 |
14.1 GB |
13.1 GB |
| 文件系统 |
Btrfs |
Btrfs |
| 关键问题 |
目标盘容量 < 源分区容量,无法使用 dd克隆 |
|
**这是一个很尴尬的局面——傲腾 M10 16GB 的实际可用容量比 eMMC 还小。用 **dd 直接克隆会失败,必须换一种思路。
二、硬件与软件环境
2.1 硬件
- 开发板:NanoPC-T4(RK3399)
- 引导盘:板载 eMMC(14.6 GB)
- 系统盘:Intel Optane M10 16GB NVMe(实际可用 13.4 GB)
本文****所有操作以 nvme0n1 作为 NVMe 设备名。实际操作中,请根据你的硬件环境替换为正确的设备名(例如 nvme1n1、nvme2n1 等)。可以使用 lsblk 命令确认。
2.2 软件
- 系统:fnOS 1.1.24
- 内核:Linux 6.12.41
- 文件系统:Btrfs
- 引导方式:U-Boot 脚本(
boot.scr + fnEnv.txt)
三、核心挑战与解决方案
| 挑战 |
解决方案 |
| NVMe 容量小于 eMMC 分区 |
放弃 dd,改用 rsync文件级复制 |
| rootfs 克隆后 Btrfs 文件系统 UUID 冲突 |
使用 btrfstune -u生成新 UUID |
| RK3399 无法从 NVMe 启动 |
eMMC 保留引导,通过内核参数指定 NVMe 为 root |
| 引导脚本动态获取分区 UUID |
修改 fnEnv.txt,用 extraargs覆盖 root 参数 |
| 双盘同时存在导致挂载混乱 |
使用 PARTUUID 而非 UUID 指定启动分区 |
四、详细操作步骤
4.1 准备工作
# 确认NVME设备名称(请根据实际输出替换教程中的nvme0n1)
lsblk
# 确认 eMMC 为 mmcblk2,NVMe 为 nvme0n1
我的输出如下:
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 配置,需要先清理干净。
4.2 清理 NVMe 硬盘上的旧配置
这一步很关键。如果 NVMe 上还有飞牛残留的 LVM 或 RAID 元数据,后续操作会出问题。
# 1. 强制卸载所有相关挂载点(替换为你的实际挂载点)
umount -l /vol1 2>/dev/null || true
umount -l /dev/md0 2>/dev/null || true
umount -l /dev/nvme0n1p1 2>/dev/null || true
# 2. 停止 LVM 和 RAID 服务(替换为你的实际 LVM 名称)
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
# 3. 清除分区表头扇区(让内核认为磁盘是空的)
dd if=/dev/zero of=/dev/nvme0n1 bs=512 count=100 conv=fsync
# 4. 通知内核重新读取分区表
partprobe /dev/nvme0n1
执行完后,lsblk 应该只显示一个空的 nvme0n1,没有任何子分区:
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
4.3 确认源系统文件系统类型
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.4 分区 NVMe 硬盘
注意:以下所有 nvme0n1 请替换为你的实际 NVMe 设备名。
# 彻底清除可能残留的元数据
wipefs -a /dev/nvme0n1
partprobe /dev/nvme0n1
# 创建 GPT 分区表
parted /dev/nvme0n1 mklabel gpt
# 输入 yes 确认
# 创建 Boot 分区(285MB,与 eMMC 保持一致)
parted /dev/nvme0n1 mkpart primary ext4 1MiB 286MiB
# 创建 Root 分区(占用剩余所有空间)
parted /dev/nvme0n1 mkpart primary ext4 286MiB 100%
# 刷新分区表
partprobe /dev/nvme0n1
# 验证分区结构
lsblk /dev/nvme0n1
4.5 格式化分区
# Boot 分区使用 ext4(兼容性最好)
mkfs.ext4 /dev/nvme0n1p1
# Root 分区使用 btrfs(与源系统一致)
mkfs.btrfs -f -L rootfs_nvme /dev/nvme0n1p2
格式化完成后,记录下新分区的 UUID 和 PARTUUID:
blkid /dev/nvme0n1p2
4.6 挂载并复制数据
# 创建挂载点
mkdir -p /mnt/dst_boot /mnt/dst_root
# 挂载目标分区
mount /dev/nvme0n1p1 /mnt/dst_boot
mount /dev/nvme0n1p2 /mnt/dst_root
# 检查空间是否足够
df -h / # 源已用空间
df -h /mnt/dst_root # 目标可用空间
# 确保 源已用 < 目标可用
**确认空间足够后,开始复制。这里用 **rsync 而不是 dd,因为目标盘比源盘小,文件级复制才能绕过这个限制。
# 复制 Boot 分区
rsync -avHAX /boot/ /mnt/dst_boot/
# 复制 Root 根文件系统
# 关键:排除虚拟文件系统和外部挂载点
rsync -avHAX \
--exclude='/proc/*' \
--exclude='/sys/*' \
--exclude='/dev/*' \
--exclude='/run/*' \
--exclude='/mnt/*' \
--exclude='/tmp/*' \
--exclude='/lost+found/*' \
--exclude='/vol00/*' \
--exclude='/media/*' \
/ /mnt/dst_root/
等待复制完成(可能需要几分钟):
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
4.7 修改 NVMe 分区的 UUID(关键步骤)
Btrfs 文件系统用 UUID 来标识。如果两个分区有相同的 UUID,系统会分不清该挂载哪一个。所以必须给 NVMe 上的 rootfs 生成一个新的 UUID。
首先卸载 root 分区(btrfstune 要求分区未挂载):
umount /mnt/dst_root
然后生成新的 UUID:
btrfstune -u /dev/nvme0n1p2
**工具会提示确认,输入 **y:
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:
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"
我的输出:
旧 UUID (EMMC): 48b0a7cb-68bc-4337-b9ad-fc605dbb31bf
新 UUID (NVMe): ff743428-987f-4649-b87e-3a94a65c94c6
新 PARTUUID (NVMe): 871c986b-5385-45ae-a241-2e875a3ecc43
4.8 修改 NVMe 内部的 /etc/fstab
重新挂载 NVMe 的 root 分区:
mount /dev/nvme0n1p2 /mnt/dst_root
备份并修改 fstab,将原来的 UUID 替换为新的:
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
验证修改结果:
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
可以看到,/ 分区已经正确指向了 NVMe 的新 UUID。
清理挂载:
umount /mnt/dst_root
umount /mnt/dst_boot
4.9 修改 eMMC 引导配置(最关键的一步)
现在需要告诉内核:从 NVMe 启动,而不是从 eMMC。
编辑 eMMC 上的引导配置文件:追加 root=PARTUUID=xxxx
PARTUUID 在分区表层面唯一标识分区,不会因为文件系统重新格式化而改变,因此比 UUID 更稳定。
vim /boot/fnEnv.txt
**修改 **extraargs 这一行,在原有参数后追加 root=PARTUUID=xxxx-xxxx-xxxx-xxxx:
修改前:
verbosity=1
bootlogo=false
console=both
extraargs=cma=256M
fdtfile=rockchip/rk3399-nanopc-t4.dtb
kernelfile=vmlinuz-6.12.41-trim
修改后:
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= 后面不要加双引号
cma=256M 和 root=... 之间必须有空格
- **确保 PARTUUID 是 NVMe 根分区的值(用 **
blkid /dev/nvme0n1p2 确认)
4.10 重启验证
**建议检查 **/boot/fnEnv.txt 中的 PARTUUID 是否与 blkid /dev/nvme0n1p2 输出一致,确保万无一失
sync
reboot
五、验证与性能测试
5.1 确认系统运行在 NVMe 上
# 查看根目录挂载点
df -hT /
# 应显示 /dev/nvme0n1p2
# 查看块设备挂载情况
lsblk
# 应显示 nvme0n1p2 挂载在 /,eMMC 的 mmcblk2p2 不应挂载在 /
# 查看文件系统类型
mount | grep " / "
# 应显示 btrfs

5.2 性能测试
**让我们立刻见证这块 **Intel Optane M10 16GB 在 RK3399 上的真实实力。
**Intel Optane M10 的强项不是顺序读写(受限于 PCIe 2.0 带宽),而是 **4K 随机读写 和 超低延迟。在运行数据库、Docker 容器和系统响应上,体验远超普通 NVMe SSD。
先安装测试工具(如果还没装):
sudo apt update && sudo apt install fio -y
运行以下测试脚本:
# 4K 随机读
fio --name=randread --ioengine=libaio --iodepth=1 --rw=randread --bs=4k --direct=1 --size=256M --num**s=1 --runtime=60 --group_reporting --filename=/tmp/fio_test_read
# 4K 随机写
fio --name=randwrite --ioengine=libaio --iodepth=1 --rw=randwrite --bs=4k --direct=1 --size=256M --num**s=1 --runtime=60 --group_reporting --filename=/tmp/fio_test_write
# 混合读写(70% 读 30% 写,典型服务器负载)
fio --name=mixed --ioengine=libaio --iodepth=4 --rw=randrw --rwmixread=70 --bs=4k --direct=1 --size=256M --num**s=2 --runtime=60 --group_reporting --filename=/tmp/fio_mixed
# 清理
rm /tmp/fio_test_* /tmp/fio_mixed_* 2>/dev/null
5.3 Intel Optane M10 16GB @ NanoPC-T4 测速结果
| 测试项目 |
速度/性能 |
延迟 |
评价 |
| 顺序写入 |
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 容器的启动和运行也会明显流畅。
六、总结
本方案成功实现了:
- 突破容量限制:目标盘小于源盘,用
rsync 完成迁移
- 解决引导限制:RK3399 无法 NVMe 启动,采用 eMMC 引导 + 内核参数覆盖
- 处理 Btrfs 特性:用
btrfstune 修改 UUID,避免双盘冲突
- 最大化性能:Optane M10 跑出 13 万 IOPS,延迟仅 6μs
- 延长硬件寿命:eMMC 仅做引导,几乎零磨损
适用场景:
- NanoPC-T4 / Rock Pi 4 等 RK3399 开发板
- 需要高性能、低延迟的边缘计算节点
- 希望延长 eMMC 寿命的长期运行设备
- 手头有小容量 Optane 硬盘闲置的用户
测试日期:2026 年 3 月 22 日** ** 测试者:LECREATE** ** 系统:fnOS 1.1.24