在操作系统底层开发领域,内核模块(Loadable Kernel Module, LKM)是扩展Linux内核功能的动态加载组件。与需要重新编译整个内核的传统方式不同,模块化设计允许我们在运行时按需加载和卸载功能,这种机制既保持了内核的稳定性,又提供了极大的灵活性。
我最初接触内核模块是在处理一块特殊硬件设备时,当时发现标准内核缺乏对该设备的支持。通过开发定制内核模块,不仅解决了设备驱动问题,还让我深入理解了Linux内核的工作机制。这种"按需扩展"的设计哲学,正是Linux系统能够适配从嵌入式设备到超级计算机等各种场景的关键所在。
内核模块开发与普通用户空间编程存在显著差异:
重要提示:内核模块调试比应用程序困难得多,建议在虚拟机中开发,并随时做好系统崩溃的准备。我在早期开发时曾因一个空指针引用导致物理机死机,损失了正在编写的文档。
完整的模块开发环境需要:
在Ubuntu系统上可通过以下命令安装基础工具:
bash复制sudo apt update
sudo apt install build-essential linux-headers-$(uname -r) git
虽然头文件足够编译简单模块,但复杂开发需要完整内核源码:
bash复制# 获取当前运行内核的源码版本
apt source linux-image-$(uname -r)
我习惯在内核源码树的drivers目录下创建专用目录存放自定义模块,这样可以方便地引用内核头文件,还能利用内核的Kbuild系统。
一个典型的模块编译Makefile如下:
makefile复制obj-m := mymodule.o
mymodule-objs := main.o helper.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
这个模板中:
每个内核模块必须包含以下基本元素:
c复制#include <linux/module.h>
#include <linux/init.h>
static int __init mymodule_init(void)
{
printk(KERN_INFO "Module loaded\n");
return 0;
}
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "Module unloaded\n");
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");
关键点解析:
模块使用printk输出日志,可以通过多种方式查看:
bash复制# 查看实时内核日志
dmesg -w
# 查看特定级别的消息
dmesg --level=info
# 跟踪系统日志文件
tail -f /var/log/kern.log
我在调试时习惯在printk消息中加入自定义前缀,方便在大量日志中快速定位:
c复制printk(KERN_DEBUG "MYMODULE: Debug message at %s:%d\n", __FILE__, __LINE__);
内核模块支持通过命令行参数配置:
c复制static char *name = "default";
static int count = 1;
module_param(name, charp, 0644);
module_param(count, int, 0644);
MODULE_PARM_DESC(name, "Device name to use");
MODULE_PARM_DESC(count, "Number of devices to create");
加载模块时可以这样传参:
bash复制sudo insmod mymodule.ko name="custom" count=5
参数权限(0644)决定了是否能在/sys/module/mymodule/parameters中查看和修改。
内核模块需要处理多处理器、中断、抢占等并发场景,主要同步机制包括:
c复制atomic_t counter = ATOMIC_INIT(0);
atomic_inc(&counter);
c复制DEFINE_SPINLOCK(my_lock);
spin_lock(&my_lock);
// 临界区
spin_unlock(&my_lock);
c复制static DEFINE_MUTEX(my_mutex);
mutex_lock(&my_mutex);
// 可能休眠的操作
mutex_unlock(&my_mutex);
我在实现一个多线程访问的环形缓冲区时,发现自旋锁在竞争激烈时会导致CPU使用率飙升,改用读写锁(rwlock_t)后性能提升了40%。
内核提供了多种时间管理机制:
c复制unsigned long timeout = jiffies + msecs_to_jiffies(100);
if (time_after(jiffies, timeout)) {
// 超时处理
}
c复制struct timer_list my_timer;
void timer_callback(struct timer_list *t)
{
// 定时处理逻辑
mod_timer(t, jiffies + msecs_to_jiffies(1000)); // 重新激活
}
// 初始化
timer_setup(&my_timer, timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
c复制static struct hrtimer my_hrtimer;
enum hrtimer_restart hrtimer_callback(struct hrtimer *timer)
{
// 回调逻辑
return HRTIMER_NORESTART;
}
// 初始化
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = hrtimer_callback;
hrtimer_start(&my_hrtimer, ms_to_ktime(10), HRTIMER_MODE_REL);
长时间运行的任务应该使用工作队列,避免阻塞中断上下文:
c复制static struct work_struct my_work;
void work_handler(struct work_struct *work)
{
// 延迟执行的任务
}
// 初始化
INIT_WORK(&my_work, work_handler);
// 调度执行
schedule_work(&my_work);
对于需要延迟执行的任务,可以使用延迟工作队列:
c复制static struct delayed_work my_delayed_work;
void delayed_work_handler(struct work_struct *work)
{
// 延迟后执行的任务
}
// 初始化
INIT_DELAYED_WORK(&my_delayed_work, delayed_work_handler);
// 调度延迟执行(1秒后)
schedule_delayed_work(&my_delayed_work, msecs_to_jiffies(1000));
内核模块崩溃时通常会产生oops消息,包含关键调试信息:
分析示例oops:
code复制[ 1234.567890] Unable to handle kernel NULL pointer dereference at virtual address 00000000
[ 1234.567901] pgd = c0004000
[ 1234.567908] [00000000] *pgd=00000000
[ 1234.567923] Internal error: Oops: 805 [#1] PREEMPT SMP ARM
[ 1234.567931] Modules linked in: mymodule(+)
[ 1234.567945] CPU: 0 PID: 1234 Comm: insmod Tainted: G W O 4.19.0-1-armmp #1 Debian 4.19.12-1
[ 1234.567954] Hardware name: ARM-Versatile Express
[ 1234.567968] PC is at mymodule_init+0x1c/0x28 [mymodule]
[ 1234.567977] LR is at do_one_initcall+0x50/0x1a0
关键信息:
c复制#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "do_fork",
};
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
printk(KERN_INFO "do_fork called by %pS\n",
(void *)regs->ARM_pc);
return 0;
}
// 注册
kp.pre_handler = handler_pre;
register_kprobe(&kp);
// 卸载
unregister_kprobe(&kp);
bash复制# 启用函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
# 设置过滤条件
echo "mymodule_*" > /sys/kernel/debug/tracing/set_ftrace_filter
# 开始跟踪
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 查看结果
cat /sys/kernel/debug/tracing/trace_pipe
bash复制# 目标机启动参数添加
kgdboc=ttyS0,115200 kgdbwait
# 主机端gdb连接
(gdb) target remote /dev/ttyUSB0
(gdb) set remotebaud 115200
c复制if (copy_from_user(&value, user_ptr, sizeof(value))) {
return -EFAULT;
}
if (value < MIN_VALUE || value > MAX_VALUE) {
return -EINVAL;
}
c复制// 分配时初始化
buf = kzalloc(size, GFP_KERNEL);
if (!buf) {
return -ENOMEM;
}
// 释放前检查
if (buf) {
kfree(buf);
buf = NULL;
}
c复制struct my_data {
struct kref refcount;
// 其他字段
};
void data_release(struct kref *ref)
{
struct my_data *data = container_of(ref, struct my_data, refcount);
kfree(data);
}
// 增加引用
kref_get(&data->refcount);
// 减少引用
kref_put(&data->refcount, data_release);
c复制static struct kmem_cache *my_cache;
// 初始化
my_cache = kmem_cache_create("my_cache",
sizeof(struct my_object),
0, SLAB_HWCACHE_ALIGN, NULL);
// 分配
struct my_object *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放
kmem_cache_free(my_cache, obj);
// 销毁
kmem_cache_destroy(my_cache);
c复制static DEFINE_PER_CPU(struct buffer, percpu_buffers);
void fast_path_function(void)
{
struct buffer *buf = this_cpu_ptr(&percpu_buffers);
// 使用预分配缓冲区
}
c复制static inline unsigned long get_cycle_count(void)
{
unsigned long cycles;
asm volatile("mrs %0, pmccntr_el0" : "=r" (cycles));
return cycles;
}
完整字符设备驱动示例:
c复制#include <linux/fs.h>
#include <linux/cdev.h>
#define DEVICE_NAME "mychardev"
static int major;
static struct cdev my_cdev;
static int device_open(struct inode *inode, struct file *file)
{
// 打开设备处理
return 0;
}
static ssize_t device_read(struct file *filp, char __user *buf,
size_t count, loff_t *pos)
{
// 读设备处理
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = device_open,
.read = device_read,
// 其他操作...
};
static int __init chardev_init(void)
{
dev_t devno;
// 动态申请设备号
if (alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME) < 0) {
return -1;
}
major = MAJOR(devno);
// 初始化cdev结构
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
// 注册字符设备
if (cdev_add(&my_cdev, devno, 1) < 0) {
unregister_chrdev_region(devno, 1);
return -1;
}
return 0;
}
static void __exit chardev_exit(void)
{
dev_t devno = MKDEV(major, 0);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
}
为设备添加控制接口:
c复制#include <linux/ioctl.h>
#define MYIOCTL_TYPE 0x93
#define MYIOCTL_RESET _IO(MYIOCTL_TYPE, 0)
#define MYIOCTL_SETVAL _IOW(MYIOCTL_TYPE, 1, int)
#define MYIOCTL_GETVAL _IOR(MYIOCTL_TYPE, 2, int)
static long device_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
static int value = 0;
switch (cmd) {
case MYIOCTL_RESET:
value = 0;
break;
case MYIOCTL_SETVAL:
if (copy_from_user(&value, (int __user *)arg, sizeof(int)))
return -EFAULT;
break;
case MYIOCTL_GETVAL:
if (copy_to_user((int __user *)arg, &value, sizeof(int)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
// 添加到file_operations
static struct file_operations fops = {
.unlocked_ioctl = device_ioctl,
// 其他操作...
};
用户空间调用示例:
c复制int fd = open("/dev/mychardev", O_RDWR);
ioctl(fd, MYIOCTL_SETVAL, 100);
int val;
ioctl(fd, MYIOCTL_GETVAL, &val);
从Linux 3.7开始支持模块签名验证,防止加载被篡改的模块:
bash复制openssl req -new -nodes -utf8 -sha256 -days 36500 \
-batch -x509 -config x509.genkey \
-outform DER -out signing_key.x509 \
-keyout signing_key.pem
code复制CONFIG_MODULE_SIG=y
CONFIG_MODULE_SIG_ALL=y
CONFIG_MODULE_SIG_SHA256=y
CONFIG_MODULE_SIG_KEY="certs/signing_key.pem"
bash复制make modules
内核提供多种模块加载限制:
bash复制echo 1 > /sys/module/module/parameters/sig_enforce
bash复制# 仅允许加载特定目录下的模块
echo "/lib/modules/$(uname -r)/extra" > /sys/module/module/parameters/source_dir
bash复制echo "blacklist malicious_module" >> /etc/modprobe.d/blacklist.conf
我在生产环境中会结合SELinux或AppArmor实现更细粒度的模块加载控制,确保只有经过审核的模块能被加载。
eBPF(extended Berkeley Packet Filter)是Linux内核中的虚拟机,允许安全地在内核空间运行沙盒化程序。与内核模块相比,eBPF具有以下优势:
可以在内核模块中加载和管理eBPF程序:
c复制#include <linux/bpf.h>
#include <linux/filter.h>
static struct bpf_prog *prog = NULL;
static int load_bpf_program(void)
{
struct bpf_insn prog_insns[] = {
BPF_MOV64_IMM(BPF_REG_0, 42), // r0 = 42
BPF_EXIT_INSN(), // return r0
};
prog = bpf_prog_alloc(bpf_prog_size(2), 0);
if (!prog)
return -ENOMEM;
memcpy(prog->insnsi, prog_insns, sizeof(prog_insns));
prog->len = 2;
prog->aux->ops = &bpf_prog_offload_ops;
return bpf_prog_select_runtime(prog, NULL);
}
static void unload_bpf_program(void)
{
if (prog)
bpf_prog_put(prog);
}
通过BPF映射实现双向通信:
c复制#include <linux/bpf.h>
static struct bpf_map *map = NULL;
static int create_bpf_map(void)
{
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
};
map = bpf_map_new(&attr);
if (IS_ERR(map))
return PTR_ERR(map);
return 0;
}
static void update_bpf_map(int key, int value)
{
bpf_map_update_elem(map, &key, &value, BPF_ANY);
}
static int lookup_bpf_map(int key)
{
int value;
if (bpf_map_lookup_elem(map, &key, &value))
return -1;
return value;
}
这种混合架构既保持了内核模块的强大功能,又获得了eBPF的安全性和灵活性,特别适合需要高性能数据处理的场景。