Linux内核模块开发实战:从Hello World到可传参的模块编写

先搞懂:内核模块到底是什么?

Linux内核模块开发实战:从Hello World到可传参的模块编写

你可能听过“内核模块”,但它和普通程序的区别得先理清楚——内核模块是能动态加载到Linux内核的代码片段,不用重新编译整个内核,也不用重启系统,就能给内核加新功能(比如驱动硬件、扩展文件系统)。

和静态编译到内核的代码比,它有三个“香”的地方:
省内存:不用的时候卸载,释放内存;
开发快:改模块后不用重新编译整个内核(内核源码动辄几GB,编译一次要半小时);
安全:模块崩了顶多卸载,不会让整个系统重启(除非你写了个能搞崩内核的bug)。

举个例子:你买了个新USB摄像头,内核没自带驱动——写个模块加载进去,摄像头就能用了,这不比重装系统香?

第一步:搭好开发环境,别上来就写代码

内核模块开发需要三个工具,少一个都不行:
1. gcc:编译内核代码(内核用C写,且依赖GCC的扩展语法);
2. make:自动化编译(内核模块的编译规则很复杂,得用内核自己的Makefile);
3. 内核开发包(kernel-devel):必须和当前内核版本一致,否则编译出来的模块装不上。

不同发行版的安装命令:
CentOS/RHELsudo yum install kernel-devel gcc make
Ubuntu/Debiansudo apt install linux-headers-$(uname -r) gcc make
Fedorasudo dnf install kernel-devel gcc make

装完验证一下:
– 用uname -r看内核版本(比如输出5.15.0-76-generic);
– 再输ls /lib/modules/$(uname -r)/build——如果能看到这个目录,说明环境ok(这个目录是内核源码的符号链接)。

如果提示“没有这个文件或目录”,说明你装的kernel-devel版本和当前内核不一致——比如内核是5.15.0-76,devel包是5.15.0-77,这时候得装对应版本:sudo yum install kernel-devel-$(uname -r)(CentOS)或者sudo apt install linux-headers-$(uname -r)(Ubuntu)。

动手写第一个模块:Hello Kernel!

内核模块的代码结构很固定,就三部分:初始化函数、退出函数、模块信息。直接上代码(保存为hello.c):

#include <linux/module.h>  // 模块基础头文件
#include <linux/kernel.h>  // 内核函数(比如printk)
#include <linux/init.h>    // 初始化/退出函数宏

