4. Linux内核配置与编译

根文件系统,说白了就是Linux启动后看到的/目录下那一整套东西。没有它,内核启动到一半就会panic——因为它找不到init进程。这一章我们就来亲手把它搭出来。

4.1 Busybox:一个程序撑起一个用户空间

Busybox是什么?我习惯叫它“瑞士军刀”。它把上百个常用Linux命令(ls、cp、sh、mount……)塞进一个可执行文件里,通过符号链接或硬链接来区分调用哪个命令。

核心原理:Busybox的main函数会检查argv[0],根据程序名跳转到对应的命令实现。所以一个二进制文件就能干所有事。

4.1.1 下载与配置

从官网或镜像站拉一份源码,我一般用长期支持版本:

wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

配置方式有三种:

配置命令 适用场景 说明
make defconfig 快速起步 启用大部分通用功能,适合初学者
make menuconfig 定制裁剪 图形界面,逐项勾选,我项目里常用这个
make allyesconfig 全功能测试 打开所有选项,镜像会很大,不推荐生产用

我个人习惯先用defconfig打底,再用menuconfig微调。比如关掉不需要的vi、telnetd,打开mdev(设备热插拔支持)。

小技巧:交叉编译时记得设置CROSS_COMPILE环境变量,比如export CROSS_COMPILE=arm-linux-gnueabihf-。否则编译出来的是x86版本,放到ARM板上跑不了。

4.1.2 编译与安装

配置好之后,编译安装一气呵成:

make -j$(nproc)
make install CONFIG_PREFIX=/path/to/rootfs

CONFIG_PREFIX指定安装目标目录。执行完后,那个目录下就会出现bin/、sbin/、usr/等文件夹,里面全是Busybox的符号链接。

注意:make install不会自动创建dev/、proc/、sys/等目录,这些需要手动补上。我曾经第一次做根文件系统时忘了建dev/,结果启动后控制台没输出,排查了半天才发现是/dev/console不存在。

4.2 根文件系统目录结构:不只是放文件

Linux根文件系统的目录结构有FHS(Filesystem Hierarchy Standard)规范,但嵌入式系统可以适当精简。下面是我常用的最小化结构:

目录 用途 是否必须
/bin 用户命令(ls, cp, sh…) 是
/sbin 系统管理命令(init, mount, ifconfig…) 是
/etc 配置文件(inittab, fstab, passwd…) 是
/dev 设备节点 是
/proc 进程信息虚拟文件系统 是
/sys 内核对象虚拟文件系统 是
/tmp 临时文件 建议
/var 可变数据(日志、锁文件) 建议
/mnt 挂载点 可选
/usr 用户程序和数据 可选

创建这些目录很简单:

mkdir -p rootfs/{bin,sbin,etc,dev,proc,sys,tmp,var,mnt,usr/{bin,sbin,lib}}

4.3 init进程与inittab:系统启动的“总指挥”

内核启动的最后一步是执行/sbin/init。这个init程序(Busybox内置的)会读取/etc/inittab,按里面的规则启动各种服务。

4.3.1 inittab文件格式

每一行代表一个动作,格式是:

id:runlevels:action:process
  • id:控制台设备名,比如ttyS0,可以留空
  • runlevels:运行级别,Busybox忽略此项,通常留空
  • action:执行时机,常见的有sysinit、respawn、askfirst、once、shutdown
  • process:要执行的程序或脚本

下面是一个典型的最小化inittab:

::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L ttyS0 115200 vt100
::askfirst:/bin/sh
::shutdown:/bin/umount -a -r

解释一下:

  • sysinit:系统初始化时运行/etc/init.d/rcS脚本
  • respawn:启动串口登录,进程退出后自动重启
  • askfirst:在控制台显示“Please press Enter to activate this console”,按回车后进入shell
  • shutdown:关机时卸载所有文件系统

经验之谈:我一般把askfirst放在最后作为保底shell,这样即使其他服务起不来,至少还能进控制台排查问题。

4.3.2 rcS初始化脚本

/etc/init.d/rcS是一个shell脚本,负责挂载虚拟文件系统、配置网络、启动服务等。一个简单的版本:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mdev -s
ifconfig lo 127.0.0.1
echo "System ready."

mdev -s会根据/sys中的设备信息自动创建/dev下的节点。这比手动创建成千上万个设备节点靠谱多了。

4.4 制作根文件系统镜像

根文件系统可以放在多种介质上:initramfs(内存盘)、SD卡分区、NFS网络挂载、Flash文件系统(jffs2、ubifs)等。这里我以最常用的initramfs和ext4镜像为例。

4.4.1 制作initramfs(cpio格式)

initramfs是内核直接支持的压缩内存文件系统。制作步骤:

cd /path/to/rootfs
find . | cpio -H newc -o | gzip > ../rootfs.cpio.gz

然后把rootfs.cpio.gz作为内核的initramfs源,编译进内核或由bootloader加载。

调试技巧:内核启动参数加rdinit=/sbin/init可以指定initramfs里的init路径。如果启动卡住,加console=ttyS0,115200 initcall_debug看详细日志。

4.4.2 制作ext4根文件系统镜像

适合放在SD卡或eMMC上:

dd if=/dev/zero of=rootfs.ext4 bs=1M count=64
mkfs.ext4 rootfs.ext4
mount -o loop rootfs.ext4 /mnt
cp -a /path/to/rootfs/* /mnt/
umount /mnt

这样就得到一个64MB的ext4镜像,可以直接dd到分区上。

4.4.3 目录结构总览

下面这张SVG图展示了根文件系统的核心组成和启动流程:

根文件系统核心结构与启动流程 Linux 内核 /sbin/init (Busybox) /etc/inittab /etc/init.d/rcS 挂载proc/sys/tmp /sbin/getty 串口登录 /bin/sh (askfirst) 控制台shell 说明: 内核启动后执行init,init读取inittab,按action类型分别执行rcS(系统初始化)、getty(登录)、shell(交互)。 这三个分支并行运行,互不阻塞。

4.5 避坑指南

做根文件系统时,有几个坑我踩过不止一次:

  • 设备节点缺失:至少要有/dev/console和/dev/null,否则控制台没输出。可以用mknod手动创建,或者靠mdev自动生成。
  • inittab格式错误:Busybox的inittab解析比较严格,多一个空格或少一个冒号都可能导致init拒绝执行。我曾经因为行尾多了个空格,折腾了一下午。
  • rcS没有执行权限:记得chmod +x /etc/init.d/rcS,否则init会报错“can't execute '/etc/init.d/rcS'”。
  • 动态链接库缺失:如果Busybox编译成动态链接,根文件系统里必须包含ld-linux.so.*和libc.so.*。用arm-linux-gnueabihf-readelf -d busybox可以查看依赖。

重要提醒:生产环境中,根文件系统不要用initramfs存放可写数据,因为它是内存盘,掉电丢失。日志、配置修改等应该挂载持久化分区(比如/data或/overlay)。

好了,到这一步,你已经有了一个可以启动的最小化根文件系统。把它和内核、设备树打包在一起,就能看到熟悉的Linux登录提示符了。