编译和运行新版本Linux内核
暮春时节,咖啡店外的街道郁郁葱葱。
对天天使用Linux系统的人来说,内核是个既神秘又强大的存在。编译内核也是探索和学习Linux的一种方式,虽然不大可能真的将自己编译的内核用在桌面或者生产系统,也不大有能力为Linux内核贡献代码,但是了解内核的基本组成还是挺有趣的。
本文使用RockyLinux9作为编译机,编译最新版的Linux内核 6.14.6 版本,并编译busybox提供基础的shell环境。之后层层深入,从构建initramfs开始、制作rootfs、最后使用GRUB引导内核启动结束。
基础环境:
编译的host系统:RockyLinux 9
Busybox版本:1.37.0
内核源码版本:6.14.6
QEMU的版本:qemu-kvm-9.0.0
不同发行版本的glibc等基础软件有差异,不是所有的系统都能编译最新版本内核,比如CentOS 7编译Linux 6的源码就有问题。
编译Busybox和Linux
创建用于编译的目录结构,下载Busybox和Linux的源码,安装编译的依赖:
$ mkdir build_linux
$ mkdir run_linux
$ cd build_linux
$ wget https://mirrors.aliyun.com/linux-kernel/v6.x/linux-6.14.6.tar.xz
$ wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
$ sudo yum install -y gcc make glibc-static flex bison openssl-devel \
ncurses-devel elfutils-libelf-devel
从Linux官网 kernel.org 下载Linux源码比较慢,这里选择从阿里云的镜像源里下载Linux源码。
解压和编译过程中,目录会大小膨胀,建议使用至少有 10GB 可用空间的分区来做编译和运行的环境。
- build_linux 目录,用来保存下载和编译源码。
- run_linux 目录,用来保存Linux运行所需的文件。
此外,上面yum安装的基础依赖不一定就是全面的,如果后续编译遇到问题,可能还需要安装缺失的特定依赖。
编译安装 busybox
Busybox 是非常知名的工具软件,里面实现了各种常用的Linux命令,可以提供一个轻量的shell环境,用于和kernel交互。
为了方便使用,需要将 Busybox 编译成静态的二进制文件,之后运行时就没有依赖了, 编译静态二进制busybox的方法如下:
在make menuconfig
时的交互选择界面中,勾选 Build static binary (no shared libs)
,路径是
Settings -->
Build Options
[*] Build static binary (no shared libs)`
下面是具体的编译命令
$ tar xvf busybox-1.37.0.tar.bz2
$ cd busybox-1.37.0
$ make mrproper
$ make menuconfig # 一定要选择 Build static binary
$ make -j $(nproc)
make mrproper
,清理之前构建的中间产物,虽然我们是新下载解压的源码,但是编译前清理一下是个好习惯make menuconfig
交互式选择编译选项,只需要开启Build static binary
即可make -j $(nproc)
使用系统上所有的CPU核心进行编译,加快编译速度,也可以用-j
指定具体编译并发数。
验证编译出来的busybox二进制文件是否静态,是否能正常使用
$ ldd busybox
not a dynamic executable
$ ./busybox pwd
如果没报错,说明busybox的编译正常。
编译Linux内核
和上面的编译过程一样,编译Linux时也可以在make menuconfig
的过程中,自定义编译选项和模块。我一般会选择关闭虚拟化,多媒体和声卡,加快编译速度。
[ ] Virtualization
Device Drivers
< > Multimedia support
< > Sound card support
做好选择之后退出并保存,会存为 .config 文件。接下来编译Linux源码的操作,和编译Busybox差不多。
$ tar xvf linux-6.14.6.tar.xz
$ cd linux-6.14.6
$ make mrproper
$ make x86_64_defconfig # 生成针对 x86_64架构的默认配置文件
$ make menuconfig
$ make -j $(nproc)
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 6.14.6 () #...
$ ls -lh arch/x86/boot/bzImage
-rw-r--r--. 1 root root 13M May 17 10:41 arch/x86/boot/bzImage
如果make时遇到中报错:X.509 certificates to be preloaded into the system blacklist keyring
。
需要进行如下的menuconfig配置,将如下路径中的具体证书路径清空,保存,重新编译即可。
Cryptographic API
> Certificates for signature checking
> X.509 certificates to be preloaded into the system blacklist keyring # 清空配置的证书路径
我的32核老服务器CPU在几分钟内就完成了编译,速度还是非常不错的。
生成的 bzImage 是用于启动引导的内核文件,是压缩格式的,保存的路径为 arch/x86/boot/bzImage。这个文件非常袖珍,只有13M大小,真是神奇。
make oldconfig
可以直接使用之前生成的 .config,而不用交互式选择模块。
使用新的Linux内核
QEMU是一款开源的仿真和虚拟化软件,可以模拟多种CPU架构和系统硬件。为简单起见,直接yum安装即可。
RockyLinux 9提供的qemu-kvm包集成了内核kvm虚拟化技术,可以提高QEMU模拟器的性能。 QEMU在不同发行版中内置的版本可能不一样,比如Debian 12中就推荐安装qemu-system-x86,并使用 qemu-system-x86_64 命令启动
$ sudo yum install -y qemu-kvm
将之前编译生成的busybox和bzImage文件都复制到run_linux 目录下,后续的生成的启动文件也都放在 run_linux 目录下。
$ cd run_linux
$ cp ../build_linux/busybox-1.37.0/busybox ./
$ cp ../build_linux/linux-6.14.6/arch/x86_64/boot/bzImage ./
initrd 、 initramfs、rootfs的区别
Linux中initrd、initramfs、rootfs的区别:
- initrd(initial ramdisk):通常是一个压缩的块设备映像,是initramfs的前身,早期Linux中有所使用。加载到内存后需要挂载为一个临时文件系统。启动过程较长,现在已经很少使用。
- initramfs:基于内存的临时初始根文件系统,通常是cpio归档的格式,在系统启动过程中使用。在真正的rootfs被挂载之前,初始化硬件设备和加载必要的驱动(比如加载LVM模块),以便挂载真正的rootfs。系统启动过程结束后就不再需要,此时系统会切换到真正的rootfs。
- rootfs:这里指Linux系统磁盘上的根文件系统,包含了启动后系统运行所需的目录和文件,比如/bin、/etc、/dev、/proc等。通常作为系统的主文件系统,所有其他文件系统都会在其上挂载。rootfs通常包含了系统的长期存储数据。一般用硬盘等介质。
GRUB的menuentry中可能还会使用initrd这个配置项名称,但指向的文件一般是nitramfs格式的。
initramfs启动
有些简单的内核调试可能是不需要硬盘和持久化的,这时用initramfs就很方便。
创建initramfs_dir,将busybox文件放到initramfs_dir/bin下,后续将initramfs_dir 目录打包成cpio格式的initramfs.img文件。等启动initramfs后,busybox在系统中的路径为 /bin/busybox。
$ mkdir -p initramfs_dir/bin
$ cp busybox initramfs_dir/bin/
创建启动文件initramfs_dir/init
、内容如下
#!/bin/busybox sh
/bin/busybox mkdir /proc && /bin/busybox mount -t proc none /proc
/bin/busybox echo "Hello World."
/bin/busybox sh
脚本中做了一些简单的初始化操作,如果有需要,也可以做的更复杂些。
- 首先创建了 /proc 目录,然后挂载,有这个目录了就能运行ps命令
- 然后输出了“Hello World”
- 最后启动一个交互式的
sh
,提供给用户操作
init脚本中所有命令都是以busybox的方式运行的。下面为init文件添加可执行权限,它会在内核加载之后启动。
$ chmod +x initramfs_dir/init
为了方便测试,接下来创建Makefile,用来管理 initramfs和QEMU的启动命令
initramfs:
cd initramfs_dir && find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img
run:
/usr/libexec/qemu-kvm \
-kernel bzImage \
-initrd initramfs.img \
-m 256M \
-nographic \
-append "earlyprintk=serial,ttyS0 console=ttyS0"
initramfs
目标用于创建新的 initramfs.img 文件-
run
目标用于启动QEMU-kernel
和-initrd
参数指定了kernel文件和initramfs文件-m
指定了QEMU虚拟机的内存-nographic
指定不要启动图形界面-append
指定虚拟机的串口等信息
注意:makefile中的缩进是tab,不能是空格
接下来正式打包 initramfs和运行编译好的Linux。
$ make initramfs # 生成 initramfs.img 文件
$ make run
...
Hello World.
/ # busybox ls
bin dev init proc root
/ # busybox uname -a
Linux (none) 6.14.6 #1 SMP PREEMPT_DYNAMIC Sat May 17 10:40:54 EDT 2025 x86_64 GNU/Linux
/ # busybox ps -ef | busybox head -5
PID USER TIME COMMAND
1 0 0:00 {init} /busybox sh /init
2 0 0:00 [kthreadd]
3 0 0:00 [rcu_gp]
4 0 0:00 [rcu_par_gp]
可以看到内核版本,以及1号进程都和我们预设的一样。
注意: 命令前都需要加上 busybox 命令。
退出QEMU的方式:先按Ctrl-a 后按x。
这样启动的QEMU虚拟机运行在内存中,重启后,系统中所有的配置和修改都会丢失。如果要对数据进行持久化,可以创建一块虚拟磁盘作为系统的rootfs。
接下来测试rootfs的方案。
rootfs启动
为了摆脱busybox ls
这种麻烦的命令行方式,接下来,完善rootfs的目录结构。进入busybox的编译目录,继续执行 make install
命令。
$ cd build_linux/busybox-1.37.0/
$ make install
$ ls -l _install/usr/bin/wget
lrwxrwxrwx. 1 appweb appweb 17 May 17 11:54 _install/usr/bin/wget -> ../../bin/busybox # wget命令是到busybox的软连接
会生成 _install
目录,里面包含各类常用的命令文件,都以软连接的方式指向二进制文件 bin/busybox
。里面的文件已经具备了基本的目录结构。
创建rootfs目录和rootfs.img虚拟磁盘,对rootfs.img执行格式化和挂载,并将busybox新生成的 _install
目录下的内容复制到 rootfs目录下。
$ cd run_linux
$ mkdir rootfs
$ dd if=/dev/zero of=rootfs.img bs=1024k count=50
$ mkfs.ext4 rootfs.img
$ sudo mount rootfs.img rootfs/
$ sudo chown $UID rootfs
$ cp -r ../build_linux/busybox-1.37.0/_install/* rootfs/
$ sudo mkdir rootfs/{proc,dev,etc}
$ sudo mkdir rootfs/etc/init.d/
- 创建一个 50M 大小的文件,作为QEMU使用的虚拟磁盘
- 先将虚拟磁盘格式化为ext4,再挂载到rootfs目录下
- 将busybox的
_install
目录下的内容复制到 rootfs下
继续为 rootfs 创建其它基础目录结构和文件。创建 rootfs/etc/inittab,启动后的路径为 /etc/inittab,定义系统的运行级别和该级别下启动的进程,内容如下
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
inittab文件的第1列通常是id,第2列通常是数字0-6,但是我们这个没这么复杂,仅指定了3个动作触发相应的任务
- sysinit 表示系统初始化阶段,指定执行 /etc/init.d/rcS 文件做初始化的任务
- respawn表示如果进程终止,则启动一个登录的/bin/sh
- ctrlaltdel 表示用户按下
Ctrl+Alt+Del
组合键时执行的命令,卸载所有文件系统,并以只读方式重新挂载
创建 rootfs/etc/init.d/rcS,内容如下,会执行etc/fstab文件中指定的挂载操作
#! /bin/sh
/bin/mount -a
创建 rootfs/etc/fstab ,内容如下,挂载 proc 文件系统
proc /proc proc defaults 0 0
给etc/init.d/rcS
赋予可执行权限。然后卸载 rootfs
$ sudo chmod +x rootfs/etc/init.d/rcS
$ sudo umount rootfs
编写新的 Makefile.rootfs
,用于让QEMU以rootfs的方式启动内核
run:
/usr/libexec/qemu-kvm \
-kernel bzImage \
-m 256M \
-nographic \
-hda rootfs.img \
-append "root=/dev/sda rw earlyprintk=serial,ttyS0 console=ttyS0 loglevel=8"
整体上和initramfs的QEMU启动命令差不多,有细微差别
-hda rootfs.img
将rootfs.img 作为额外硬盘挂载到QEMU虚拟机中。- 不需要 initrd 参数指定initramfs文件了。
-
-append
中指定了kernel的参数- root=/dev/sda:指定根文件系统的位置。rw 或 ro:指定根文件系统以读写或只读模式挂载。
- 还可以指定 init 程序:init=/path/to/init。没指定的话,默认会尝试
/sbin/init
、/etc/init
、/bin/init
、/bin/sh
这些文件。
启动rootfs的虚拟机,使用make
命令的-f
选项指定为rootfs编写的Makefile文件Makefile.rootfs
$ make run -f Makefile.rootfs
之后进去系统就可以直接执行命令了
~ # ls -l /bin/ls
lrwxrwxrwx 1 0 0 7 May 17 09:07 /bin/ls -> busybox
~ # df -hT
Filesystem Type Size Used Available Use% Mounted on
/dev/root ext4 41.8M 10.1M 28.3M 26% /
devtmpfs devtmpfs 106.1M 0 106.1M 0% /dev
~ # free -m
total used free shared buffers cached
Mem: 227 15 211 0 0 2
-/+ buffers/cache: 13 213
Swap: 0 0 0
可以看到 ls 命令是到 busybox 文件的软连接。df和free命令也能正常显示
如此就具备了一个基本的Linux的雏形。此时修改系统的文件和配置,内容就会被持久化到虚拟磁盘中了,实际上就是 rootfs.img 这个文件中。
配置GRUB
GRUB 是Linux系统中常用的引导加载程序,负责在计算机启动时加载操作系统内核并移交控制权。与之相关的配置文件一般位于/boot/ 目录下。 接下来创建新的虚拟磁盘,并使用parted划分为2个分区,一个用于GRUB引导,一个用于主分区。
接下来,使用GRUB把我们的新Linux系统打造的更像样一些。
$ dd if=/dev/zero of=disk.img bs=1M count=50 # 50MB大小的磁盘
# 使用GPT分区
$ sudo parted disk.img mklabel gpt
$ sudo parted disk.img mkpart primary 1MiB 2MiB # 第一个分区
$ sudo parted disk.img set 1 bios_grub on # 标记为 BIOS Boot 分区
$ sudo parted disk.img mkpart primary ext4 2MiB 100% # 第二个分区,根分区
$ sudo parted disk.img print # 查看分区表
Number Start End Size File system Name Flags
1 1049kB 2097kB 1049kB primary bios_grub
2 2097kB 52.4MB 50.3MB ext4 primary
- 创建了50M大小的disk.img文件,作为后续QEMU挂载使用的虚拟磁盘。用parted标记为gpt格式
- 划定了2个分区,1M的那个用于BIOS Boot分区,剩余的空间是rootfs的根分区,并格式化为ext4
将新创建的disk.img 也挂载到本地,这里使用 losetup 工具挂载。
$ sudo losetup -Pf disk.img # 挂载
$ losetup -a # 查看挂载,获取实际的盘符,可能是loop0,也可以是其他的编号
$ ls /dev/loop0*
/dev/loop0 /dev/loop0p1 /dev/loop0p2 # 可以看到虚拟磁盘的设备和分区的设备了
$ sudo mkfs.ext4 /dev/loop0p2 # 谨慎确认后操作
$ mkdir disk
$ sudo mount /dev/loop0p2 disk
注意: 必须确定 /dev/loop0 是我们新创建的虚拟盘,如果误操作到物理磁盘,会产生主机系统损坏的严重后果。
- 挂载之后可以看到有
/dev/loop0p1
和/dev/loop0p2
两个分区(前提是之前没有挂载过文件)。 用mount进行的虚拟磁盘mount也会占用设备编号,我的环境中是 loop0,在你的设备中不一定,操作前注意用losetup -a
检查下实际的盘符,并修改为自己环境的配置。 - 挂载之后将根分区格式化为 ext4
- 将格式化好的根分区挂载到本地新创建的disk目录,然后就可以往里面写数据了;
下面为新磁盘写入rootfs的内容,我们直接从之前的rootfs.img 里复制过来
$ sudo mount rootfs.img rootfs
$ sudo cp -r rootfs/* disk/
使用grub2-install
往虚拟磁盘中安装GRUB 2引导加载程序。并将 bzImage 内核文件复制到 boot 目录下
$ sudo grub2-install \
--target=i386-pc \
--boot-directory=disk/boot \
/dev/loop0 # 操作前谨慎确认
$ sudo cp bzImage disk/boot/
注意: 必须确定 /dev/loop0 是我们新创建的虚拟盘,如果误操作到物理磁盘,会产生主机系统损坏的严重后果。
继续创建和编写GRUB 文件 disk/boot/grub2/grub.cfg
,指定加载模块和内核文件与内核参数。
# Begin /boot/grub2/grub.cfg
set default=0
set timeout=5
insmod part_gpt
insmod ext2
set root=(hd0,2)
menuentry "Linux 6.14.6" {
linux /boot/bzImage root=/dev/sda2 ro console=ttyS0,115200n8 earlyprintk=serial
}
卸掉挂载,然后用QEMU启动系统
$ sudo umount rootfs
$ sudo umount disk
$ /usr/libexec/qemu-kvm -hda disk.img -nographic
很快就能看到GRUB界面了!
GRUB结束后继续启动,就进入了内核启动阶段,最终就能看到一个不太完善的Linux系统了。
至此,我们自己编译的当前最新版本Linux内核就成功的通过GRUB引导了。
总结
首先,我们编译了一个Linux内核,又编译了busybox为新内核提供简单而又基础的shell环境。通过制作initramfs在QEMU中快速启动新内核,方便快速测试和验证;通过虚拟磁盘构建rootfs的方式,实现虚拟机数据的持久化;最后,通过配置GRUB引导,使得这个新的系统看起来更加有模有样。
但实际上,busybox提供的功能十分有限,缺乏大量的基础软件,远远不能作为真实的系统使用。如要想要进一步深入了解如何构建一个真实的Linux发行版,建议查看Linux From Scratch,书中逐步讲解了如何从头编译一个完整Linux系统,这本书也不长,主要的篇幅都是编译过程,读起来并不吃力。
本篇博客就到这里为止。后续还有一篇博客,将讲解如何开发一个简单而又无用的Linux内核模块,并使用gdb对其进行调试。
total 0 comments