欢迎光临
我们一直在努力

【内核驱动基础】超详细一文介绍Linux驱动模块

目录

一、什么是内核模块

二、为什么要用内核模块

三、模块和驱动的关系

四、内核模块实验

4.0 实验程序

4.1 模块程序解释

4.1.1 驱动头文件解释

4.1.2 init/exit:模块的“生命周期回调”

4.1.3 printk介绍

4.1.4 module_init/module_exit

4.1.5 MODULE_* 元信息

4.2 Makefile文件编写与模块装载卸载

4.2.1 Makefile文件详解

4.2.2 模块的装载卸载

4.2.3 模块相关的指令

参考资料


一、什么是内核模块

内核模块可以理解为“给正在运行的 Linux 内核安装的插件”。

Linux 内核本身是一套常驻内存、负责管理硬件和系统资源的核心程序。

内核模块(Kernel Module)就是一段可以在系统运行过程中,动态加载到内核里执行的代码文件(通常以 .ko 结尾)。

加载后,它就和内核代码一样运行在内核态,可以调用内核提供的接口,做驱动、文件系统、网络协议等内核级工作;不用时还可以卸载。

二、为什么要用内核模块

如果把所有驱动和功能都写死在内核里,会带来几个问题:

  • 内核会变得很大:很多硬件你根本用不到,却都被打包进去了。
  • 更新不方便:改一个驱动就要重新编译、替换整个内核。
  • 调试效率低:驱动开发时频繁改动,重启换内核成本高。
  • 模块解决的就是这些问题:

    • 用到什么功能就加载什么模块(按需加载)
    • 驱动更新时只替换 .ko 文件(无需动整个内核)
    • 开发调试可以“改完就加载试一次”(效率高)

    一个直观的对比如下:

    • 编译进内核(built-in):像把功能焊死在主板上,启动就存在,不能随时拆。
    • 内核模块(module):像插在主板插槽里的扩展卡,用的时候插上,不用可以拔掉。

    三、模块和驱动的关系

    大多数 Linux 设备驱动(网卡、USB、摄像头、GPIO 等)都是以内核模块形式提供的:

    • 插入 USB 网卡时,系统会加载对应的驱动模块
    • 拔掉设备后,模块可以保持加载(也可以手动卸载)

    所以你写驱动时,通常也是写一个模块:

    • 模块被加载 → 驱动初始化、注册设备
    • 模块被卸载 → 驱动注销、释放资源

    四、内核模块实验

    4.0 实验程序

    从一个简单的模块程序开始介绍内核模块:

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

    static int __init hello_init(void)
    {
    printk(KERN_EMERG "[ KERN_EMERG ] Hello Module Init\\\\n");
    printk( "[ default ] Hello Module Init\\\\n");
    return 0;
    }

    static void __exit hello_exit(void)
    {
    printk("[ default ] Hello Module Exit\\\\n");
    }

    module_init(hello_init);
    module_exit(hello_exit);

    MODULE_LICENSE("GPL2");
    MODULE_AUTHOR("cc ");
    MODULE_DESCRIPTION("hello world module");
    MODULE_ALIAS("test_module");

    这个程序实现了一个最小可运行的内核模块:

    • 模块被加载(insmod/modprobe)时,内核会调用 hello_init(),打印两条日志,然后返回 0 表示初始化成功。
    • 模块被卸载(rmmod)时,内核会调用 hello_exit(),打印一条日志并退出。

    模块的“入口/出口”由:

    module_init(hello_init);
    module_exit(hello_exit);

    这两句注册。

    4.1 模块程序解释

    4.1.1 驱动头文件解释

    Linux内核中常常用到如下的头文件:

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

    • linux/module.h

      提供模块框架的核心宏和声明:

      module_init/module_exit、MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_ALIAS 等。

    • linux/init.h

      提供 __init、__exit 这类“生命周期标记”宏。

    • linux/kernel.h

      提供常用内核宏/函数声明(包含 printk 所需的一些定义;在部分内核版本里日志相关宏也会经由这里间接引入)。

    4.1.2 init/exit:模块的“生命周期回调”

    __init 的意义:

    static int __init hello_init(void)

    __init 表示:初始化阶段用到的代码。

    对于“编译进内核”的代码(built-in),内核启动完成后,这段 init 代码所在内存往往可以被释放/回收,以节省内存。

    对于“可加载模块”(.ko),它依然表示“初始化代码段”,但具体是否回收以及何种方式回收,取决于内核实现;你可以把它理解为一种“告诉内核和工具链:这段代码只在 init 阶段需要”。

    init 返回值决定模块是否“算加载成功”

    return 0;

    • 返回 0:模块加载成功,随后你会在 lsmod 里看到它。
    • 返回 非 0:模块加载失败,内核会回滚已做的部分工作,模块不会留在系统里。驱动开发中常用这个机制处理“硬件不存在/资源申请失败”等情况。

    __exit 的意义:

    static void __exit hello_exit(void)

    • __exit 表示:卸载阶段才需要的代码。
    • 如果某段代码是 built-in(无法卸载),__exit 标记通常会让编译器/链接阶段倾向于丢弃它(因为根本不会被执行)。
    • 而在模块场景下,rmmod 会触发卸载流程,这段代码会被执行,用来释放资源。

    在C语言中,static关键字的作用如下:

  • static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期。
  • static的修饰全局变量只能在本文件中访问,不能在其它文件中访问。
  • static修饰的函数只能在本文件中调用,不能被其他文件调用。
  • 内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。

    4.1.3 printk介绍

    printf是glibc实现的打印函数,工作于用户空间

    printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。

    • #define KERN_EMERG “<0>” 通常是系统崩溃前的信息
    • #define KERN_ALERT “<1>” 需要立即处理的消息
    • #define KERN_CRIT “<2>” 严重情况
    • #define KERN_ERR “<3>” 错误情况
    • #define KERN_WARNING “<4>” 有问题的情况
    • #define KERN_NOTICE “<5>” 注意信息
    • #define KERN_INFO “<6>” 普通消息
    • #define KERN_DEBUG “<7>” 调试信息

    查看当前系统printk打印等级:cat /proc/sys/kernel/printk, 从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。

    打印内核所有打印信息:dmesg,注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。

    4.1.4 module_init/module_exit

    module_init(hello_init);
    module_exit(hello_exit);

    这两句的核心作用是:把函数指针放到模块的“特殊段/结构”中,让内核的模块加载器在合适时机调用。

    从行为上你可以理解为:

    • insmod hello.ko 时:内核完成装载与符号解析后调用 hello_init()
    • rmmod hello 时:如果引用计数允许卸载,内核调用 hello_exit()

    补充一点常见认知:

    • 模块加载卸载本质上对应内核的模块管理流程(底层会涉及 ELF 解析、重定位、符号解析、vermagic 校验等),但对写模块而言你只需要把 init/exit “挂上去”。

    4.1.5 MODULE_* 元信息

    在驱动模块的尾部,我们添加了一些辅助信息:

    MODULE_LICENSE("GPL2");
    MODULE_AUTHOR("cc ");
    MODULE_DESCRIPTION("hello world module");
    MODULE_ALIAS("test_module");

    函数作用
    MODULE_LICENSE() 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。
    MODULE_AUTHOR() 描述模块的作者信息
    MODULE_DESCRIPTION() 对模块的简单介绍
    MODULE_ALIAS() 给模块设置一个别名

    4.2 Makefile文件编写与模块装载卸载

    4.2.1 Makefile文件详解

    根据先前的实验环境,编写Makefile文件,主要目的是把编译工作交给内核源码树里的Kbuild系统

    KERNEL_DIR=../../../../kernel/

    ARCH=arm64
    CROSS_COMPILE=aarch64-linux-gnu-
    export ARCH CROSS_COMPILE

    obj-m := hellomodule.o
    all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

    .PHONE:clean

    clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

    核心命令是:

    $(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) modules

    含义是:

    • C $(KERNEL_DIR):先切到内核源码/构建目录去执行内核的 Makefile
    • M=$(CURDIR):告诉内核:当前这个目录是一个“外部模块目录”,里面有 obj-m 等规则
    • modules:让内核 Kbuild 以“构建外部模块”的方式去编译并最终产出 .ko

    这也是官方推荐的外部模块构建方式(即利用 M= 机制)。

    KERNEL_DIR=../../../kernel/

    这一句定义了变量KERNEL_DIR,用来保存内核源码的目录,需要指定到内核编译输出目录下,此处使用的是相对路径,改成绝对路径也可以。

    ARCH=arm64
    CROSS_COMPILE=aarch64-linux-gnu-
    export ARCH CROSS_COMPILE

    • ARCH=arm64:告诉内核 Kbuild 当前目标架构是 ARM64。
    • CROSS_COMPILE=aarch64-linux-gnu-:告诉 Kbuild 交叉工具链前缀,Kbuild 会自动拼出:
      • aarch64-linux-gnu-gcc
      • aarch64-linux-gnu-ld
      • aarch64-linux-gnu-objcopy 等
    • export ARCH CROSS_COMPILE:把变量导出到子 make 进程中,否则进入 $(KERNEL_DIR) 后内核 Makefile 未必能收到这些变量。

    说明:如果你从命令行传参(如 make ARCH=arm64 CROSS_COMPILE=…),也可以不 export,但 export 是一种稳妥写法。

    obj-m := hellomodule.o

    这是外部模块目录里最关键的一行。它告诉 Kbuild:

    • 你要构建一个模块,模块名为 hellomodule
    • 最终输出:hellomodule.ko
    • “模块的主对象”是 hellomodule.o(由 hellomodule.c 编译而来)

    直观映射关系:

    • hellomodule.c → 编译 → hellomodule.o
    • hellomodule.o → 链接成模块 → hellomodule.ko

    补充两点工程常识:

  • := 与 +=

    • obj-m := hellomodule.o 表示“只构建这一个模块”(覆盖式赋值)
    • obj-m += hellomodule.o 更常见,便于后续再追加其他模块
  • 若模块由多个 .o 组成(例如 a.c、b.c、c.c),写法是:

    obj-m += hellomodule.o
    hellomodule-y := a.o b.o c.o

    这样最终还是产出 hellomodule.ko,只是内部由多个对象文件组成。

  • all:
    $(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) modules

    • all 是你在模块目录执行 make 时默认会走的目标。
    • 这条命令触发 Kbuild 外部模块构建流程,典型会生成:
      • hellomodule.ko
      • hellomodule.mod.o
      • hellomodule.mod.c
      • modules.order
      • Module.symvers(是否生成与内核配置/构建状态有关)
      • 一堆 .cmd 依赖文件

    这里的 $(CURDIR) 是 GNU make 的内置变量,表示当前目录的绝对路径(通常等价于 pwd 的结果)。用它传 M= 很合适。

    clean:
    $(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) clean

    • 同样借助内核 Kbuild 的清理规则。
    • 会删掉本目录下 Kbuild 生成的中间文件与 .ko。

    我们在终端执行make编译模块,可以看到生成了很多文件,其中.ko文件便是我们想要的模块。

    使用如下命令查看模块信息:

    file hellomodule.ko # 看看是否是 aarch64
    modinfo hellomodule.ko

    vermagic 应与 RK3588 板卡 uname -r 匹配,否则上板极易 invalid module format。

    4.2.2 模块的装载卸载

    使用scp可以将该模块拷贝到开发板上,具体的方法可以自行搜索。

    在板卡上,执行以下命令可以装载卸载模块

    # 板卡上装载模块
    insmod hellomodule.ko
    #查看内核输出信息
    dmesg | tail -n 50
    # 板卡上卸载模块
    rmmod hellomodule
    #查看内核输出信息
    dmesg | tail -n 50

    可以看到内核的打印信息中已经成功装载卸载模块了

    4.2.3 模块相关的指令

    下面按“模块开发与上板调试最常用的指令链路”把 Linux 内核模块(.ko)相关命令做一套系统详解。默认你使用的是常见的 kmod 工具集(insmod/rmmod/lsmod/modprobe/modinfo/depmod)

    1) 查看模块是否已加载:lsmod / /proc/modules

    lsmod

    用途:列出当前已加载模块、占用大小、被谁引用。

    lsmod
    lsmod | grep hello

    输出含义(典型):

    • Module:模块名(通常不含 .ko 后缀)
    • Size:模块占用大小(字节)
    • Used by:引用计数及依赖该模块的其他模块列表

    常见解读:

    • Used by 0:没有其他模块依赖它,通常允许卸载(但仍可能因“正在使用的设备句柄”导致卸载失败)。
    • Used by >0:有依赖或引用,rmmod 多半会失败(除非强制)。

    /proc/modules

    用途:更底层、脚本化读取已加载模块。

    cat /proc/modules |head

    这与 lsmod 信息本质相同;很多情况下 lsmod 就是格式化读取这里的数据。

    2) 查看模块文件信息:modinfo

    用途:查看 .ko 的元信息(license、author、description、alias、depends、vermagic 等),判断是否可能与当前内核匹配。

    modinfo hellomodule.ko
    modinfo -F vermagic hellomodule.ko
    modinfo -F license hellomodule.ko
    modinfo -F depends hellomodule.ko
    modinfo -Falias hellomodule.ko

    关键字段解读:

    • filename:模块文件路径
    • license:MODULE_LICENSE() 声明的许可
    • description/author:模块元信息
    • alias:MODULE_ALIAS() 声明的别名(影响 modprobe 自动匹配)
    • depends:依赖模块(modprobe 会用到)
    • vermagic:极关键,表示模块编译时绑定的内核版本/特性(不匹配常见报错:invalid module format)

    3) 加载模块:insmod 与 modprobe

    insmod

    用途:直接把指定 .ko 插入内核,不解析依赖、不查索引。

    sudo insmod hellomodule.ko
    sudo insmod hellomodule.ko param1=123 debug=

    特点:

    • 适合开发调试、单文件模块测试。
    • 不自动加载依赖:依赖缺失时常见 Unknown symbol …。

    常见错误定位:

    dmesg |tail -n 100


    modprobe

    用途:按模块名加载,会自动处理依赖(基于 modules.dep / modules.alias 等索引)。

    sudo modprobe hellomodule
    sudo modprobe -v hellomodule# 显示执行细节
    sudo modprobe -n -v hellomodule# dry-run:只显示将要做什么,不执行

    特点:

    • 推荐在工程化场景使用(依赖自动拉起、别名匹配、黑名单可控)。
    • 加载对象来自标准目录 /lib/modules/$(uname -r)/ 下的索引;因此你若只是把 .ko 放在某个临时目录,modprobe 可能找不到。

    4) 卸载模块:rmmod 与 modprobe -r

    rmmod

    用途:卸载指定模块(不处理依赖关系)。

    sudo rmmod hellomodule
    sudo rmmod -f hellomodule# 强制卸载(需要内核配置允许;不建议常用)

    常见报错:

    • Module is in use:模块被引用(lsmod 的 Used by 不为 0)或设备节点仍被占用(例如有进程打开了设备文件)。

    排查“谁在用”常用:

    lsmod | grep hello
    lsof | grep /dev/xxx# 字符设备场景

    modprobe -r

    用途:按依赖顺序卸载(更“聪明”)。

    sudo modprobe -r hellomodule
    sudo modprobe -r -v hellomodule

    5) 建立模块索引/依赖:depmod

    用途:扫描 /lib/modules/$(uname -r)/ 下所有模块,生成依赖索引文件,如 modules.dep、modules.alias 等,供 modprobe 使用。

    典型场景:你把自己编译的 hellomodule.ko 放到了标准目录下,需要让系统“认识它”。

    modprobe是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中,depmod起到了决定性作用,当执行modprobe时, 它会在模块的安装目录下搜索module.dep文件,这是depmod创建的模块依赖关系的文件。

    在Linux系统中,/lib/modules目录通常包含内核相关的模块和配置文件,该文件夹包含了与内核版本号相关文件夹,用来存放的模块和配置信息。

    以上配置文件或者目录说明如下:

    配置文件或文件夹作用
    build 指向当前正在运行的内核源代码的符号链接
    kernel 包含编译后的内核模块文件(.ko)
    modules.alias 定义模块别名的文件
    modules.alias.bin 模块别名文件的二进制缓存版本
    modules.builtin 列出了由内核构建的模块(静态连接在内核中)
    modules.builtin.bin 由内核构建的模块列表的二进制缓存版本
    modules.dep 列出了模块之间的依赖关系
    modules.dep.bin 模块依赖关系文件的二进制缓存版本
    modules.devname 包含了每个模块设备的名称
    modules.order 定义模块加载顺序的文件
    modules.symbols 保存导出的符号信息
    modules.symbols.bin 导出的符号信息的二进制缓存版本
    modules.softdep 包含模块软依赖关系的文件

    我们最关心的配置文件是modules.dep,该文件列出了模块之间的依赖关系,当我们执行depmod -a建立模块之间的依赖关系时,就会把依赖关系写入到modules.dep当中。

    6) 查看内核日志/模块打印:dmesg / journalctl -k

    dmesg

    用途:读取内核环形缓冲区日志(模块 printk/pr_info 输出在这里非常常见)。 journalctl -k(systemd 系统) 用途:从 systemd journal 查看内核日志(部分发行版会比 dmesg 更完整或更易检索)。

    至此,详细介绍了内核驱动模块的详细内容,后续,笔者会进一步深入Linux内核驱动开发,欢迎关注。

    参考资料

    • 野火Linux驱动资料:https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/base_first_module.html
    • LINUX设备驱动程序(第三版)
    赞(0)
    未经允许不得转载:171主机测评 » 【内核驱动基础】超详细一文介绍Linux驱动模块
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址