1. 内核模块概述
可加载内核模块(Loadable Kernel Module,LKM)是 Linux 内核的扩展机制,允许在运行时动态添加或移除内核功能,无需重新编译内核或重启系统。设备驱动程序、文件系统、网络协议栈等都以模块形式存在。
模块与内核运行在同一地址空间,具有完全的内核权限。这使得模块极其强大,但也极其危险——一个错误的模块可能导致系统崩溃。因此,内核模块开发需要严格的编码规范、充分的测试和完善的错误处理。
内核编译时可以选择将功能内置(built-in)或编译为模块。关键功能如进程调度、内存管理必须内置;设备驱动、可选文件系统通常编译为模块。使用 modprobe 按需加载模块减少内存占用。
2. 第一个内核模块
让我们从一个最简单的模块开始,它只在加载和卸载时打印消息。这将帮助我们建立基本的开发环境和理解模块生命周期。
2.1 源代码
/* hello.c - 第一个内核模块 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/* 模块信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Glen <glen@example.com>");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
/*
* 模块加载时调用
* __init 宏表示该函数仅在初始化期间使用,之后内存可回收
*/
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel World!\n");
return 0; /* 返回 0 表示成功,非零表示失败 */
}
/*
* 模块卸载时调用
* __exit 宏表示该函数仅在模块卸载时可用
*/
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel World!\n");
}
/* 注册入口和出口函数 */
module_init(hello_init);
module_exit(hello_exit);
2.2 Makefile
# Makefile for kernel module
# 模块名称(与源文件同名,不带 .c)
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
# 加载模块
load:
sudo insmod hello.ko
# 卸载模块
unload:
sudo rmmod hello
# 查看日志
log:
sudo dmesg | tail -20
2.3 编译与测试
# 编译模块
$ make
make -C /lib/modules/6.5.0/build M=/home/glen/module_test modules
CC [M] hello.o
MODPOST Module.symvers
CC [M] hello.mod.o
LD [M] hello.ko
# 查看模块信息
$ modinfo hello.ko
filename: /home/glen/module_test/hello.ko
version: 1.0
description: A simple Hello World kernel module
author: Glen <glen@example.com>
license: GPL
# 加载模块
$ sudo insmod hello.ko
# 或使用 modprobe(自动处理依赖)
$ sudo modprobe hello
# 查看内核日志
$ sudo dmesg | tail
[ 1234.567890] Hello, Kernel World!
# 确认模块已加载
$ lsmod | grep hello
hello 16384 0
# 卸载模块
$ sudo rmmod hello
# 查看卸载消息
$ sudo dmesg | tail
[ 1234.567890] Hello, Kernel World!
[ 1245.678901] Goodbye, Kernel World!
printk 支持 8 个日志级别(KERN_EMERG 到 KERN_DEBUG)。只有优先级高于 console_loglevel 的消息才会显示在控制台。使用 dmesg 查看所有消息,或调整 /proc/sys/kernel/printk 改变阈值。
3. 符号导出与模块依赖
模块可以导出符号供其他模块使用,形成模块间的依赖关系。这是构建复杂驱动的基础——例如,网络设备驱动可能依赖通用的 MDIO 总线模块。
3.1 导出符号
/* math_utils.c - 提供数学工具函数的模块 */
#include <linux/module.h>
#include <linux/kernel.h>
/* 导出的符号 */
int math_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(math_add);
int math_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(math_sub);
/* 仅 GPL 模块可用 */
int math_mul(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(math_mul);
static int __init math_init(void)
{
printk(KERN_INFO "Math utils module loaded\n");
return 0;
}
static void __exit math_exit(void)
{
printk(KERN_INFO "Math utils module unloaded\n");
}
module_init(math_init);
module_exit(math_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Glen");
MODULE_DESCRIPTION("Math utilities for kernel modules");
3.2 使用导出符号
/* calculator.c - 使用 math_utils 提供的符号 */
#include <linux/module.h>
#include <linux/kernel.h>
/* 声明外部符号 */
extern int math_add(int a, int b);
extern int math_sub(int a, int b);
extern int math_mul(int a, int b);
static int __init calc_init(void)
{
int a = 10, b = 5;
printk(KERN_INFO "Calculator module loaded\n");
printk(KERN_INFO "%d + %d = %d\n", a, b, math_add(a, b));
printk(KERN_INFO "%d - %d = %d\n", a, b, math_sub(a, b));
printk(KERN_INFO "%d * %d = %d\n", a, b, math_mul(a, b));
return 0;
}
static void __exit calc_exit(void)
{
printk(KERN_INFO "Calculator module unloaded\n");
}
module_init(calc_init);
module_exit(calc_exit);
MODULE_LICENSE("GPL"); /* 必须使用 GPL 才能访问 EXPORT_SYMBOL_GPL */
MODULE_AUTHOR("Glen");
MODULE_DESCRIPTION("Simple calculator using math utils");
3.3 模块依赖关系
# 查看模块导出的符号
$ nm hello.ko | grep " T "
0000000000000000 T cleanup_module
0000000000000000 T init_module
# 查看模块依赖的符号
$ nm calculator.ko | grep " U "
U __stack_chk_fail
U math_add
U math_mul
U math_sub
# modprobe 自动加载依赖
$ sudo modprobe calculator # 会自动加载 math_utils
# 查看模块依赖关系
$ modinfo calculator.ko
depends: math_utils
$ lsmod | grep math
math_utils 16384 1 calculator
calculator 16384 0
4. 字符设备实现
字符设备是 Linux 中最简单的设备类型,以字节流方式处理数据。我们将实现一个支持读写操作的字符设备,作为理解设备驱动的基础。
4.1 字符设备结构
/* simple_char.c - 简单字符设备驱动 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "simple_char"
#define CLASS_NAME "simple_char_class"
#define BUFFER_SIZE 1024
static int major;
static struct class *simple_char_class = NULL;
static struct device *simple_char_device = NULL;
static struct cdev simple_char_cdev;
/* 设备私有数据 */
struct simple_char_data {
char *buffer;
size_t size;
size_t capacity;
};
static struct simple_char_data *device_data;
/* 文件操作:打开 */
static int simple_char_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "simple_char: device opened\n");
return 0;
}
/* 文件操作:释放 */
static int simple_char_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "simple_char: device closed\n");
return 0;
}
/* 文件操作:读取 */
static ssize_t simple_char_read(struct file *file, char __user *user_buffer,
size_t len, loff_t *offset)
{
size_t bytes_to_read;
if (*offset >= device_data->size)
return 0; /* EOF */
bytes_to_read = min(len, (size_t)(device_data->size - *offset));
if (copy_to_user(user_buffer, device_data->buffer + *offset, bytes_to_read))
return -EFAULT;
*offset += bytes_to_read;
printk(KERN_INFO "simple_char: read %zu bytes\n", bytes_to_read);
return bytes_to_read;
}
/* 文件操作:写入 */
static ssize_t simple_char_write(struct file *file,
const char __user *user_buffer,
size_t len, loff_t *offset)
{
size_t bytes_to_write;
/* 限制写入大小 */
bytes_to_write = min(len, (size_t)(device_data->capacity - 1));
if (copy_from_user(device_data->buffer, user_buffer, bytes_to_write))
return -EFAULT;
device_data->buffer[bytes_to_write] = '\0';
device_data->size = bytes_to_write;
printk(KERN_INFO "simple_char: wrote %zu bytes: %s\n",
bytes_to_write, device_data->buffer);
return bytes_to_write;
}
/* 文件操作表 */
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = simple_char_open,
.release = simple_char_release,
.read = simple_char_read,
.write = simple_char_write,
};
4.2 设备初始化与清理
static int __init simple_char_init(void)
{
dev_t dev;
int ret;
/* 分配设备号 */
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "simple_char: failed to allocate device number\n");
return ret;
}
major = MAJOR(dev);
/* 初始化字符设备 */
cdev_init(&simple_char_cdev, &fops);
simple_char_cdev.owner = THIS_MODULE;
ret = cdev_add(&simple_char_cdev, dev, 1);
if (ret < 0) {
printk(KERN_ERR "simple_char: failed to add cdev\n");
goto unregister;
}
/* 创建设备类 */
simple_char_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(simple_char_class)) {
printk(KERN_ERR "simple_char: failed to create class\n");
ret = PTR_ERR(simple_char_class);
goto del_cdev;
}
/* 创建设备节点 */
simple_char_device = device_create(simple_char_class, NULL, dev, NULL,
DEVICE_NAME);
if (IS_ERR(simple_char_device)) {
printk(KERN_ERR "simple_char: failed to create device\n");
ret = PTR_ERR(simple_char_device);
goto destroy_class;
}
/* 分配设备数据 */
device_data = kzalloc(sizeof(*device_data), GFP_KERNEL);
if (!device_data) {
ret = -ENOMEM;
goto destroy_device;
}
device_data->buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!device_data->buffer) {
ret = -ENOMEM;
goto free_data;
}
device_data->capacity = BUFFER_SIZE;
device_data->size = 0;
printk(KERN_INFO "simple_char: loaded with major %d\n", major);
return 0;
free_data:
kfree(device_data);
destroy_device:
device_destroy(simple_char_class, dev);
destroy_class:
class_destroy(simple_char_class);
del_cdev:
cdev_del(&simple_char_cdev);
unregister:
unregister_chrdev_region(dev, 1);
return ret;
}
static void __exit simple_char_exit(void)
{
dev_t dev = MKDEV(major, 0);
kfree(device_data->buffer);
kfree(device_data);
device_destroy(simple_char_class, dev);
class_destroy(simple_char_class);
cdev_del(&simple_char_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "simple_char: unloaded\n");
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Glen");
MODULE_DESCRIPTION("Simple character device driver");
4.3 测试字符设备
# 编译并加载
$ make
$ sudo insmod simple_char.ko
# 确认设备节点已创建
$ ls -l /dev/simple_char
crw-rw---- 1 root root 236, 0 Feb 18 10:00 /dev/simple_char
# 测试写入
$ echo "Hello from userspace" | sudo tee /dev/simple_char
Hello from userspace
# 测试读取
$ sudo cat /dev/simple_char
Hello from userspace
# 查看日志
$ sudo dmesg | tail
[ 2345.678901] simple_char: loaded with major 236
[ 2356.789012] simple_char: device opened
[ 2356.789123] simple_char: wrote 21 bytes: Hello from userspace
[ 2356.789234] simple_char: device closed
[ 2367.890123] simple_char: device opened
[ 2367.890234] simple_char: read 21 bytes
[ 2367.890345] simple_char: device closed
内核不能安全地解引用用户空间指针。必须使用 copy_from_user() 和 copy_to_user() 进行数据传输。这些函数处理页错误、访问权限检查等边界情况。
5. 调试技术
内核模块调试困难——没有 printf,不能设置断点(通常),一个错误就导致系统崩溃。本节介绍实用的内核调试技术。
5.1 打印调试
/* 使用 pr_* 宏(推荐) */
#include <linux/printk.h>
pr_info("Informational message\n"); /* KERN_INFO */
pr_err("Error occurred: %d\n", errno); /* KERN_ERR */
pr_debug("Debug info: %s\n", info); /* KERN_DEBUG(需要定义 DEBUG)*/
/* 带模块名的打印 */
pr_info_once("This prints only once\n"); /* 仅打印一次 */
pr_warn_ratelimited("Rate limited warning\n"); /* 限速打印 */
/* 使用动态调试(无需重新编译) */
/* 开启特定文件的调试 */
$ echo 'file mymodule.c +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
/* 开启特定函数的调试 */
$ echo 'func my_function +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
5.2 使用 kgdb
# 在内核命令行启用 kgdb
console=ttyS0,115200 kgdboc=ttyS0,115200 kgdbwait
# 在目标机进入 kgdb
$ echo g | sudo tee /proc/sysrq-trigger
# 在宿主机连接 gdb
$ gdb ./vmlinux
(gdb) target remote /dev/ttyS0
(gdb) break my_function
(gdb) continue
# 常用命令
(gdb) p variable # 打印变量
(gdb) info registers # 查看寄存器
(gdb) bt # 回溯调用栈
(gdb) list # 显示源代码
5.3 使用 eBPF 和 bpftrace
# 跟踪函数调用
$ sudo bpftrace -e 'kprobe:do_sys_open { printf("%s opened %s\n",
comm, str(arg1)); }'
# 统计函数调用频率
$ sudo bpftrace -e 'kprobe:schedule { @[comm] = count(); }'
# 查看内核函数参数
$ sudo bpftrace -e 'kprobe:__kmalloc /comm == "mymodule"/ {
printf("%s allocated %d bytes\n", comm, arg0);
}'
# 使用 BCC 工具
$ sudo funccount 'tcp_sendmsg' # 统计 tcp_sendmsg 调用次数
$ sudo stackcount 'schedule' # 查看 schedule 的调用栈
6. 模块参数与 sysfs
模块参数允许用户在加载模块时传递配置。sysfs 提供运行时配置接口。这使得模块更加灵活,无需重新编译即可调整行为。
/* param_demo.c - 模块参数与 sysfs 示例 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
/* 模块参数 */
static int mode = 0; /* 工作模式 */
static char *device_name = "mydevice"; /* 设备名称 */
static int buffer_size = 1024; /* 缓冲区大小 */
/* 参数声明 */
module_param(mode, int, 0644);
MODULE_PARM_DESC(mode, "Working mode: 0=normal, 1=debug");
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name");
module_param(buffer_size, int, 0444); /* 只读 */
MODULE_PARM_DESC(buffer_size, "Buffer size in bytes");
/* 数组参数 */
static int irq_list[8];
static int irq_count;
module_param_array(irq_list, int, &irq_count, 0644);
MODULE_PARM_DESC(irq_list, "List of IRQ numbers to handle");
/* sysfs 动态属性 */
static struct kobject *demo_kobj;
static int stats_counter = 0;
static ssize_t stats_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", stats_counter);
}
static ssize_t stats_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
int ret = kstrtoint(buf, 10, &stats_counter);
return ret ? ret : count;
}
static struct kobj_attribute stats_attr = __ATTR(stats, 0664,
stats_show, stats_store);
static struct attribute *demo_attrs[] = {
&stats_attr.attr,
NULL,
};
static struct attribute_group demo_attr_group = {
.attrs = demo_attrs,
};
static int __init param_demo_init(void)
{
int ret;
printk(KERN_INFO "param_demo: mode=%d, device=%s, buffer=%d\n",
mode, device_name, buffer_size);
printk(KERN_INFO "param_demo: %d IRQs specified\n", irq_count);
for (int i = 0; i < irq_count; i++) {
printk(KERN_INFO " IRQ[%d] = %d\n", i, irq_list[i]);
}
/* 创建 sysfs 入口 */
demo_kobj = kobject_create_and_add("param_demo", kernel_kobj);
if (!demo_kobj)
return -ENOMEM;
ret = sysfs_create_group(demo_kobj, &demo_attr_group);
if (ret) {
kobject_put(demo_kobj);
return ret;
}
return 0;
}
static void __exit param_demo_exit(void)
{
sysfs_remove_group(demo_kobj, &demo_attr_group);
kobject_put(demo_kobj);
}
6.1 使用模块参数
# 加载时传递参数
$ sudo insmod param_demo.ko mode=1 device_name=eth0 buffer_size=4096
# 或使用 modprobe 配置文件
$ cat /etc/modprobe.d/param_demo.conf
options param_demo mode=1 device_name=eth0 irq_list=10,11,12
# 运行时修改参数(如果权限允许)
$ cat /sys/module/param_demo/parameters/mode
1
$ echo 0 | sudo tee /sys/module/param_demo/parameters/mode
# 访问 sysfs 属性
$ cat /sys/kernel/param_demo/stats
42
$ echo 100 | sudo tee /sys/kernel/param_demo/stats
7. 完整示例:简单计算器驱动
综合前面所学,我们实现一个支持 ioctl 的计算器设备驱动,展示完整的模块开发流程。
/* calc_driver.c - 完整计算器驱动 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>
#define DEVICE_NAME "calc"
#define CLASS_NAME "calc_class"
/* IOCTL 命令定义 */
#define CALC_MAGIC 'C'
#define CALC_ADD _IOWR(CALC_MAGIC, 0, struct calc_args)
#define CALC_SUB _IOWR(CALC_MAGIC, 1, struct calc_args)
#define CALC_MUL _IOWR(CALC_MAGIC, 2, struct calc_args)
#define CALC_DIV _IOWR(CALC_MAGIC, 3, struct calc_args)
#define CALC_RESET _IO(CALC_MAGIC, 4)
/* 传递给驱动的参数 */
struct calc_args {
int a;
int b;
int result;
};
static int major;
static struct cdev calc_cdev;
static struct class *calc_class;
/* 驱动状态 */
static int op_count = 0; /* 操作计数 */
static int last_result = 0; /* 上次结果 */
static long calc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct calc_args args;
int err = 0;
/* 拷贝用户参数 */
if (_IOC_DIR(cmd) & _IOC_READ) {
if (copy_from_user(&args, (void __user *)arg, sizeof(args)))
return -EFAULT;
}
switch (cmd) {
case CALC_ADD:
args.result = args.a + args.b;
pr_info("calc: %d + %d = %d\n", args.a, args.b, args.result);
break;
case CALC_SUB:
args.result = args.a - args.b;
pr_info("calc: %d - %d = %d\n", args.a, args.b, args.result);
break;
case CALC_MUL:
args.result = args.a * args.b;
pr_info("calc: %d * %d = %d\n", args.a, args.b, args.result);
break;
case CALC_DIV:
if (args.b == 0) {
pr_err("calc: division by zero!\n");
return -EINVAL;
}
args.result = args.a / args.b;
pr_info("calc: %d / %d = %d\n", args.a, args.b, args.result);
break;
case CALC_RESET:
op_count = 0;
last_result = 0;
pr_info("calc: reset\n");
return 0;
default:
return -EINVAL;
}
op_count++;
last_result = args.result;
/* 返回结果给用户 */
if (_IOC_DIR(cmd) & _IOC_WRITE) {
if (copy_to_user((void __user *)arg, &args, sizeof(args)))
return -EFAULT;
}
return 0;
}
static struct file_operations calc_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = calc_ioctl,
};
static int __init calc_init(void)
{
dev_t dev;
int ret;
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret) return ret;
major = MAJOR(dev);
cdev_init(&calc_cdev, &calc_fops);
ret = cdev_add(&calc_cdev, dev, 1);
if (ret) goto unregister;
calc_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(calc_class)) {
ret = PTR_ERR(calc_class);
goto del_cdev;
}
device_create(calc_class, NULL, dev, NULL, DEVICE_NAME);
pr_info("calc: loaded (major %d)\n", major);
return 0;
del_cdev:
cdev_del(&calc_cdev);
unregister:
unregister_chrdev_region(dev, 1);
return ret;
}
static void __exit calc_exit(void)
{
dev_t dev = MKDEV(major, 0);
device_destroy(calc_class, dev);
class_destroy(calc_class);
cdev_del(&calc_cdev);
unregister_chrdev_region(dev, 1);
pr_info("calc: unloaded (%d operations performed)\n", op_count);
}
module_init(calc_init);
module_exit(calc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Glen");
MODULE_DESCRIPTION("Simple calculator driver");
7.1 测试程序
/* calc_test.c - 用户空间测试程序 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define CALC_MAGIC 'C'
#define CALC_ADD _IOWR(CALC_MAGIC, 0, struct calc_args)
#define CALC_SUB _IOWR(CALC_MAGIC, 1, struct calc_args)
#define CALC_MUL _IOWR(CALC_MAGIC, 2, struct calc_args)
#define CALC_DIV _IOWR(CALC_MAGIC, 3, struct calc_args)
#define CALC_RESET _IO(CALC_MAGIC, 4)
struct calc_args {
int a;
int b;
int result;
};
int main(int argc, char *argv[])
{
int fd = open("/dev/calc", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
struct calc_args args = {.a = 20, .b = 10};
/* 测试加法 */
ioctl(fd, CALC_ADD, &args);
printf("20 + 10 = %d\n", args.result);
/* 测试除法 */
ioctl(fd, CALC_DIV, &args);
printf("20 / 10 = %d\n", args.result);
close(fd);
return 0;
}
/* 编译和运行 */
// $ gcc -o calc_test calc_test.c
// $ sudo ./calc_test
// 20 + 10 = 30
// 20 / 10 = 2
8. 小结
本章系统介绍了 Linux 内核模块的开发技术。从最简单的 "Hello World" 模块,到支持 ioctl 的完整字符设备驱动,我们逐步构建了内核编程的能力。
核心要点回顾:
- 模块生命周期:理解 __init/__exit 和 module_init/module_exit 的作用
- 符号导出:使用 EXPORT_SYMBOL 构建模块化驱动
- 字符设备:cdev、device_create 创建设备节点,实现 file_operations
- 安全边界:使用 copy_from_user/copy_to_user 安全访问用户空间
- 调试技术:printk、dynamic_debug、kgdb、eBPF 组合使用
- 用户接口:模块参数、sysfs、ioctl 提供配置和控制接口
内核模块开发是系统编程的高阶技能。安全性和稳定性是首要考虑——一个内核模块的错误可能导致整个系统崩溃。始终检查返回值,正确处理并发,并在真实环境部署前充分测试。
后续可以探索更复杂的主题:块设备驱动、网络设备驱动、PCI/USB 设备驱动、使用 Rust 编写内核模块(Linux 6.1+)等。