3. 内核模块编译基础:Makefile编写、Kconfig配置、模块加载与卸载命令

说到驱动开发,很多人一上来就撸代码,结果编译都过不了。我见过不少新手,写了个hello模块,make的时候报一堆错,然后就开始怀疑人生。其实说白了,内核模块的编译有一套固定的套路,你只要把Makefile和Kconfig搞明白了,后面就顺了。

这一章,我就带你走一遍内核模块编译的完整流程。从Makefile怎么写,到Kconfig怎么配,再到模块怎么装怎么卸,咱们一个一个来。

3.1 内核模块的Makefile编写

内核模块的Makefile和普通应用程序的Makefile不一样。它不是一个独立的编译过程,而是借用了内核的构建系统。你想想看,模块要跑在内核空间,它依赖内核的头文件、函数接口、甚至一些符号,所以必须用内核的Kbuild系统来编译。

先看一个最简单的例子:

# Makefile for a simple kernel module
obj-m := hello.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

这里有几个关键点:

  • obj-m:告诉内核构建系统,我要编译一个模块,目标文件是hello.o。如果模块由多个源文件组成,比如hello.c和world.c,那就写成 obj-m := hello.o,然后加一行 hello-objs := hello.o world.o。
  • KDIR:指向当前内核的构建目录。我习惯用 uname -r 动态获取版本号,这样换内核也不用改Makefile。
  • -C $(KDIR):切换到内核源码目录,执行它的Makefile。
  • M=$(PWD):告诉内核构建系统,我的模块源码在当前目录,编译完放回这里。
我的习惯: 我一般会在Makefile里加一个 modules_install 目标,方便直接把模块装到系统目录。但注意,开发阶段我建议手动拷贝,避免污染系统。

如果你的模块需要传递参数,比如一个整型参数 count,那在Makefile里不需要额外配置,参数定义在C代码里用 module_param 宏就行。但如果你需要条件编译,比如调试模式下打印更多日志,那就得靠Kconfig了。

3.2 Kconfig配置:让模块可配置

Kconfig是内核的配置系统。它决定了你的模块能不能被选中、是编译进内核还是编译成模块、以及依赖哪些其他功能。说白了,它就是给 make menuconfig 用的菜单描述文件。

一个典型的Kconfig条目长这样:

config HELLO_MODULE
    tristate "Hello World Module"
    depends on EXPERIMENTAL
    default n
    help
      This is a simple hello world kernel module.
      Say Y here to compile it into the kernel,
      say M to compile it as a module.

解释一下各个字段:

字段 含义 取值说明
config 配置项名称,最终会变成 CONFIG_HELLO_MODULE 全大写,下划线分隔
tristate 三态:不编译/编译进内核/编译成模块 驱动模块一般用tristate
depends on 依赖项,必须满足才能显示 可以写多个,用 && 连接
default 默认值,n/m/y 开发阶段建议default n
help 帮助信息,给用户看的 简要说明模块功能
我曾经踩过的坑: 有一次我写了个驱动,Kconfig里忘了写 depends on,结果在某个内核版本上编译通过,但运行时因为依赖的某个内核接口被移除了,直接Oops。所以,依赖一定要写清楚,尤其是那些非通用接口。

配置好Kconfig后,把它放到驱动源码目录下,然后在上一级目录的Kconfig里加一行 source "drivers/xxx/Kconfig",这样 make menuconfig 就能看到你的模块了。

3.3 模块加载与卸载命令

模块编译出来是 .ko 文件。加载和卸载就靠两个命令:insmod 和 rmmod。当然,modprobe 更智能,它会自动处理依赖。

先看基本操作:

# 加载模块
sudo insmod hello.ko

# 查看已加载的模块
lsmod | grep hello

# 卸载模块
sudo rmmod hello

# 查看模块信息
modinfo hello.ko

这里有几个细节:

  • insmod:直接加载,不处理依赖。如果模块依赖其他模块,你得先手动加载依赖。
  • rmmod:卸载模块。如果模块被其他模块引用,卸载会失败,提示 Module is in use。
  • modprobe:推荐使用。它会根据 /lib/modules/$(uname -r)/modules.dep 自动加载依赖。

调试小技巧: 加载模块后,用 dmesg | tail 查看内核日志。如果模块的 init 函数里打印了信息,会在这里显示。我习惯在 init 和 exit 函数里都加一句 printk,方便确认加载和卸载是否成功。

如果你的模块需要传递参数,可以在 insmod 时指定:

sudo insmod hello.ko count=10 name="test"

参数类型要和C代码里 module_param 定义的一致。比如 int、charp(字符串指针)、bool 等。

3.4 本章知识体系

下面这张图把内核模块编译、配置、加载的整个流程串起来了。你可以把它当作一个快速参考:

内核模块编译与加载流程 内核模块编译与加载流程 模块源码 (.c / .h) Makefile Kconfig 内核构建系统 (Kbuild) make menuconfig 模块文件 (.ko) insmod / modprobe rmmod lsmod / modinfo dmesg 查看内核日志

嗯,这张图基本把流程说清楚了。从源码和Kconfig开始,经过内核构建系统编译出.ko文件,然后用insmod/modprobe加载,rmmod卸载,lsmod/modinfo查看状态,最后用dmesg看日志。每一步都有对应的工具和命令。

3.5 常见问题与避坑

  • 编译报错: 最常见的是内核头文件路径不对。检查 KDIR 是否指向了正确的内核构建目录。如果你自己编译了内核,记得先 make modules_prepare。
  • 加载报错: insmod: ERROR: could not insert module hello.ko: Invalid parameters。这通常是模块参数类型不匹配,或者依赖的内核接口版本不对。用 modinfo 查看模块的 vermagic,确认和当前内核一致。
  • 卸载失败: rmmod: ERROR: Module hello is in use。说明有其他模块或进程在使用它。用 lsmod 查看引用计数,或者检查是否有应用程序打开了模块对应的设备文件。
  • Kconfig不生效: 修改Kconfig后,记得重新 make menuconfig,或者至少 make oldconfig。我有时候改了Kconfig但忘了重新配置,结果模块选项死活不出来。
我的开发习惯: 在模块的 init 函数里,我会加一个 printk(KERN_INFO "Hello module loaded (count=%d)\n", count);,这样加载后一眼就能看到参数是否正确传递。调试阶段,我还会在 exit 函数里打印卸载信息,方便确认模块被正常清理。

好了,这一章的内容就这些。Makefile、Kconfig、加载卸载命令,这三样东西是驱动开发的入门三板斧。你只要把这三样搞熟了,后面写再复杂的模块,编译和调试都不会卡住你。