news 2026/4/16 7:24:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【内核驱动基础】超详细一文详解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结尾)。

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

二、为什么要用内核模块

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

  1. 内核会变得很大:很多硬件你根本用不到,却都被打包进去了。
  2. 更新不方便:改一个驱动就要重新编译、替换整个内核。
  3. 调试效率低:驱动开发时频繁改动,重启换内核成本高。

模块解决的就是这些问题:

  • 用到什么功能就加载什么模块(按需加载)
  • 驱动更新时只替换.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_exitMODULE_LICENSEMODULE_AUTHORMODULE_DESCRIPTIONMODULE_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关键字的作用如下:

  1. static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期。
  2. static的修饰全局变量只能在本文件中访问,不能在其它文件中访问。
  3. 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

补充两点工程常识:

  1. :=+=

    • obj-m := hellomodule.o表示“只构建这一个模块”(覆盖式赋值)
    • obj-m += hellomodule.o更常见,便于后续再追加其他模块
  2. 若模块由多个.o组成(例如a.cb.cc.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:模块文件路径
  • licenseMODULE_LICENSE()声明的许可
  • description/author:模块元信息
  • aliasMODULE_ALIAS()声明的别名(影响modprobe自动匹配)
  • depends:依赖模块(modprobe会用到)
  • vermagic:极关键,表示模块编译时绑定的内核版本/特性(不匹配常见报错:invalid module format

3) 加载模块:insmodmodprobe

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) 卸载模块:rmmodmodprobe -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.depmodules.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设备驱动程序(第三版)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/9 20:22:44

黑苹果配置不再难:如何用工具化方案解决90%的安装痛点

黑苹果配置不再难&#xff1a;如何用工具化方案解决90%的安装痛点 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 当你第三次尝试黑苹果失败时&#x…

作者头像 李华
网站建设 2026/4/15 2:48:44

AI编程助手部署完全指南:从零基础到企业级配置

AI编程助手部署完全指南&#xff1a;从零基础到企业级配置 【免费下载链接】opencode 一个专为终端打造的开源AI编程助手&#xff0c;模型灵活可选&#xff0c;可远程驱动。 项目地址: https://gitcode.com/GitHub_Trending/openc/opencode 作为开发者&#xff0c;你是否…

作者头像 李华
网站建设 2026/4/12 22:08:36

一分钟启动翻译服务,Hunyuan-MT-7B-WEBUI太省心

一分钟启动翻译服务&#xff0c;Hunyuan-MT-7B-WEBUI太省心 你有没有过这样的经历&#xff1a;刚收到一封法语技术邮件&#xff0c;急着看懂却卡在专业术语上&#xff1b;或者要给维吾尔语用户写产品说明&#xff0c;反复查词典仍拿不准语气&#xff1b;又或者团队正在做跨境项…

作者头像 李华
网站建设 2026/4/8 12:21:55

Intern-S1-FP8:免费科学多模态推理新标杆

Intern-S1-FP8&#xff1a;免费科学多模态推理新标杆 【免费下载链接】Intern-S1-FP8 项目地址: https://ai.gitcode.com/InternLM/Intern-S1-FP8 导语 InternLM团队推出的Intern-S1-FP8模型&#xff0c;以其卓越的科学多模态推理能力和显著降低的硬件门槛&#xff0c…

作者头像 李华
网站建设 2026/4/3 21:21:02

GPT-OSS-20B:16GB内存开启本地AI推理新体验

GPT-OSS-20B&#xff1a;16GB内存开启本地AI推理新体验 【免费下载链接】gpt-oss-20b-BF16 项目地址: https://ai.gitcode.com/hf_mirrors/unsloth/gpt-oss-20b-BF16 导语&#xff1a;OpenAI推出的开源大模型GPT-OSS-20B以其突破性的内存效率&#xff0c;首次让210亿参…

作者头像 李华
网站建设 2026/4/5 5:01:24

在线图表工具:高效创建专业流程图的全方位指南

在线图表工具&#xff1a;高效创建专业流程图的全方位指南 【免费下载链接】mermaid-live-editor Edit, preview and share mermaid charts/diagrams. New implementation of the live editor. 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-live-editor 想…

作者头像 李华