
注册字符设备可以分为两个步骤:
- 字符设备初始化
- 字符设备的添加
一、字符设备初始化
字符设备初始化所用到的函数为 cdev_init(...),在对该函数讲解之前,首先对 cdev 结构体进行介绍。
1、cdev 结构体
Linux 内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体等信息。定义在 内核源码/include/linux/cdev.h 文件中:
c
struct cdev {
struct kobject kobj; // 内嵌的内核对象
struct module *owner; // 该字符设备所在的内核模块的对象指针
const struct file_operations *ops; // 该结构描述了字符设备所能实现的方法
struct list_head list; // 用来将已经向内核注册的所有字符设备形成链表
dev_t dev; // 字符设备的设备号
unsigned int count; // 隶属于同一主设备号的次设备号的个数
};1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
关键字段说明:
struct kobject kobj:内嵌的内核对象,用于将cdev集成到 Linux 的设备模型中,与 sysfs 文件系统集成。struct module *owner:指向字符设备所在模块的结构体指针,确保在操作设备时,所属模块不会被卸载。const struct file_operations *ops:指向与该字符设备关联的操作函数集。struct list_head list:用来将所有已注册的cdev形成链表。dev_t dev:字符设备的设备号,由主设备号和次设备号组成。
2、cdev_init 初始化
设备初始化所用到的函数为 cdev_init():
c
void cdev_init(struct cdev *, const struct file_operations *);1
头文件:

实现:

c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev); // 将整个结构体清零
INIT_LIST_HEAD(&cdev->list); // 初始化 list 成员使其指向自身
kobject_init(&cdev->kobj, &ktype_cdev_default); // 初始化 kobj 成员
cdev->ops = fops; // 建立 cdev 和 file_operations 之间的连接
}1
2
3
4
5
6
7
2
3
4
5
6
7
3、字符设备的注册和注销
通过调用 cdev_add,驱动程序可以将字符设备与一个指定的设备号关联,使其可以被用户空间访问。
c
int cdev_add(struct cdev *p, dev_t dev, unsigned count);1
参数说明:
struct cdev *p:指向字符设备的结构体指针dev_t dev:设备号,由主设备号和次设备号组成unsigned count:需要注册的次设备号数量,通常为 1
返回值:成功时返回 0,失败时返回负数错误码。
代码实现目录:fs/char_dev.c
c
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
4、cdev_map
其中比较关键的是 cdev_map:

文件位置
drivers/base/map.c

c
struct kobj_map {
struct probe {
struct probe *next; // 链表指针
dev_t dev; // 起始设备号
unsigned long range; // 设备号范围
struct module *owner; // 拥有该设备的内核模块
kobj_probe_t *get; // 回调函数
int (*lock)(dev_t, void *);
void *data; // 用户自定义数据
} *probes[255]; // 管理 255 个主设备号范围
struct mutex *lock; // 互斥锁
};1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
初始化:

kobj_map 是 Linux 内核中用于注册设备号到设备对象的映射关系的函数。它通过将设备号范围与设备回调函数绑定,允许后续使用设备号查找到设备对象。
关键点
- 主设备号和次设备号的管理:
probes[255]数组按照主设备号进行管理。 - 链表的有序插入:按照
probe->range的大小升序排列。 - 并发保护:使用互斥锁
domain->lock,保证kobj_map的线程安全。

字符设备删除所用到的函数为 cdev_del():
c
void cdev_del(struct cdev *);1
二、实验代码
源代码下载
bash
git clone git@gitee.com:yangxuesong314/linux-driver.git1
- 代码位于:
linux-driver/05.字符设备驱动模型/02.注册字符设备
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
static dev_t dev_num;
static struct cdev cdev_test;
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
};
static int __init module_cdev_init(void)
{
int ret;
int major, minor;
ret = alloc_chrdev_region(&dev_num, 0, 1, "chrdev_name");
if (ret < 0) {
printk("alloc_chrdev_region is error\n");
}
printk("alloc_register_region is ok\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d\n",major);
printk("minor is %d\n",minor);
cdev_init(&cdev_test, &cdev_test_ops);
cdev_test.owner = THIS_MODULE;
ret = cdev_add(&cdev_test, dev_num, 1);
if (ret < 0) {
printk("cdev_add is error\n");
}
printk("cdev_add is ok\n");
return 0;
}
static void __exit module_cdev_exit(void)
{
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
printk("module exit \n");
}
module_init(module_cdev_init);
module_exit(module_cdev_exit);
MODULE_LICENSE("GPL v2");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
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
三、实验运行结果
