前面的部分介绍了申请设备号,本文紧接着介绍通过关联设备号注册字符设备。然后创建设备节点类,生成设备节点。
了解内容
抽丝剥茧,驱动的核心就是ops操作结构体,注册设备也就是将ops和设备结构体关联,然后将设备结构体和设备号关联起来,得以实现通过设备号找到对应ops操作结构体的目的。
单链表插入新节点
cdev_init后,字符设备每cdev_add一次就会在链表中插入一个新节点,其实现方式如下图。

回看内核程序,cdev_add调用的关键程序段如下:

其中probes[index%255]是关于主设备号的数组,也就是每一个主设备号都对应了一个链表,且&(*cdev_map->probes[index%255])就是表头指针。解释一下,因为cdev_map中probes成员声明的是数组指针,所以cdev_map->probes[index%255]是指向表头空间的指针,*cdev_map->probes[index%255]是表头空间,&(*cdev_map->probes[index%255])是表头空间的实际地址。这也是下列程序中声明**s的原因。它的意义在于256个probes结构体空间不需要放在cdev_map结构体空间里面,也不需要连续。

cdev_add节点测试
修改内核中kobj_map函数,打印每一步操作前后*s的地址。然后在测试模块中加入如下内容:提前声明mycdev结构体和my_fops结构体,共进行五次cdev_add。

可以看到最终的结果是,范围越大(连续数量越多)的节点在链表中位置越靠后。

