02-内核模块编程.md 26 KB


title: 内核模块编程 tags: [Linux内核, 模块, insmod, 驱动开发, 嵌入式] created: 2026-09-16 updated: 2026-09-16

pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十六章 字符设备驱动原理"

内核模块编程

💡 关联知识: [[02-内核模块机制]] | [[03-Linux驱动开发核心/01-字符设备驱动框架]]

一、内核模块概述

1.1 什么是内核模块

内核模块(Kernel Module)是可在运行时动态加载到 Linux 内核中的代码片段,无需重新编译整个内核即可扩展功能。模块编译后生成 .ko(Kernel Object)文件,通过 insmodmodprobe 命令加载到内核空间执行。

flowchart LR
    accTitle: 内核模块加载流程
    accDescr: 展示从源码到模块加载到内核的完整流程

    src[".c 源码"] -->|编译| ko[".ko 文件"]
    ko -->|insmod/modprobe| kernel["内核空间"]
    kernel -->|rmmod| unload["卸载模块"]

    classDef source fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
    classDef kernel fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
    classDef action fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d

    class src source
    class kernel kernel
    class ko,unload action

1.2 内核模块 vs 应用程序

特性 应用程序 内核模块
入口点 main() module_init() 指定的初始化函数
运行空间 用户态(Ring 3) 内核态(Ring 0)
标准库 libc(printf, malloc 等) 无 libc,使用内核 API(printk, kmalloc)
内存访问 虚拟地址(MMU 保护) 直接访问物理地址或通过内核映射
错误处理 errno 返回负错误码(-ENOMEM 等)
调试手段 gdb、printf printk、ftrace、kdump
编译方式 gcc 链接 libc 需要内核源码树,使用 obj-m 编译为 .ko
加载方式 直接执行 insmod / modprobe 动态加载
崩溃影响 进程崩溃,系统继续运行 系统崩溃(kernel panic)

1.3 内核模块应用场景

  • 设备驱动:字符设备、块设备、网络设备驱动
  • 文件系统:自定义文件系统实现(如 ext4、NFS 模块)
  • 网络协议栈:自定义网络过滤规则、VPN 隧道
  • 系统调用拦截:安全审计、行为监控
  • 硬件抽象层:为特定硬件提供统一接口

1.4 模块生命周期

stateDiagram-v2
    accTitle: 内核模块生命周期
    accDescr: 展示模块从编译到卸载的完整状态转换

    [*] --> 编译: make
    编译 --> 未加载: 生成 .ko
    未加载 --> 初始化: insmod
    初始化 --> 运行中: module_init() 返回 0
    初始化 --> 失败: module_init() 返回负值
    失败 --> [*]: 内核拒绝加载
    运行中 --> 清理: rmmod
    清理 --> [*]: module_exit() 执行完成

二、内核模块 API

2.1 模块注册宏

/* 注册模块初始化函数 —— insmod 时执行 */
module_init(xxx_init);

/* 注册模块退出函数 —— rmmod 时执行 */
module_exit(xxx_exit);

