引言
当物联网设备从「一个传感器节点」升级为「一个带屏、跑协议转换、做边缘推理、还能远程运维的网关」时,裸机或 RTOS 就不再合适了。你需要进程隔离、文件系统、网络协议栈、包管理、多任务调度,以及一个能跑容器和 Python 的运行环境——这些正是 Linux 的强项。嵌入式 Linux 与服务器 Linux 的差别不在内核本身,而在「构建」:你不能用发行版预编译的包,必须为自己的板子定制一套完整镜像。
定制的复杂度集中在一个问题上:一个可启动的 Linux 系统由引导固件、内核、设备树、根文件系统四部分组成,它们各自有版本,且必须互相匹配。手工维护这套东西(下载交叉工具链、编内核、拼 rootfs)在第一个项目还能忍,一旦要支持三个硬件版本、每月跟一次安全补丁,就会失控。Yocto 就是为解决这个失控问题而生的:用配方(recipe)描述每个组件怎么构建,用层(layer)组织定制,用 BitBake 做依赖解析与缓存,最终产出可复现的镜像。
本文按「选型判断 → 引导链 → U-Boot → BSP → Yocto 基础 → 层与配方 → 设备树 → 根文件系统 → systemd → 长期维护 → 与 RTOS 的边界 → 调试」的顺序展开。MCU 侧的开发见 ESP32 与 STM32 嵌入式开发 ,实时任务调度见 RTOS 与 FreeRTOS 任务调度 。
目录
- 什么时候该上嵌入式 Linux
- 引导链:从 ROM 到内核
- U-Boot 与启动参数
- BSP 与 SoC 支持
- Yocto 与 BitBake 基础
- 层、配方与 bbappend
- 设备树
- 根文件系统与只读设计
- systemd 与启动优化
- 长期维护与安全更新
- 与裸机、RTOS 的边界
- 调试手段
- 权衡取舍
- 常见坑清单
- 小结
1. 什么时候该上嵌入式 Linux
选 Linux 还是 RTOS,本质是问「这个产品需要通用计算环境吗」。下面这张表给出判断依据:
| 需求 | 倾向 RTOS / 裸机 | 倾向 Linux |
|---|---|---|
| 实时性 | 硬实时(微秒级) | 软实时(毫秒级) |
| 内存 | 几十 KB 到几百 KB | 64MB 起,通常 256MB 以上 |
| 功耗 | 电池数年 | 常供电或大电池 |
| 功能 | 单一采集或控制 | 多协议、多进程、文件系统 |
| 升级 | 单固件 A/B | 全镜像 A/B 或包级更新 |
| 成本 | 几元到几十元 | 几十元到几百元 |
典型分界点:需要跑 TCP/IP 之上的多种协议(MQTT、HTTP、CoAP、数据库客户端)、需要文件系统与日志、需要跑容器或脚本语言、需要多进程隔离时,Linux 更划算;只是采样加通信、对功耗和成本敏感、需要确定性响应时,RTOS 更合适。
很多产品是异构的:一颗 Cortex-A 跑 Linux 做网关和界面,一颗 Cortex-M 跑 RTOS 做实时采集与控制,两者用 UART、SPI 或共享内存通信。这种「A 加 M」组合兼顾了通用计算与硬实时,代价是两颗芯片的固件要分别维护。
2. 引导链:从 ROM 到内核
嵌入式 Linux 的启动是一串接力,每一棒负责把下一棒加载到内存并跳过去。
上电
└─ BootROM(SoC 固化代码)
└─ SPL / TPL(一级引导,初始化 DDR)
└─ U-Boot(二级引导,加载内核)
└─ Linux Kernel(解压自解压镜像,挂载根文件系统)
└─ init(systemd / busybox init)
BootROM 是 SoC 厂商固化在芯片里的代码,不可改,它从固定的介质(eMMC、SD、SPI NOR)读第一级引导。SPL(Secondary Program Loader)很小(几十 KB),唯一职责是初始化 DDR 并把完整的 U-Boot 加载到内存——因为在 DDR 初始化之前,片上 SRAM 装不下 U-Boot。有些 SoC 用 TPL(Tertiary)再多一级,或者直接把 SPL 和 U-Boot 合成一个镜像。
关键工程点:引导链上每一级的「偏移」和「格式」都由 SoC 规定,写错偏移设备就是砖。所以量产烧录必须用厂商工具(如 i.MX 的 mfgtools、全志的 PhoenixSuit)或经过验证的脚本,不能手工 dd。另一个点是「启动介质选择」:多数 SoC 通过引脚或 eFuse 决定从 SD、eMMC 还是网络启动,量产板要固化从 eMMC 启动,并把 SD 作为救援通道。
3. U-Boot 与启动参数
U-Boot 是事实标准的二级引导,负责加载内核、传参、以及提供命令行做救援。
printenv # 查看所有环境变量
setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw"
setenv bootcmd "fatload mmc 0:1 ${kernel_addr_r} Image; booti ${kernel_addr_r} - ${fdt_addr_r}"
saveenv # 保存环境变量到持久存储
boot # 执行 bootcmd
几个核心概念:bootcmd 是自动启动时执行的命令序列,bootargs 是传给内核的启动参数(内核命令行),${kernel_addr_r}、${fdt_addr_r} 是内存地址变量。现代做法是用 FIT 镜像(Flattened Image Tree,.its 文件描述),把内核、设备树、ramdisk 打包成一个带签名与校验的镜像,U-Boot 一次加载并校验完整性——这是做安全启动的基础。
内核命令行(bootargs)决定根文件系统在哪、控制台在哪、以及一堆内核行为。常见参数:root= 指定根分区(/dev/mmcblk0p2 或 PARTUUID=),rootwait 等存储就绪,console= 指定串口控制台,ro/rw 指定读写模式,quiet 抑制日志。用 PARTUUID 而不是设备名更稳,因为设备枚举顺序可能变。
安全启动(Secure Boot)在 U-Boot 层做两件事:一是校验下一级镜像的签名(FIT 镜像带签名,U-Boot 用内置公钥验证),二是锁死环境变量(防止攻击者改 bootargs 挂载自己的 rootfs)。这一步是整条信任链的锚点,公钥的哈希要烧进 SoC 的 OTP/eFuse。
4. BSP 与 SoC 支持
BSP(Board Support Package)是「让 Linux 在特定板子上跑起来」的一整套东西:引导固件、内核补丁、设备树、驱动、以及构建配置。选 SoC 时 BSP 的成熟度比芯片参数更重要。
| 因素 | 说明 | 影响 |
|---|---|---|
| 上游支持 | 内核主线是否有该 SoC 的支持 | 有则长期维护省事 |
| 厂商 BSP 版本 | 厂商提供的内核版本与补丁质量 | 决定初始开发速度 |
| 文档完整度 | 数据手册、引脚复用、参考设计 | 决定调试难度 |
| 社区活跃度 | 论坛、issue、第三方板 | 决定踩坑成本 |
| 长期供货 | 芯片生命周期承诺 | 决定产品寿命 |
优先选「内核主线有支持」的 SoC(如 i.MX、Allwinner 的部分型号、TI AM335x、瑞芯微 RK 系列),因为上游支持意味着安全补丁能持续跟。纯靠厂商 BSP 的芯片,一旦厂商停止维护,内核就停在某个老版本,CVE 越积越多。
BSP 版本策略:锁定一个厂商 BSP 作为基线,把厂商补丁与自己的改动分层管理(Yocto 的层机制正好做这个)。升级 BSP 时先在一个分支上验证,再合并到产品分支,不要直接在产品线上跟厂商最新版。
5. Yocto 与 BitBake 基础
Yocto 是一套构建框架,BitBake 是它的构建引擎(类似 Make,但带依赖解析和任务调度)。核心概念:
| 概念 | 含义 | 例子 |
|---|---|---|
| Recipe(配方) | 描述一个组件怎么构建 | busybox_1.36.bb |
| Layer(层) | 一组相关配方的集合 | meta-oe、meta-imx |
| Class(类) | 可复用的构建逻辑 | autotools.bbclass |
| Task(任务) | 配方内的构建步骤 | do_fetch、do_configure、do_compile |
| Image(镜像) | 最终产出的根文件系统 | core-image-minimal |
| DISTRO | 发行版配置 | poky、fsl-imx-xwayland |
| MACHINE | 目标硬件 | imx8mm-evk |
source poky/oe-init-build-env build # 初始化构建环境(每个终端会话一次)
bitbake core-image-minimal # 构建最小镜像
bitbake -c compile busybox # 构建某个具体配方
bitbake -c listtasks busybox # 查看某配方的可用任务
bitbake -e busybox | grep ^SRC_URI # 查看配方的实际变量取值
devtool modify busybox # 用 devtool 修改并回写配方
devtool finish busybox meta-mylayer
BitBake 的两个重要机制是共享状态缓存(sstate-cache)和下载缓存(DL_DIR)。sstate 缓存让没改动的配方不重复构建,第一次全量构建可能几小时,之后增量几分钟。DL_DIR 存所有源码包,CI 上要持久化,否则每次都重新下载。这两个缓存是 Yocto 团队协作和 CI 提速的关键。
6. 层、配方与 bbappend
Yocto 的定制原则是「不改上游,只加层」。所有修改都放在自己的层里,通过 bbappend 覆盖或追加上游配方,这样升级上游时冲突最小。
meta-mylayer/
├── conf/
│ └── layer.conf 声明层名、优先级、依赖
├── recipes-core/
│ └── images/
│ └── my-product-image.bb 自定义镜像
└── recipes-kernel/
└── linux/
└── linux-imx_%.bbappend 覆盖内核配方(% 匹配任意版本)
my-product-image.bb 示例:
require recipes-core/images/core-image-base.bb
IMAGE_INSTALL:append = " my-app mqtt-client openssh-sshd"
IMAGE_FEATURES:append = " ssh-server-openssh"
IMAGE_FEATURES:remove = "debug-tweaks" 关闭调试特性以缩小镜像
bbappend 的命名规则是 原配方名_版本.bbappend,用 % 通配版本可以跨版本生效。覆盖变量的三种语法:= 直接赋值,+=/:append 追加,=+/:prepend 前置。注意 :append 和 += 有微妙差别(前者在解析末期展开,后者立即展开),处理变量拼接时优先用 :append。
conf/layer.conf 里要声明 BBFILE_PRIORITY,优先级高的层覆盖低的。层依赖用 LAYERDEPENDS 声明,bitbake-layers 工具能检查层之间的依赖与冲突:
bitbake-layers show-layers # 列出所有层与优先级
bitbake-layers show-recipes # 列出所有配方与所在层
bitbake-layers create-layer meta-new # 创建新层骨架
7. 设备树
设备树(Device Tree)用数据描述硬件,让同一个内核二进制支持不同板子。它把「硬件长什么样」从内核代码里剥离出来,是 ARM 嵌入式 Linux 的标准做法。
// my-board.dts 片段:定义一个 I2C 上的传感器
&i2c2 {
status = "okay";
clock-frequency = <400000>;
bme280: sensor@76 {
compatible = "bosch,bme280";
reg = <0x76>;
vddd-supply = <&vdd_3v3>;
};
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
关键概念:compatible 是节点与驱动匹配的字符串(驱动里用 of_match_table 匹配它),reg 是设备地址,status = "okay" 启用节点(默认可能是 disabled)。设备树源文件分 .dts(板级)与 .dtsi(SoC 级,被 include)。编译用 dtc,产物是 .dtb,运行时内核把它展开成 /proc/device-tree。
设备树覆盖(Overlay)允许在基础设备树之上动态叠加改动,常用于「同一 SoC、不同扩展板」的产品线。改设备树时最容易犯的错是引脚复用(pinctrl)配错——引脚要么没配、要么与其他外设冲突,表现为设备探测不到或功能异常。调试时先看 dmesg 里驱动是否 probe 成功,再对照原理图核对 pinctrl 与 reg。
8. 根文件系统与只读设计
根文件系统(rootfs)是设备运行时的一切。生产环境的核心原则是根分区只读,把可变数据放到独立分区。
分区布局示例(eMMC):
p1 boot 64MB 内核、设备树、U-Boot 环境(可写但很少动)
p2 rootfs-a 512MB 根文件系统 A(只读,squashfs 或 ext4 只读挂载)
p3 rootfs-b 512MB 根文件系统 B(A/B 更新的另一半)
p4 data 1GB+ 可变数据:日志、配置、数据库(可写)
只读根文件系统的做法有两种:一是直接烧 squashfs(压缩只读文件系统),二是 ext4 挂载为 ro 再加 overlayfs 把写操作重定向到内存或 data 分区。overlayfs 方案更灵活——系统看起来可写,但所有改动都在上层,重启即还原,配合 tmpfs 还能省 flash 写入。
A/B 更新(也叫双分区更新)是嵌入式 Linux 的标准升级方式:新镜像写到非当前分区,切换启动标志,重启进入新系统,确认健康后再标记为稳定;启动失败则自动回滚到旧分区。实现框架有 RAUC、SWUpdate、mender 三套,它们在「镜像格式」「签名」「回滚判定」上各有取舍,细节见 OTA 固件升级与差分更新 。
数据分区要挂载到 data,并在这里放日志、配置、以及任何需要持久化的东西。把日志写到只读根分区会导致「写失败但程序不报错」,是现场最难查的一类问题。另外要给数据分区留足空间并监控剩余量,写满会导致服务崩溃。
9. systemd 与启动优化
systemd 是嵌入式 Linux 的主流 init,它用「单元(unit)」描述服务、挂载、定时器等,按依赖关系并行启动。相比 SysV init 的串行脚本,systemd 的并行能力能显著缩短启动时间。
; /lib/systemd/system/my-app.service
[Unit]
Description=My IoT App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/my-app --config /data/etc/app.conf
Restart=on-failure
RestartSec=5
WatchdogSec=30
; 资源限制与安全加固
MemoryMax=128M
ProtectSystem=strict
ReadWritePaths=/data
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
几个实用点:Restart=on-failure 让服务崩溃后自动重启,WatchdogSec 配合 sd_notify 实现应用级看门狗(比硬件看门狗更精细),MemoryMax 限制内存防止单个服务拖垮系统。安全加固项(ProtectSystem、NoNewPrivileges、PrivateTmp)能限制服务的攻击面,边缘设备尤其值得开。
启动优化先量化再优化:
systemd-analyze # 总启动时间
systemd-analyze blame # 每个服务耗时排序
systemd-analyze critical-chain # 关键路径
systemd-analyze plot > boot.svg # 可视化启动时序
常见优化:把非关键服务设为 After= 而非 Requires=(不阻塞启动)、用 Type=notify 精确报告就绪、合并或延迟启动耗时服务、精简镜像里的服务数量。目标是把「到业务可用的时间」压到几秒内,而不是纠结内核启动的零点几秒。
10. 长期维护与安全更新
嵌入式 Linux 产品通常要求 5 到 10 年生命周期,长期维护是必答题而非加分项。
| 维护项 | 做法 | 频率 |
|---|---|---|
| 内核 | 用 LTS 内核(如 6.6 LTS),跟稳定分支补丁 | 按需 |
| 用户空间 | 跟 Yocto 的 LTS 发行版(如 kirkstone) | 年度小版本 |
| CVE 扫描 | 用 cve-check 类工具扫镜像里的已知漏洞 | 每月 |
| 依赖更新 | 定期升级有漏洞的库(openssl、busybox) | 按 CVE 优先级 |
| 复现构建 | 固定所有 SRCREV,保证可复现 | 每次发布 |
Yocto 提供了 cve-check 类,能在构建时扫描配方对应的 CVE 并输出报告。把它接进 CI,每次构建产出 CVE 清单,才能系统性地管理安全债。关键是固定版本:所有配方用 SRCREV 锁死到具体提交,不用 AUTOREV,否则每次构建拉到的代码不同,无法复现、也无法审计。
安全更新的节奏建议:高危 CVE 在评估影响后一周内出补丁,中低危按季度汇总。更新包要能通过 OTA 通道下发,且升级本身要可回滚——这正是第 8 节 A/B 分区的价值。软件物料清单(SBOM)越来越被合规要求,Yocto 能生成 SPDX 格式的 SBOM,建议从第一个版本就开始产出。
11. 与裸机、RTOS 的边界
Linux 和 RTOS 不是互斥的,理解各自边界才能设计好异构系统。
| 维度 | 裸机 / RTOS | 嵌入式 Linux |
|---|---|---|
| 实时性 | 硬实时,微秒级抖动 | 软实时,毫秒级抖动(PREEMPT_RT 可到几十微秒) |
| 启动时间 | 毫秒级 | 秒级 |
| 内存占用 | KB 级 | MB 级 |
| 功耗 | 极低 | 高(需常供电) |
| 开发效率 | 低(无文件系统、无进程) | 高(脚本、容器、调试工具全) |
| 可靠性 | 无 MMU 隔离 | 进程隔离,崩溃不拖垮系统 |
需要硬实时的控制回路(电机、编码器、安全联锁)应留在 MCU 上,Linux 侧只做非实时的高层逻辑。Linux 侧即使开了 PREEMPT_RT 补丁,也受调度、中断、缓存等因素影响,抖动通常在几十微秒量级,达不到 MCU 的确定性。
异构通信的常见方式:UART(简单、慢)、SPI(快、需协议)、共享内存加中断(最快、最复杂)。设计时要明确「谁主导」:通常 Linux 侧做主控与决策,MCU 侧做采集与执行,通过一个明确定义的二进制协议交互。协议要带长度、校验、序列号,并处理一端重启的情况。
12. 调试手段
嵌入式 Linux 的调试工具链比 MCU 丰富,但需要知道用哪个。
| 问题类型 | 工具 | 说明 |
|---|---|---|
| 启动失败 | 串口控制台 | 看 U-Boot 与内核早期日志 |
| 驱动不工作 | dmesg、lsmod、/proc/device-tree | 看 probe 是否成功 |
| 应用崩溃 | strace、gdb、core dump | 跟踪系统调用与栈 |
| 性能问题 | perf、ftrace、top | CPU 热点与调度延迟 |
| 内核问题 | JTAG + kgdb | 内核级单步 |
| 网络问题 | tcpdump、ss、ip | 抓包与连接状态 |
串口控制台是第一工具:从 U-Boot 到内核到 init,所有早期日志都从串口出来,没有串口基本等于盲调。生产板一定要引出串口测试点。ftrace 能跟踪内核函数调用与调度延迟,是做实时性分析和定位卡顿的利器。perf 做用户空间与内核的性能剖析,火焰图能一眼看出热点。
内核崩溃(panic)时如果配了 pstore 或串口日志,能看到最后的调用栈。应用崩溃要开 core dump 并用交叉 gdb 分析。建议在产品上保留一个「诊断模式」,能按需开启详细日志并通过 边缘计算与边缘网关
里讲的日志回传通道送到云端,避免现场靠猜。
13. 权衡取舍
| 决策点 | 方案 A | 方案 B | 判据 |
|---|---|---|---|
| 发行版 | Yocto 自建 | Debian/Ubuntu 定制 | 需要极致裁剪与长期可控选 Yocto,快速原型可用 Debian |
| 引导 | U-Boot | 直接内核(无 U-Boot) | 需要 A/B、救援、多启动项用 U-Boot,极简设备可省 |
| 根文件系统 | squashfs 只读 | ext4 加 overlayfs | 追求最小与防篡改用 squashfs,需灵活写用 overlayfs |
| 更新粒度 | 全镜像 A/B | 包级更新 | 全镜像简单可靠,包级省流量但依赖管理复杂 |
| 内核 | 厂商 BSP 内核 | 上游 LTS 内核 | 初期用 BSP 快速上手,成熟后向 LTS 靠拢 |
| 实时性 | 普通内核 | PREEMPT_RT | 软实时用普通内核,毫秒级确定性需求上 RT 补丁 |
一条原则:把「可复现构建」和「可回滚更新」当作第一天就要建的基线,而不是后期补。嵌入式 Linux 的复杂度不在于让系统跑起来,而在于让它在五年后还能安全地升级。
14. 常见坑清单
- 现象:板子完全没输出。原因:引导镜像写错偏移或 DDR 初始化失败。规避:用厂商烧录工具,核对引导介质与偏移。
- 现象:内核起来了但挂不上根文件系统。原因:
root=设备名不对或rootwait缺失。规避:用PARTUUID并加rootwait。 - 现象:设备树改了但没生效。原因:用了旧 dtb 或没重新编译进 boot 分区。规避:确认启动加载的是新 dtb,检查 dtb 时间戳。
- 现象:驱动探测不到设备。原因:pinctrl 引脚复用配置错误或
status未置okay。规避:对照原理图核对 pinctrl,检查dmesg的 probe 日志。 - 现象:系统跑几天后无法写入。原因:只读根分区上写日志失败,或 data 分区写满。规避:日志写 data 分区,监控剩余空间并轮转。
- 现象:增量构建行为不一致。原因:用了
AUTOREV或未固定SRCREV。规避:所有配方锁死版本,CI 持久化 sstate 与 DL_DIR。 - 现象:OTA 升级后设备起不来。原因:A/B 标志未切换或新镜像校验失败。规避:实现启动计数与自动回滚,升级后校验签名。
- 现象:systemd 服务启动顺序错乱。原因:依赖关系写成
Requires=导致阻塞或时序错误。规避:用After=/Wants=表达顺序与弱依赖,用sd_notify报告就绪。 - 现象:CVE 越积越多无人管。原因:没有 CVE 扫描流程,依赖停在老版本。规避:CI 接入 cve-check,定期升级高危库。
- 现象:量产板与开发板行为不同。原因:eFuse、启动模式、器件差异未纳入构建配置。规避:为量产板单独建 MACHINE,固化启动介质与安全启动。
15. 小结
嵌入式 Linux 的工程核心是「用构建系统管住复杂度」。引导链、内核、设备树、根文件系统四部分必须版本匹配,Yocto 用配方与层把这套匹配关系固化下来,让定制可复现、可审计、可升级。掌握引导链、设备树、只读根文件系统与 A/B 更新这四块,就能覆盖大部分产品的需求。
长期维护是嵌入式 Linux 最容易被低估的部分。选主线支持的 SoC、锁死版本、接 CVE 扫描、产出 SBOM、保证可回滚更新,这些工作在项目初期看不出价值,却决定了产品五年后是否还能安全地打补丁。
下一步建议:实时任务与 MCU 侧调度在 RTOS 与 FreeRTOS 任务调度一文中有系统展开;镜像级升级与差分的实现细节属于 OTA 固件升级一文的范围;把 Linux 网关接进云边体系的整体设计见边缘计算与边缘网关一文。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。