1、linux设备驱动的模块
1.1 内核模块的三要素(面试题)
入口:在入口函数中申请资源
出口:在出口函数中释放资源
许可证:遵循GPL的开源协议
1.2 内核模块代码的编写
// 头文件的包含可以使用vi -t 函数名/宏名 查看最终的头文件
// 在linux驱动中的头文件一般都是<linux/头文件名.h>
#include <linux/init.h>
#include <linux/module.h>
// 入口函数:申请资源
static int __init demo_init(void)
{
// static : 静态的成员函数,只可以在本文件中使用,其他文件不可以使用
// int : 返回类型
// __init : 修饰demo_init函数,给编译器使用,
// 告诉编译器将demo_init函数编译时放到__init段,
// 当加载驱动时直接从__init段查找对应的入口函数,最终加载驱动,提高查找的效率。
/*
vi -t __init ----> 查看__init在哪里定义
50 #define __init __section(".init.text") // include/linux/init.h
在linux内核的连接脚本文件中:arch/arm/kernel/vmlinux.lds得到以下内容。
32 . = ALIGN(8); .init.text : AT(ADDR(.init.text) - 0)
*/
// demo_init : 入口函数的名字,自己随意编写,符合函数名的命名规则
// (void) : 函数的参数无
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
// __exit : 修饰出口函数,给编译器使用的,
// 告诉编译器将出口函数都存到__exit对应的代码段。
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
1.3 对模块化的驱动进行编译
1.3.1 采用内部编译的方式
1> 将驱动文件拷贝到drivers/char目录下
2> 修改drivers/char目录下的Makefile文件
obj-$(CONFIG_****) += ****.o
3> 修改drivers/char目录下的Kconfig文件
config ****(配置选项的名称)
tristate "菜单选项的名称"
default y/n/m
help
帮助信息
4> 执行make menuconfig配置为模块化的方式进行编译
5> 执行make modules模块化编译驱动生成.ko驱动文件
6> 拷贝.ko文件到跟文件系统中
7> 启动开发板,使用insmod rmmod lsmod加载驱动,卸载驱动,查看驱动
1.3.2 采用外部编译的方式
# 1. 外部编译模块化驱动需要借助于内核源码中的Makefile文件完成驱动的编译
# 2. 内核源码必须执行过make modules命令。
# 指定ubuntu系统的内核源码目录
KERNELDIR := /lib/modules/$(shell uname -r)/build
# 指定当前的驱动的路径
CURRENTDIR := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
@ # -C : 进入到内核源码目录下,借助内核源码目录下的Makefile文件,
@ # 根据内核源码目录下模块化编译驱动的规则完成对当前目录下的模块化驱动的编译
@ # 进入内核源码目录下执行make modules
@ # M=$(CURRENTDIR) : 只对当前目录下的模块化驱动进行模块化的编译
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
obj-m := demo.o
1.4 模块化驱动相关的命令
linux@ubuntu:01modules$ insmod demo.ko
insmod: ERROR: could not insert module demo.ko: Operation not permitted
linux@ubuntu:01modules$ sudo insmod demo.ko ---> 加载驱动
linux@ubuntu:01modules$ lsmod | grep "demo" ---> 查看驱动
linux@ubuntu:01modules$ sudo rmmod demo ---> 卸载驱动
2、linux内核中的打印语句的使用
2.1 printk函数的使用
用法:
printk(打印级别 “格式化字符串”, 可变参数);
打印级别:过滤打印信息的。
2.2 printk支持的打印级别的分析
vi -t KERN_ERR
8 #define KERN_EMERG KERN_SOH "0" /* system is unusable */
9 #define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
10 #define KERN_CRIT KERN_SOH "2" /* critical conditions */
11 #define KERN_ERR KERN_SOH "3" /* error conditions */
12 #define KERN_WARNING KERN_SOH "4" /* warning conditions */
13 #define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
14 #define KERN_INFO KERN_SOH "6" /* informational */
15 #define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
printk函数最多支持8个打印级别,编号越小打印的级别越高。
只有打印几倍大于终端的打印级别时,打印信息才会在终端进行回显。
2.3 过滤信息的作用
在linux内核中的打印信息,在终端有其自己的打印的级别,当内核中的打印信息的级别大于终端的
打印级别时,内核的打印信息才会在终端回显。
如何查看终端信息的打印级别:cat /proc/sys/kernel/printk
4 ----------------- 4 ------------------ 1 --------------- 7
终端打印级别 打印消息的默认级别 终端消息的最大级别 终端消息的最小级别
2.4 打印函数的练习
#include <linux/init.h>
#include <linux/module.h>
// 入口函数:申请资源
static int __init demo_init(void)
{
// 消息的打印级别大于终端的默认级别
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 使用消息的默认打印的级别
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 消息的打印级别小于终端的默认级别
printk(KERN_INFO "%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
2.5 加载驱动,查看打印信息
1. 直接加载驱动
linux@ubuntu:02printk$ sudo insmod demo.ko
[sudo] password for linux:
linux@ubuntu:02printk$
终端并没有打印任何的消息,原因是终端将所有的消息全部都屏蔽。
2. 查看驱动的打印的消息的方式1:打开ubuntu系统的虚拟的终端
打开ubuntu系统的虚拟终端:fn + ctrl + alt + (F2-F7)
退出ubuntu系统的虚拟终端:fn + ctrl + alt + F1
.png" alt=“image-20220808115728525” style />
3. 使用dmesg命令查看内核中的打印的信息
dmesg : 查看内核启动之后的所有的打印的信息
dmesg -C : 不回显清除所有的打印的信息
dmesg -c : 先回显在清除所有的打印的信息
2.6 设置printk默认的打印级别大于终端的打印级别
1. ubuntu系统
sudo echo "4 3 1 7" > /proc/sys/kernel/printk
2. 开发板中
开发板系统启动之后,执行以下命令:
echo "4 3 1 7" > /proc/sys/kernel/printk
只要开发板一重启, /proc/sys/kernel/printk文件中的内容又恢复默认
将echo "4 3 1 7" > /proc/sys/kernel/printk写到跟文件系统中的etc/init.d/rcS
脚本文件的最后一行,然后重启开发板即可生效。
3、内核模块传参
3.1 内核模块中接收参数的API接口函数
* 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).
*/
1. module_param(name, type, perm)
功能:获取内核模块加载时传递的参数
参数:
@ name : 变量的名字,将传递的数据存到哪个变量中
@ type : 参数的类型,支持的数据类型如下
/*
* 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).
*/
@ perm : 文件的权限 0664
在linux内核中一切且文件,当需要给内核传递模块化的参数时,
linux内核在会在/sys/module/驱动命名的文件夹/parameters/变量名命名的文件。
以变量名命名的文件的权限为perm,文件的最大权限就是0664.
2. module_param_array(name, type, nump, perm)
功能:获取内核模块加载时,传递的数组
参数:
@ name : 数组的名字
@ type : 数据的类型
/*
* 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).
*/
@ nump : 获取数据的长度,需要传递一个变量地址
@ perm:
文件的权限,最大为0664
3. MODULE_PARM_DESC(_parm, desc)
功能:用来进行模块化传参的变量的描述的
参数:
@ _parm : 变量的名字
@ desc : 变量描述信息的字符串
使用modinfo命令可以查看模块化驱动相关的信息。
void strlen (const char *s, int *len){
求字符串的长度
}
int len;
strlen ("hello world", &len);
3.2 内核模块传参的测试代码
#include <linux/init.h>
#include <linux/module.h>
int backLight = 200;
module_param(backLight, int, 0664);
MODULE_PARM_DESC(backLight, "lcd back light range 0-255");
// 入口函数:申请资源
static int __init demo_init(void)
{
// 消息的打印级别大于终端的默认级别
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 使用消息的默认打印的级别
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 消息的打印级别小于终端的默认级别
printk(KERN_INFO "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
3.3 内核模块传参的方式
1. 使用modinfo查看模块化驱动的详细的信息
linux@ubuntu:03module_param$ modinfo demo.ko
filename: /home/linux/DC22041/drivers/day01/03module_param/demo.ko
license: GPL
srcversion: 332ED78D32CB5E76F82148F
depends:
retpoline: Y
name: demo
vermagic: 5.15.0-41-generic SMP mod_unload modversions
parm: backLight:lcd back light range 0-255 (int)
2. 加载驱动时给模块化的驱动传递参数
linux@ubuntu:03module_param$ sudo insmod demo.ko
-----> 变量使用的是默认的值
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=100
-----> 变量使用的通过模块加载时传递的值
3. 通过给以变量名命名的文件中写入数据也可以传递参数
linux@ubuntu:03module_param$ sudo insmod demo.ko
[sudo] password for linux:
linux@ubuntu:03module_param$ ls /sys/module/demo/parameters/
backLight
linux@ubuntu:03module_param$ ls /sys/module/demo/parameters/backLight -l
-rw-rw-r-- 1 root root 4096 Aug 8 00:45 /sys/module/demo/parameters/backLight
linux@ubuntu:03module_param$ cat /sys/module/demo/parameters/backLight
200
linux@ubuntu:03module_param$ echo 150 > /sys/module/demo/parameters/backLight
3.4 byte类型的模块传参
#include <linux/init.h>
#include <linux/module.h>
int backLight = 200;
char ch = 'A';
module_param(backLight, int, 0664);
MODULE_PARM_DESC(backLight, "lcd back light range 0-255");
module_param(ch, byte, 0664);
MODULE_PARM_DESC(ch, "this is a char!");
// 入口函数:申请资源
static int __init demo_init(void)
{
// 消息的打印级别大于终端的默认级别
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 使用消息的默认打印的级别
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 消息的打印级别小于终端的默认级别
printk(KERN_INFO "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
printk("ch = %c\n", ch);
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch='c'
insmod: ERROR: could not insert module demo.ko: Invalid parameters
----> 错误,byte类型的参数,传递时不可以传递字符
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99
----> 正确,byte类型的参数,传递时需要传递字符对应的ASCII码对应的值
linux@ubuntu:03module_param$ sudo rmmod demo
3.5 数组类型的模块传参
#include <linux/init.h>
#include <linux/module.h>
int backLight = 200;
char ch = 'A';
int arr[10] = {0};
int len = 0;
module_param(backLight, int, 0664);
MODULE_PARM_DESC(backLight, "lcd back light range 0-255");
module_param(ch, byte, 0664);
MODULE_PARM_DESC(ch, "this is a char!");
module_param_array(arr, int, &len, 0664);
MODULE_PARM_DESC(arr, "this is a integer array!");
// 入口函数:申请资源
static int __init demo_init(void)
{
int i;
// 消息的打印级别大于终端的默认级别
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 使用消息的默认打印的级别
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 消息的打印级别小于终端的默认级别
printk(KERN_INFO "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
printk("ch = %c\n", ch);
for (i = 0; i < len; i++) {
printk("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50
3.6 charp(字符指针)类型的模块传参
#include <linux/init.h>
#include <linux/module.h>
int backLight = 200;
char ch = 'A';
int arr[10] = {0};
int len = 0;
char *p = "hello world";
module_param(backLight, int, 0664);
MODULE_PARM_DESC(backLight, "lcd back light range 0-255");
module_param(ch, byte, 0664);
MODULE_PARM_DESC(ch, "this is a char!");
module_param_array(arr, int, &len, 0664);
MODULE_PARM_DESC(arr, "this is a integer array!");
module_param(p, charp, 0664);
MODULE_PARM_DESC(p, "this is a string!");
// 入口函数:申请资源
static int __init demo_init(void)
{
int i;
// 消息的打印级别大于终端的默认级别
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 使用消息的默认打印的级别
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 消息的打印级别小于终端的默认级别
printk(KERN_INFO "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
printk("ch = %c\n", ch);
for (i = 0; i < len; i++) {
printk("arr[%d] = %d\n", i, arr[i]);
}
printk("p = %s\n", p);
return 0;
}
// 出口函数:释放资源
static void __exit demo_exit(void)
{
printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("backLight = %d\n", backLight);
}
// module_init修饰入口函数,当加载驱动时,最终会调用module_init修饰的函数
module_init(demo_init);
// module_exit修饰出口函数,当卸载驱动时,最终会调用module_exit修饰的函数
module_exit(demo_exit);
// 许可证:遵循GPL的开源协议
MODULE_LICENSE("GPL");
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50 p=nozuonodie ----> OK
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50 p="nozuonodie" ----> OK
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50 p=no zuo no die ---> 遇到空格结束
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50 p="no zuo no die" ---> 遇到空格结束
linux@ubuntu:03module_param$ sudo insmod demo.ko backLight=222 ch=99 arr=10,20,30,40,50 p=no_zuo_no_die ----> OK
3.7 通用版的Makefile文件
# 1. 外部编译模块化驱动需要借助于内核源码中的Makefile文件完成驱动的编译
# 2. 内核源码必须执行过make modules命令。
modname ?=
arch ?= x86
ifeq ($(arch),x86)
# 指定ubuntu系统的内核源码目录
KERNELDIR := /lib/modules/$(shell uname -r)/build
else
# 指定开发板使用的内核源码的路径
KERNELDIR := /home/linux/DC22041/porting/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
endif
# 指定当前的驱动的路径
CURRENTDIR := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
@ # -C : 进入到内核源码目录下,借助内核源码目录下的Makefile文件,
@ # 根据内核源码目录下模块化编译驱动的规则完成对当前目录下的模块化驱动的编译
@ # 进入内核源码目录下执行make modules
@ # M=$(CURRENTDIR) : 只对当前目录下的模块化驱动进行模块化的编译
install:
@cp $(modname).ko ~/nfs/rootfs/
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
obj-m := $(modname).o
make arch=arm|x86 modname=驱动文件名字
4、导出符号表
4.1 导出符号表的原理
导出符号表的使用场合?
购买某个传感器之后,厂家只提供此传感器对应的驱动的.ko文件,将特殊的函数通过导出符号表的方式给购买者使用。
4.2 导出符号表的API接口函数
EXPORT_SYMBOL_GPL(name)
功能:导出符号表
参数:
@ name : 要导出的函数的名字
4.3 编写demoA驱动程序
#include <linux/init.h>
#include <linux/module.h>
int add (int a, int b)
{
return (a + b);
}
// 导出符号表
EXPORT_SYMBOL_GPL(add);
static int __init demoA_init(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
static void __exit demoA_exit(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
module_init(demoA_init);
module_exit(demoA_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk zhouk_bj@hqyj.com");
4.4 编译demoA模块化驱动的Makefile文件
modname ?=
arch ?= x86
ifeq ($(arch),x86)
KERNELDIR := /lib/modules/$(shell uname -r)/build
else
KERNELDIR := /home/linux/DC22041/porting/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
endif
CURRENTDIR := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
install:
@cp $(modname).ko ~/nfs/rootfs/
help:
@echo "make arch=arm|x86 modname=<modules drivers source file name>"
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
obj-m := $(modname).o
4.5 编译demoA模块化的驱动
linux@ubuntu:demoA$ make arch=x86 modname=demoA
编译成功之后,会生成一个Module.symvers文件,此文件中存放的就是导出符号表的哪个函数信息。
此文件中的内容如下:
0x72f367e8 add /home/linux/DC22041/drivers/day02/01export_symbol/demoA/demoA EXPORT_SYMBOL_GPL
4.6 编写demoB驱动程序
#include <linux/init.h>
#include <linux/module.h>
// 使用extern将add函数进行声明
extern int add (int a, int b);
static int __init demoA_init(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
printk("100 + 200 = %d\n", add(100, 200));
return 0;
}
static void __exit demoA_exit(void)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
module_init(demoA_init);
module_exit(demoA_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk zhouk_bj@hqyj.com");
4.7 编译demoB模块化驱动的Makefile文件
modname ?=
arch ?= x86
ifeq ($(arch),x86)
KERNELDIR := /lib/modules/$(shell uname -r)/build
else
KERNELDIR := /home/linux/DC22041/porting/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
endif
CURRENTDIR := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
install:
@cp $(modname).ko ~/nfs/rootfs/
help:
@echo "make arch=arm|x86 modname=<modules drivers source file name>"
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
obj-m := $(modname).o
4.8 编译demoB模块化的驱动
内核源码的版本为5.4:(ubuntu 18.04 5.4 /
使用uname -r命令查看ubuntu使用的linux内核的版本)
在编译demoB前需要将demoA模块下的Module.symbers文件拷贝(5.4内核)到demoB目录下,在编译demoB模块。否则会出现add函数未定义。
编译的demoB命令 : make arch=x86 modname=demoB
内核源码的版本为5.10以后:(开发板:5.10)
在5.10的内核版本中取消了拷贝符号表的方式,如果想指定符号表,
需要在Makefile中使用KBUILD_EXTRA_SYMBOLS变量指定符号表文件的绝对路径
KBUILD_EXTRA_SYMBOLS := /home/linux/DC22041/drivers/day02/01export_Symbol/demoA/Module.symbers
注:需要修改为自己的路径
编译的demoB命令 : make arch=x86 modname=demoB
4.9 加载驱动并卸载驱动
加载驱动时:先加载demoA模块的驱动,再加载demoB模块的驱动
linux@ubuntu:demoB$ sudo insmod ../demoA/demoA.ko
linux@ubuntu:demoB$ sudo insmod demoB.ko
卸载驱动时:先卸载demoB模块的驱动,再卸载demoA模块的驱动
linux@ubuntu:demoB$ sudo rmmod demoB
linux@ubuntu:demoB$ sudo rmmod demoA