关键说明

  • module_init()module_exit() 必须成对出现
  • 若模块编译为 built-in(内建),则 module_init() 变为 early_initcall 级别
  • 初始化函数返回 0 表示成功,负值表示失败(如 -ENOMEM-ENODEV

2.2 模块信息宏

MODULE_LICENSE("GPL");                    /* 必须,否则内核被标记为 tainted */
MODULE_AUTHOR("ALIENTEK");               /* 模块作者 */
MODULE_DESCRIPTION("A simple example");  /* 模块简短描述 */
MODULE_VERSION("1.0");                   /* 模块版本号 */
MODULE_ALIAS("my-alias");                /* 模块别名 */
MODULE_INFO(intree, "Y");                /* 标记为内核源码树内模块 */

许可证选项

许可证 说明
GPL GNU General Public License,推荐使用
GPL v2 GNU General Public License v2
Dual MIT/GPL 双许可证
Proprietary 专有(会标记内核为 tainted)

2.3 printk 内核打印

#include <linux/kernel.h>

printk(KERN_INFO "info message\n");
printk(KERN_WARNING "warning message\n");
printk(KERN_ERR "error message\n");
printk(KERN_DEBUG "debug message\n");

日志级别对照表

数值 含义 使用场景
KERN_EMERG 0 系统崩溃 不可用于模块
KERN_ALERT 1 必须立即处理 严重安全问题
KERN_CRIT 2 严重错误 硬件故障
KERN_ERR 3 一般错误 驱动初始化失败
KERN_WARNING 4 警告 非致命问题
KERN_NOTICE 5 正常但重要 状态变化
KERN_INFO 6 信息性 模块加载成功
KERN_DEBUG 7 调试信息 开发调试

printk 注意事项

  • 必须以 \n 结尾,否则输出会混乱
  • 不同于 printfprintk 是内核函数,无需链接 libc
  • 输出到内核环形缓冲区,通过 dmesg 查看
  • 可通过 /proc/sys/kernel/printk 控制控制台日志级别

2.4 模块参数

#include <linux/module.h>
#include <linux/moduleparam.h>

/* 整型参数 */
static int myvalue = 0;
module_param(myvalue, int, 0644);
MODULE_PARM_DESC(myvalue, "An integer parameter");

/* 字符串参数 */
static char *mystring = "hello";
module_param(mystring, charp, 0644);
MODULE_PARM_DESC(mystring, "A string parameter");

/* 数组参数 */
static int myarray[3] = {1, 2, 3};
module_param_array(myarray, int, NULL, 0644);
MODULE_PARM_DESC(myarray, "An integer array");

参数权限值

权限 含义 sysfs 行为
0444 只读 用户可读,不可写
0644 可读写 用户可读写
0600 所有者读写 仅 root 可读写

加载时传参

# 加载时传递参数
sudo insmod mymodule.ko myvalue=100 mystring="world"

# 运行时通过 sysfs 修改(需权限 0644)
echo 200 > /sys/module/mymodule/parameters/myvalue

2.5 init/exit 函数修饰符

/* __init 标记:初始化函数,执行后释放内存 */
static int __init my_init(void)
{
    /* 初始化代码 */
    return 0;
}

/* __exit 标记:退出函数,模块编译为 built-in 时被丢弃 */
static void __exit my_exit(void)
{
    /* 清理代码 */
}

修饰符说明

修饰符 作用 使用场景
__init 标记函数为初始化代码 module_init 注册的函数
__exit 标记函数为退出代码 module_exit 注册的函数
__initdata 标记数据为初始化数据 初始化阶段使用的全局变量
__exitdata 标记数据为退出数据 退出阶段使用的全局变量

2.6 符号导出

/* 导出函数供其他模块使用(任何模块可用) */
EXPORT_SYMBOL(my_function);

/* 导出 GPL-only 符号(仅 GPL 模块可用) */
EXPORT_SYMBOL_GPL(my_function);

使用导出符号

/* 在另一个模块中使用 */
extern void my_function(void);

static int __init other_init(void)
{
    my_function();  /* 调用导出的函数 */
    return 0;
}

三、内核模块加载与卸载

3.1 insmod 命令

# 加载指定 .ko 文件
sudo insmod mymodule.ko

# 加载并传递参数
sudo insmod mymodule.ko myvalue=100 mystring="hello"

特点

  • 直接加载指定的 .ko 文件
  • 不处理模块间的依赖关系
  • 不检查模块版本兼容性
  • 适合调试和开发阶段

3.2 modprobe 命令

# 加载模块(自动处理依赖)
sudo modprobe mymodule

# 加载并传递参数
sudo modprobe mymodule myvalue=100

# 卸载模块(自动处理依赖)
sudo modprobe -r mymodule

特点

  • 自动处理模块依赖关系
  • 默认在 /lib/modules/$(uname -r)/ 目录查找模块
  • 卸载时自动递归卸载未被使用的依赖模块
  • 生产环境推荐使用

3.3 rmmod 命令

# 卸载模块
sudo rmmod mymodule

# 强制卸载(慎用)
sudo rmmod -f mymodule

3.4 lsmod 命令

# 列出已加载模块
lsmod

# 输出示例:
# Module                  Size  Used by
# mymodule               16384  0
# hid_logitech_dj        28672  0
# usbhid                 65536  0

字段说明

  • Module:模块名称
  • Size:模块大小(字节)
  • Used by:引用计数(0 表示未被使用)

3.5 /proc/modules

# 查看已加载模块(等同于 lsmod)
cat /proc/modules

# 查看模块参数
cat /sys/module/mymodule/parameters/myvalue

3.6 modinfo 命令

# 查看模块信息
modinfo mymodule.ko

# 输出示例:
# filename:       /home/user/mymodule.ko
# license:        GPL
# description:    A simple example module
# author:         ALIENTEK
# vermagic:       4.1.15 SMP preempt mod_unload ARMv7
# parm:           myvalue:An integer parameter (int)
# parm:           mystring:A string parameter (charp)

3.7 加载与卸载流程对比

操作 insmod modprobe rmmod modprobe -r
加载模块
卸载模块
处理依赖
指定路径 需要 不需要 不需要 不需要
推荐场景 调试 生产 生产 生产

四、内核模块编译

4.1 本机编译 Makefile

# 内核模块 Makefile(放在与 .c 文件同一目录)

obj-m := mymodule.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

编译命令

# 编译模块
make

# 清理编译产物
make clean

# 生成的文件
# mymodule.ko  - 编译后的内核模块
# mymodule.mod.c - 模块元信息
# mymodule.o - 目标文件
# modules.order - 模块列表

4.2 交叉编译 Makefile

# 交叉编译 Makefile

obj-m := mymodule.o

# 内核源码路径(根据实际路径修改)
KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga

# 交叉编译工具链
CROSS_COMPILE := arm-linux-gnueabihf-

# 目标架构
ARCH := arm

# 当前目录
PWD := $(shell pwd)

all:
	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules

clean:
	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean

IMX6ULL 交叉编译示例

# 设置环境变量
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

# 编译模块
make

# 拷贝到开发板
scp mymodule.ko root@192.168.1.200:/home/root/

4.3 编译选项说明

选项 作用
obj-m 编译为可加载模块
obj-y 编译进内核(built-in)
-C $(KDIR) 切换到内核源码目录执行
M=$(PWD) 指定模块源码目录
ARCH=arm 指定目标架构
CROSS_COMPILE= 指定交叉编译工具链前缀

4.4 编译常见问题

问题 原因 解决方法
vermagic 不匹配 模块与内核版本不一致 使用对应版本的内核源码编译
insmod: ERROR 模块签名验证失败 使用 insmod -f(慎用)或禁用签名
Module not found modprobe 执行 depmod 运行 depmod -a 更新模块依赖
tainted kernel 缺少 MODULE_LICENSE 添加 MODULE_LICENSE("GPL")

五、完整源码分析

5.1 hello 模块示例

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

static int hello_value = 0;
module_param(hello_value, int, 0644);
MODULE_PARM_DESC(hello_value, "A demonstration parameter");

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel world! value=%d\n", hello_value);
    return 0;  /* 返回 0 表示成功 */
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_DESCRIPTION("A simple hello world kernel module");
MODULE_VERSION("1.0");

逐行解释

行号 代码 说明
1-3 #include 包含内核头文件
5-6 static int hello_value 定义模块参数变量
7 module_param(...) 注册模块参数,权限 0644
8 MODULE_PARM_DESC(...) 描述参数用途
10-13 hello_init() 模块初始化函数,加载时执行
14-17 hello_exit() 模块退出函数,卸载时执行
19-20 module_init/exit 注册初始化和退出函数
22-25 MODULE_* 模块元信息(许可证、作者、描述、版本)

5.2 带参数模块示例

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

/* 定义多种类型的参数 */
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of iterations");

static char *msg = "hello";
module_param(msg, charp, 0644);
MODULE_PARM_DESC(msg, "Message to display");

static int flags[3] = {0, 0, 0};
module_param_array(flags, int, NULL, 0644);
MODULE_PARM_DESC(flags, "An array of flags");

static int __init param_init(void)
{
    int i;
    printk(KERN_INFO "Module loaded with:\n");
    printk(KERN_INFO "  count = %d\n", count);
    printk(KERN_INFO "  msg = %s\n", msg);
    for (i = 0; i < 3; i++)
        printk(KERN_INFO "  flags[%d] = %d\n", i, flags[i]);
    return 0;
}

static void __exit param_exit(void)
{
    printk(KERN_INFO "Module unloaded\n");
}

module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");

使用方法

# 加载时传递参数
sudo insmod param.ko count=5 msg="world" flags=1,2,3

# 查看输出
dmesg | tail -5
# [xxxxx] Module loaded with:
# [xxxxx]   count = 5
# [xxxxx]   msg = world
# [xxxxx]   flags[0] = 1
# [xxxxx]   flags[1] = 2
# [xxxxx]   flags[2] = 3

# 运行时修改参数
echo 10 > /sys/module/param/parameters/count
cat /sys/module/param/parameters/msg

5.3 字符设备模块示例

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NAME  "chrtest"
#define DEVICE_COUNT 1

/* 字符设备结构体 */
static struct cdev chrtest_cdev;
static dev_t devno;
static int major = 0;  /* 0 表示动态分配 */
static int minor = 0;
static struct class *chrtest_class;
static struct device *chrtest_device;

/* 设备数据缓冲区 */
static char readbuf[100];
static char writebuf[100];
static int data_len = 0;

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "chrtest: device opened\n");
    return 0;
}

