在CentOS系统上折腾内核编译,最让人后背发凉的时刻,不是编译报错,而是重启后系统直接黑屏。一旦新内核无法正常引导,而你又没有提前做好旧配置的备份,恢复系统往往要耗费大量时间。解决这个问题的核心思路其实很简单:在动手修改任何配置之前,把能让你系统成功启动的所有关键文件原封不动地保留下来,并建立一套清晰、可执行的回退机制。
备份哪些文件才能真正兜底很多人以为备份内核编译配置只需要保存.config文件,这在同一个小版本号内反复调试时够用,但一旦涉及内核版本升级或者补丁回退,光有.config远远不够。真正能让你在灾难发生后快速恢复的,是下面这四类文件。
第一类是完整的内核源码树配置。如果你是在内核源码目录下直接编译,除了隐藏文件.config,还应该把include/config目录、Module.symvers以及System.map一并打包。include/config里存放着根据.config展开后的所有宏定义,Module.symvers记录了模块符号版本信息,System.map则是内核符号表,调试和模块加载时都会用到。执行下面这条命令可以把这些关键状态一次性备份:
tar czf kernel-config-backup-$(date +%Y%m%d-%H%M%S).tar.gz \
.config \
include/config \
Module.symvers \
System.map \
arch/x86/boot/bzImage
第二类是/boot分区下的实际启动文件。CentOS的/boot目录里,每个内核版本对应一组vmlinuz、initramfs、System.map和config文件。在安装新内核之前,务必确认旧内核的这些文件都完好存在。可以用rpm -qa | grep kernel命令查看当前已安装的内核包,然后手动检查/boot下对应的文件是否齐全。如果发现缺失,立刻用yum reinstall kernel-版本号的方式补全。
第三类是GRUB引导配置。CentOS 7及以后版本使用GRUB2,主配置文件位于/boot/grub2/grub.cfg,但这个文件是由/etc/default/grub和/etc/grub.d/目录下的脚本自动生成的。因此,备份的重点应该是/etc/default/grub文件和整个/etc/grub.d/目录。执行cp -a /etc/default/grub /etc/default/grub.bak以及cp -a /etc/grub.d /etc/grub.d.bak即可。千万不要只备份grub.cfg,因为一旦重新生成,你手动备份的那个文件就会被覆盖。
第四类是正在运行的内核模块依赖关系。文件/lib/modules/$(uname -r)/modules.dep和modules.dep.bin记录了当前内核所有模块的依赖关系。如果新内核编译后模块加载异常,对比这两个文件往往能快速定位问题。建议在编译前执行cp -a /lib/modules/$(uname -r) /lib/modules/$(uname -r).backup,把整个模块目录保留一份。
编译前建立系统快照的完整流程在真正开始make menuconfig或者修改.config之前,花五分钟做一次完整的快照备份,这个习惯能避免90%以上的回退困境。具体操作步骤如下。
第一步,记录当前运行内核的精确版本。执行uname -r,把输出结果记下来。这个版本号就是你的救命稻草,后面所有回退操作都要以它为准。
第二步,确认当前内核对应的rpm包状态。运行rpm -qa | grep kernel-$(uname -r),确保这个内核是通过rpm包安装的,而不是手动编译后直接拷贝到/boot的。如果是rpm包安装的,回退会简单很多;如果是手动安装的,则需要依赖你自己做的文件备份。
第三步,备份/boot分区。最简单粗暴但也最有效的方法是把整个/boot目录打包:tar czf boot-backup-$(date +%Y%m%d).tar.gz /boot。注意,这个备份包不要放在/boot分区本身,应该存到/root或者外接存储上。
第四步,备份/lib/modules下当前内核的模块目录。执行cp -a /lib/modules/$(uname -r) /root/modules-backup-$(uname -r)。如果磁盘空间紧张,至少要把modules.dep、modules.dep.bin以及kernel子目录下的核心驱动模块保留。
第五步,导出当前内核的完整配置。如果当前运行的内核在编译时开启了CONFIG_IKCONFIG_PROC选项,可以直接从/proc/config.gz获取配置:zcat /proc/config.gz > /root/running-kernel-config。如果没有开启这个选项,就从/boot/config-$(uname -r)复制一份。这份配置可以作为新内核编译的起点,也可以用来对比差异。
第六步,备份GRUB配置。除了前面提到的/etc/default/grub和/etc/grub.d/目录,还建议执行grub2-mkconfig > /root/grub.cfg-backup,把当前生成的完整引导菜单保存下来,方便后续对照。
编译过程中的增量备份策略内核编译往往不是一次就能成功的,中间会经历多次配置调整。如果在make menuconfig里改了几十个选项后系统编译失败,想要回到上一次能编译通过的状态,没有增量备份就只能从头再来。一个高效的策略是利用git来管理.config文件的变更历史。
在内核源码目录下执行git init && git add .config && git commit -m "初始配置",之后每次修改.config并确认编译通过后,都执行一次git add .config && git commit -m "描述本次改动"。这样做的好处是,你可以随时用git diff对比任意两次配置之间的差异,也能用git checkout快速回滚到之前的某个版本。如果源码目录本身已经是git仓库(比如从kernel.org克隆的),那就更简单了,直接创建一个专门存放配置的分支即可。
对于没有使用git的场景,也可以在每次编译前用带时间戳的文件名保存.config副本,比如cp .config config-backups/config-$(date +%Y%m%d-%H%M%S)。配合一个简单的脚本,每次make之前自动执行备份,能省去很多手动操作。
新内核安装失败后的三种回退方案假设你已经编译并安装了新内核,重启后系统无法正常启动,这时需要根据故障的严重程度选择合适的回退方案。
方案一:通过GRUB菜单临时选择旧内核启动。这是最快速的方法,前提是GRUB菜单能够正常显示。开机时按住Shift键(UEFI系统按Esc),进入GRUB菜单后选择旧版本内核启动。如果系统能正常进入,说明问题只出在新内核本身,旧内核的文件都还完好。进入系统后,你可以从容地分析新内核的问题,或者直接卸载新内核。
方案二:使用救援模式修复。如果GRUB菜单都无法显示,或者选择旧内核后仍然启动失败,就需要借助CentOS安装光盘或USB启动盘进入救援模式。启动后在引导界面选择Troubleshooting,然后选择Rescue a CentOS system。系统会提示你挂载原有的根分区,选择Continue后,你的原有系统会被挂载到/mnt/sysimage目录下。执行chroot /mnt/sysimage切换到原有系统环境,然后就可以操作GRUB和内核文件了。
在chroot环境中,首先检查/boot目录是否完整。如果发现vmlinuz或initramfs文件丢失,可以从之前做的备份中恢复。然后执行grub2-install /dev/sda(假设系统盘是sda)重新安装GRUB到MBR或EFI分区,再执行grub2-mkconfig -o /boot/grub2/grub.cfg重新生成引导配置。完成这些操作后退出chroot并重启,通常就能恢复引导。
方案三:从rpm包层面彻底回滚。如果旧内核是通过yum安装的,而新内核是手动编译安装的,可以直接在救援模式的chroot环境下用yum reinstall kernel-旧版本号来恢复。如果连旧内核的rpm包都已经卸载了,则需要从CentOS的安装光盘或镜像站下载对应版本的kernel rpm包,用rpm -ivh --force命令强制安装。安装完成后务必重新生成initramfs:dracut -f /boot/initramfs-旧版本号.img 旧版本号。
利用dracut重建initramfs解决模块加载问题有一种常见但容易被忽视的启动失败原因,是新内核编译时模块安装路径与initramfs中的不一致,导致启动时无法加载必要的磁盘驱动或文件系统模块。这种情况下,内核本身没有问题,但initramfs里缺少关键模块。
解决方法是在chroot环境下,针对旧内核重新生成initramfs。命令格式为dracut -f /boot/initramfs-$(uname -r).img $(uname -r)。如果系统无法确定当前应该使用哪个内核版本,就手动指定:dracut -f /boot/initramfs-3.10.0-1160.el7.x86_64.img 3.10.0-1160.el7.x86_64。重建完成后,检查/boot目录下对应的initramfs文件大小是否正常(通常应该在几十MB),如果文件只有几KB,说明生成过程出错,需要检查/lib/modules下对应版本的模块目录是否完整。
建立自动化备份与回退脚本对于需要频繁编译内核的场景,手动备份效率太低且容易遗漏。下面这个脚本可以在每次编译前自动执行完整备份,并在/boot分区保留最近三次的备份快照。
#!/bin/bash
BACKUP_DIR="/root/kernel-backups"
BOOT_BACKUP_DIR="/root/boot-backups"
CURRENT_KERNEL=$(uname -r)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p $BACKUP_DIR $BOOT_BACKUP_DIR
# 备份当前运行内核的模块
if [ ! -d "$BACKUP_DIR/modules-$CURRENT_KERNEL" ]; then
cp -a /lib/modules/$CURRENT_KERNEL $BACKUP_DIR/modules-$CURRENT_KERNEL
fi
# 备份当前内核配置
cp /boot/config-$CURRENT_KERNEL $BACKUP_DIR/config-$CURRENT_KERNEL-$TIMESTAMP
# 备份GRUB配置
cp -a /etc/default/grub $BACKUP_DIR/grub-default-$TIMESTAMP
cp -a /etc/grub.d $BACKUP_DIR/grub.d-$TIMESTAMP
# 备份/boot分区,保留最近三份
tar czf $BOOT_BACKUP_DIR/boot-$TIMESTAMP.tar.gz /boot
ls -t $BOOT_BACKUP_DIR/boot-*.tar.gz | tail -n +4 | xargs -r rm
echo "备份完成,时间戳:$TIMESTAMP"
将这个脚本保存为/usr/local/bin/kernel-backup.sh,赋予执行权限chmod +x /usr/local/bin/kernel-backup.sh,然后在每次编译前执行即可。如果希望更进一步,可以在make install命令之前自动触发备份,只需要在编译流程中加入对这个脚本的调用。
从灾难中总结出的配置管理原则经历过内核编译失败的人都会形成一套自己的防御性操作习惯。总结下来,最重要的原则有三条。第一,永远不要删除能正常启动的内核,yum remove kernel时务必确认当前运行的内核不在删除列表里。第二,修改配置时一次只改一个功能模块,改完就编译测试,不要贪多。第三,/boot分区至少保留两个已知可用的内核版本,这样即使最新的编译结果有问题,也始终有一个兜底选项。
这些原则听起来简单,但在实际运维中很容易因为磁盘空间不足或者赶进度而被忽略。CentOS的/boot分区默认大小通常只有500MB到1GB,如果同时保留多个内核版本和对应的initramfs,空间很容易吃紧。解决方法是定期清理确实不再需要的旧内核,但清理之前务必确认当前系统不是通过这些旧内核启动的。一个安全的清理命令是package-cleanup --oldkernels --count=2,这会保留最新的两个内核,删除其余的历史版本。
掌握了这些备份与回退的方法,内核编译就不再是一场豪赌。即使新内核启动失败,你也能在几分钟内让系统恢复到之前的状态,然后从容地分析日志、调整配置,准备下一次尝试。