// 初始化函数:加载模块时触发
static int __init hello_init(void) {
    printk(KERN_INFO "Hi! I'm your first kernel module.
");
    return 0;  // 返回0=成功,非0=失败
}

// 退出函数:卸载模块时触发
static void __exit hello_exit(void) {
    printk(KERN_INFO "Bye! See you next time.
");
}

// 注册初始化和退出函数(内核要知道这两个函数是干啥的)
module_init(hello_init);
module_exit(hello_exit);

// 模块信息:内核要求必须写,否则会警告
MODULE_LICENSE("GPL");          // 许可证(GPL是内核模块的“通行证”)
MODULE_AUTHOR("Your Name");     // 作者(随便写)
MODULE_DESCRIPTION("My first kernel module"); // 描述
MODULE_VERSION("0.1");          // 版本号

几个关键知识点:
__init/__exit:告诉内核这两个函数是初始化/退出用的,内核会在加载后释放__init函数的内存(省空间);
printk:内核态的“printf”,不能用printfprintf是用户态的);
KERN_INFO:日志级别(4级),内核默认会输出≤4级的日志(可以改,后面说);
MODULE_LICENSE("GPL"):必须写!否则内核会报“污染内核”的警告——Linux内核是GPL许可证,模块得兼容才能加载。

写Makefile:让模块能编译

内核模块的Makefile不能自己瞎写,得“借”内核的编译系统。保存为Makefile(首字母大写,别写错):

# 目标:把hello.c编译成可加载模块(obj-m=模块,obj-y=静态编译)
obj-m += hello.o

# 编译规则:调用内核的Makefile
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

# 清理规则:删除编译生成的文件
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

解释一下:
obj-m += hello.o:告诉内核要编译hello.c生成hello.ko(模块文件);
make -C /lib/.../build-C表示进入内核源码目录,执行内核的Makefile;
M=$(PWD):告诉内核模块源码在当前目录;
modules:内核Makefile的目标,编译可加载模块;
clean:清理编译生成的.ko.o等文件。

注意:Makefile里的命令行必须用Tab键缩进,用空格会报错!

编译+加载:看你的代码跑起来

现在编译模块:
打开终端,进入hello.cMakefile所在目录,输make——如果没报错,会生成hello.ko文件(这就是模块的二进制文件)。

加载模块:
sudo insmod hello.ko(必须用root权限,否则装不上),然后输dmesg | tail看输出:

$ dmesg | tail
[12345.678901] Hi! I'm your first kernel module.

卸载模块:
sudo rmmod hello(不用写.ko后缀),再看dmesg

$ dmesg | tail
[12345.678901] Hi! I'm your first kernel module.
[12346.123456] Bye! See you next time.

如果没看到输出,别急:
– 先确认printk的日志级别——输echo 4 > /proc/sys/kernel/printk(把日志级别调到4,允许KERN_INFO输出);
– 或者用dmesg -w实时看日志(-w是“跟随”模式,类似tail -f)。

踩坑指南:我踩过的坑,你别踩

  1. 模块装不上,提示“Invalid module format”
  2. 原因:编译用的内核版本和当前运行的内核版本不一致(比如编译用5.15.0-77,运行用5.15.0-76);
  3. 解决:装和当前内核版本一致的kernel-devel包,或者升级内核到kernel-devel的版本(sudo yum update kernel)。

  4. printk没输出

  5. 原因:内核默认只输出KERN_WARNING(3级)及以上的日志,KERN_INFO(4级)没被输出;
  6. 解决:用dmesg看(printk的输出在内核日志里,不是终端),或者改日志级别:sudo echo 4 > /proc/sys/kernel/printk

  7. 权限不够,提示“Operation not permitted”

  8. 原因:加载/卸载模块需要root权限;
  9. 解决:加sudo,或者切换到root用户(su -)。

  10. Makefile报错“No rule to make target ‘all’”

  11. 原因:Makefile的缩进用了空格,不是Tab;
  12. 解决:把空格换成Tab(用vim的话,输:set ts=4 sw=4 sts=4 et,然后按Tab键)。

进阶:给模块加参数,让它更灵活

Hello World太简单?给模块加个参数,加载时能传值!

修改hello.c

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

// 定义模块参数:name(字符指针),默认值是“Kernel”
static char *name = "Kernel";
// 注册参数:参数名、类型、权限(S_IRUGO=所有用户可读)
module_param(name, charp, S_IRUGO);
// 参数描述:用modinfo能看到
MODULE_PARM_DESC(name, "The name to greet (default: Kernel)");

static int __init hello_init(void) {
    printk(KERN_INFO "Hi! %s, this is my first kernel module.
", name);
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Bye! %s, see you next time.
", name);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A kernel module with parameters");
MODULE_VERSION("0.2");

编译后,加载时传参数:
sudo insmod hello.ko name=Linux

然后看dmesg:

$ dmesg | tail
[12347.789012] Hi! Linux, this is my first kernel module.

卸载时:

$ sudo rmmod hello
$ dmesg | tail
[12347.789012] Hi! Linux, this is my first kernel module.
[12348.345678] Bye! Linux, see you next time.

modinfo hello.ko能看到参数描述:

$ modinfo hello.ko
filename:       /path/to/hello.ko
version:        0.2
description:    A kernel module with parameters
author:         Your Name
license:        GPL
parm:           name:The name to greet (default: Kernel) (charp)

调试技巧:模块出问题了怎么查?

内核模块的调试比用户态麻烦,但有几个“土办法”很好用:
1. 看内核日志dmesgjournalctl -k(systemd的日志),这是最常用的;
2. 查模块信息modinfo hello.ko看版本、依赖、参数;
3. 看加载的模块lsmod | grep hello过滤出你的模块;
4. 加printk日志:在关键位置加printk输出变量值,比如printk(KERN_INFO "name=%s
", name);
——虽然土,但管用。

下一步:该学啥?

学会Hello World模块后,可以进阶学这些:
字符设备驱动:写一个虚拟的字符设备(比如模拟一个串口,支持read/write);
中断处理:模块怎么处理硬件中断(比如按键按下时触发中断);
内存管理:内核态的内存分配(kmallocvmalloc的区别);
进程管理:模块怎么创建内核线程(kthread_run)。

推荐几本书:
– 《Linux内核设计与实现》(Robert Love):讲内核基础,适合入门;
– 《Linux设备驱动程序》(Jonathan Corbet):驱动开发的“圣经”;
– 《深入理解Linux内核》(Daniel P. Bovet):深入内核内部,适合进阶。

最后说一句:内核模块开发的核心是“理解内核的规则”——内核是个很严谨的系统,你得按它的规则来,比如内存不能随便用,函数不能随便调用,参数要检查……但只要你慢慢来,多写多试,肯定能学会!

原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/413

(0)