04、注册字符设备
注册字符设备可以分为两个步骤:
- 字符设备初始化
- 字符设备的添加
一、字符设备初始化
字符设备初始化所用到的函数为 cdev_init(…),在对该函数讲解之前, 首先对 cdev 结构体进行介绍。
1.1、 cdev
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象, cdev 记录了字符设备号、 内核对象、 文件操作 file_operations 结构体(设备的打开、读写、 关闭等操作接口) 等信息, 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
**struct kobject kobj**
作用:
- 内嵌的内核对象,用于将
cdev
集成到 Linux 的设备模型中。 kobject
是 Linux 内核中的基础对象,提供了对象生命周期管理、设备树关系管理等功能。
功能:
- 与 sysfs 文件系统集成,使字符设备能够在
/sys/class/
或/sys/devices/
下暴露设备信息。 - 提供引用计数功能,防止对象在使用过程中被释放。
**struct module *owner**
作用:
- 指向字符设备所在模块的
struct module
结构体。 - 确保在操作设备时,所属模块不会被卸载。
- 指向字符设备所在模块的
功能:
- 当用户对字符设备进行访问时,内核会递增模块的引用计数,防止模块被卸载。
- 当文件关闭时,引用计数会减少。
**const struct file_operations *ops**
作用:
- 指向与该字符设备关联的操作函数集。
- 定义了设备文件的行为,例如
open
、read
、write
等。
功能:
- 每个字符设备都需要通过
file_operations
指定具体实现的设备操作。 - 用户通过系统调用(如
open
、read
)访问字符设备时,内核会调用file_operations
中对应的回调函数。
- 每个字符设备都需要通过
**struct list_head list**
作用:
- 用来将所有已注册的
cdev
形成链表。
- 用来将所有已注册的
功能:
- 内核中所有注册的字符设备会被维护在一个链表中,方便内核统一管理。
- 链表头通常由内核子系统或驱动代码初始化。
**dev_t dev**
作用:
- 字符设备的设备号,由主设备号和次设备号组成。
功能:
- 每个字符设备都有唯一的设备号,用于标识设备。
- 主设备号(
major
):表示设备类型,内核根据主设备号找到对应的驱动。 - 次设备号(
minor
):表示具体的设备实例。
1.2、cdev_init 初始化
设备初始化所用到的函数为 cdev_init(),该函数同样在“内核源码/include/linux/cdev.h” 文件中所引用如下:
void cdev_init(struct cdev *, const struct file_operations *);
方法的填充:
- file_operations :行为
- cdev:人身份证
该函数的详细内容在“内核源码/include/fs/char_dev.c” 文件中定义, 如下:
头文件:
include/linux/cdev.h
实现:
fs/char_dev.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;//初始化 ops 成员, 建立 cdev 和 file_operations 之间的连接
}
2
3
4
5
6
7
1.3、字符设备的注册和注销
通过调用 cdev_add
,驱动程序可以将字符设备与一个指定的设备号关联,使其可以被用户空间访问。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数说明:
struct cdev *p
:指向字符设备的结构体指针,表示需要添加到内核的字符设备。dev_t dev
:设备号,由主设备号和次设备号组成。unsigned count
:需要注册的次设备号数量,通常为 1。如果需要管理多个次设备号,可以设置大于 1。
返回值:
- 成功时返回
0
。 - 失败时返回负数错误码。
代码实现目录:fs/char_dev.c
相当 管理作用
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
//调用 kobj_map 将字符设备添加到内核的字符设备映射表中
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
//对 cdev 的父对象(kobj.parent)进行引用计数的增加
kobject_get(p->kobj.parent);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
调用 kobj_map
函数
error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
作用:
- 调用
kobj_map
将字符设备添加到内核的字符设备映射表中。 kobj_map
是内核管理设备号的核心机制,通过它可以将设备号和struct cdev
绑定。
参数说明:
**cdev_map**
:- 全局变量,字符设备映射表(
struct kobj_map
类型)。 - 用于存储设备号和
struct cdev
的映射关系。
- 全局变量,字符设备映射表(
**dev**
:- 要注册的起始设备号。
**count**
:- 需要注册的次设备号数量。
**NULL**
:- 回调数据(通常为 NULL)。
**exact_match**
:- 匹配函数,用于查找设备号是否与已存在的字符设备冲突。
**exact_lock**
:- 锁定函数,用于确保线程安全。
**p**
:- 当前的
struct cdev
指针,表示要注册的字符设备。
- 当前的
返回值:
- 如果设备号成功注册,则返回 0。
- 如果设备号冲突或其他错误,则返回负数错误码。
1.4、cdev_map
其中比较关键的是cdev_map:
目录:drivers/base/map.c
struct kobj_map {
struct probe {
struct probe *next; // 链表指针,用于将多个 probe 节点串联起来
dev_t dev; // 起始设备号
unsigned long range; // 设备号范围,表示从 dev 开始的范围长度
struct module *owner; // 拥有该设备的内核模块
kobj_probe_t *get; // 回调函数,用于获取设备对象
int (*lock)(dev_t, void *); // 用于设备操作的锁定回调函数
void *data; // 用户自定义数据,用于设备管理的附加信息
} *probes[255]; // 指向 `probe` 的数组,管理 255 个主设备号范围
struct mutex *lock; // 互斥锁,用于保护 kobj_map 的并发访问
};
2
3
4
5
6
7
8
9
10
11
12
初始化:
drivers/base/map.c
kobj_map
是 Linux 内核中用于注册设备号到设备对象的映射关系的函数。它通过将设备号范围与设备回调函数绑定,允许后续使用设备号查找到设备对象(例如 struct cdev
或其他设备数据)。
代码中的关键点
主设备号和次设备号的管理:
probes[255]
数组按照主设备号进行管理。- 每个主设备号范围用链表存储多个
probe
节点。
链表的有序插入:
- 按照
probe->range
的大小升序排列,方便后续查找设备号范围。
- 按照
并发保护:
- 使用互斥锁
domain->lock
,保证kobj_map
的线程安全。
- 使用互斥锁
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;
//最多255次设备
if (n > 255)
n = 255;
//分配 probe 结构数组
p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
//初始化 probe 结构体
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
}
//按主设备号插入链表
//插入步骤:
//根据主设备号 index 计算 probes 的数组索引:index % 255。
//找到链表头 domain->probes[index % 255]。
//遍历链表,根据设备号范围 range 的大小找到合适的插入点。
//将当前 probe 插入链表。
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
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
字符设备删除所用到的函数为 cdev_del(), 该函数同样在“内核源码/include/linux/cdev.h”文件中所引用, 如下:
void cdev_del(struct cdev *);
三、实验代码
驱动:
#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;//定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
static struct cdev cdev_test;//定义cdev结构体类型的变量cdev_test
static struct file_operations cdev_test_ops = {
.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_ops
static int __init module_cdev_init(void)//驱动入口函数
{
int ret;//定义int类型变量ret,进行函数返回值判断
int major,minor;//定义int类型的主设备号major和次设备号minor
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名为chrdev_name
if (ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_register_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);
cdev_init(&cdev_test,&cdev_test_ops);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&cdev_test,dev_num,1);//使用cdev_add()函数进行字符设备的添加
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);//使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev_num,1);//释放字符驱动设备号
printk("module exit \n");
}
module_init(module_cdev_init);//注册入口函数
module_exit(module_cdev_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
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