时间轴
时间轴
2025-11-09
init
本文介绍了Linux驱动框架的基础知识,包括驱动三大分类(字符设备、块设备、网络设备)、Linux内核源码主要目录结构及其功能,并解析了最简单的Linux驱动模块的组成部分(头文件、加载/卸载函数、许可证声明、模块参数等),说明了模块加载成功与返回值的对应关系及模块信息存储机制。
Linux 驱动笔记
| 目录 | 链接 |
|---|---|
| 1. Linux 驱动框架 | |
| 2. Linux 驱动加载逻辑 | |
| 3. 字符设备基础 | |
| 4. 并发与竞争 | |
| 5. 高级字符设备进阶 | |
| 6. 中断 | |
| 7. 平台总线 | |
| 8. 设备树 | |
| 9. 设备模型 | |
| 10. 热插拔 | |
| 11. pinctrl 子系统 | |
| 12. gpio 子系统 | |
| 13. 输入子系统 | |
| 14. 单总线 | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. 网络设备 | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
Linux内核相关资料:
https://github.com/0voice/linux_kernel_wiki/blob/main/README.md
Linux 驱动抽象




Linux 将存储器和外设分为 3 个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
Linux 源码目录结构
| 目录 | 说明 |
|---|---|
| arch | 架构相关目录,存放多种 CPU 架构(如 arm、x86、MIPS 等)的适配代码 |
| block | 块设备相关代码目录,Linux 中以块设备形式管理硬盘、SD 卡等存储设备 |
| crypto | 加密算法目录,存放各类加密相关的实现代码 |
| Documentation | 官方 Linux 内核文档目录,包含内核功能、接口等详细说明 |
| drivers | 驱动目录,存放 Linux 系统支持的各类硬件设备驱动代码 |
| firmware | 固件目录,存放硬件设备所需的固件文件 |
| fs | 文件系统目录,存放 ext2、ext3、fat 等文件系统的实现代码 |
| include | 公共头文件目录,提供内核各模块共用的头文件 |
| init | 内核启动初始化目录,存放 Linux 内核启动阶段的初始化代码 |
| ipc | 进程间通信目录,存放管道、消息队列、共享内存等 IPC 机制的实现代码 |
| kernel | 内核核心目录,存放内核本身的核心功能代码 |
| lib | 库函数目录,存放内核使用的各类库函数 |
| mm | 内存管理目录(mm 为 memory management 缩写),负责内核内存管理功能 |
| net | 网络相关目录,存放 TCP/IP 协议栈等网络功能的实现代码 |
| scripts | 脚本目录,存放内核编译、测试等流程中使用的脚本文件 |
| security | 安全相关目录,存放内核安全机制的实现代码 |
| sound | 音频相关目录,存放音频设备驱动及音频处理的代码 |
| tools | 工具目录,存放 Linux 内核开发、调试用到的工具程序 |
| usr | 与 Linux 内核启动相关的代码目录 |
| virt | 内核虚拟机相关目录,存放内核级虚拟化功能的实现代码 |
最简单的 Linux 驱动结构解析
- 组成部分
- 头文件(必须):驱动需包含内核相关头文件,其中
<linux/module.h>和<linux/init.h>是必备的。 - 驱动加载函数(必须):加载驱动时,该函数会被内核自动执行。
- 驱动卸载函数(必须):卸载驱动时,该函数会被内核自动执行。
- 许可证声明(必须):因 Linux 内核遵循 GPL 协议,驱动加载时也需遵守相关协议,常见的有 GPL v2 等多种许可证类型。
- 模块参数(可选):是模块加载时传递给内核模块的值。
- 作者和版本信息(可选):用于声明驱动的作者及代码版本信息。
- 头文件(必须):驱动需包含内核相关头文件,其中
- 例子
1234567891011121314151617181920212223 | static int __init hello_world_init(void){ printk(KERN_INFO "Hello World: Module loaded\n"); return 0;}static void __exit hello_world_exit(void){ printk(KERN_INFO "Hello World: Module unloaded\n");}module_init(hello_world_init);module_exit(hello_world_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("Hello World Kernel Module"); |
当且仅当 module_init 中的 init 函数返回 0 时模块才会被加载成功;返回负值表示加载失败
模块信息
123456789101112131415161718192021222324252627282930313233343536373839404142 | $ objdump -h hello_world.kohello_world.ko: file format elf64-littleSections:Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .init.text 00000034 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 2 .exit.text 00000024 0000000000000000 0000000000000000 00000074 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 3 .note.gnu.property 00000020 0000000000000000 0000000000000000 00000098 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .note.gnu.build-id 00000024 0000000000000000 0000000000000000 000000b8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .note.Linux 00000018 0000000000000000 0000000000000000 000000dc 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .rodata.str1.8 0000002b 0000000000000000 0000000000000000 000000f8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .modinfo 000000b2 0000000000000000 0000000000000000 00000123 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 __versions 00000080 0000000000000000 0000000000000000 000001d8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 __patchable_function_entries 00000008 0000000000000080 0000000000000080 00000258 2**3 CONTENTS, ALLOC, LOAD, RELOC, DATA 10 .data 00000000 0000000000000000 0000000000000000 00000260 2**0 CONTENTS, ALLOC, LOAD, DATA 11 .gnu.linkonce.this_module 000003c0 0000000000000000 0000000000000000 00000260 2**6 CONTENTS, ALLOC, LOAD, RELOC, DATA, LINK_ONCE_DISCARD 12 .plt 00000001 0000000000000000 0000000000000000 00000620 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .init.plt 00000001 0000000000000000 0000000000000000 00000621 2**0 ALLOC, READONLY 14 .text.ftrace_trampoline 00000001 0000000000000000 0000000000000000 00000621 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 15 .bss 00000000 0000000000000000 0000000000000000 00000622 2**0 ALLOC 16 .comment 00000026 0000000000000000 0000000000000000 00000622 2**0 CONTENTS, READONLY 17 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000648 2**0 CONTENTS, READONLY |
内核模块使用其.modinfo部分来存储关于模块的信息,所有MODULE_*宏都用参数传递的值更新这部分的内容。其中一些宏是MODULE_DESCRIPTION()、MODULE_AUTHOR()和MODULE_LICENSE()。内核提供的在模块信息部分添加条目的真正底层宏是MODULE_INFO(tag,info),它添加的一般信息形式是tag=info。这意味着驱动程序作者可以自由添加其想要的任何形式信息,例如:
1 | MODULE_INFO(my_field_name, "What easy value"); |
.modinfo部分的内容:
123456789101112131415 | $ readelf -x .modinfo hello_world.koHex dump of section '.modinfo': 0x00000000 64657363 72697074 696f6e3d 68656c6c description=hell 0x00000010 6f20776f 726c6421 00617574 686f723d o world!.author= 0x00000020 6576656e 36323900 6c696365 6e73653d even629.license= 0x00000030 47504c00 73726376 65727369 6f6e3d41 GPL.srcversion=A 0x00000040 34303030 33444636 33303137 39423643 40003DF630179B6C 0x00000050 46314430 34350064 6570656e 64733d00 F1D045.depends=. 0x00000060 6e616d65 3d68656c 6c6f5f77 6f726c64 name=hello_world 0x00000070 00766572 6d616769 633d352e 31302e31 .vermagic=5.10.1 0x00000080 31302d76 382b2053 4d502070 7265656d 10-v8+ SMP preem 0x00000090 7074206d 6f645f75 6e6c6f61 64206d6f pt mod_unload mo 0x000000a0 64766572 73696f6e 73206161 72636836 dversions aarch6 0x000000b0 3400 4. |
也可以使用modinfo查看
123456789 | $ modinfo hello_world.kofilename: /home/zhaohang/repository/linux/linux_driver_learning/01_hello_world/hello_world.kodescription: hello world!author: even629license: GPLsrcversion: A40003DF630179B6CF1D045depends:name: hello_worldvermagic: 5.10.110-v8+ SMP preempt mod_unload modversions aarch64 |
编译 Linux 驱动
- 将驱动放在 Linux 内核里面,然后编译 Linux 内核。将驱动编译到 Linux 内核里面。
- 将驱动编译成内核模块,独立于 Linux 内核之外
- 内核模块是 Linux 系统中的一个特殊的机制,可以将一些使用频率很少或者暂时不用的功能编译成内核模块,在需要的时候再动态加载到内核里面。
- 使用内核模块可以减小内核体积,加快启动速度。并且可以在系统运行时插入或者卸载驱动,无需重启系统。内核模块的后缀是.ko
obj-<X>kbuild变量
这实际上对应于obj-<X>模式,其中<X>应该是y、m、空白或n。总的来说,位于内核构建系统头部的makefile使用它。
1 | obj-y += mymodule.o |
这告诉 kbuild 在当前目录中有一个名为 mymodule.o 的对象。mymodule.o将从mymodule.c或mymodule.S构建。
如何以及是否构建或链接mymodule.o取决于<X>的值。
- 如果
<X>设置为m,则使用变量 obj-m,并将 mymodule.o构建为模块。 - 如果
<X>设置为y,则使用变量obj-y,mymodule.o将构建为内核的一部分。也可以说它是一个内置模块。 - 如果
<X>设置为n,则表示不编译该模块。
因此,经常用到obj-$(CONFIG_XXX)模式(其中CONFIG_XXX是内核配置选项),在内核配置过程中可以设置或者不设置它。下面是一个例子:
1 | obj-$(CONFIG_MYMODULE) += mymodule.o |
$(CONFIG_MYMODULE)根据内核配置期间的值计算为 y或 m(make menuconfig)。如果CONFIG_MYMODULE既不是y也不是m,则文件不会被编译或链接。
还有一种情况:
1 | obj-<X> += somedir/ |
这意味着kbuild应该进入somedir目录,查找其中所有的makefile并处理它们,以决定应该构建哪些对象。
编译成内核模块
123456789101112 | obj-m += hello_world.oKERNEL_SRC:=/home/zhaohang/repository/linux/linux-5.10.246PWD ?=$(shell pwd)ARCH = arm64CROSS_COMPILE = aarch64-linux-gnu-all: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modulesclean: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modules clean rm -rf *.ko *.o *.mod.o *.mod.c *.symvers *.order |
本地的 Linux 代码在/lib/modules/$(uname -r)/kernel 下
将驱动编译到内核
在drivers/char(以字符驱动为例)创建文件夹 helloworld,然后将驱动源代码放入,然后创建 Kconfig 文件
12345 | config helloworld bool "helloworld support" default y help helloworld |
更改 drivers/char/Kconfig
123 | emacs ../Kconfig# 添加source "drivers/char/helloworld/Kconfig" |
在驱动源码里创建 Makefile
1 | obj-$(CONFIG_helloworld) += helloworld.o |
然后在上一级的 Makefile 中添加:
123 | emacs ../Makefile# 添加obj-y += helloworld/ |
模块相关命令
模块加载命令
- insmod
- 功能:载入 Linux 内核模块
- 语法:insmod 模块名
- 例子:insmod hello_world.ko
- modprobe
- 功能:加载内核模块,同时这个模块所依赖的模块也同时被加载
- 语法:modprobe 模块名
- 举例:modprobe hello_world
系统管理员或在生产系统中则常用 modprobe。modprobe 更智能,它在加载指定的模块之前解析文件 modules.dep,以便首先加载依赖关系。它会自动处理模块依赖关系,就像包管理器所做的那样。
但是**
modprobe不能直接加载任意路径下的.ko文件** —— 它只从标准内核模块目录(如/lib/modules/$(uname -r)/)中查找模块,并依赖modules.dep索引。
模块卸载命令
rmmod
- 功能:移除已经载入 Linux 的内核模块
- 语法:rmmod 模块名
- 举例:rmmod hello_world
modprobe -r
- 它不仅会尝试卸载
mymodule,还会自动检查并卸载那些只被mymodule依赖、且当前没有其他模块/进程在使用的依赖模块。 - 语法:
modprobe -r mymodule
- 它不仅会尝试卸载
卸载内核模块功能的启用或禁用由 CONFIG_MODULE_UNLOAD 配置选项的值决定。没有这个选项,就不能卸载任何模块。
在运行时,如果模块卸载会导致其他不良影响,则即使有人要求卸载,内核也将阻止这样做。这是因为内核通过引用计数记录模块的使用次数,这样它就知道模块是否在用。如果内核认为删除一个模块是不安全的,就不会删除它。然而,以下设置可以改变这种行为:MODULE_FORCE_UNLOAD=y
设置启动时加载模块
如果要在启动的时候加载一些模块,则只需创建文件/etc/modules-load.d/<filename>.conf,并添加应该加载的模块名称(每行一个)。人们通常使用模块:/etc/modules-load.d/modules.conf。当然也可以根据需要创建多个.conf 文件。
12345 | emacs /etc/modules-load.d/mymodule.conf# 下面时mymodule.conf的内容uioiwlwifii2c-dev |
查看模块信息命令
- lsmod命令
- 功能:列出已经载入 Linux 的内核模块
- 也可以使用命令 cat /proc/modules 来查看模块是否加载成功
- modinfo命令
- 功能:查看内核模块信息
- 语法:modinfo 模块名
- 举例:modinfo hello_world.ko
配置文件
menuconfig配置驱动选项状态操作:
驱动状态:
- 把驱动编译成内核模块,用 M 来表示
- 把驱动编译到内核里面,用*来表示
- 不编译
使用空格来切换这三种不同状态。
选项的状态有:
[]: 表示有两种状态只能设置成选中或者不选中<>: 表示有三种状态,可以设置成选中,不选中,和编译成模块- () : 表示用来存放字符串或者 16 进制数
Kconfig 文件
Kconfig 文件是图形化配置界面的源文件,图形化配置界面中的选项由 Kconfig 文件决定。当我们执行命令make menuconfig命令的时候,内核的配置工具会读取内核源码目录下的arch/xxx/Kconfig。xxx 是 ARCH 的值,比如 arm64,然后生成对应的配置界面供开发者使用。
config 文件与.config 文件
config 文件和.config 文件都是 Linux 内核的配置文件。
config 文件位于 Linux 内核源码的 arch/$(ARCH)/configs 目录下,是Linux 系统默认的配置文件。
.config 文件位于 Linux 内核源码的顶层目录下,编译 linux 内核时会使用.config 文件里面的配置来编译内核镜像。
若.config 存在,make menuconfig 界面的默认配置即当前.config 文件的配置,若修改了图形化配置界面的设置并保存,则.config 文件会被更新。
若.config 文件不存在,make menuconfig 界面的默认配置则为 Kconfig 文件中的默认配置。
使用命令 make xxx_defconfig 命令会根据 arch/$(ARCH)/configs 目录下默认文件生成.config 文件。
Kconfig 语法
参考:
主菜单
mainmenu用来设置主菜单的标题
举例:mainmenu "Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration"
上述名字设置的菜单名字为Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration
菜单结构
可以用 menu/endmenu 来生成菜单,menu 是菜单开始的标志,endmenu 是菜单结束的标志。这两个是成对出现的。
如下描述的是一个名字为:"Network device support"的菜单
1234 | menu "Network device support" config NETDEVICE ...endmenu |
配置选项
使用关键字config来定义一个新的选项。每个选项都必须指定类型,类型包括bool, tristate, string, hex, int。最常见的是 bool, tristate, string 这三个。
bool 类型有两种值:y 和 n;
tristate 有三种值:y, m 和 n;
string 为字符串类型。
help 表示帮助信息,当我们在图形化界面按下 h 按键,弹出来的就是 help 的内容。
举例:
12345 | config helloworld bool "hello world support" default y help hello world |
依赖关系
Kconfig 中的依赖关系可以用 depends on 和 select
depends on 表示直接依赖关系:
12 | config A depends on B |
表示选项 A 依赖选项 B,只有选项 B 被选中时,A 选项才可以被选中
select 表示反向依赖关系:
12 | config A select B |
在 A 选项被选中的情况下,B 选项自动被选中
可选选项
使用 choice 和 endchoice 定义可选择项
12345678 | choice prompt "a"config b bool b1config c bool c1...endchoice |
注释
在图形化配置界面显示一个注释
123456 | config TEST_CONFIG bool "test" default y help just testcomment "just for test" |
source
source 用于读取另一个 Kconfig 文件,如 source “init/Kconfig” 就是读取 init 目录下的 Kconfig 文件到当前 Kconfig 文件
驱动模块传参
驱动传参的意义:
优势:
- 通过驱动传参,可以让驱动程序更加灵活,兼容性更强。
- 可以通过驱动传参,设置安全校验,防止驱动被盗用
不足
- 使驱动代码变得复杂化
- 增加了驱动的资源占用
驱动可以传递的参数类型
C 语言常用的数据类型内核大部分都支持驱动传参。这里将内核支持的驱动传递参数类型分为三类:
- 基本类型:char, bool, int, long, short, byte, ushort, uint
- 数组:array
- 字符串:string
给 Linux 驱动传递参数的方式
参数类型与对应函数
参数类型 对应函数 功能 基本类型 module_param传递基本类型参数 数组类型 module_param_array传递数组类型参数 字符串类型 module_param_string传递字符串类型参数 函数定义位置:这三个函数在 Linux 内核源码的
include/linux/moduleparam.h中定义。
module_param
123456789101112131415161718192021222324252627 | /** * module_param - typesafe helper for a module/cmdline parameter * @name: the variable to alter, and exposed parameter name. * @type: the type of the parameter * @perm: visibility in sysfs. * * @name becomes the module parameter, or (prefixed by KBUILD_MODNAME and a * ".") the kernel commandline parameter. Note that - is changed to _, so * the user can use "foo-bar=1" even for variable "foo_bar". * * @perm is 0 if the variable is not to appear in sysfs, or 0444 * for world-readable, 0644 for root-writable, etc. Note that if it * is writable, you may need to use kernel_param_lock() around * accesses (esp. charp, which can be kfreed when it changes). * * The @type is simply pasted to refer to a param_ops_##type and a * param_check_##type: for convenience many standard types are provided but * you can create your own by defining those variables. * * Standard types are: * byte, hexint, short, ushort, int, uint, long, ulong * charp: a character pointer * bool: a bool, values 0/1, y/n, Y/N. * invbool: the above, only sense-reversed (N = true). */ |
module_param_array
12345678910111213141516 | /** * module_param_array - a parameter which is an array of some type * @name: the name of the array variable * @type: the type, as per module_param() * @nump: optional pointer filled in with the number written * @perm: visibility in sysfs * * Input and output are as comma-separated values. Commas inside values * don't work properly (eg. an array of charp). * * ARRAY_SIZE(@name) is used to determine the number of elements in the * array, so the definition must be visible. */ |
module_param_string
123456789101112131415161718 | /** * module_param_string - a char array parameter * @name: the name of the parameter * @string: the string variable * @len: the maximum length of the string, incl. terminator * @perm: visibility in sysfs. * * This actually copies the string when it's set (unlike type charp). * @len is usually just sizeof(string). */ |
MODULE_PARM_DESC
函数功能:描述模块参数的信息。在 include/linux/moduleparam.h 定义
函数原型:MODULE_PARM_DESC(_parm, desc)
函数参数:_parm:要描述的参数的参数名称。desc:描述信息
| 参数名 | 含义 |
|---|---|
name | 参数名(可在命令行传入) |
type | 参数类型(如:int、bool、charp) |
perm | 在/sys/module/<modname>/parameters/中的权限,如0644 |
nump | (仅数组)保存数组元素数量的变量地址 |
len | (仅字符串)缓冲区长度 |
string | 指向字符串缓冲的指针 |
权限定义
perm 是指 sysfs 中参数文件的 文件权限位。
读写权限在include/linux/stat.h和include/uapi/linux/stat.h中定义。
- include/linux/stat.h
12345678910111213141516 | /* SPDX-License-Identifier: GPL-2.0 */ |
- include/uapi/linux/stat.h
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ |
相关的权限主要是文件访问权限宏(File Permission Bits)
| 宏名 | 八进制值 | 含义 |
|---|---|---|
S_IRWXU | 00700 | 所有者(U)读、写、执行权限(RWX) |
S_IRUSR | 00400 | 所有者(USR)读权限® |
S_IWUSR | 00200 | 所有者(USR)写权限(W) |
S_IXUSR | 00100 | 所有者(USR)执行权限(X) |
S_IRWXG | 00070 | 所属组(G)读、写、执行权限(RWX) |
S_IRGRP | 00040 | 所属组(GRP)读权限® |
S_IWGRP | 00020 | 所属组(GRP)写权限(W) |
S_IXGRP | 00010 | 所属组(GRP)执行权限(X) |
S_IRWXO | 00007 | 其他用户(O)读、写、执行权限(RWX) |
S_IROTH | 00004 | 其他用户(OTH)读权限® |
S_IWOTH | 00002 | 其他用户(OTH)写权限(W) |
S_IXOTH | 00001 | 其他用户(OTH)执行权限(X) |
还有组合权限
S_IRWXUGO
- 定义:
(S_IRWXU | S_IRWXG | S_IRWXO) - 含义(展开后):
00700 | 00070 | 00007 = 00777 - 实际意义:允许 所有者、组、其他用户 (UGO) 拥有 读、写、执行 权限
- 举例用途:设置所有人可读写执行,如临时目录
/tmp
S_IALLUGO
- 定义:
(S_ISUID | S_ISGID | S_ISVTX | S_IRWXUGO) - 含义(展开后):
0004000 | 0002000 | 0001000 | 00777 = 01777 - 实际意义:包含特殊位(SUID、SGID、Sticky)以及所有人读写执行权限
- 举例用途:常见权限:
drwxrwxrwt(如/tmp)
S_IRUGO
- 定义:
(S_IRUSR | S_IRGRP | S_IROTH) - 含义(展开后):
00400 | 00040 | 00004 = 00444 - 实际意义:所有人 读 权限
- 举例用途:常用于只读文件
S_IWUGO
- 定义:
(S_IWUSR | S_IWGRP | S_IWOTH) - 含义(展开后):
00200 | 00020 | 00002 = 00222 - 实际意义:所有人 写 权限
- 举例用途:少用,一般仅限特定目录
S_IXUGO
- 定义:
(S_IXUSR | S_IXGRP | S_IXOTH) - 含义(展开后):
00100 | 00010 | 00001 = 00111 - 实际意义:所有人 执行 权限
- 举例用途:使所有人可执行某脚本或程序
示例
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 | static int myint = 0;module_param(myint,int, 0644);MODULE_PARM_DESC(myint, "A sample int parameter");static char* mycharp = "hello";module_param(mycharp, charp, 0644);MODULE_PARM_DESC(mycharp, "A sample charp parameter");static int myarr[3] = {1, 2, 3};static int myarr_argc = ARRAY_SIZE(myarr);module_param_array(myarr, int, &myarr_argc, 0644);MODULE_PARM_DESC(myarr, "A sample array parameter");static char mystring[] = "default_value";module_param_string(mystr, mystring, ARRAY_SIZE(mystring), 0644);MODULE_PARM_DESC(mystr, "A sample string parameter");static void print_param(void){ int i; pr_info("[myint]: %d\n", myint); pr_info("[mycharp]: %s\n", mycharp); pr_info("[myarr]: "); for(i =0;i<myarr_argc;i++){ pr_info("%d ", myarr[i]); } pr_info("[mystr]: %s\n", mystring);}static int __init param_test_init(void){ printk("param test init\n"); print_param(); return 0;}static void __exit param_test_exit(void){ print_param(); printk("param test exit\n");}module_init(param_test_init);module_exit(param_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("linux driver parameter test"); |
加载模块时可以传参:
1 | insmod parameter.ko myint=42 mycharp="hello world" myarr=9,8,7 mystr="hello" |
运行时候查看参数:
123 | cd /sys/module/param_test/parameters/cat myintecho 99 > myint |
内核符号表导入导出
驱动程序可以编译成内核模块,也就是 KO 文件。每个 KO 文件是相互独立的,也就是说模块之间无法相互访问。但是在某些使用场景下要相互访问,如 B 模块要用 A 模块中的函数。(B 模块依赖于 A 模块)
符号表
”符号“就是内核中的函数名,全局变量名等。符号表就是用来记录这些”符号“的文件。
模块依赖关系
linux 内核中的模块可以提供函数或变量,用EXPORT_SYMBOL宏导出它们即可供其他模块使用,这些被称作符号。
模块 B 对模块 A 的依赖是指模块 B 使用从模块 A 导出的符号。
depmod是用户空间工具(通常由kmod包提供),在内核安装后运行:
1 | depmod -a <kernel_version> |
- 它扫描
/lib/modules/<kernel_release>/下所有.ko模块文件:- 使用
modinfo -F depends或直接解析 ELF 符号表; - 确定每个模块 需要哪些符号(imports) 和 提供哪些符号(exports);
- 构建模块间的依赖关系图。
- 使用
生成的依赖文件
| 文件 | 作用 |
|---|---|
modules.dep | 文本格式,每行格式:module.ko: dependent_module1.ko dependent_module2.ko ... |
modules.dep.bin | 二进制格式,供modprobe快速加载(避免每次解析文本) |
modules.symbols/modules.symbols.bin | 记录所有导出符号及其所属模块(用于反向查找) |
depmod 还处理模块文件以提取和收集该信息,并在/lib/modules/<kernel_release>/modules.alias中生成 modules.alias 文件,该文件将设备映射到其对应的驱动程序。
modprobe 会解析 modules.alias 文件。
内核符号表导出
导出宏
| 宏名称 | 适用场景 |
|---|---|
EXPORT_SYMBOL | 导出符号到内核符号表 |
EXPORT_SYMBOL_GPL | 仅适用于包含 GPL 许可的模块 |
导出的符号可被其他模块使用,使用前只需声明即可。
例子
123456789101112131415161718192021222324 | int add(int a, int b){ return a+b;}EXPORT_SYMBOL(add);static int __init module_export_init(void){ pr_info("add init\n"); return 0;}static void __exit module_export_exit(void){ pr_info("add exit\n");}module_init(module_export_init);module_exit(module_export_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("A sample for module export"); |
导入
1234567891011121314151617181920212223242526272829303132 | static int a = 0;module_param(a,int, 0644);MODULE_PARM_DESC(a, "add test left num a, default 0");static int b = 0;module_param(b, int, 0644);MODULE_PARM_DESC(b, "add test left num b, default 0");extern int add(int a, int b);static int __init module_export_init(void){ pr_info("hello init\n"); pr_info("a=%d, b=%d\n", a, b); pr_info("a+b=%d\n", add(a, b)); return 0;}static void __exit module_export_exit(void){ pr_info("hello exit");}module_init(module_export_init);module_exit(module_export_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<731005515@qq.com>");MODULE_DESCRIPTION("A sample for module export"); |
注意:加载时要先加载导出的那个模块,卸载时要先卸载导入的那个模块。因为此时两个模块已经有了依赖关系,modprobe 会自动处理这些依赖关系,不需要显式声明。
使用 makefile 中定义的宏
核心思路:想要让它被 C 代码可见,必须通过 编译器命令行传入宏定义:
1 | cc -D宏名=值 source.c |
例子:
123456789 | obj-m += mydriver.o# 定义一个宏MY_DRIVER_VER := 0x10# 把它传给编译器KBUILD_CFLAGS_MODULE += -DMY_DRIVER_VER=$(MY_DRIVER_VER)# 或者使用ccflags-y += -DMY_DRIVER_VER=$(MY_DRIVER_VER) |
其他变量名:
| 变量名 | 作用范围 | 典型用途 |
|---|---|---|
ccflags-y | 当前模块的所有.c文件 | 普通 C 编译选项 |
asflags-y | 汇编文件 | 汇编参数 |
subdir-ccflags-y | 当前目录及子目录 | 全局作用 |
KBUILD_CFLAGS | 全局(由内核顶层 Makefile 设置) | 平台级 CFLAGS |
KBUILD_CFLAGS_MODULE | 内核模块 | |
EXTRA_CFLAGS | 已废弃(旧版用法) | 临时附加选项 |
然后在 driver 中可以使用:
123456789101112131415161718 | static int __init mydriver_init(void){ pr_info("mydriver version: 0x%x\n", MY_DRIVER_VER); return 0;}static void __exit mydriver_exit(void){ pr_info("mydriver exit\n");}module_init(mydriver_init);module_exit(mydriver_exit);MODULE_LICENSE("GPL"); |