/* 读取设备 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
                            size_t cnt, loff_t *offt)
{
    int ret = 0;

    if (cnt > data_len)
        cnt = data_len;

    if (copy_to_user(buf, readbuf, cnt)) {
        ret = -EFAULT;
        goto out;
    }

    printk(KERN_INFO "chrtest: read %zu bytes\n", cnt);
    ret = cnt;

out:
    return ret;
}

/* 写入设备 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf,
                             size_t cnt, loff_t *offt)
{
    int ret = 0;

    if (cnt > sizeof(writebuf))
        cnt = sizeof(writebuf);

    if (copy_from_user(writebuf, buf, cnt)) {
        ret = -EFAULT;
        goto out;
    }

    memcpy(readbuf, writebuf, cnt);
    data_len = cnt;

    printk(KERN_INFO "chrtest: wrote %zu bytes\n", cnt);
    ret = cnt;

out:
    return ret;
}

/* 关闭设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "chrtest: device closed\n");
    return 0;
}

/* 文件操作结构体 */
static struct file_operations chrtest_fops = {
    .owner   = THIS_MODULE,
    .open    = chrtest_open,
    .read    = chrtest_read,
    .write   = chrtest_write,
    .release = chrtest_release,
};

/* 模块初始化 */
static int __init chrtest_init(void)
{
    int ret = 0;

    /* 动态分配设备号 */
    if (major) {
        devno = MKDEV(major, minor);
        ret = register_chrdev_region(devno, DEVICE_COUNT, DEVICE_NAME);
    } else {
        ret = alloc_chrdev_region(&devno, 0, DEVICE_COUNT, DEVICE_NAME);
        major = MAJOR(devno);
        minor = MINOR(devno);
    }

    if (ret < 0) {
        printk(KERN_ERR "chrtest: failed to get device number\n");
        goto fail_devno;
    }

    /* 初始化 cdev */
    cdev_init(&chrtest_cdev, &chrtest_fops);
    ret = cdev_add(&chrtest_cdev, devno, DEVICE_COUNT);
    if (ret < 0) {
        printk(KERN_ERR "chrtest: failed to add cdev\n");
        goto fail_cdev;
    }

    /* 创建设备类 */
    chrtest_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(chrtest_class)) {
        ret = PTR_ERR(chrtest_class);
        printk(KERN_ERR "chrtest: failed to create class\n");
        goto fail_class;
    }

    /* 创建设备节点 */
    chrtest_device = device_create(chrtest_class, NULL, devno,
                                   NULL, DEVICE_NAME);
    if (IS_ERR(chrtest_device)) {
        ret = PTR_ERR(chrtest_device);
        printk(KERN_ERR "chrtest: failed to create device\n");
        goto fail_device;
    }

    printk(KERN_INFO "chrtest: module loaded, major=%d minor=%d\n",
           major, minor);
    return 0;

