1. 嵌入式Linux系统概述

嵌入式Linux,说白了就是把Linux这套操作系统塞进那些不是电脑的设备里。你家的路由器、智能电视、汽车中控台,甚至医院里的CT机,里面跑的很可能就是嵌入式Linux。

我刚开始接触这行的时候,也觉得不就是把Linux装上去嘛。后来踩了不少坑才明白——嵌入式Linux和桌面Linux完全是两码事。桌面Linux是通用平台,而嵌入式Linux是为特定硬件量身定做的。

什么是嵌入式Linux

嵌入式Linux,就是经过裁剪、定制,运行在嵌入式设备上的Linux操作系统。它保留了Linux内核的核心能力——进程管理、内存管理、文件系统、网络协议栈——但去掉了桌面系统那些花里胡哨的东西。

举个例子,你电脑上的Ubuntu安装完占好几个G,但一个嵌入式Linux系统,可能整个系统才几兆。为什么?因为不需要图形桌面、不需要办公软件、不需要那些你用不上的驱动。

核心特点:

  • 可裁剪——按需配置,要什么功能就加什么
  • 实时性——通过RT补丁或PREEMPT_RT实现硬实时
  • 资源受限——可能只有几MB的Flash和几十MB的内存
  • 稳定性要求高——设备可能几年不关机

我记得有一次给一个工业控制器做系统,Flash只有8MB,还得塞进内核、文件系统和应用程序。那段时间天天跟配置选项较劲,删一个模块省几百K,加一个功能多几十K。嗯,这就是嵌入式Linux的日常。

系统三大件:Bootloader、内核、根文件系统

一个完整的嵌入式Linux系统,说白了就三样东西:引导程序、内核、根文件系统。少了哪个都跑不起来。

组件 作用 常见实现 存储位置
Bootloader 硬件初始化,加载内核到内存 U-Boot、Barebox Flash起始扇区
内核 管理硬件资源,提供系统调用 Linux Kernel Flash或eMMC
根文件系统 提供目录结构、应用程序、库文件 BusyBox、Buildroot、Yocto Flash、RAM或网络

Bootloader

设备上电后,CPU首先执行的就是Bootloader。它负责初始化时钟、内存、串口这些最基本的硬件,然后把内核从Flash拷贝到内存,跳转过去执行。

U-Boot是目前最主流的Bootloader。我习惯在U-Boot里加一个倒计时,按任意键进入命令行,不按就自动启动。调试阶段特别有用——万一内核起不来,还能在U-Boot里改启动参数。

我的经验:U-Boot的环境变量保存在Flash里,可以设置bootargs传给内核。比如指定console=ttyS0,115200,这样内核启动信息就能从串口输出了。调试时这个参数救过我很多次。

内核

内核是系统的核心。它管理CPU、内存、中断、设备驱动、网络协议栈……几乎所有硬件资源都由内核统一调度。

嵌入式Linux内核和桌面Linux内核用的是同一套源码,区别在于配置。桌面系统会编译进大量驱动和功能模块,而嵌入式系统只选需要的。你想想看,一个路由器需要显卡驱动吗?不需要。所以配置内核就是做减法。

根文件系统

内核启动完成后,需要挂载一个根文件系统。根文件系统里放着init进程、shell、各种命令、库文件、配置文件。

嵌入式系统常用的根文件系统有几种:

  • initramfs——和内核打包在一起,启动快,适合小系统
  • JFFS2/UBIFS——针对Flash优化的文件系统,支持磨损均衡
  • NFS——网络挂载,开发调试阶段用,不用反复烧写Flash

我曾经在一个项目里用initramfs,整个根文件系统才2MB,启动时间不到3秒。客户要求上电即用,这个方案正好满足。

系统启动流程

从按下电源到进入shell,整个流程是这样的:

  1. 上电——CPU从复位向量执行,通常是Flash的0地址
  2. Bootloader第一阶段——初始化CPU、时钟、内存控制器
  3. Bootloader第二阶段——初始化串口、网口、Flash,加载内核
  4. 内核解压——如果内核是压缩的,先解压到内存
  5. 内核初始化——初始化中断、调度器、内存管理、驱动
  6. 挂载根文件系统——根据bootargs指定的root参数挂载
  7. 启动init进程——通常是/sbin/init,读取inittab
  8. 启动shell或应用程序——进入用户态

这个流程我背得滚瓜烂熟。为什么?因为每次系统起不来,就得顺着这个链条排查——是Bootloader没加载成功?内核解压出错?还是根文件系统挂载失败?

嵌入式Linux系统启动流程 上电 Bootloader 内核解压 内核初始化 挂载根文件系统 启动init进程 用户态应用程序 💡 调试要点: 串口输出是排查启动问题的第一利器。Bootloader阶段能看到U-Boot版本号,内核阶段能看到驱动加载日志。

开发环境搭建

做嵌入式Linux开发,首先得把环境搭好。说白了就是两件事:交叉编译工具链和虚拟机。

交叉编译工具链

什么是交叉编译?你在x86的电脑上写代码,编译出ARM架构的可执行文件,这就叫交叉编译。因为目标设备(ARM)性能太弱,没法在上面直接编译。

工具链包含三部分:

  • 编译器——arm-linux-gnueabihf-gcc
  • 链接器——arm-linux-gnueabihf-ld
  • 调试器——arm-linux-gnueabihf-gdb

获取工具链有几种方式:

方式 说明 推荐场景
Linaro预编译工具链 ARM官方维护,直接下载解压 快速上手,初学者首选
Buildroot构建 自动下载源码编译工具链 需要定制glibc版本
Yocto/OpenEmbedded 完整构建系统,生成工具链 大型项目,商业产品
手动交叉编译 从源码编译gcc、binutils、glibc 学习目的,特殊架构

⚠️ 避坑指南:我曾经在Linaro官网下载了一个armv7的工具链,结果目标芯片是armv5。编译出来的程序一运行就报非法指令。所以一定要确认工具链的架构版本和你的CPU匹配。

安装工具链其实很简单:

# 下载Linaro工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到/opt
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt

# 添加环境变量
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证
arm-linux-gnueabihf-gcc --version

验证通过后,你就可以编译第一个嵌入式程序了:

arm-linux-gnueabihf-gcc -o hello hello.c
file hello
# 输出: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)

虚拟机配置

我习惯用Ubuntu作为开发主机。为什么?因为嵌入式Linux社区的工具链、文档、示例大多基于Ubuntu/Debian。你用其他发行版也行,但遇到问题查资料时,Ubuntu的解决方案最多。

虚拟机配置建议:

  • 内存——至少4GB,编译内核时内存越大越快
  • 磁盘——至少40GB,内核源码、工具链、编译产物都很占空间
  • CPU——分配2-4核,make -j参数可以并行编译
  • 网络——桥接模式,方便NFS挂载和TFTP下载

我的习惯:在虚拟机里装好Ubuntu后,第一件事就是安装必备软件包:build-essential、git、flex、bison、libncurses-dev。这些是编译内核的必需品。我一般写个脚本一键安装,省得每次重装虚拟机再折腾一遍。

sudo apt update
sudo apt install -y build-essential git flex bison \
  libncurses-dev libssl-dev device-tree-compiler \
  u-boot-tools qemu-system-arm

嗯,环境搭好了,后面就可以开始真正的内核配置和编译了。记住,开发环境是地基,地基不稳后面全是坑。