第二章:Linux内核编译与模块化
内核编译这事儿,说难不难,说简单也不简单。我刚开始接触时也踩过不少坑,今天就把这些经验掰开了揉碎了讲给你听。
2.1 内核源码获取
获取内核源码,最靠谱的方式就是从官方渠道来。我个人习惯用 git 直接拉取,这样后续切换版本、打补丁都方便。
# 从 kernel.org 克隆主线内核
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# 或者下载稳定版源码包
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
tar -xf linux-6.1.tar.xz
小提示: 如果你只是做驱动开发测试,没必要每次都拉完整的历史记录。加个
--depth=1 参数,只取最新版本,能省不少时间。
源码目录结构你得心里有数:
arch/— 架构相关代码,x86、ARM、RISC-V 都在这里drivers/— 设备驱动,我们写模块主要关注这块fs/— 文件系统实现kernel/— 核心调度、进程管理等mm/— 内存管理
2.2 内核配置与编译
配置内核,说白了就是告诉编译器:哪些功能要编进内核,哪些编成模块,哪些干脆不要。
我常用的配置命令:
# 基于当前系统的配置
make oldconfig
# 菜单式配置(推荐新手用)
make menuconfig
# 如果你在搞嵌入式,用这个
make ARCH=arm64 defconfig
配置选项有三种状态:
| 符号 | 含义 | 说明 |
|---|---|---|
Y |
编译进内核 | 启动时自动加载,无法卸载 |
M |
编译为模块 | 生成 .ko 文件,可按需加载 |
N |
不编译 | 完全排除 |
注意: 我曾经在生产环境上犯过一个错——把文件系统驱动编成了模块,结果内核启动时根本挂载不了根分区,系统直接崩了。核心驱动一定要编进内核,别偷懒。
编译命令也很直接:
# 编译内核和模块
make -j$(nproc)
# 只编译模块
make modules
# 安装模块到 /lib/modules/ 下
make modules_install
# 安装内核
make install
-j$(nproc) 这个参数很关键,它让编译并行跑,能快好几倍。我见过有人编译内核时只敲 make,结果等了半小时……
2.3 内核模块的编写与加载
写一个最简单的内核模块,其实就两个函数:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, kernel module loaded!\n");
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded!\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module example");
对应的 Makefile 长这样:
obj-m := my_module.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 my_module.ko # 加载模块
sudo rmmod my_module # 卸载模块
lsmod | grep my_module # 查看模块是否加载
核心要点: 模块的入口和出口函数必须用
module_init 和 module_exit 宏声明,否则内核根本不知道从哪开始执行。
2.4 模块参数与依赖
模块参数,说白了就是让用户能在加载时传值进去。我经常用这个来调试驱动,不用重新编译就能改行为。
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传参:
sudo insmod my_module.ko debug_level=2 device_name="my_usb_dev"
模块依赖这块,你想想看:如果你的模块 A 调用了模块 B 导出的函数,那加载 A 之前必须先加载 B。
查看依赖关系:
modinfo my_module.ko # 查看模块信息
depmod # 生成依赖关系文件
modprobe my_module # 自动处理依赖加载
经验之谈: 我建议尽量用
modprobe 而不是 insmod。前者会自动处理依赖,后者不会。有一次我手动 insmod 一个驱动,结果它依赖的模块没加载,系统直接报错,排查了半天才发现是依赖问题。
知识体系总览
下面这张图,把内核编译与模块化的核心流程串起来了:
嗯,到这里,内核编译和模块化的核心内容就讲完了。你想想看,从源码获取到配置编译,再到模块编写和参数传递,其实每一步都有它的道理。我在项目中见过太多人栽在依赖管理和参数传递上,所以这两个地方我特意多说了几句。
记住一句话:内核模块不是普通的应用程序,它运行在最高权限级别,一个空指针就能让整个系统挂掉。写模块时,多一分谨慎,少一分麻烦。