Linux 内核模块编写
bash sudo apt update sudo apt install build-essential linux-headers-$(uname -r)
**CentOS/RHEL/Fedora:**
```bash
sudo dnf install kernel-devel kernel-headers gcc make
或对于旧版本:
sudo yum install kernel-devel kernel-headers gcc make
验证安装:
ls /lib/modules/$(uname -r)/build
如果该目录存在,说明内核构建环境已就绪。
注意:当前使用的内核版本可通过
uname -r查看,部分系统可能需要将linux-headers替换为linux-headers-generic。
第一个内核模块:Hello World
内核模块必须包含两个入口函数:初始化函数(模块加载时调用)和 退出函数(模块卸载时调用)。下面是一个最简单的示例。
创建文件 hello.c:
#include <linux/init.h> // 包含 module_init 和 module_exit 宏
#include <linux/module.h> // 包含 MODULE_* 宏和基本模块函数
#include <linux/kernel.h> // 包含 printk 等内核日志函数
// 初始化函数
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, kernel module loaded!\n");
return 0; // 返回 0 表示加载成功
}
// 退出函数
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // 许可证,必选
MODULE_AUTHOR("Your Name"); // 模块作者
MODULE_DESCRIPTION("A simple hello world module"); // 描述
代码解读:
module_init(fn):指定模块初始化函数,模块被插入内核时执行。module_exit(fn):指定模块清理函数,模块被移除时执行。printk:内核日志打印函数。KERN_INFO是日志级别,可通过dmesg查看。MODULE_LICENSE必须存在,否则加载时会产生内核污染警告。常用许可证有"GPL"、"GPL v2"、"Dual BSD/GPL"等。
编写 Makefile
内核模块的编译依赖于内核构建系统,不能使用普通的 gcc 命令。需要编写一个专用的 Makefile。
创建 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告诉内核构建系统要编译的模块目标。-C $(KDIR)切换到内核源码目录,M=$(PWD)让构建系统回到模块源码目录进行编译。- 执行
make时会生成hello.ko等文件。
编译与加载模块
在包含 hello.c 和 Makefile 的目录下执行 make:
make
如果一切正常,会生成 hello.ko(内核模块对象文件)。
加载模块:
sudo insmod hello.ko
查看模块是否加载:
lsmod | grep hello
查看内核日志:
dmesg | tail
应能看到 Hello, kernel module loaded!。
卸载模块:
sudo rmmod hello
再次查看 dmesg 会显示退出信息。
模块信息与许可证
MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION 等宏会自动将信息嵌入模块。可以通过 modinfo 命令查看:
modinfo hello.ko
输出示例:
filename: /path/to/hello.ko
description: A simple hello world module
author: Your Name
license: GPL
srcversion: ...
depends:
retpoline: Y
name: hello
vermagic: ...
为模块添加参数
模块可以接受命令行参数,方便用户动态配置行为。使用 module_param 宏来声明参数。
修改 hello.c,添加一个整数参数和字符串参数:
#include <linux/moduleparam.h>
static int repeat = 1;
module_param(repeat, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(repeat, "Number of times to print hello");
static char *who = "world";
module_param(who, charp, 0000);
MODULE_PARM_DESC(who, "Whom to greet");
在初始化函数中使用这些参数:
static int __init hello_init(void)
{
int i;
for (i = 0; i < repeat; i++)
printk(KERN_INFO "Hello, %s!\n", who);
return 0;
}
参数说明:
module_param(变量名, 类型, 权限)。int,charp(字符串指针),bool等是常用类型。- 权限用于在 sysfs 中创建对应的文件节点,普通用户可设为
S_IRUSR | S_IWUSR或0000表示不可见。
测试带参数加载:
sudo insmod hello.ko who="LKM" repeat=3
查看 dmesg 会看到三次问候。
创建简单的 /proc 接口
很多时候模块需要与用户空间交换数据,最简单的方式之一是通过 /proc 文件系统。利用 proc_create 和相关函数可以创建一个可读写的 proc 文件。
在模块中加入 proc 文件示例:
#include <linux/proc_fs.h>
#include <linux/uaccess.h> // copy_from_user / copy_to_user
#define PROC_NAME "hello_proc"
static struct proc_dir_entry *proc_entry;
// 用户读取 proc 文件时调用
static ssize_t proc_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
const char *msg = "Hello from kernel module via /proc!\n";
int len = strlen(msg);
if (*ppos > 0 || count < len)
return 0;
if (copy_to_user(buf, msg, len))
return -EFAULT;
*ppos = len;
return len;
}
static const struct proc_ops proc_fops = {
.proc_read = proc_read,
};
// 在初始化函数中创建 proc 条目
static int __init hello_init(void)
{
proc_entry = proc_create(PROC_NAME, 0444, NULL, &proc_fops);
if (!proc_entry)
return -ENOMEM;
printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
return 0;
}
// 退出时删除
static void __exit hello_exit(void)
{
proc_remove(proc_entry);
printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}
编译加载后,执行:
cat /proc/hello_proc