fail_device:
    class_destroy(chrtest_class);
fail_class:
    cdev_del(&chrtest_cdev);
fail_cdev:
    unregister_chrdev_region(devno, DEVICE_COUNT);
fail_devno:
    return ret;
}

/* 模块退出 */
static void __exit chrtest_exit(void)
{
    device_destroy(chrtest_class, devno);
    class_destroy(chrtest_class);
    cdev_del(&chrtest_cdev);
    unregister_chrdev_region(devno, DEVICE_COUNT);
    printk(KERN_INFO "chrtest: module unloaded\n");
}

module_init(chrtest_init);
module_exit(chrtest_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");

六、内核空间与用户空间

6.1 内存映射

flowchart TB
    accTitle: 内核空间与用户空间内存布局
    accDescr: 展示 32 位 Linux 系统的内存空间划分

    user["用户空间<br/>0x00000000 - 0xBFFFFFFF<br/>应用程序运行区域"]
    kernel["内核空间<br/>0xC0000000 - 0xFFFFFFFF<br/>内核代码和数据"]

    user --- kernel

    classDef user fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
    classDef kernel fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12

    class user user
    class kernel kernel

6.2 数据传递函数

#include <linux/uaccess.h>

/* 将内核数据复制到用户空间 */
unsigned long copy_to_user(void __user *to,
                           const void *from,
                           unsigned long n);

/* 将用户数据复制到内核空间 */
unsigned long copy_from_user(void *to,
                             const void __user *from,
                             unsigned long n);

返回值说明

  • 返回 0:成功
  • 返回非 0:失败,未复制的字节数

6.3 数据传递示例

/* 内核读取用户数据(用户空间 → 内核空间) */
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t cnt, loff_t *offt)
{
    if (copy_to_user(buf, kernel_buf, cnt))
        return -EFAULT;
    return cnt;
}

