学习目标:
掌握设备号的作用和申请、注册和注销的方法。
学习内容:
为什么说“字符设备驱动”是软柿子?这是我从一本书上看到的,经过提炼后总结的。原话是“在Linux设备驱动程序的家族中,字符设备驱动程序是较为简单的驱动程序,同时也是 应用非常广泛的驱动程序。所以学习字符设备驱动程序,对构建Linux设备驱动程序的知识结构非常重要。”
那么,我们先说说要完成一个字符设备驱动程序,至少需要哪些要素呢?
一、设备号
Linux 规定每一个字符设备或者块设备都必须有一个专属设备号。一个设备号由主设备号和次设备号组成。
- 主设备号用来表示某一类特定驱动程序;
- 次设备号用来表示该驱动下的各个设备。
例如,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么 ,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备驱动程序,次设备号 分别为1和2。这里,次设备号就分别表示两个LED灯。
因此,开发字符设备驱动程序,申请设备号是第一步,只有有了设备号,才能向系统注册设备。
1、设备号的类型
Linux中使用 dev_t 数据类型表示设备号。dev_t 的定义在内核源码的 include/linux/types.h 文件里。设备号是一个32位的数据类型(unsigned int),其中高12位为主设备号,低20位为次设备号。
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
- MINORBITS 表示次设备号位数。
- MINORMASK 用于计算次设备号使用。
- MAJOR 表示从dev_t中获取主设备号,本质是将dev_t右移20位。
- MINOR 表示从dev_t中获取次设备号,本质是取低20位的值。
- MKDEV 用于将主设备号和次设备号组成dev_t类型的设备号。
2、设备号分配
在内核中,提供了动态分配设备号和静态分配设备号的函数,声明在include/linux/fs.h 里面。定义在fs/char_dev.c 中
功能 | 函数 | 参数 |
---|---|---|
静态分配设备号函数 | register_chrdev_region | 设备号起始值,次设备号数量,设备的名称 |
动态分配设备号函数 | alloc_chrdev_region | 保存自动申请到的设备号,次设备号的起始值,要申请的数量,设备名称 |
设备号释放函数 | unregister_chrdev_region | 要释放的设备号,释放的设备号数量 |
二、举例
1、例程:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
// 这里是驱动传参
static int major = 0;
static int minor = 0;
module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);
// 设备号变量
dev_t dev_num;
// 驱动入口函数
static int module_dev_t_init(void)
{
int ret = 0;
if(major) // 如果有主设备号,则进行静态设备注册
{
printk("major=%d!\n", major);
printk("minor=%d!\n", minor);
// 合成设备号
dev_num = MKDEV(major, minor);
// 注册设备号
ret = register_chrdev_region(dev_num, 1, "test1");
if(ret < 0)
{
printk("register_chrdev_region failed!!\n");
return -1;
}
printk("register_chrdev_region success!!\n");
}
else // 如果没有主设备号,则进行动态设备注册
{
ret = alloc_chrdev_region(&dev_num, 0, 1, "test2");
if(ret < 0)
{
printk("alloc_chrdev_region failed!!\n");
return -1;
}
printk("alloc_chrdev_region success!!\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("Major=%d, Minor=%d\n", major, minor);
}
return 0;
}
// 驱动卸载函数
static void module_dev_t_exit(void)
{
unregister_chrdev_region(dev_num, 1); // 注销设备号
printk("dev_t test module bye!\n");
}
module_init(module_dev_t_init);
module_exit(module_dev_t_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BF");
MODULE_VERSION("V1");
这个例子我是在ubuntu 的虚拟机上进行实验的,Makefile 如下
KERNEL_DIR=/lib/modules/5.15.0-69-generic/build
obj-m := dev_t.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
~
编译完成之后,使用insmod加载模块,查看dmesg 出现了两种状况,看图:
2、遇到问题:
1、loading out-of-tree module taints kernel.
这种错误可以不用理会,不影响驱动程序使用,但是看着就是不爽,就像编译过程中的warning 一样。
问题原因1:
因为驱动用到了设备树,编译驱动的linux内核与insmod模块的linux的内核设备树不相同导致的。
解决方法:把驱动使用当前linux设备树重新编译一下,可以解决。
问题原因2:
没有把驱动模块编译到 Kconfig 文件中,即 make menuconfig 的配置选项中没有此驱动。
解决方法:
把驱动信息加入到Kconfig树中,普可以通过配置内核来决定哪些驱动需要加载,系统也就不会再报loading out-of-tree module taints kernel的错误了。
2、module verification failed: signature and/or required key missing - tainting kernel
问题原因:
这是加载驱动程序时驱动签名或需要的密钥找不到,导致驱动module认证失败。
解决方法:
方式一、重新配置内核
方式二、修改驱动Makefile文件,在第一行增加以下语句:CONFIG_MODULE_SIG=n
因此,我的Makefile修改为
CONFIG_MODULE_SIG=n
KERNEL_DIR=/lib/modules/5.15.0-69-generic/build
obj-m := dev_t.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
再次编译后,加载后,没有出现刚才的提示。
3、例程验证
1、动态
加载成功后,怎么确认设备注册成功?
在系统中使用命令 cat /proc/devices 查看设备名称、设备号
可以看到和dmesg中打印的一样的设备号 236 名称是 test2,说明设备注册没问题。
rmmod 之后就查询不到,说明设备卸载成功。
到这里,我们可以从test2 设备名称以及dmesg 中打印的申请设备号的函数名:alloc_chrdev_region 确认,我们这次加载使用的是 动态申请设备号。
2、静态
我们再来试试静态申请设备号,我们程序中判断是否给驱动程序传递了 主设备号 这个参数,刚才我们没有给驱动传递。那现在我们传递一个试试。
注意,注意,注意!重要的事情说三遍!“静态申请设备号,要自己查询一下,使用没有被申请的设备号”
通过场次 cat /proc/devcies 我们就用一个没有被申请的 222 号设备号。还记开头说的设备号是由 主设备号左移20位 再和 次设备号 位或 得到的。那我们可以直接传递 主设备号为 222,次设备号为0。命令如下:
sudo insmod dev_t.ko major=222 minor=0
查看dmesg 和 cat /proc/devcies 验证一下。