从零构建定制Linux内核与系统镜像:嵌入式开发与系统裁剪实战

📅 2026/8/26 10:26:02
从零构建定制Linux内核与系统镜像:嵌入式开发与系统裁剪实战
1. 项目概述与核心价值最近在折腾一个嵌入式项目需要为一块特定的开发板定制一个轻量级的Linux系统。官方的Ubuntu Server镜像虽然方便但内核版本固定驱动支持不全还带了一堆我用不上的服务和软件包占用了宝贵的存储空间和内存。于是我决定从最底层开始自己动手编译一个精简的Linux内核并以此为基础制作一个完全贴合我需求的系统镜像。这个过程说白了就是“内核编译”加“镜像制作”两件大事。对于很多从应用层开发转向底层系统或者从事嵌入式、IoT、云计算基础设施工作的朋友来说掌握这套流程是进阶的必经之路。它不仅能让你彻底理解一个Linux系统是如何从源代码“生长”出来的更能让你获得对系统的绝对控制权你可以裁剪掉所有冗余功能只保留必需的驱动和子系统让系统跑得更快、更省资源你可以打上最新的安全补丁或者集成还未被主流发行版收录的硬件驱动你甚至可以为特定的应用场景比如高实时性、高安全性调整内核的成百上千个配置参数。自己编译内核并制作镜像就像是为你的硬件和应用量身定制一套最合身的“操作系统西装”而不是去商场买一件可能袖子长、腰身松的成衣。接下来我将以Ubuntu 22.04 LTS作为编译环境详细拆解从获取内核源码、配置、编译到创建根文件系统、打包成可启动镜像的全过程。我会重点分享我在这个过程中踩过的坑和总结出的实用技巧目标是让你看完后能独立完成一次完整的内核定制与镜像构建。2. 编译环境准备与内核源码获取工欲善其事必先利其器。编译内核是个资源消耗型任务尤其是链接阶段对内存和CPU要求较高。一个稳定、纯净的编译环境能避免很多稀奇古怪的问题。2.1 基础编译环境搭建我选择在一台物理机上的Ubuntu 22.04 LTS中进行操作当然在配置足够的虚拟机中也可以。首先我们需要安装所有必要的开发工具和库文件。打开终端执行以下命令来更新软件包列表并安装基础编译工具链sudo apt update sudo apt upgrade -y sudo apt install -y build-essential libncurses-dev libssl-dev bc \ flex bison libelf-dev dwarves zstd cpio git这里解释一下几个关键包的作用build-essential: 提供了gcc,g,make等最核心的编译工具。libncurses-dev: 用于支持make menuconfig这种文本图形化配置界面没有它你就只能面对纯文本配置文件或者用make xconfig需要图形界面。libssl-devbc: 内核配置和编译过程中某些环节如生成证书会用到。flexbison: 语法分析器生成器编译内核时需要。libelf-dev: 处理ELF可执行与可链接格式文件所需的库对现代内核构建至关重要。dwarves: 包含pahole工具用于优化内核数据结构布局对生成更高效的内核有帮助。zstd: 一种高效的压缩算法内核镜像压缩可能会用到。注意如果你的Ubuntu版本较新或较旧某些包名可能有细微差别。安装完成后可以用gcc --version和make --version验证工具链是否就绪。2.2 获取内核源码获取内核源码主要有两种途径一是从Linux内核官方仓库下载二是使用你的发行版如Ubuntu提供的内核源码包。方案一从 kernel.org 获取推荐用于深度定制这是最直接、最上游的方式。你可以访问 https://www.kernel.org/ 查看最新稳定版stable或长期支持版longterm。使用wget下载例如下载6.6版本cd ~ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz tar -xvf linux-6.6.tar.xz cd linux-6.6方案二使用Ubuntu提供的内核源码这种方式获取的内核已经包含了Ubuntu特定的补丁和配置如果你只是想基于当前系统内核进行微调这可能更方便。sudo apt install linux-source-$(uname -r)安装后源码包通常是tar.xz格式会放在/usr/src/目录下需要解压。我个人更倾向于方案一因为它版本清晰没有额外的补丁干扰更适合学习原理和进行干净的定制。本次演示我们就使用从kernel.org下载的6.6版本源码。实操心得在解压源码的目录外单独建立一个编译输出目录build是很好的实践可以将源码树和编译生成的文件分离保持源码目录的整洁也方便进行多种配置的编译。我们可以这样操作mkdir -p ~/kernel_build cd ~/linux-6.6 # 后续的编译输出将指向 ~/kernel_build 目录3. 内核配置定制的艺术这是整个过程中最核心、最考验经验的一步。内核有上万个配置选项决定了最终内核支持什么硬件、包含什么功能、性能如何。3.1 配置界面与基础方法进入内核源码目录我们有几种配置方式make defconfig: 生成一个基于当前架构x86_64的默认配置。这是起点。make olddefconfig: 在已有.config文件基础上用新内核版本的默认值来更新并自动解决新出现的选项。从旧配置升级时常用。make menuconfig: 基于ncurses的文本图形化界面最常用不需要完整的图形桌面环境。make xconfig: 基于Qt的图形化配置界面更直观但需要安装Qt库和图形界面。make gconfig: 基于GTK的图形化配置界面。对于服务器环境make menuconfig是首选。我们先生成一个默认配置然后进入定制界面cd ~/linux-6.6 make O~/kernel_build defconfig # 使用O指定输出目录 make O~/kernel_build menuconfig执行menuconfig后你会看到一个分层级的菜单界面。使用方向键导航空格键选择[*]表示编译进内核[M]表示编译为模块[ ]表示不编译/键可以搜索配置项?键查看帮助。3.2 关键配置项解析与裁剪策略面对海量选项新手容易无从下手。我的策略是先继承后裁剪再特化。继承现有配置一个非常高效的方法是使用当前运行系统的配置作为起点。这能最大程度保证新内核能驱动你现有的硬件。cp /boot/config-$(uname -r) ~/kernel_build/.config make O~/kernel_build olddefconfig这条命令复制当前系统的配置并用新内核的默认值填充新增的选项。核心裁剪方向文件系统如果你只为特定设备制作镜像可以只保留它需要的文件系统。例如只用于SD卡或eMMC的嵌入式设备可以主要保留ext4去掉btrfs,xfs,ntfs等。在File systems菜单下操作。设备驱动这是裁剪的大头。在Device Drivers菜单下你可以根据目标硬件移除所有不相关的驱动。比如你的目标设备是ARM架构的嵌入式板没有USB摄像头、没有特定的PCIe网卡就可以大胆移除。但要小心一些基础驱动如ATA/ATAPI/MFM/RLL support,SCSI device support即使你用不到SCSI但很多子系统依赖它可能需要保留。网络支持如果设备不需要网络可以在Networking support中彻底禁用。如果需要则只启用必要的协议如TCP/IP和网络设备驱动如具体的网卡型号。调试与性能分析Kernel hacking下的很多调试选项会显著增大内核体积并影响性能在生成最终产品镜像时应关闭。内核压缩模式在General setup-Kernel compression mode中可以选择压缩算法。zstd压缩率好且解压速度快是比较现代的选择。一个重要的技巧使用localmodconfig如果你是为当前编译的这台机器定制内核有一个神器make O~/kernel_build localmodconfig这个命令会分析当前系统加载了哪些内核模块lsmod的输出然后修改.config文件只将当前正在使用的驱动和功能编译进内核或模块其他的全部禁用。这能生成一个非常精简的配置。执行后记得再运行make olddefconfig处理一下依赖。踩坑记录曾经有一次我裁剪得太激进把CONFIG_BLK_DEV_INITRD初始RAM磁盘支持给关掉了。结果内核无法加载initramfs导致系统启动失败。对于通用系统一些看似无关的基础功能如initramfs支持、EFI支持、/dev和/proc文件系统支持一定要确认是否需要。配置完成后保存退出。配置文件会保存在~/kernel_build/.config。强烈建议备份这个文件。4. 内核编译与安装配置搞定接下来就是相对自动化的编译过程了。4.1 启动编译过程使用make命令进行编译。为了充分利用多核CPU加速编译可以加上-j参数指定并行任务数。通常设置为CPU核心数 1或CPU核心数 * 2。你可以用nproc命令查看核心数。cd ~/linux-6.6 make O~/kernel_build -j$(($(nproc)1))这个过程视硬件性能而定可能需要十几分钟到一小时以上。你会看到大量的编译信息滚动。如果中途出现错误通常是缺少某个依赖库根据错误信息安装对应的-dev包即可。4.2 安装编译产物编译成功后我们需要安装内核模块和内核镜像文件。安装模块模块会被安装到指定的目录我们通常将其安装到一个临时目录以便后续打包进根文件系统。sudo make O~/kernel_build modules_install INSTALL_MOD_PATH~/rootfs这里INSTALL_MOD_PATH~/rootfs指定了安装路径~/rootfs是我们接下来要构建的根文件系统目录。如果不指定默认会安装到宿主系统的/lib/modules下这可能会干扰现有系统。准备内核镜像编译生成的核心文件是vmlinuz压缩的内核映像和System.map内核符号表。我们将其复制出来。cp ~/kernel_build/arch/x86/boot/bzImage ~/my_custom_kernel/vmlinuz-6.6.0-custom cp ~/kernel_build/System.map ~/my_custom_kernel/ cp ~/kernel_build/.config ~/my_custom_kernel/config-6.6.0-custom注意如果你是ARM等其它架构bzImage的路径会是arch/arm/boot/zImage等。至此内核部分的工作就完成了。我们得到了一个定制化的内核文件vmlinuz-6.6.0-custom和对应的模块位于~/rootfs/lib/modules/下。5. 构建根文件系统内核负责管理硬件和进程但一个可用的系统还需要用户空间userspace的程序比如基本的shell、工具集ls,cp,mkdir等、初始化系统。这些就构成了根文件系统rootfs。5.1 使用debootstrap构建最小Ubuntu根文件系统对于基于Debian/Ubuntu的环境debootstrap是构建根文件系统的标准工具。它可以安装一个最小的Ubuntu系统到指定目录。sudo apt install debootstrap sudo mkdir -p ~/rootfs sudo debootstrap --archamd64 jammy ~/rootfs http://archive.ubuntu.com/ubuntu/--archamd64: 指定架构。jammy: Ubuntu 22.04的代号。~/rootfs: 目标目录。http://archive.ubuntu.com/ubuntu/: 软件源地址。这个过程会下载并安装最基础的包。完成后~/rootfs目录下就是一个干净的Ubuntu最小系统。5.2 基础系统配置与必要软件安装现在我们需要chroot进入这个新系统进行一些配置。# 挂载必要的虚拟文件系统 sudo mount --bind /dev ~/rootfs/dev sudo mount --bind /dev/pts ~/rootfs/dev/pts sudo mount -t proc proc ~/rootfs/proc sudo mount -t sysfs sysfs ~/rootfs/sys # 使用chroot进入新环境 sudo chroot ~/rootfs /bin/bash现在你的终端环境已经切换到了新的根文件系统内。设置基础信息# 设置主机名 echo my-custom-ubuntu /etc/hostname # 设置root密码 passwd # 配置软件源可选保持默认也可 # cat /etc/apt/sources.list EOF # deb http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse # deb http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse # deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse # EOF apt update安装必要软件根据你的需求安装。至少需要apt install -y systemd-sysv udev sudo net-tools iproute2 iputils-ping vimsystemd-sysv: 提供systemd并替换sysvinit。udev: 动态设备管理。sudo: 权限管理。网络工具用于调试。创建普通用户可选但推荐adduser myuser usermod -aG sudo myuser配置网络最简单的配置是使用DHCP。cat /etc/netplan/01-netcfg.yaml EOF network: version: 2 renderer: networkd ethernets: eth0: dhcp4: yes EOF配置串口终端对于无显示器的嵌入式设备很重要编辑/etc/systemd/system/getty.target.wants/gettytty1.service但更简单的方法是创建一个systemd的覆盖或直接修改inittab的替代品。对于使用systemd的系统可以启用串口gettysystemctl enable serial-gettyttyS0.service # 将ttyS0替换为你的实际串口设备如ttyAMA0树莓派退出chroot并卸载exit # 退出chroot环境 sudo umount ~/rootfs/dev/pts sudo umount ~/rootfs/dev sudo umount ~/rootfs/proc sudo umount ~/rootfs/sys5.3 整合自定义内核模块还记得我们之前把模块安装到~/rootfs了吗现在它们已经在正确的位置了~/rootfs/lib/modules/6.6.0-custom/。你需要确保模块的依赖关系正确生成# 回到宿主环境在rootfs目录外操作 sudo chroot ~/rootfs depmod -a 6.6.0-custom6. 制作可启动磁盘镜像现在我们有内核vmlinuz和根文件系统rootfs需要把它们打包成一个硬盘镜像文件如img或qcow2这个镜像可以被虚拟机直接启动或者写入到物理SD卡、硬盘中。6.1 创建空白镜像并分区我们将创建一个1GB大小的RAW格式镜像并分成两个区一个小的boot分区存放内核和一个大的root分区存放根文件系统。cd ~ dd if/dev/zero ofmy-ubuntu.img bs1M count1024 # 创建1GB空白文件 sudo parted my-ubuntu.img --script mklabel msdos # 创建MBR分区表 sudo parted my-ubuntu.img --script mkpart primary fat32 1MiB 256MiB # 创建256M的FAT32 boot分区 sudo parted my-ubuntu.img --script mkpart primary ext4 256MiB 100% # 剩余空间创建ext4根分区6.2 挂载镜像并格式化分区我们需要使用loop设备将镜像文件虚拟成块设备然后对分区进行格式化。# 查找可用的loop设备并关联镜像文件 LOOP_DEV$(sudo losetup --show -f -P my-ubuntu.img) echo $LOOP_DEV # 会输出类似 /dev/loop0 # 格式化分区 sudo mkfs.vfat -F 32 ${LOOP_DEV}p1 # 格式化boot分区为FAT32 sudo mkfs.ext4 ${LOOP_DEV}p2 # 格式化root分区为ext4 # 创建挂载点并挂载 mkdir -p mnt/{boot,root} sudo mount ${LOOP_DEV}p1 mnt/boot sudo mount ${LOOP_DEV}p2 mnt/root6.3 复制文件系统与内核复制根文件系统使用rsync或cp复制rsync能更好地保留权限和属性。sudo rsync -a ~/rootfs/ mnt/root/安装内核与引导程序复制内核到boot分区sudo cp ~/my_custom_kernel/vmlinuz-6.6.0-custom mnt/boot/vmlinuz安装引导程序这里以extlinux为例它比GRUB更轻量sudo apt install extlinux sudo extlinux --install mnt/boot/创建extlinux配置文件sudo tee mnt/boot/extlinux.conf EOF DEFAULT linux LABEL linux SAY Booting My Custom Ubuntu... KERNEL /vmlinuz APPEND root/dev/sda2 rw quiet splash # 注意root/dev/sda2 指定了根分区。在虚拟机或实际设备中这个设备名可能会变如/dev/mmcblk0p2。 # 更稳妥的做法是使用UUID或PARTUUID。 EOF获取root分区的UUIDsudo blkid ${LOOP_DEV}p2然后将root/dev/sda2替换为rootUUIDxxxx-xxxx。6.4 收尾工作# 同步数据并卸载 sync sudo umount mnt/boot sudo umount mnt/root sudo losetup -d $LOOP_DEV # 解除loop设备关联 # 清理挂载点 rm -rf mnt现在my-ubuntu.img就是一个包含了你的自定义内核和根文件系统的可启动磁盘镜像了。你可以用qemu虚拟机测试它qemu-system-x86_64 -drive filemy-ubuntu.img,formatraw -m 10247. 常见问题与排查技巧实录在整个过程中你可能会遇到各种问题。这里记录几个我遇到过的典型问题及解决方法。7.1 编译阶段错误错误fatal error: openssl/opensslv.h: No such file or directory原因缺少OpenSSL开发头文件。解决安装libssl-dev包。sudo apt install libssl-dev错误recipe for target ‘scripts/mod/empty.o’ failed原因通常是因为bc计算器工具未安装或版本问题。解决确保已安装bc。sudo apt install bc错误No rule to make target ‘debian/canonical-certs.pem’原因这是Ubuntu内核源码特有的签名问题。使用上游kernel.org源码通常不会遇到。解决在.config中关闭CONFIG_SYSTEM_TRUSTED_KEYS和CONFIG_SYSTEM_REVOCATION_KEYS将其值设为空字符串。7.2 启动阶段错误现象内核panic提示VFS: Unable to mount root fs on unknown-block(0,0)原因内核找不到根文件系统。这是最常见的问题。排查检查内核配置确认CONFIG_BLK_DEV_INITRD如果需要initramfs和对应文件系统如CONFIG_EXT4_FSy已编译进内核y而不是模块m。检查引导参数检查extlinux.conf或grub.cfg中的root参数。设备名如/dev/sda2在虚拟机、物理机、不同控制器下会变化。强烈建议使用UUID。获取UUIDsudo blkid /dev/sdX2(或你的分区)。修改引导配置rootUUIDxxxx-xxxx检查根文件系统完整性确保rootfs复制完整且必要的目录/dev,/proc,/sys,/run在启动后能被正确挂载由systemd或init脚本完成。现象启动后卡住无法进入登录提示符原因可能是初始化系统systemd或getty服务出现问题。排查在引导参数的root后面加上init/bin/bash例如root/dev/sda2 rw init/bin/bash。这会让内核直接启动bashshell而不是systemd。如果能进入bash检查/var/log/boot.log或journalctl的日志如果journal可用。检查/etc/systemd/system/getty.target.wants/下的服务是否正常。可以尝试手动启动/lib/systemd/systemd-getty tty1。7.3 镜像相关与测试问题QEMU启动报错Could not read from CD-ROM (code 0003)原因QEMU默认会尝试从CD-ROM启动。解决在QEMU命令中显式指定从硬盘启动并关闭CD-ROM-boot orderc -cdrom none制作的镜像文件太大原因dd创建的镜像文件是“稀疏文件”的相反它实实在在地写入了1GB的零数据。优化可以使用truncate创建稀疏文件或者编译完成后用qemu-img convert转换并压缩为qcow2格式后者支持动态分配空间更省磁盘。qemu-img convert -f raw -O qcow2 my-ubuntu.img my-ubuntu.qcow2使用qemu-img启动时指定-drive filemy-ubuntu.qcow2,formatqcow2。整个流程走下来虽然步骤繁多但每一步都有其明确的目的。从内核配置的精细打磨到根文件系统的精心组装再到镜像的最终合成你获得的不仅仅是一个可启动的镜像更是对Linux系统从内核到用户空间的完整认知。当你第一次看到屏幕上滚动着自己编译的内核的启动信息并成功登录到自己构建的系统时那种成就感是无可替代的。这为你后续进行更深入的性能调优、安全加固或嵌入式定制打下了坚实的基础。