多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Ubuntu 22.04内核源码修改与编译全流程实战指南

Ubuntu 22.04内核源码修改与编译全流程实战指南 1. 项目概述为什么要修改内核源码在Linux世界里内核是操作系统的核心它管理着硬件资源并为所有运行的程序提供服务。对于绝大多数Ubuntu用户来说使用官方预编译的内核就足够了。但总有一些场景会把你推到需要亲手修改内核源码的境地。比如你正在开发一个全新的硬件驱动需要在内核中增加支持或者你发现了一个影响你特定工作负载的内核Bug官方补丁还没发布你需要自己动手修复又或者你正在进行内核相关的学术研究或安全测试需要在内核中植入特定的监控或调试代码。这些都不是日常操作但一旦遇到掌握从源码修改到编译安装的完整流程就成了从“系统使用者”迈向“系统理解者”甚至“贡献者”的关键一步。修改内核源码听起来很“硬核”容易让人联想到复杂的交叉编译、漫长的等待时间。但得益于Ubuntu完善的包管理体系和社区支持这个过程在Ubuntu 22.04 LTS上可以被梳理得相对清晰和可控。本教程的目标就是为你提供一个从零开始、手把手的实操指南涵盖获取源码、配置环境、修改代码、编译打包、安装测试的全流程。我会基于我多次为特定硬件如定制工控板和性能调优如调整调度器参数而修改内核的实际经验不仅告诉你每一步怎么做更会解释为什么这么做以及过程中可能遇到的“坑”和应对技巧。无论你是驱动开发者、系统研究员还是单纯想深入学习Linux内核的爱好者这篇指南都将为你铺平道路。2. 环境准备与源码获取动手修改内核之前一个稳定、纯净的编译环境至关重要。在虚拟机里操作是个好主意这样即使编译失败或新内核无法启动也不会影响你的主力工作机。2.1 基础编译环境搭建首先确保你的Ubuntu 22.04系统是最新的并安装所有必要的编译工具和依赖库。打开终端执行以下命令sudo apt update sudo apt upgrade -y sudo apt install build-essential libncurses-dev libssl-dev bc flex bison libelf-dev dwarves -y我们来拆解一下这些包的作用build-essential: 这是编译任何C/C项目的基石包含了gcc、g、make等核心工具链。没有它编译无从谈起。libncurses-dev: 提供字符终端下的图形界面库后续使用make menuconfig进行内核配置时那个经典的蓝色配置界面就依赖它。libssl-dev: 提供OpenSSL开发库。现代内核的模块签名等功能需要加密支持缺少它可能导致配置或编译错误。bc: 一个任意精度计算器语言。内核的配置脚本Kconfig在计算某些依赖关系时会用到它。flex bison: 词法分析器和语法分析器生成器。内核构建系统在解析某些配置文件时会用到它们。libelf-dev dwarves: 用于处理ELF可执行与可链接格式文件。dwarves工具包中的pahole工具尤为重要它用于优化内核数据结构的内存布局消除“空洞”对生成高效的内核映像至关重要。在较新的内核版本中pahole已经是编译的硬性要求。注意安装这些包会占用几百MB的磁盘空间这是编译内核的必要投资。如果后续编译报错提示缺少某个头文件或库通常可以通过apt search和apt install来查找并安装对应的-dev包。2.2 获取Ubuntu官方内核源码获取源码有多种方式我强烈推荐使用Ubuntu官方提供的apt source方法。这能确保你获取的源码版本与当前系统使用的内核版本完全一致并且包含了Ubuntu特定的补丁和配置最大程度保证兼容性。首先查看你当前运行的内核版本uname -r假设输出是5.15.0-91-generic。其中5.15.0-91是版本号generic是风味flavor。启用源码仓库并安装源码包默认情况下/etc/apt/sources.list中的源码仓库是注释掉的。我们需要启用它并安装对应版本的源码。# 启用源码行通常只需要取消注释deb-src开头的行 sudo sed -i /^#\sdeb-src /s/^#// /etc/apt/sources.list sudo apt update # 安装特定版本的内核源码包 # linux-source-5.15.0 是源码包名它会拉取该主版本下的最新源码 sudo apt source linux-source-5.15.0执行apt source后它会在当前目录下载并解压源码包。你会看到一个名为linux-5.15.0的目录具体名称可能带有Ubuntu版本号如linux-5.15.0-91.101这就是我们的内核源码树。如果你想获取与uname -r输出完全一致的精确版本包括子版本和ABI编号可以尝试安装linux-source-$(uname -r)但通常主版本源码包就足够了因为它包含了所有子版本的补丁。实操心得使用apt source相比从 kernel.org 下载原始源码最大的好处是它已经打好了所有Ubuntu维护的补丁并且其默认配置.config就是Ubuntu发行版使用的配置。这能避免很多因配置差异导致的驱动缺失或硬件兼容性问题。对于初学者和大多数针对Ubuntu系统的修改这是最稳妥的起点。2.3 源码树结构与初步准备进入解压后的内核源码目录cd linux-5.15.0*/花几分钟熟悉一下顶级目录结构这对后续定位要修改的文件很有帮助arch/: 与CPU架构相关的代码。x86_64的在arch/x86ARM的在arch/arm和arch/arm64。drivers/: 所有设备驱动代码这是最常修改的目录之一。fs/: 文件系统实现如ext4, btrfs, proc等。include/: 内核头文件。kernel/: 核心内核代码如进程调度sched/、定时器等。net/: 网络协议栈。Makefile: 顶层Makefile定义编译规则。在开始修改前我们需要一个基础的配置文件。最方便的方法是复制当前运行内核的配置cp /boot/config-$(uname -r) .config这个.config文件包含了当前系统内核的所有编译选项以此为基础进行修改能确保新内核具备你现有系统的所有功能。3. 内核配置与修改策略有了源码和基础配置下一步就是根据你的需求调整配置或直接修改源代码。这是整个过程中最具技术含量的一步。3.1 使用交互式菜单配置内核直接编辑.config文件是困难的因为有成千上万个选项。我们使用交互式工具make menuconfig这会打开一个基于ncurses的文本图形界面。你可以通过方向键导航空格键选中/取消选中[*]表示编译进内核[M]表示编译为模块[ ]表示不编译Enter键进入子菜单Esc键返回。常见配置场景添加新的驱动支持例如你需要一个实验性的USB驱动。导航到Device Drivers - USB support - USB Serial Converter support找到对应驱动按M或Y启用。启用内核调试功能导航到Kernel hacking可以启用KGDB内核调试、Ftrace函数跟踪等这对开发者和研究者非常有用。精简内核如果你在为资源受限的环境如嵌入式设备编译内核可以在这里关闭大量不需要的驱动和功能以减小内核体积。修改完成后选择 Save 保存到.config文件然后退出。注意事项make menuconfig会自动处理选项间的依赖关系。如果你手动编辑.config文件可能会破坏这种依赖导致编译错误或功能异常。因此强烈建议通过menuconfig进行主要修改。对于只修改一两个已知选项的情况可以使用scripts/config脚本例如./scripts/config --enable CONFIG_XXX。3.2 定位与修改源代码假设我们的目标是修改一个已有驱动的行为或者添加一小段调试代码。首先需要找到对应的源文件。以修改进程调度器的某个参数为例确定目标比如我们想修改CFS完全公平调度器的sched_min_granularity_ns最小调度粒度。搜索定位在源码根目录下使用grep搜索。grep -r sched_min_granularity_ns --include*.c --include*.h搜索结果显示该变量定义在kernel/sched/fair.c文件中。分析上下文用你喜欢的编辑器如vim, vscode打开该文件搜索变量名。你会发现它可能是一个static const变量或者通过sysctl接口暴露。如果是const直接修改其数值即可。但要注意内核中很多参数为了性能被定义为常量修改后可能需要重新编译相关文件。进行修改/* 在 kernel/sched/fair.c 中找到类似定义 */ // 修改前 unsigned int sysctl_sched_min_granularity 750000ULL; // 修改后例如调整为1毫秒 unsigned int sysctl_sched_min_granularity 1000000ULL;修改原则保持代码风格一致缩进、注释理解你修改的内容的含义。如果不确定最好查阅内核文档或邮件列表。另一个常见场景添加打印调试信息在驱动或内核代码的关键路径添加printk是常用的调试手段。#include linux/printk.h // 通常已包含 // 在你想调试的函数中 printk(KERN_INFO MyDriver: Entering function %s with value %d\n, __func__, some_var);KERN_INFO是日志级别。调试完成后务必记得移除或注释掉这些调试打印以免污染系统日志dmesg影响性能。3.3 为修改打上补丁最佳实践如果你计划将修改提交给上游社区或者只是想优雅地管理你的更改应该使用git和生成补丁的方式。Ubuntu的内核源码目录通常已经是一个git仓库。检查git状态并创建分支cd linux-5.15.0*/ git status # 查看是否有未跟踪文件通常是正常的 git checkout -b my-custom-modification进行你的代码修改。提交更改到本地分支git add . git commit -s -m sched: Adjust min granularity to 1ms for my workload-s选项会自动添加你的Signed-off-by行这是向内核社区提交补丁的要求。生成补丁文件git format-patch HEAD~1这会生成一个像0001-sched-Adjust-min-granularity-to-1ms-for-my-workload.patch的文件。这个补丁文件可以应用于其他同版本内核源码树方便分享和复用。核心技巧即使不打算提交社区我也强烈建议在修改前先git init如果目录不是git仓库或直接利用现有git跟踪。每次修改前先git add . git commit -m snapshot before change X。这样当你的修改导致编译失败或系统崩溃时你可以轻松地git reset --hard回退到上一个可工作状态这比重新解压源码要高效得多。4. 编译与构建内核包配置和修改完成后就进入了编译阶段。对于Ubuntu我们通常不是直接编译出bzImage就结束而是编译生成Debian格式的安装包.deb这样可以利用包管理系统来安装、卸载和清理。4.1 启动编译过程在源码根目录下执行编译命令。为了充分利用多核CPU加速编译使用-j参数指定并行任务数通常设为CPU核心数 1。# 首先确保配置是最新的 make olddefconfig # 这会将新的默认值应用到现有.config上静默处理新选项 # 开始编译内核映像和模块 time make -j$(nproc) bindeb-pkg命令详解make olddefconfig: 这是一个非常重要的步骤。在你从旧内核复制.config或通过menuconfig修改后内核版本间可能会有新增的配置选项。这个命令会检查这些新选项并自动将其设置为默认值def而不进行交互式提问确保.config对于当前源码树是完整且有效的。make bindeb-pkg: 这是针对Debian/Ubuntu发行版的关键目标。它不仅仅编译内核还会编译内核映像vmlinuz、所有启用的模块。生成一个包含内核头文件用于开发外部模块的包。生成一个包含内核映像和核心模块的包。生成一个包含调试符号的包如果配置中启用了CONFIG_DEBUG_INFO。将所有这些东西打包成标准的.deb安装包。time: 用于统计编译耗时让你对整个过程的时间有个预期。首次完整编译一个内核在普通台式机上可能需要30分钟到2小时不等取决于硬件性能和内核配置的复杂度。编译过程会在终端输出大量信息。只要没有以Error结尾的致命错误就可以继续。常见的警告Warning可能很多尤其是关于未使用的参数等对于自定义编译通常可以忽略但如果是向主线提交代码则需要处理。4.2 处理编译中的常见问题即使环境准备充分编译也可能出错。以下是一些典型问题及解决方法paholenot found 或版本过低错误信息BASH: line 1: pahole: command not found或pahole version v1.XX is too old, need v1.XX or newer原因dwarves工具包中的pahole工具缺失或版本不满足内核构建要求。解决# 确保已安装最新版 dwarves sudo apt install --upgrade dwarves # 如果仍报版本旧可能需要从源码编译 dwarves # sudo apt install cmake libelf-dev zlib1g-dev # git clone https://github.com/acmel/dwarves.git # cd dwarves mkdir build cd build cmake .. make sudo make installOpenSSL相关错误错误信息openssl/bio.h: No such file or directory原因libssl-dev未安装或安装不完整。解决重新安装libssl-dev:sudo apt install --reinstall libssl-dev内存不足OOM Killer现象编译进程突然被杀死终端显示Killed。原因并行编译任务过多消耗内存超过系统物理内存交换空间。解决减少并行编译任务数。将-j$(nproc)改为-j2或-j4。也可以临时增加交换空间# 创建一个4GB的交换文件 sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile # 编译完成后可以关闭sudo swapoff /swapfile sudo rm /swapfile依赖缺失导致的头文件找不到错误信息fatal error: xxx.h: No such file or directory原因缺少某个特定功能的开发包。解决使用apt search和apt-file来查找提供该头文件的包。sudo apt install apt-file sudo apt-file update apt-file search xxx.h找到包名后安装对应的-dev包。编译成功后你会在源码目录的上一级目录../找到生成的.deb包通常有以下几个linux-headers-5.15.0-custom_5.15.0-custom-1_amd64.deb内核头文件linux-image-5.15.0-custom_5.15.0-custom-1_amd64.deb内核映像与模块linux-libc-dev_5.15.0-custom-1_amd64.debC库头文件包名中的custom是你的本地版本标识符在make bindeb-pkg时可以通过LOCALVERSION变量自定义例如make LOCALVERSION-mydebug bindeb-pkg。5. 安装、测试与回滚生成安装包后安装过程就变得和安装任何其他软件包一样简单。但内核是系统的基石安装新内核需要格外谨慎。5.1 安装新内核包在存放.deb文件的目录中执行安装# 安装内核映像和头文件包 sudo dpkg -i linux-image-*.deb linux-headers-*.debdpkg -i会完成以下工作将新内核映像vmlinuz-5.15.0-custom和System.map文件复制到/boot/。将编译好的内核模块安装到/lib/modules/5.15.0-custom/目录下。运行update-initramfs为新的内核版本生成初始内存磁盘映像initrd.img这个映像包含了启动早期阶段必需的驱动和工具。运行update-grub将新内核条目添加到GRUB引导菜单中。关键检查点 安装完成后务必检查以下文件是否已正确生成ls -lh /boot/vmlinuz-5.15.0-custom /boot/initrd.img-5.15.0-custom ls -lh /lib/modules/5.15.0-custom/如果initrd.img没有生成可以手动创建sudo update-initramfs -c -k 5.15.0-custom。5.2 重启并进入新内核重启你的系统sudo systemctl reboot在GRUB引导菜单界面选择“高级选项”Advanced options你应该能看到新内核的条目例如“Ubuntu with Linux 5.15.0-custom”。选择它启动。成功进入系统后验证内核版本uname -r输出应该是5.15.0-custom。同时检查你之前所做的修改是否生效。例如如果你修改了调度器参数可以通过检查/proc/sys/kernel/下的对应sysctl文件或者通过dmesg | grep查找你添加的打印信息来确认。5.3 系统稳定性测试与性能评估新内核安装后不要立即投入生产环境。建议进行一段时间的稳定性测试基础功能测试网络、声音、显卡、外设USB、蓝牙是否正常工作。压力测试可以运行一些压力测试工具如stress-ng观察系统在高负载下是否稳定。sudo apt install stress-ng stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 1G --timeout 300s特定工作负载测试运行你修改内核所要支持或优化的那个特定应用程序或工作负载观察其行为是否符合预期。5.4 安全回滚方案这是最关键的保障步骤。在安装新内核前必须确保有可靠的回退路径。GRUB菜单回滚最简单的方法。重启电脑在GRUB菜单中选择之前稳定工作的旧内核版本启动即可。GRUB通常会保留最近几个内核版本。使用包管理器卸载如果新内核问题严重可以在旧内核中启动后卸载自定义内核包。# 查看已安装的linux-image包 dpkg -l | grep linux-image # 卸载自定义内核包 sudo dpkg -r linux-image-5.15.0-custom # 重新配置GRUB移除无效条目 sudo update-grub紧急救援如果新内核导致无法进入任何系统极罕见但可能发生你需要使用Live USBUbuntu安装U盘启动然后挂载你的系统根分区chroot进去进行修复或卸载操作。为此在开始整个修改流程前手边准备一个可启动的Ubuntu安装U盘是明智的。血泪教训我曾有一次为了优化网络延迟激进地修改了TCP拥塞控制参数编译安装后导致服务器网络吞吐量急剧下降且不稳定。幸亏GRUB里还有旧内核重启切换后立刻恢复正常。从此我养成了一个习惯永远在虚拟机或非关键测试机上做第一次内核修改和启动测试在生产环境或主力机上确保至少有一个经过充分测试的旧内核作为备份。6. 进阶技巧与深度定制当你熟悉了基本流程后可以尝试一些更高级的操作让内核定制更贴合你的需求。6.1 优化编译速度与产物使用ccache加速后续编译ccache会缓存C/C编译结果当重复编译相同代码时直接使用缓存能极大提升增量编译速度。sudo apt install ccache # 在源码目录的Makefile中或在编译命令前设置 export CCccache gcc export CXXccache g # 或者更简单在调用make时 make CCccache gcc -j$(nproc) bindeb-pkg精简调试信息以减小内核体积默认配置可能包含大量调试符号CONFIG_DEBUG_INFO这会使内核映像和模块变得非常庞大。如果你不需要使用kgdb等底层调试工具可以在make menuconfig中关闭它Kernel hacking - Compile-time checks and compiler options - Compile the kernel with debug info (取消选中)关闭后重新编译生成的内核包体积会显著减小。6.2 为内核打第三方补丁有时你的需求已经有社区高手写好了补丁比如为特定硬件提供更好支持的性能补丁如著名的xanmod、liquorix内核就包含了许多优化补丁。应用补丁的方法如下下载补丁文件通常以.patch或.diff结尾。确保你的内核源码目录是干净的使用git status查看或先git stash暂存你的修改。在源码根目录应用补丁# 使用 patch 命令-p1 表示忽略补丁文件中的第一级目录前缀 patch -p1 /path/to/your.patch如果补丁应用成功它会提示哪些文件被修改。如果失败有reject文件说明补丁与当前源码版本不兼容需要手动解决冲突这需要一定的经验。6.3 构建特定风味的内核Ubuntu提供了不同“风味”flavor的内核如generic通用、lowlatency低延迟适用于音频制作、azure针对Azure云优化等。这些风味主要体现在内核配置上。你可以基于某个风味配置来构建自己的内核。获取风味配置# 例如获取低延迟配置 apt source linux-source-5.15.0 cd linux-*/ # Ubuntu将风味配置放在 debian/ 目录下 cp debian/config/amd64/config.低延迟 .config注意实际文件名可能类似config.低延迟.normal请查看debian/config/amd64/目录下的文件列表然后运行make olddefconfig和make menuconfig进行自定义最后编译。6.4 将自定义内核纳入APT管理如果你需要在一批机器上部署自定义内核手动安装.deb包很麻烦。可以搭建一个简单的本地APT仓库。将编译好的.deb包放入一个目录例如/var/www/html/mykernel/。在该目录下生成Packages索引文件cd /var/www/html/mykernel dpkg-scanpackages . /dev/null | gzip -9c Packages.gz在目标机器上添加这个本地源echo deb [trustedyes] file:///var/www/html/mykernel ./ | sudo tee /etc/apt/sources.list.d/mykernel.list sudo apt update之后就可以像安装普通软件一样安装你的内核了sudo apt install linux-image-5.15.0-custom。7. 故障排除与问题实录即使按照教程操作你也可能会遇到一些独特的问题。这里记录几个我亲身经历过的“坑”及其解决方法。问题一编译成功安装成功但重启后卡在紫屏或黑屏无法进入系统。可能原因1显卡驱动问题。NVIDIA等专有驱动与特定内核版本绑定。新内核安装后专有驱动模块未正确编译或加载。排查与解决在GRUB菜单选择新内核启动时按e进入编辑模式在linux行末尾添加nomodeset。这会让内核在启动初期不使用内核模式设置KMS使用最基本的显示驱动。如果能进入系统问题很可能出在显卡驱动。进入系统后重新安装显卡驱动。对于NVIDIA# 先确保旧驱动已清理可选但推荐 sudo apt purge nvidia-* # 安装对应新内核的头文件如果之前没装 sudo apt install linux-headers-$(uname -r) # 重新安装驱动例如使用ubuntu-drivers sudo ubuntu-drivers autoinstall # 或者从NVIDIA官网下载.run文件安装更新initramfs并重启sudo update-initramfs -u sudo reboot。可能原因2文件系统或存储控制器驱动缺失。在.config中无意间关闭了系统根分区所使用的文件系统如EXT4或SATA/NVMe控制器驱动。排查在旧内核中检查.config文件确保以下选项为y或mCONFIG_EXT4_FSy CONFIG_BLK_DEV_SDy # 对于SATA/AHCI CONFIG_NVME_COREy # 对于NVMe SSD CONFIG_ATA_PIIXy # 对于老式IDE/SATA常见于虚拟机问题二新内核启动后某个USB设备或网卡无法识别。可能原因对应的驱动被编译成了模块[M]但没有被自动加载或者模块依赖关系有问题。排查与解决检查设备是否存在lspciPCI设备或lsusbUSB设备。检查所需内核模块是否已加载lsmod | grep 驱动关键词。如果未加载尝试手动加载sudo modprobe 模块名。如果加载失败查看内核日志dmesg | tail -50通常会有错误信息。如果模块根本不存在于/lib/modules/$(uname -r)/目录下说明在配置时该驱动未被启用。需要回到内核配置中将其设为[Y]或[M]重新编译模块部分即可无需全量编译# 只编译模块 make -j$(nproc) modules # 安装模块 sudo make modules_install # 更新initramfs sudo update-initramfs -u -k $(uname -r)问题三系统可以启动但运行不稳定随机卡死或报错。可能原因内核代码修改引入了Bug或者硬件与某个新启用的内核功能不兼容。排查这是最棘手的情况。首先确保你能稳定地启动到旧内核。收集日志在新内核崩溃前尽可能多地收集日志。dmesg和journalctl -k内核日志是首要信息来源。关注Oops、BUG、general protection fault等关键词。简化配置如果你通过menuconfig启用了大量实验性功能或调试选项尝试回到一个最接近原版Ubuntu配置的状态只应用你必需的修改看问题是否消失。二分法定位如果修改的代码量不大可以尝试注释掉一半的修改编译测试逐步缩小问题范围。使用稳定内核基线如果你是从 kernel.org 下载的“mainline”内核它可能不如Ubuntu提供的“稳定”内核带-generic后缀的稳定。对于生产环境建议始终以Ubuntu的稳定内核源码为基础进行修改。修改内核源码是一条深入理解Linux系统的必经之路它充满了挑战但也带来了巨大的掌控感和灵活性。从最初战战兢兢地修改一个打印信息到后来能为自己的硬件编写驱动补丁这个过程让我对操作系统的认知上了一个全新的台阶。记住耐心和细致的记录是你的最佳伙伴。每次修改前做好备份每次编译后保留好日志每次遇到问题都当成一次学习的机会。当你第一次看到自己定制编译的内核成功启动并完美运行着你修改过的功能时那种成就感是无与伦比的。
返回列表