
1. 为什么非要动内核层1.1 从“能用”到“好用”的分水岭很多项目跑在标准发行版内核上功能一样不少但真到量产、交付或者性能压测的时候问题就来了设备明明接了传感器数据就是收不完整网卡跑满带宽时CPU占用直接飙到80%某个业务进程的实时性要求很苛刻系统却老是调度不及时。这些问题的根因往往不在应用层而在内核层的资源管理策略、驱动行为、调度参数甚至编译选项上。我在实际项目里遇到过最典型的一件事某个采集设备标准内核跑起来后中断延迟抖动达到几十毫秒业务侧怎么调都压不下去后来把内核的实时抢占选项打开、再配合中断亲和性设置抖动直接降到微秒级。这种收益用户态优化很难做到。所以从“能用”到“好用”内核层定制几乎是绕不开的一步。这一章我们重点聊的是在内核层做定制时怎么选、怎么改、怎么验证。不会停留在“下载源码、编译安装”这种入门操作而是把每步操作背后的逻辑摊开讲清楚至少让你在遇到问题时不至于全凭运气。1.2 内核定制到底动的是什么内核定制绝不等于“换个新内核”。日常所谓的内核层定制大致可以分成四种配置层定制通过内核配置项决定哪些功能编进内核、哪些编译成模块、哪些直接去掉。这是最常见的定制形式也最容易被低估。源码层定制修改内核源码逻辑比如调整某个驱动的行为、修改调度策略、新增系统调用。这类改动需要重新编译也要维护补丁集。模块层定制编写并加载内核模块在运行期扩展内核能力不需要重新编译整个内核。适合开发调试和增量功能。参数层定制通过内核启动参数、sysctl、sysfs接口在运行期调整内核行为。改动最小回滚最容易但能覆盖的问题范围也有限。定制之前必须分清自己当前卡在哪一层。我见过不少同学一上来就准备改源码结果最后发现只是某个内核启动参数没开。也有反过来搞了老半天参数最后发现是某个驱动根本没编进去。先判断层级再决定手段这是定制工作的第一原则。1.3 定制前必须想清楚的几个问题动手之前有几个问题一定要先回答不然后面会反复返工。第一你的目标是什么是要缩小内核体积、提升实时性、增强某个硬件的兼容性还是只是想去掉不需要的模块目标不同配置策略完全不同。第二你的内核会被部署到多少台机器上只部署一台开发机怎么折腾都行。如果是几十台上百台设备就必须考虑维护成本、驱动差异和回滚方案。第三你愿意承担多大的长期维护成本定制越多后续跟进上游版本就越麻烦。如果你要长期维护一个私有内核分支那么对上游代码的改动越少越好每次版本升级时你的补丁就要重新打一遍。第四出问题怎么回滚很多人第一次定制内核满怀信心编译完直接覆盖系统内核启动失败后才发现自己连旧内核都没保留。这种东西必须提前规划好。把这些问题想清楚再进入实操后面每一步都会踏实很多。2. 源码获取与构建环境准备2.1 版本选择与源码获取的讲究内核版本的选择直接决定了后续所有工作的舒适度。我的建议很简单生产环境选长期维护版本维护周期长的版本不要追最新。学习研究和实验环境可以选择相对新的稳定版本但别用RC版本。如果你用的用户态程序和硬件驱动是某个发行版自带的内核版本最好能对齐发行版默认版本或者稍高一点避免ABI差异。以某个常用长期支持版本为例可以在内核官网或镜像站获取源码wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.x.tar.xz tar -xf linux-6.6.x.tar.xz cd linux-6.6.x还需要强调一点源码完整性要校验。内核官网会提供对应的签名文件用gpg验证一下签名避免下载到被篡改的源码包。这种事平时感觉不到重要性但真遇到供应链问题时后悔都来不及。2.2 构建工具链与依赖准备编译内核需要一套完整的工具链和库。不同的发行版包名略有差异但核心依赖基本一致。编译器GCC或Clang版本不宜太老。make、flex、bison、bc等经典工具。libssl-dev、libncurses-dev、libelf-dev等库分别对应配置界面的图形支持和符号处理。如果需要交叉编译还要安装对应的交叉工具链。以我常用的环境为例安装完依赖后基本就是这些工具。配置的时候如果缺依赖会直接报错提示缺哪个库这个时候网上搜索报错信息按发行版对应的包名装好再继续。第一次准备构建环境时建议先在真实环境里完整编译一次默认配置。目的不是真的要用这个内核而是验证工具链是否完整、构建流程是否通畅。别等改完一堆配置后再去编译结果报错都不知道是配置问题还是环境问题。2.3 第一次编译顺不顺全看配置这一步内核编译本身其实没什么门槛无非就是make、make modules_install、make install这几步。真正让很多人卡住的是配置过程。内核的配置体系非常庞大配置项几千个。很多人第一次打开配置界面就被淹没了完全不知道哪些该开、哪些该关。这里给大家一个清晰的思路第一步先基于发行版现有的配置作为起点而不是从空配置开始。第二步基于这个起点做增量修改改你真正关心的那部分。第三步每次改动尽量少并且记录改动内容方便回溯。为什么强调从发行版配置起步因为发行版的默认配置是经过大量硬件兼容性测试的直接基于它裁剪至少不会把系统搞到无法启动。而如果你自己从头定义一套精简配置没有常年积累的硬件适配表大概率会漏掉某个Boot设备或文件系统驱动启动时直接卡住。复制当前系统内核的配置作为起点cp /boot/config-$(uname -r) .config拿到这个配置文件后后续的修改就可以在这个基础上做了。3. 内核配置不是改选项是找平衡3.1 defconfig、menuconfig 与 olddefconfig 怎么配合很多人搞混这几个工具这里帮你理清楚。defconfig生成某个架构的默认配置。一般是芯片厂商或开发板厂商提供的起点对x86桌面用途参考价值有限但嵌入式场景很常用。menuconfig基于ncurses的图形化交互配置界面。优势是直观能按类别浏览配置项支持搜索。olddefconfig用现有.config文件去更新配置把新内核版本中新增的配置项设为默认值。跨版本升级配置时非常有用。我的习惯是先用已有的.config文件执行olddefconfig把配置格式刷新到当前内核版本然后进入menuconfig做定向修改。这样既不会丢原有配置也能利用图形界面精确定位要改的选项。menuconfig里搜索功能特别重要。按/键输入关键词比如搜索“PREEMPT”会列出所有相关选项及其当前状态。对于几千个配置项没有搜索功能基本等于大海捞针。3.2 裁剪内核的黄金法则裁剪内核是定制中最容易出错、也最容易上瘾的事情。把不需要的驱动统统去掉内核体积缩小了启动速度快了编译时间短了看起来非常美好。但问题在于你怎么定义“不需要的驱动”。黄金法则只有一句话在裁剪之前先把目标硬件的完整清单列出来。主板/开发板的芯片组、存储控制器、网卡芯片。外设接口对应的芯片型号USB转串口、音频Codec、触摸屏控制器等。引导方式对应的文件系统驱动ext4、xfs、btrfs、vfat等。然后所有能够编译成模块的优先编译成模块而不要直接去掉。模块可以在需要时加载即使没用到也最多占一点磁盘空间不会影响运行。直接去掉则意味着真需要时你必须重新编译整个内核。3.3 关键子系统配置项的选型思路不同项目对内核的需求差异很大但有几个子系统的配置是普遍需要重点关注的。抢占模型桌面和服务器一般用自愿抢占或低延迟抢占实时性要求高的系统要考虑完整实时抢占但也需要评估吞吐量损失。CPU调度器大多数场景下默认调度器足够。多核负载不均衡时可以調整调度域参数。内存管理默认的SLUB分配器通常没有问题重点应关注透明大页的使用策略某些数据库类负载需要显式关闭THP。网络协议栈如果做网络转发关注网卡多队列、RPS/RFS、TCP拥塞控制算法等选项。电源管理嵌入式设备做电池供电时需要把cpufreq governor调成按需模式同时检查suspend/resume相关驱动的配置。上面这些只是方向具体配置参数要到对应子系统的菜单下逐个看。配置项的含义确实晦涩但每个选项的help文本里都有解释把这些解释读一遍比看十篇二手教程都有用。4. 实操全流程从配置到可启动内核4.1 生成配置并做定向修改这里用一个实际场景做演示假设我们要为一台x86_64开发机定制内核目标是降低延迟抖动并去掉部分用不到的驱动模块。复制当前系统配置cp /boot/config-$(uname -r) .config刷新到当前内核版本的配置格式make olddefconfig打开图形化配置界面make menuconfig在menuconfig里按下面思路做修改进入Kernel Features把抢占模型改成低延迟抢占如果不需要完整实时不建议直接开完整实时抢占。进入Device Drivers把确定用不到的驱动改为模块化比如不用的网卡驱动、不用的声卡驱动模块化即可。进入General Setup检查内核模块压缩选项选上能减小模块占用的磁盘空间。修改完保存退出。此时.config文件已经被更新。建议备份一份到单独的文件比如config.custom方便后续对比。4.2 编译内核与模块的完整过程配置确认后就可以开始编译了。编译速度主要取决于CPU核心数并行编译是必须的。查看CPU核心数nproc开始编译-j后面的数字一般设置为核心数的1.5到2倍不要设太离谱make -j16编译过程中如果有警告一般不用管但如果出现错误要关注具体是哪个配置引起的。很多时候是因为某个驱动源码在高版本编译器下报错这种时候优先检查该驱动的编译开关如果不影响功能可以直接关掉。编译完成后会生成arch/x86/boot/bzImage以及一堆.ko模块文件。这两个产出物接下来都要安装到系统里。4.3 安装、生成初始化文件系统与更新引导安装分三步顺序不能乱安装模块把编译出的.ko文件拷贝到系统模块目录。安装内核本体把vmlinuz、System.map、config等文件拷贝到/boot。生成initramfs因为新内核可能需要新的模块来挂载根文件系统。安装编译出的内核模块make modules_install安装内核本体文件make install重新生成 initramfs不同发行版命令略有差异mkinitramfs -o /boot/initrd.img-6.6.x 6.6.x如果使用 dracut 的发行版则用 dracut 命令更新引导项之后重启才是真正的考验。很多定制内核的失败不是编译失败而是启动阶段失败。所以重启之前最好先确认一下当前引导管理器里是否保留了原有内核入口。绝大多数发行版的引导管理器默认会保留但如果怀疑配置有问题可以手动检查。4.4 启动验证与回滚重启后进入新内核第一件事不是验收新功能而是确认基础系统是否正常。根文件系统有没有正常挂载。网卡有没有被识别并配置好IP。关键的驱动模块是否已经自动加载。dmesg里有没有红色的严重错误。查看当前内核版本uname -r查看内核日志中的错误信息dmesg | grep -i error如果一切正常再继续验证你定制前设定的目标。比如延迟是否下降、驱动是否正常等等。如果有问题回滚的办法很简单在引导菜单里选择旧内核启动然后重新检查配置差异。这条回滚路径必须提前确认好用不要等到启动失败才去找。5. 内核模块定制能力的最佳延伸5.1 编写一个最小内核模块源码级定制内核毕竟成本高很多增量能力用内核模块就够了。一个最小的内核模块对理解内核模块的运行机制特别有帮助。下面这个模块的功能是在加载和卸载时打印日志#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init demo_init(void) { printk(KERN_INFO [demo] module loaded\n); return 0; } static void __exit demo_exit(void) { printk(KERN_INFO [demo] module unloaded\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A minimal demo module);对应的Makefileobj-m : demo.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译方式很简单make sudo insmod demo.ko sudo rmmod demo然后通过dmesg查看日志就能看到加载和卸载的打印。这个例子虽小但已经包含了模块的加载入口、卸载入口、许可证声明这几个核心要素后续写真正的功能模块框架都是这样。5.2 模块参数与设备树配合真实项目里模块经常需要接收外部参数。比如一个SPI设备模块要传片选引脚号和最大频率。可以通过module_param宏实现static int cs_gpio -1; static int max_speed_hz 1000000; module_param(cs_gpio, int, 0644); module_param(max_speed_hz, int, 0644); MODULE_PARM_DESC(cs_gpio, SPI chip select GPIO number);加载时传参sudo insmod my_spi_dev.ko cs_gpio25 max_speed_hz2000000有了参数模块的通用性大大提升但不要在模块里写死任何硬件信息。设备树的存在本质上就是让内核和驱动能通过设备树节点描述硬件资源和配置信息模块代码保持通用具体的板级差异交给设备树文件去描述。如果你做的是嵌入式相关定制设备树就是绕不开的必修课。写设备树节点时注意compatible属性、寄存器地址、中断号的描述要跟驱动匹配否则内核根本probe不到你的设备。5.3 模块签名与安全加载定制内核时如果开启了强制模块签名校验那么未签名的模块是加载不进去的。这个机制在安全启动和生产设备上是必要的但对开发调试会带来额外步骤。生成签名密钥make modules_prepare openssl req -new -x509 -newkey rsa:2048 -keyout signing_key.pem-outform DER -out signing_key.der -nodes -days 36500对模块签名scripts/sign-file sha256 signing_key.pem signing_key.der module.ko调试阶段如果不想费这个功夫可以在内核配置里把模块签名校验改成警告但不拒绝加载等开发稳定后再收紧。我在实际项目中吃过这个亏设备启用了安全启动结果模块编译出来之后怎么都加载不了搞了半天才意识到是签名校验把它拒了。所以定制内核时只要涉及安全启动或者模块签名第一件事就要确认当前配置策略是哪一种。6. 内核调参与故障排查实录6.1 调试手段选型内核出问题比用户态程序出问题难查得多。主要原因是内核崩溃直接影响整个系统日志可能都来不及写盘。所以排查手段要分层从轻到重依次来。查内核日志dmesg是最后的救命稻草很多驱动加载失败、内存分配失败、寄存器读写异常都能从这里找到线索。动态调试开启动态调试后按模块或函数过滤打印日志比用printk反复改源码重编效率高得多。trace工具跟踪调度、中断、函数调用等运行时信息适合定位延迟抖动这类问题。kdump崩溃转储内核panic时把内存转储下来离线分析崩溃现场是生产环境必配的兜底手段。我在定位一类随机性很强的中断延迟问题时光靠dmesg根本看不出规律后来用trace工具在中断入口和退出路径打点才抓到是某个驱动在中断处理里占了太长时间。这类问题靠猜永远猜不出来。6.2 启动失败的三类典型原因定制内核启动失败原因几乎可以归成三类。第一类是根文件系统挂载不上。如果内核里没编入根文件系统所依赖的驱动或者initramfs里缺少对应模块内核会在启动早期报错然后进入紧急模式。排查时在启动参数里打开调试输出看它卡在哪个挂载步骤。第二类是引导配置错误比如内核路径写错、initramfs路径写错、启动参数里指定了不存在的设备。这类问题相对好查因为引导管理器界面上的错误信息通常很直接。第三类是硬件初始化失败比如某个驱动在probe阶段访问了错误的寄存器导致系统崩溃。这种问题通常在dmesg里能看到具体驱动名和调用栈用动态调试进一步定位。针对这些问题的排查顺序我的建议是先确认引导配置再检查initramfs内容最后才考虑驱动和源码问题。6.3 运行期性能调优参数速查内核定制完成后很多性能问题不需要改源码通过sysctl和一些虚拟文件系统接口就能调。网络转发性能调整接收队列数量RPS、CPU亲和性、TCP拥塞控制算法。IO性能调整调度器策略SSD用none或mq-deadline机械硬盘用bfq或kyber。延迟优化调整中断合并参数和CPU频率调节器策略。内存策略根据负载特征调整脏页回写比例、overcommit策略、THP开关。sysctl的改动虽然是运行期的但很多参数在重启后会重置。如果确认某些参数对业务稳定性很重要建议把它们写进系统配置目录下让系统启动时自动应用而不是每次手动设置。调试阶段还要注意一次尽量只调一个变量。同时改多个参数出了问题根本不知道是哪一步导致的。定好基线每次改一项保留记录这才是高效的调优节奏。查看当前相关内核参数sysctl -a | grep -E net.core|vm.*dirty临时修改某个参数sysctl -w net.core.netdev_max_backlog4096按这样的方式把每一次修改记录下来后续就能形成一份完整的参数基线文档对系统维护和问题回溯都有很大帮助。7. 我踩过的坑与最后建议内核层定制这件事说起来就是配置、编译、模块、调参这些套路但真正决定项目成败的往往是一些看起来很不起眼的细节。第一个坑是编译参数。有人为了追求编译速度把优化等级调高结果某个驱动在运行时出现诡异的内存问题折腾一周才发现是编译优化导致的。内核对编译器的版本和优化等级其实很敏感不要为了一点编译时间在这些地方冒险。第二个坑是模块依赖。裁剪内核时把某个看起来无用的功能关掉结果另一个依赖它的模块编译失败或者加载时报依赖缺失。配置内核时打开依赖查询功能查看每个选项的依赖关系能少踩不少坑。第三个坑是补丁管理。修改内核源码时如果直接在源码目录里改升级版本时全部改动都会被覆盖根本没法迁移。正确的做法是每次改动都生成独立的补丁文件统一管理并记录改动动机。这样上游版本更新时可以快速把补丁重新应用。第四个坑是备份。有多重要前面说过了再强调一次每次编译安装新内核确认旧内核的引导入口还在再重启。这是最基本、最简单、也最容易被轻视的一步。我个人在实际操作中的体会是内核定制不是一次性的工程而是一个持续维护的过程。系统在变硬件在变业务负载也在变今天觉得完美的配置下一批设备或者下一轮需求可能就不适用了。所以把整个定制过程的每个决策都记录下来让每个选项都有据可查比一次性搞出一个“完美内核”重要得多。这个内容后续还可以扩展的方向包括实时性调优的完整方法论、设备树从零到实战、以及如何把定制内核应用在量产设备上。如果你正卡在内核定制的某个环节不妨先对照这一章的内容确认自己处于哪个阶段再继续往下走。