03、主次设备号
字符设备通过字符(一个接一个的字符)以流方式向用户程序传递数据,就像串行端口那样。字符设备驱动通过/dev
目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户应用程序之间交换数据,也可以通过它来控制实际的物理设备。这也是Linux
的基本概念,一切皆文件。字符设备驱动程序是内核源码中最基本的设备驱动程序。字符设备在内核中表示为struct cdev
的实例,它定义在include/linux/cdev.h
中:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
2
3
4
5
6
7
8
一、主设备和次设备的概念
字符设备在/dev目录下,不能简单地把它们当作普通文件。字符设备文件的类型是可以识别的,用ls -l命令能够查看。主设备号和次设备号标识设备,并将其与驱动程序进行绑定。下面列出/dev目录(ls -l /dev)的内容,让我们看一看其工作原理
每一列的第一个字符代表文件类型,它有如下取值。
- c:字符设备文件。
- b:块设备文件。
- l:符号链接。
- d:目录。
- s:套接字。
- p:命名管道。
主设备和次设备用<X, Y>格式表示。其中,X代表主设备号,Y代表次设备号。比如,第三行是<1, 2>,最后一行是<7, 3>。
二、设备号类型
申请的设备号类型为 dev_t , 在“内核源码/include/linux/types.h” 文件中定义如下:
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
2
3
dev_t 为 u32 类型, 而 u32 定义在文件 “内核源码/include/uapi/asm-generic/int-ll64.h”
typedef unsigned int __u32;
__u32 为 unsigned int 类型, 所以 dev_t 是一个无符号的 32 位整形类型。 其中:
- 高 12 位表示主设备号
- 低 20 位表示次设备号。 在“内核源码/include/linux/kdev_t.h” 中提供了设备号相关的宏定义:
#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))
#define print_dev_t(buffer, dev) \
sprintf((buffer), "%u:%u\n", MAJOR(dev), MINOR(dev))
#define format_dev_t(buffer, dev) \
({ \
sprintf(buffer, "%u:%u", MAJOR(dev), MINOR(dev)); \
buffer; \
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
设备注册时,必须使用主设备号和次设备号,前者标识这个设备,后者用作本地设备列表中的数组索引,因为同一个驱动程序的一个实例可以处理多个设备,而不同的驱动程序可以处理相同类型的不同设备。
三、设备号的分配和释放
设备号在系统范围内标识设备文件,这意味着可以有两种不同的方法来分配设备号码(实际上是主设备号和次设备号)。
- register_chrdev_region(静态)
- alloc_chrdev_region(动态)
把名字定下来。
3.1、静态方法
主设备号 --- 没有被其他人使用
register_chrdev_region(主次设备号 必须固定)
假设一个主设备号还没被其他驱动程序调用register_chrdev_region()函数所占用,即可使用。
下面是这个函数的原型,目录:fs/char_dev.c
int register_chrdev_region(dev_t first, unsigned int count, \
char *name);
2
参数:
**dev_t first**
: 起始的设备号,包括主设备号 (MAJOR
) 和次设备号 (MINOR
)。**unsigned count**
: 需要分配的连续设备号的数量。**const char *name**
: 注册的设备名称,用于用户态与内核交互时的设备识别。
返回值:
- 返回
0
表示成功。 - 如果失败,返回一个负的错误码。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
//用于保存每次注册的字符设备结构指针
struct char_device_struct *cd;
//计算出目标设备号的结束范围 (from + count)
dev_t to = from + count;
//用来遍历和管理设备号的分配
dev_t n, next;
//遍历设备号范围,分段分配
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
案例:
media/v4l2-dev.h
#define VIDEO_MAJOR 81
3.2、动态方法
alloc_chrdev_region
是 Linux 内核中的一个函数,用于动态分配主设备号和次设备号,并将它们注册到系统中。与 register_chrdev_region
不同的是,它会自动分配一个空闲的主设备号,而不是要求开发者手动指定。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
2
3
4
5
6
7
8
9
10
参数解析:
**dev_t *dev**
:- 用于返回分配的设备号(包含主设备号和次设备号)。
- 主设备号通过自动分配的方式确定。
- 次设备号起始值通过
baseminor
指定。
**unsigned baseminor**
:- 指定起始的次设备号。
**unsigned count**
:- 要分配的连续设备号的数量。
**const char *name**
:- 设备的名称,用于用户态和内核的设备识别。
返回值:
- 返回
0
表示成功。 - 返回负数表示失败,失败值是一个错误码(通过
PTR_ERR
提供)。
3.3、__register_chrdev_region 分析
__register_chrdev_region
是 Linux 内核中的一个静态函数,用于分配和注册字符设备号范围。它是alloc_chrdev_region
和 register_chrdev_region
的底层实现。
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
2
参数解析:
**unsigned int major**
:- 指定主设备号。
- 如果为
0
,函数将动态分配一个空闲主设备号。
**unsigned int baseminor**
:- 起始的次设备号。
**int minorct**
:- 次设备号的数量。
**const char *name**
:- 设备的名称,用于标识设备。
返回值:
- 成功时返回一个指向
struct char_device_struct
的指针。 - 失败时返回一个错误指针(通过
ERR_PTR
包装的错误码)。
/*
* Register a single major with a specified minor range.
*
* If major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If major > 0 this function will attempt to reserve the passed range of
* minors and will return zero on success.
*
* Returns a -ve errno on failure.
*/
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
//分配 char_device_struct 结构
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
//动态分配主设备号
if (major == 0) {
//动态分配
ret = find_dynamic_major();
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;
}
major = ret;
//pritk
}
//检查主设备号有效性
//#define CHRDEV_MAJOR_MAX 512
if (major >= CHRDEV_MAJOR_MAX) {
pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
name, major, CHRDEV_MAJOR_MAX-1);
ret = -EINVAL;
goto out;
}
//初始化 char_device_struct 结构
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
i = major_to_index(major);
//插入到设备列表
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
//检查次设备号范围是否冲突
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
if (new_min < old_min && new_max > old_max) {
ret = -EBUSY;
goto out;
}
}
//插入设备信息
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
所有的字符设备的信息(主设备号)记录在chrdevs(全局变量)
这个函数如果执行成功,则返回0;否则返回负的错误码。dev是唯一的输出参数,它代表内核分配的第一个数字。firstminor代表申请的次设备号范围内的第一个数字,count代表申请的次设备的数量,name代表相关设备或者驱动程序的名字。
这两种分配方法的区别在于,第一种方法必须事先知道所需的设备号,这就是注册制:把所需的设备号告诉内核。这可能仅在教学中使用,只有自己在用该驱动程序时,才会这样选择。如果在其他机器上加载该驱动程序,就无法保证所选择的设备号在这台机器未被占用,这会引起设备号的冲突和麻烦。第二种方法更清晰、更安全。因为内核帮助获取一个合适的设备号,所以我们甚至不需要关心在其他机器上加载该模块所出现的问题,内核将根据具体情况来自动分配。
无论如何,上面这些函数一般不在驱动程序中直接调用,它们会被驱动程序所依赖框架(IIO框架、输入框架、RTC等)的专用API所屏蔽。
四、案例
4.1、静态案例
4.2、动态案例
五、实验程序的编写
驱动代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
static int major;//定义静态加载方式时的主设备号参数major
static int minor;//定义静态加载方式时的次设备号参数minor
module_param(major,int,S_IRUGO);//通过驱动模块传参的方式传递主设备号参数major
module_param(minor,int,S_IRUGO);//通过驱动模块传参的方式传递次设备号参数minor
static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num
static int __init dev_t_init(void)//驱动入口函数
{
int ret;//定义int类型的变量ret,用来判断函数返回值
/*以主设备号进行条件判断,即如果通过驱动传入了major参数则条件成立,进入以下分支*/
if(major){
dev_num = MKDEV(major,minor);//通过MKDEV函数将驱动传参的主设备号和次设备号转换成dev_t类型的设备号
printk("major is %d\n",major);
printk("minor is %d\n",minor);
ret = register_chrdev_region(dev_num,1,"chrdev_name");//通过静态方式进行设备号注册
if(ret < 0){
printk("register_chrdev_region is error\n");
}
printk("register_chrdev_region is ok\n");
}
/*如果没有通过驱动传入major参数,则条件成立,进入以下分支*/
else{
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_num");//通过动态方式进行设备号注册
if(ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_chrdev_region is ok\n");
major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取
minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取
printk("major is %d\n",major);
printk("minor is %d\n",minor);
}
return 0;
}
static void __exit dev_t_exit(void)//驱动出口函数
{
unregister_chrdev_region(dev_num,1);//注销字符驱动
printk("module exit \n");
}
module_init(dev_t_init);//注册入口函数
module_exit(dev_t_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
makefile:
export ARCH=arm64
export CROSS_COMPILE=/home/book/rk/tspi/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
obj-m += char.o #此处要和你的驱动源文件同名
KDIR := /home/book/rk/tspi/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make#操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
2
3
4
5
6
7
8
9
六、实验程序的运行
insmod char.ko
insmod char.ko major=200 minor=0