内核模块编程实践

1. 内核模块概述

可加载内核模块(Loadable Kernel Module,LKM)是 Linux 内核的扩展机制,允许在运行时动态添加或移除内核功能,无需重新编译内核或重启系统。设备驱动程序、文件系统、网络协议栈等都以模块形式存在。

模块与内核运行在同一地址空间,具有完全的内核权限。这使得模块极其强大,但也极其危险——一个错误的模块可能导致系统崩溃。因此,内核模块开发需要严格的编码规范、充分的测试和完善的错误处理。

模块 vs 内置

内核编译时可以选择将功能内置(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 与日志级别

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+)等。

← 返回文章列表