/* 内核写入用户数据(内核空间 → 用户空间) */
static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t cnt, loff_t *offt)
{
    if (copy_from_user(kernel_buf, buf, cnt))
        return -EFAULT;
    return cnt;
}

6.4 用户空间与内核空间对比

特性 用户空间 内核空间
权限级别 Ring 3 Ring 0
地址空间 虚拟地址(进程隔离) 统一虚拟地址
内存访问 不能直接访问硬件 可直接访问硬件
崩溃影响 进程终止 系统崩溃
内存分配 malloc/free kmalloc/kfree
打印输出 printf printk
调试工具 gdb, strace printk, ftrace, kdump

6.5 常见数据传递 API

API 方向 说明
copy_to_user() 内核 → 用户 复制数据到用户空间
copy_from_user() 用户 → 内核 从用户空间复制数据
put_user() 内核 → 用户 写入单个值到用户空间
get_user() 用户 → 内核 从用户空间读取单个值

七、跨平台对比

7.1 IMX6ULL vs STM32 vs RK3568 模块差异

特性 IMX6ULL STM32MP1 RK3568
架构 ARM Cortex-A7 ARM Cortex-A7/A9 ARM Cortex-A55
内核版本 4.1.15 5.10 5.10
交叉编译器 arm-linux-gnueabihf- arm-linux-gnueabihf- aarch64-linux-gnu-
内核源码 NXP 官方 ST 官方 Rockchip 官方
模块加载 支持 支持 支持
设备树 支持 支持 支持
开发工具 VSCode + GCC STM32CubeIDE VSCode + GCC

7.2 模块编译差异

# IMX6ULL 交叉编译
KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm

# RK3568 交叉编译(64位)
KDIR := /home/user/kernel-5.10
CROSS_COMPILE := aarch64-linux-gnu-
ARCH := arm64

八、面试精选

题目 1:什么是内核模块?它的优势是什么?

考察点:模块概念

参考答案

  • 定义:可在运行时加载/卸载的内核代码片段,编译为 .ko 文件
  • 优势
    • 不需要重新编译整个内核
    • 可以动态添加/移除功能
    • 便于调试和开发(加载失败不影响系统启动)
    • 节省内核镜像大小
  • 应用:设备驱动、文件系统、网络协议、安全模块

题目 2:insmod 和 modprobe 有什么区别?

考察点:模块加载机制

参考答案

特性 insmod modprobe
依赖处理 不处理 自动处理
路径要求 需要完整路径 自动在 /lib/modules/$(uname -r)/ 查找
推荐场景 调试 生产环境
卸载功能 rmmod modprobe -r 自动递归卸载
依赖信息 读取 modules.dep

题目 3:为什么内核模块中推荐使用 printk 而不是 printf?

考察点:内核 API

参考答案

  • 内核没有链接 libc,printf 不可用
  • printk 是内核自带的打印函数,无需额外依赖
  • 支持日志级别控制(KERN_INFOKERN_ERR 等)
  • 输出到内核环形缓冲区,可通过 dmesg 查看
  • 可通过 /proc/sys/kernel/printk 动态调整日志级别

题目 4:__init__exit 修饰符的作用是什么?

考察点:内存优化

参考答案

  • __init:标记初始化函数,执行后释放其内存(节省内核内存)
  • __exit:标记退出函数,模块编译为 built-in 时该函数被丢弃
  • __initdata:标记初始化阶段使用的数据,初始化完成后释放
  • 这些修饰符帮助内核优化内存使用,仅在动态加载模块时保留相关代码

题目 5:如何实现内核空间与用户空间的数据传递?

考察点:数据传递机制

参考答案

  • copy_to_user():内核数据 → 用户空间(如 read 函数)
  • copy_from_user():用户空间 → 内核数据(如 write 函数)
  • 返回值为 0 表示成功,非 0 表示失败(需返回 -EFAULT
  • 不能直接使用指针传递,因为两个空间的虚拟地址不同
  • 大数据量场景可使用 mmap 将内核内存映射到用户空间

最后更新: 2026-09-16