cdev_init初始化函数
主要功能:将ops结构体和设备结构体关联。
原型为cdev_init(struct cdev *cdev, const struct file_operations *fops);,该函数在路径“include/linux/cdev.h”下有定义。
- cdev:是设备结构体,编写前可以直接声明实体或者声明一个指针然后申请内存。推荐后者。
- fops:是操作结构体。
静态内存定义初始化:
每有一个同类设备(指ops相同的)就按下面做一次,声明然后cdev_init。同类设备用cdev_add加进去。当然你也可以忽略同类设备的概念,一设备一init一add都是可以的。多少个cdev_init就需要多少个cdev_del对应。
1
2
3
4
5
6
7
static struct cdev my_cdev; //建议静态全局
...
//init()时
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
//exit()时
cdev_del(&my_cdev); //包含了kfree结构体空间
动态内存定义初始化:
-
初始化一类设备,还可以使用库函数cdev_alloc动态分配空间,它调用了kzalloc(理解为kmalloc和memset的结合体)自动分配了一个cdev大小的空间。cdev_del函数可以删除设备对应链表节点,以及能kfree这部分空间。
1 2 3 4 5 6 7 8
static struct cdev *my_cdev; //建议静态全局 ... //init()时 my_cdev = cdev_alloc(); my_cdev->ops = &fops; my_cdev->owner = THIS_MODULE; //exit()时 cdev_del(my_cdev); //包含了kfree结构体空间
-
如果想一次性统一注册多类设备,可以手动申请空间清零然后cdev_init关联。或者把上面的多写几遍(多定义写变量,多调用几次函数)效果都是相同的。
下面的kmalloc(申请内核空间)和memset(清零)也可以合起来写kzalloc函数(推荐)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
static struct cdev *my_cdev; //建议静态全局 ... //init()时 my_cdev = kmalloc(DEVICE_CLASS_NUM*sizeof(struct cdev),GFP_KERNEL); memset(my_cdev,0,DEVICE_CLASS_NUM*sizeof(struct cdev)); if(!my_cdev) goto fail; cdev_init(&my_cdev[0], &fops); //关联第一类设备和ops my_cdev[0].owner = THIS_MODULE; cdev_add(&my_cdev[0],devno,2); //在第一类设备下添加设备号为devno、devno+1的两个设备 cdev_init(&my_cdev[1], &fops1); //关联第二类设备和ops1 my_cdev[1].owner = THIS_MODULE; cdev_add(&my_cdev[1],devno+2,1); //在第二类设备下添加设备号为devno+2的一个设备 ... //exit()时 cdev_del(&my_cdev[0]); //包含了kfree第一类设备结构体空间 cdev_del(&my_cdev[1]); //包含了kfree第二类设备结构体空间 ...
cdev_add添加设备链表节点函数
主要功能:将设备结构体和设备号关联。当cdev_init初始化一类设备后,使用cdev_add在该类设备中添加设备,特点是这些设备将会有同一种ops操作结构体,每add一次,将会在字符设备链表地图中新增一个节点。
原型为int cdev_add(struct cdev *p, dev_t dev, unsigned count);,该函数在路径“include/linux/cdev.h”下有定义。
- p:初始化后的一类设备(已经关联了ops的一类设备)。
- dev:将添加的设备起始设备号;
- count:设备号连续数量;比如2就是添加dev、dev+1设备为p类设备。
与上对应的删除设备链表函数为 cdev_del(struct cdev *p);,该函数除了从字符设备链表地图中删除有关该设备的链表节点外还可以kfree该类设备申请的空间。
开始
编写注册字符设备模块
主要变动点:
- 加入申请内核空间的头文件
#include <linux/slab.h>; - 定义设备的种数DEVICE_CLASS_NUM;
- 声明设备结构体指针,手动申请清空的内存kzalloc;
- 每初始化一类设备加入申请到的设备号;
- 退出时删除设备类。
regdev_demo.c:
1
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stat.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h> //kzalloc
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Jerry");
static dev_t dev; //设备号
static int dev_major; //主设备号
static int dev_minor; //从设备号
#define DEVICE_NAME "dev_demo"
#define DEVICE_NUM 3 //连续分配的设备个数(优先递增从设备号)
#define DEVICE_CLASS_NUM 2 //设备种类数量
module_param(dev_major,int,S_IRUSR);
module_param(dev_minor,int,S_IRUSR);
static struct file_operations my_fops = {
.owner = THIS_MODULE,
};
static struct file_operations my_fops1 = {
.owner = THIS_MODULE,
};
static struct cdev *my_cdev; //建议静态全局
static int demo_init(void){
int ret;
dev = MKDEV(dev_major,dev_minor);
if(dev_major == 0){ // 自动分配
ret = alloc_chrdev_region(&dev,dev_minor,DEVICE_NUM,DEVICE_NAME);
}
else{
ret = register_chrdev_region(dev,DEVICE_NUM,DEVICE_NAME);
}
if(ret < 0)
goto fail;
printk(KERN_EMERG "dev_major is : %d!\n",MAJOR(dev));
printk(KERN_EMERG "dev_minor is : %d!\n",MINOR(dev));
my_cdev = kzalloc(DEVICE_CLASS_NUM*sizeof(struct cdev),GFP_KERNEL);//申请内核内存并清空
if(!my_cdev)
goto fail;
cdev_init(&my_cdev[0], &my_fops); //关联第一类设备和ops
my_cdev[0].owner = THIS_MODULE;
cdev_add(&my_cdev[0],dev,2); //在第一类设备下添加设备号为dev、dev+1的两个设备
cdev_init(&my_cdev[1], &my_fops1); //关联第二类设备和ops
my_cdev[1].owner = THIS_MODULE;
cdev_add(&my_cdev[1],dev,1); //在第二类设备下添加设备号为dev+2的设备
return 0;
fail:
printk(KERN_EMERG "something is fail!\n");
unregister_chrdev_region(dev,DEVICE_NUM); //失败就释放
return 0;
}
static void demo_exit(void){
int i=0;
//del cdev and free kmalloc/kzalloc
for(i=0;i<DEVICE_CLASS_NUM;i++)
cdev_del(&my_cdev[i]);
unregister_chrdev_region(dev,DEVICE_NUM);
return;
}
module_init(demo_init);
module_exit(demo_exit);
进行验证
编写简单Makefile:参考这里
编译生成模块后,拷贝到开发板中:
-
直接加载模块,不传值(初值为0)

-
加载后进行筛选设备名,可以看到248已经被注册为dev_demo。
虽然现象和申请设备号相同,但没有出现错误提示,说明操作是成功了的。

