3. 开发环境搭建

说实话,写内核模块这事儿,没你想的那么玄乎。我当年第一次接触时也紧张,生怕一个 insmod 下去系统就挂了。后来发现,只要按规矩来,内核比你想象中扛造。今天我们就从最经典的 Hello World 开始,把整个流程走一遍。

3.1 模块的骨架:最少代码长什么样

一个内核模块,说白了就是一段可以在运行时插入内核的 C 代码。它不需要 main() 函数,而是用两个固定入口:module_init() 和 module_exit()。

来看代码:

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel World!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");

这里有几个关键点,我挨个说一下:

  • __init 和 __exit:这两个宏告诉内核,初始化函数用完后可以扔掉,节省内存。我在项目里见过有人忘了加,结果模块占着内存不放,虽然不影响功能,但看着难受。
  • printk():内核版的 printf()。注意它有个日志级别 KERN_INFO,默认会打到 /var/log/kern.log 或者 dmesg 里。你想想看,内核没有标准输出,所以调试信息全靠它。
  • MODULE_LICENSE("GPL"):这行不是摆设。如果不声明 GPL,某些内核符号你调用不了。我曾经踩过这个坑,编译通过但 insmod 报 Unknown symbol,查了半天才发现是许可证问题。
小技巧: 调试阶段建议把 printk 级别设成 KERN_ALERT,这样信息会直接打到控制台,不用每次都敲 dmesg。

3.2 Makefile 的写法:别自己手写编译命令

内核模块的编译和普通程序不一样。它不能直接用 gcc 编译,必须借助内核的构建系统。说白了,你得告诉内核:嘿,帮我把这个文件编成模块。

标准写法如下:

# Makefile
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 编成模块。如果是多个源文件,写法会复杂一点,但单文件就这么简单。
  • KDIR:指向当前内核的源码树。注意 uname -r 会获取你当前运行的内核版本,确保编译出来的模块和内核匹配。
  • -C $(KDIR) M=$(PWD):切换到内核源码目录执行 make,同时把当前目录传进去。内核构建系统会自动找到你的 hello.c 并编译。
注意: 编译前确保已安装内核头文件。Ubuntu 上执行 sudo apt install linux-headers-$(uname -r)。我见过不少新手卡在这一步,报错说找不到 linux/module.h。

3.3 编译与加载:见证奇迹的时刻

在终端里执行 make,你会看到类似这样的输出:

make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
  CC [M]  /home/user/hello/hello.o
  MODPOST /home/user/hello/Module.symvers
  CC [M]  /home/user/hello/hello.mod.o
  LD [M]  /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'

编译成功后,你会得到一个 hello.ko 文件。这就是内核模块,后缀 .ko 就是 Kernel Object 的意思。

加载和卸载的命令很简单:

# 加载模块
sudo insmod hello.ko

# 查看模块是否加载成功
lsmod | grep hello

# 卸载模块
sudo rmmod hello

# 查看内核日志
dmesg | tail -5

你应该能看到:

[ 1234.567890] Hello, Kernel World!
[ 1234.678901] Goodbye, Kernel World!

核心流程总结:

  1. 编写 .c 源文件,定义 init 和 exit 函数
  2. 编写 Makefile,利用内核构建系统编译
  3. insmod 加载,rmmod 卸载
  4. 用 dmesg 查看输出

3.4 常见问题与避坑指南

问题 现象 解决办法
找不到头文件 编译报错 linux/module.h: No such file 安装 linux-headers-$(uname -r)
模块版本不匹配 insmod 报 Invalid module format 确保 KDIR 指向当前内核版本
权限不足 insmod 报 Operation not permitted 加 sudo,或者检查内核是否禁用模块加载
printk 不显示 dmesg 里看不到输出 检查日志级别,或尝试 KERN_ALERT

我曾经遇到过最诡异的问题:模块编译成功,加载也没报错,但 dmesg 里就是没有输出。后来发现是 printk 的日志级别被系统默认过滤掉了。嗯,这里要注意,KERN_INFO 在某些发行版上默认不打印到控制台,但 dmesg 里应该能看到。如果连 dmesg 都没有,检查一下是不是 printk 根本没被执行——比如 init 函数返回了非零值。

3.5 知识结构图:模块开发全流程

下面这张图把整个流程串起来了,从源码到加载卸载,一目了然:

内核模块开发全流程 hello.c 源码 Makefile 内核构建系统 hello.ko 模块 insmod 加载 rmmod 卸载 dmesg 查看日志 说明:insmod 调用 module_init,rmmod 调用 module_exit 所有 printk 输出通过 dmesg 查看

3.6 模块信息与工具链

除了 insmod 和 rmmod,还有几个常用工具:

  • modinfo hello.ko:查看模块的详细信息,包括作者、描述、依赖等。
  • lsmod:列出当前加载的所有模块,可以看到模块大小和引用计数。
  • modprobe:比 insmod 更智能,会自动处理依赖。但新手阶段建议先用 insmod,更直观。

举个例子,执行 modinfo hello.ko 会输出:

filename:       /home/user/hello/hello.ko
license:        GPL
author:         Your Name
description:    A simple Hello World kernel module
depends:
retpoline:      Y
name:           hello
vermagic:       5.15.0-91-generic SMP mod_unload modversions

注意那个 vermagic 字段,它记录了编译时的内核版本和配置。如果和你当前运行的内核不匹配,模块就加载不了。这就是为什么我强调 KDIR 一定要指向正确的内核源码。

调试建议: 开发阶段可以写一个简单的脚本,一键编译、加载、查看日志,省得每次敲三遍命令。我习惯在 Makefile 里加一个 test 目标,自动执行这些步骤。

好了,到这里你已经掌握了内核模块的完整开发流程。从写代码、写 Makefile,到编译、加载、卸载、看日志,每一步都走了一遍。这个 Hello World 虽然简单,但它是你进入内核世界的敲门砖。后面我们会在这个基础上,加入参数传递、文件操作、设备节点等更复杂的功能。