第二章: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 一个驱动,结果它依赖的模块没加载,系统直接报错,排查了半天才发现是依赖问题。

知识体系总览

下面这张图,把内核编译与模块化的核心流程串起来了:

Linux内核编译与模块化知识体系 源码获取 内核配置 内核编译 编译产物 内核镜像 (vmlinuz) 内核模块 (.ko) 模块加载/卸载 模块参数传递 模块依赖管理 insmod / rmmod / modprobe / modinfo / depmod

嗯,到这里,内核编译和模块化的核心内容就讲完了。你想想看,从源码获取到配置编译,再到模块编写和参数传递,其实每一步都有它的道理。我在项目中见过太多人栽在依赖管理和参数传递上,所以这两个地方我特意多说了几句。

记住一句话:内核模块不是普通的应用程序,它运行在最高权限级别,一个空指针就能让整个系统挂掉。写模块时,多一分谨慎,少一分麻烦。