创建设备节点
系统的设备节点类可以使用:ls /sys/class进行查看。
系统的设备节点可以使用:ls /dev进行查看。
创建设备节点类
class_create是一个创建设备节点类的宏定义,原型等同struct class *class_create(owner, name);,这个宏在include/linux/device.h中有定义。
- owner参数一般填写THIS_MODULE;
- name参数填写自定的设备节点类名称,创建后会出现在
/sys/class中;
返回值是class类型的指针,下一步就是把它和已处理的设备号关联起来了。也就是说要产生一个可供交互的设备节点文件,class类就是接口。
对应的摧毁类函数为class_destroy(my_class);
创建设备节点
device_create是创建设备节点的函数,原型是struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...),这个宏在include/linux/device.h中有定义。
- class参数,填写由class_create返回的类指针;
- parent参数,没有就填NULL;
- devt参数,填写关联的设备号,一节点关联一号,一号可以关联多个节点(每必要);
- drvdata参数,一般NULL;
- fmt,…参数,填写格式化节点名称,比如
NODE_NAME"%d",i,可产生“dev_demo0”、“dev_demo1”这样的节点。
对应的摧毁设备节点函数为device_destroy(my_class,devt);
完整例程
主要变动点:
- 加入头文件
#include <linux/device.h> - 新增宏CLASS_NAME、NODE_NAME
- 声明my_class类指针,调用class_create和device_create
- 退出时摧毁类和设备节点
cdevnode_demo.c:
1
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
95
96
97
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stat.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/slab.h> //kzalloc
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Jerry");
static dev_t dev; //设备号
static int dev_major; //主设备号
static int dev_minor; //从设备号
#define DEVICE_NAME "dev_demo"
#define DEVICE_NUM 3 //连续分配的设备个数(优先递增从设备号)
#define DEVICE_CLASS_NUM 2 //设备种类数量
#define CLASS_NAME "class_demo"
#define NODE_NAME "node_demo"
module_param(dev_major,int,S_IRUSR);
module_param(dev_minor,int,S_IRUSR);
static struct file_operations my_fops = {
.owner = THIS_MODULE,
};
static struct file_operations my_fops1 = {
.owner = THIS_MODULE,
};
static struct cdev *my_cdev; //建议静态全局
static struct class *my_class; //设备节点类
static int demo_init(void){
int ret;
dev = MKDEV(dev_major,dev_minor);
if(dev_major == 0){ // 自动分配
ret = alloc_chrdev_region(&dev,dev_minor,DEVICE_NUM,DEVICE_NAME);
}
else{
ret = register_chrdev_region(dev,DEVICE_NUM,DEVICE_NAME);
}
if(ret < 0)
goto fail;
printk(KERN_EMERG "dev_major is : %d!\n",MAJOR(dev));
printk(KERN_EMERG "dev_minor is : %d!\n",MINOR(dev));
my_cdev = kzalloc(DEVICE_CLASS_NUM*sizeof(struct cdev),GFP_KERNEL);//申请内核内存并清空
if(!my_cdev)
goto fail;
cdev_init(&my_cdev[0], &my_fops); //关联第一类设备和ops
my_cdev[0].owner = THIS_MODULE;
cdev_add(&my_cdev[0],dev,2); //在第一类设备下添加设备号为dev、dev+1的两个设备
cdev_init(&my_cdev[1], &my_fops1); //关联第二类设备和ops
my_cdev[1].owner = THIS_MODULE;
cdev_add(&my_cdev[1],dev+2,1); //在第二类设备下添加设备号为dev+2的设备
//生成设备类、设备节点
my_class = class_create(THIS_MODULE,CLASS_NAME);
device_create(my_class,NULL,dev,NULL,NODE_NAME);
device_create(my_class,NULL,dev+1,NULL,NODE_NAME"%d",1);
device_create(my_class,NULL,dev+2,NULL,NODE_NAME"%d",2);
return 0;
fail:
printk(KERN_EMERG "something is fail!\n");
unregister_chrdev_region(dev,DEVICE_NUM); //失败就释放
return 0;
}
static void demo_exit(void){
int i=0;
device_destroy(my_class,dev);
device_destroy(my_class,dev+1);
device_destroy(my_class,dev+2);
class_destroy(my_class);
//del cdev and free kmalloc/kzalloc
for(i=0;i<DEVICE_CLASS_NUM;i++)
cdev_del(&my_cdev[i]);
unregister_chrdev_region(dev,DEVICE_NUM);
return;
}
module_init(demo_init);
module_exit(demo_exit);
进行验证
编写简单Makefile:参考这里
编译生成模块后,拷贝到开发板中:
-
直接加载模块,不传值(初值为0)

-
加载后可以看到已经生成了类节点和设备节点。
