TIP
本文适用于希望在物理机器上安装使用新内核的情况,记录一些笔者遇到过的问题。如果读者只是通过 qemu 模拟器进行内核学习并不需要阅读此节
假定读者已经完成编译内核并且成功得到了一个属于自己独立编译出来的 linux kernel,拿到新玩具当然要试一试啦,接下来希望可以在机器安装使用我们的新内核
仅安装内核是不够的,相信读者在编译的时候一定注意到了很多带有 [M] 的信息,它表示代码会被编译为内核模块而并不是直接被编译进内核,他往往需要在内核启动之后才会被装载(insmod)到内核
sudo make modules_install编译好的模块会被 cp 到 /lib/modules/ 下,实际上内核大量的代码都是内核模块的形式(各种驱动),包括 USB 驱动,硬盘网卡设备驱动等等等等,这些驱动对于一个真实的系统来说是必不可少的
安装内核,install 不会替换掉原有的内核, 只会将内核以及符号表等放到 /boot 目录下, 并更新 /boot/grub/grub.cfg
sudo make install更新 grub 启动项
sudo update-grub如果要使用新的内核只需要重启电脑然后进入 GRUB 中选择新的内核版本即可
如果使用真机的话长按 F12, 如果是 Vmware 的话长按 shift 进入 grub, 对于 Ubuntu 来说选择第二个
Advanced options for Ubuntu
或者额外新建两个目录, 分别用于保存安装内核所需的文件和所有的模块
mkdir install
mkdir modules
make install INSTALL_PATH=install/
make modules_install INSTALL_MOD_PATH=modules/其中 install/ 下保存着内核文件 vmlinuz-6.6.0, 系统符号表 System.map-6.6.0(并非 Linux 启动所必须的) 和编译内核的配置文件 config-6.6.0
modules/ 下保存着所有的模块, 它们可以在内核启动之后通过 insmod 动态的加载到内核中
按照上述说明编译后的内核可能会很大, 笔者编译的 linux6.6 已经有 300+ MB 的 vmlinux 和 12MB 的 bzImage 了。但大部分情况下我们并不需要 kernel 全部的功能, 因此可以在基础之上做一些内核的裁剪
这一步并不是必要的, 视个人情况而定, 很难讲怎么样是最好的
内核配置信息比较复杂, 笔者这里按经验总结了一些 linux6.6 内核配置文件, 适用于 linux6.6 版本
读者可以根据需要直接下载对应的文件, 即跳过前面的 make menuconfig 的部分, 直接使用现成的 .config 配置文件。可以关闭一些诸如 文件系统支持, 设备驱动, 无线网络支持, USB 支持, 图形支持, 声音等。具体见 realease 中的信息
如果希望给你的内核起一个名字, 可以修改 CONFIG_LOCALVERSION, 该内容会加在内核版本之后
CONFIG_LOCALVERSION=""大部分编译的内核由于没有驱动等支持, 所以只能在虚拟机上启动, 没有办法在真机启动 ubuntu. 如果希望在真机(ubuntu)启动可以下载提供 linux6.6 内核配置文件 中的 ubuntu.config 并打包为 deb
打包完成后会在上级目录生成一些文件, 启动 *.deb 文件是我们需要的, 安装 headers 和 image
sudo dpkg -i linux-headers-6.6.0+_6.6.0-ga472b7d4a578-12_amd64.deb
sudo dpkg -i linux-image-6.6.0+_6.6.0-ga472b7d4a578-12_amd64.deb此时会将 vmlinuz initrd.img config 等安装到 /boot 下, 可以使用如下 switch_kernel.sh 脚本替换内核, 可以输入需要选择内核, 此脚本将会自动修改 grub 配置并将该内核设为默认启动项
#!/bin/bash
# Check if the script is run as root
if [ "$(id -u)" -ne "0" ]; then
echo "This script must be run as root" 1>&2
exit 1
fi
# Define the directory where the kernel images are stored
KERNEL_DIR="/boot"
# List available kernel versions and assign a number to each
echo "Available kernel versions:"
kernels=($(ls ${KERNEL_DIR}/vmlinuz-*))
count=0
for kernel in "${kernels[@]}"; do
kernel_version=$(echo $kernel | sed 's/.*\/vmlinuz-//')
echo "[$count]: $kernel_version"
((count++))
done
echo ""
# Prompt the user to select a kernel version by number
read -p "Enter the number of the kernel version you want to switch to: " kernel_number
# Check if the input is a number and within the range
if ! [[ "$kernel_number" =~ ^[0-9]+$ ]] || [ "$kernel_number" -lt "0" ] || [ "$kernel_number" -ge "$count" ]; then
echo "Invalid selection"
exit 1
fi
# Get the kernel version based on the number
kernel_version=$(echo ${kernels[$kernel_number]} | sed 's/.*\/vmlinuz-//')
echo "Switching to kernel version: $kernel_version"
# Check if the selected kernel version exists
if [ ! -e "$KERNEL_DIR/vmlinuz-$kernel_version" ]; then
echo "Kernel version $kernel_version does not exist"
exit 1
fi
# Extract the menu entry for the default kernel
MID=`awk '/Advanced options.*/{print $(NF-1)}' /boot/grub/grub.cfg`
MID="${MID//\'/}"
KID=`awk -v kern="with Linux $kernel_version" '$0 ~ kern && !/recovery/ { print $(NF - 1) }' /boot/grub/grub.cfg`
KID="${KID//\'/}"
# Update GRUB configuration
sed -i "s/GRUB_DEFAULT=.*/GRUB_DEFAULT=\"$MID>$KID\"/" /etc/default/grub
update-grub
echo -e "\e[31mPlease reboot machine\e[0m"前文调试内核中介绍了 initramfs,这是一个内存文件系统,用于内核启动初期加载一些必要的文件系统驱动。当时笔者构建了一个非常简单的 initramfs 镜像,并使用了一个简单的 init 脚本完成了启动,下面我们学习一下现代 linux 发行版(Ubuntu)的 initramfs 构建和启动过程。
查看 /boot 目录我们可以发现在 Ubuntu 中并没有 initramfs.img,而是一个 initrd.img 的文件(以及不同版本的initrd.img)
(base) lzx@cxl2:~$ ls /boot/ | grep init
initrd.img
initrd.img-6.6.0autonuma+
initrd.img-6.6.0damon+
initrd.img-6.6.0vtism+
initrd.img.old在 grub 的启动项中我们也可以看到 initrd 参数
(base) lzx@cxl2:~$ grep initrd /boot/grub/grub.cfg
initrd /boot/initrd.img-6.6.0vtism+
initrd /boot/initrd.img-6.6.0vtism+
initrd /boot/initrd.img-6.6.0vtism+
initrd /boot/initrd.img-6.6.0damon+在 qemu 的启动项中我们也可以看到 -initrd 参数
qemu:
$(QEMU) \
-kernel $(KERNEL) \
-initrd $(INITRAMFS) \
-m 1G \
-nographic \
-append "earlyprintk=serial,ttyS0 console=ttyS0"那么initrd和initramfs有什么关系么? 这其实是一个历史遗留问题。要想解释这个问题需要从根源出发。
首先需要明确的是 initrd 和 initramfs 是两种不同的解决方案
initrd 的首先需要内置一个文件系统驱动(通常是ext2),同时由于 initrd.img 是一个块设备,Linux 的设计理念是缓存所有从块设备读取或写入的文件和目录项,因此 Linux 会将数据从块设备复制到内存中,这也浪费了内存缓存。Initrd 上的所有读写操作都被冗余地缓冲到主内存中。
相比之下 initramfs 的设计更加简洁轻量,解压 cpio 然后通过 ramfs 加载到内存中
NOTE
几年前,Linus Torvalds 提出了一个绝妙的想法:如果 Linux 的缓存可以像文件系统一样挂载会怎么样?只需将文件保存在缓存中,直到被删除或系统重启才会释放它们?Linus 为缓存编写了一个名为“ramfs”的轻量级封装,其他内核开发者创建了一个改进版本,名为“tmpfs”(它可以将数据写入交换空间,并限制给定挂载点的大小,使其在耗尽所有可用内存之前被填满)。Initramfs 是 tmpfs 的一个实例。
这些基于内存的文件系统会根据数据量自动增长或收缩。向内存文件系统 (ramfs) 添加文件(或扩展现有文件)会自动分配更多内存,而删除或截断文件则会释放这些内存。由于没有块设备,因此块设备和缓存之间不存在数据重复。缓存中的副本是数据的唯一副本。最重要的是,这并非新代码,而是对现有 Linux 缓存代码的新应用,这意味着它几乎不增加任何体积,非常简单,并且基于经过充分测试的基础架构。
「所以为什么还会在 Ubuntu 中看到已经被抛弃的 initrd 呢?」这只是一个历史遗留的命名问题。在非常早的 Linux(2.4 时代)中确实使用的是 initrd,当后来的 Linux 切换到 initramfs 机制后,很多发行版(包括 Ubuntu 和 Debian)为了保持命名稳定不改文件名,继续使用 initrd.img 的名称,但内部内容已经不是 initrd,而是 cpio 的 initramfs
$ file /boot/initrd.img-$(uname -r)
/boot/initrd.img-6.6.0vtism+: ASCII cpio archive (SVR4 with no CRC)简单来说为了兼容老的脚本,配置文件,文档,所以没有改名字,依然叫 initrd,不过其实已经都是 initramfs 的格式了(cpio)。GRUB、update-initramfs、initramfs-tools 的体系从 Debian 系诞生以来就使用,如果改动名字的话所有依赖这个路径的脚本都会被破坏,所以保持旧名称最安全。
GRUB 自己不会解析 initrd/initramfs 的内部格式,它只是把文件读进内存,然后调用内核参数
initrd (hd0,1)/boot/initrd.img-*qemu 同样如此,也是通过 initrd 参数传递给内核
内核会根据文件的魔数判断:
所以名字无论是什么,都不影响内核的判定。
initrd 和压缩过的内核 vmlinuz 都会保存在 /boot 目录下,可以使用 unmkinitramfs 对 initrd.img 进行解压操作
mkdir initrd_root
cd initrd_root
unmkinitramfs /boot/initrd.img-$(uname -r) .解压后的看到四部分,early/2/3 和 main
lzx@cxl2:~/klinux/initrd_root$ tree -L 2
.
|-- early
| `-- kernel
|-- early2
| `-- kernel
|-- early3
| `-- usr
`-- main
|-- bin -> usr/bin
|-- conf
|-- etc
|-- init
|-- lib -> usr/lib
|-- lib.usr-is-merged -> usr/lib.usr-is-merged
|-- lib32 -> usr/lib32
|-- lib64 -> usr/lib64
|-- libexec -> usr/libexec
|-- libx32 -> usr/libx32
|-- run
|-- sbin -> usr/sbin
|-- scripts
|-- usr
`-- var#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ -d /dev ] || mkdir -m 0755 /dev
[ -d /root ] || mkdir -m 0700 /root
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
# Note that this only becomes /dev on the real filesystem if udev's scripts
# are used; which they will be, but it's worth pointing out
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
# Prepare the /dev directory
[ ! -h /dev/fd ] && ln -s /proc/self/fd /dev/fd
[ ! -h /dev/stdin ] && ln -s /proc/self/fd/0 /dev/stdin
[ ! -h /dev/stdout ] && ln -s /proc/self/fd/1 /dev/stdout
[ ! -h /dev/stderr ] && ln -s /proc/self/fd/2 /dev/stderr
mkdir /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
# Export the dpkg architecture
export DPKG_ARCH=
. /conf/arch.conf
# Set modprobe env
export MODPROBE_OPTIONS="-qb"
# Export relevant variables
export init=/sbin/init
export readonly=y
export rootmnt=/root
# mdadm needs hostname to be set. This has to be done before the udev rules are called!
if [ -f "/etc/hostname" ]; then
/bin/hostname -F /etc/hostname >/dev/null 2>&1
fi
# Parse command line options
# shellcheck disable=SC2013
for x in $(cat /proc/cmdline); do
case $x in
init=*)
init=${x#init=}
;;
root=*)
ROOT=${x#root=}
if [ -z "${BOOT}" ] && [ "$ROOT" = "/dev/nfs" ]; then
BOOT=nfs
fi
;;
esac
done
# Default to BOOT=local if no boot script defined.
if [ -z "${BOOT}" ]; then
BOOT=local
fi
# Don't do log messages here to avoid confusing graphical boots
run_scripts /scripts/init-top
maybe_break modules
[ "$quiet" != "y" ] && log_begin_msg "Loading essential drivers"
[ -n "${netconsole}" ] && /sbin/modprobe netconsole netconsole="${netconsole}"
load_modules
[ "$quiet" != "y" ] && log_end_msg
log_begin_msg "Mounting root file system"
# Always load local and nfs (since these might be needed for /etc or
# /usr, irrespective of the boot script used to mount the rootfs).
. /scripts/local
. /scripts/nfs
. "/scripts/${BOOT}"
parse_numeric "${ROOT}"
maybe_break mountroot
mount_top
mount_premount
mountroot
log_end_msg
# Mount cleanup
mount_bottom
nfs_bottom
local_bottom
maybe_break bottom
[ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-bottom"
# We expect udev's init-bottom script to move /dev to ${rootmnt}/dev
run_scripts /scripts/init-bottom
[ "$quiet" != "y" ] && log_end_msg
# Move /run to the root
mount -n -o move /run ${rootmnt}/run
validate_init() {
run-init -n "${rootmnt}" "${1}"
}
# Check init is really there
if ! validate_init "$init"; then
echo "Target filesystem doesn't have requested ${init}."
init=
for inittest in /sbin/init /etc/init /bin/init /bin/sh; do
if validate_init "${inittest}"; then
init="$inittest"
break
fi
done
fi
# No init on rootmount
if ! validate_init "${init}" ; then
panic "No init found. Try passing init= bootarg."
fi
maybe_break init
# Move virtual filesystems over to the real filesystem
mount -n -o move /sys ${rootmnt}/sys
mount -n -o move /proc ${rootmnt}/proc
# Chain to real filesystem
# shellcheck disable=SC2086,SC2094
exec run-init ${drop_caps} "${rootmnt}" "${init}" "$@" <"${rootmnt}/dev/console" >"${rootmnt}/dev/console" 2>&1
echo "Something went badly wrong in the initramfs."
panic "Please file a bug on initramfs-tools."lzx@cxl2:~/klinux/initrd_root/main$ ./usr/bin/run-init --help
BusyBox v1.36.1 (Ubuntu 1:1.36.1-6ubuntu3.1) multi-call binary.
Usage: run-init [-d CAP,CAP...] [-n] [-c CONSOLE_DEV] NEW_ROOT NEW_INIT [ARGS]
Free initramfs and switch to another root fs:
chroot to NEW_ROOT, delete all in /, move NEW_ROOT to /,
execute NEW_INIT. PID must be 1. NEW_ROOT must be a mountpoint.
-c DEV Reopen stdio to DEV after switch
-d CAPS Drop capabilities
-n Dry run使用 Linux 发行版镜像的主要目的就是借用其已经编译好的软件包环境, 通常内核则使用自己修改后重新编译的 bzImage
qemu-system-x86_64 \
-m 4G \
-kernel bzImage \
-drive format=raw,file=disk/ubuntu.raw,if=virtio \
-append "root=/dev/vda2 console=ttyS0" \
-nographic -no-reboot -d guest_errors
-d guest_errors是 QEMU 的一个调试选项,用于记录和报告客户机操作系统中的错误, 这些错误信息会被输出到 QEMU 的日志中,以便开发人员或系统管理员进行调试和问题排查。
如果设置了 kernel 参数那么就会使用新内核而不是当前 ubuntu.raw 中的内核, 相当于不再使用 /dev/vda1 的 /boot 分区, 不会读取其中 /boot/grub/grub.cfg 的配置文件, 因此需要手动指定挂载的根分区所在的位置
启动的时候可能会出现如下报错: VFS: Unable to mount root fs on unknown wn-block(0,0)
可能有两个原因, 一个是因为现在使用的是 qemu 的虚拟机环境, 所以需要内核也支持虚拟化的设备/网络/PCI模拟, 需要在内核选项中开启如下的配置
CONFIG_EXT4_FS=y
CONFIG_XFS_FS=y
CONFIG_JBD2=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_BALLOON=y
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_NET=y
CONFIG_VIRTIO=y
CONFIG_VIRTIO_RING=y正常来说使用
defconfig的应该都已经开启了, 如果没有可以在 menuconfig 中手动搜索一下开启
不出意外的话就可以正常进入了