本文目录
一、杂项设备框架和字符设备框架的区别
我们在编写字符设备时,使用杂项设备框架会比使用字符设备框架编写更为简便。但是由于使用杂项设备框架会使得注册的设备变为杂项设备,虽然功能都一样,但是分类会不同。如果分类严格,则我们需要使用字符设备驱动框架来编写,这样注册的设备就被分到了字符设备类中。
如果正在开发的设备需要明确地归类到字符设备中(例如在 /sys/class/char 中),你需要对设备号进行特殊的管理,那么使用字符设备框架是更合适的选择。如果对设备分类要求不高,杂项设备框架则可以帮助简化开发流程。
二、字符设备驱动框架相关知识
1. 设备号
Linux2.6 版本以后,使用 dev_t
来表示一个设备号,dev_t 实际上是一个 u32 类型。其中高 12 位是主设备号,低 20 位是次设备号。
设备号使用前需要先申请: register_chrdev_region
(静态分配)或 alloc_chrdev_region
(动态分配)设备号函数申请。这里我们常使用动态申请的方式。
●内核提供了设备号合成与分解的宏方便开发者使用。
(1)合成设备号:
dev_t dev_num;
unsigned int ma = 240; // 示例主设备号
unsigned int mi = 0; // 示例次设备号
dev_num=MKDEV(ma,mi); //ma:主设备号;mi:次设备号
(2)分解设备号:
dev_t dev_num;
MAJOR(dev_num); //从设备号 dev 中分解出主设备号
MINOR(dev_num); //从设备号 dev 中分解出次设备号
2. 特性
(1)设备驱动模块安装后,不会自动创建/dev/
目录下设备文件节点,需要开发者在/dev/
目录下手动使用 mknod
命令创建或者在驱动程序中调用自动创建设备文件的函数进行创建。mknod
命令如下:
mknod /dev/file-name c 247 1
/*
c: 这个参数指定了设备文件的类型为“字符设备”。在 Linux 中,设备文件可以是字符设备(c)或块设备(b)。
247:主设备号
1:次设备号
*/
(2)调用 cdev_add 注册后,只占用指定数量的次设备号。
3. 常用API
(1)动态申请设备号
头文件:#include <linux/fs.h>
返回值:成功:0;失败:返回负数。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/*
dev:存放分配到的第一个设备(包含主次设备号)。
baseminor:要分配起始次设备号。
count:连续的次设备号数量。
name:设备名,不需要和/dev/的设备文件名相同。
*/
(2)注销设备号
void unregister_chrdev_region( dev_t from, unsigned count);
//from:起始设备号(主、次) (包含主次设备号) 。
//count:连续的次设备号数量。
(3)字符设备结构体初始化
头文件:#include <linux/ cdev.h>
功能:初始化核心结构,具体做的是清零核心结构,初始化核心结构的 list,kobj,ops 成员。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
/*
cdev:需要初始化的核心结构指针。
fops:文件操作方法结构指针。
*/
(4)注册字符设备
头文件:#include <linux/ cdev.h>
返回值:成功:返回 0; 失败:返回负数
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
/*
cdev:需要注册的核心结构指针(已完成初始化)。
dev_t dev:设备号。
unsigned count :连续次设备号数量。
*/
(5)注销字符设备
void cdev_del(struct cdev *cdev);
(6)创建设备类
用于创建一个设备类。设备类在 /sys/class
中表示设备的类型,使得设备管理更加方便。
struct class *class_create(struct module *owner, const char *name);
//struct module *owner :通常用于指定模块的所有权。如果该类是由内核模块创建的,通常会传递 THIS_MODULE 宏。
//const char *name :这是类的名称,通常是一个字符串。这个名称将出现在 /sys/class/ 目录中,表示该类下的所有设备。
(7)注销设备类
void class_destroy(struct class *cls);
(8)创建设备节点
创建并注册一个设备。该设备通常与设备类(struct class)相关联,并且会在 /dev
目录下生成相应的设备节点。
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);
/*
struct class *class :指向 struct class 的指针,表示设备所属的类。这是在调用 class_create 时创建的设备类。
parent:指向父设备的指针,如果设备没有父设备,可以传递 NULL。
dev_t devt :设备号(dev_t),这是一个组合了主设备号和次设备号的值。
void *drvdata:驱动程序数据指针。这通常是驱动程序为该设备维护的私有数据。
const char *fmt:设备的名称格式字符串。
... (可变参数):用于 fmt 的可变参数,可以指定设备名称中的可变部分。
*/
(9)注销设备节点
void device_destroy(struct class *class, dev_t devt);
/*
struct class *class :指向 struct class 的指针,表示设备所属的类。
dev_t devt :设备号。
*/
三、字符设备驱动框架编写流程
1. 操作总流程
- 为指针/结构体分配内存空间。(在内核中使用
kzalloc
函数申请) - 动态申请设备号。
- 初始化字符设备结构体。
- 注册已经初始化好的字符设备。
- 创建设备类。
- 创建设备节点。
- 在驱动出口函数中完成内存释放、注销等功能。
2. 具体代码(使用命令创建设备节点文件)
优化封装管理:因为我们用到了设备号、字符设备结构体等变量,这些变量都属于字符设备。我们可以将这些变量进行封装,方便操作管理。具体代码如下所示。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#define DEVICE_NAME "chrdev-linux-26"
struct cdev_obj {
dev_t dev_num; // 设备号
struct cdev cdev; // 字符设备结构体
};
struct cdev_obj *cdev_one; // 初始化封装的结构体
const struct file_operations my_fops = {
.read = NULL,
.write = NULL,
.open = NULL,
};
static int __init linux26_device_init(void)
{
int ret;
// 1. 使用kzalloc分配 cdev_obj 结构空间
cdev_one = kzalloc(sizeof(struct cdev_obj), GFP_KERNEL);
if (cdev_one == NULL) {
printk("kzalloc error\n");
ret = -ENOMEM;
goto error0;
}
// 2. 申请设备号: 动态 。次设备号的范围为0~2
ret = alloc_chrdev_region(&cdev_one->dev_num, 0, 2, DEVICE_NAME);
if (ret < 0) {
printk("alloc_chrdev_region error\n");
goto error1;
}
// 3. 初始化 cdev 结构
cdev_init(&cdev_one->cdev, &my_fops);
// 4. 注册已经初始化好的 cdev 结构,2个次设备号
ret = cdev_add(&cdev_one->cdev, cdev_one->dev_num, 2);
if (ret < 0) {
printk("cdev_add error\n");
goto error2;
}
return 0;
error2: //要释放掉前面成功的资源
unregister_chrdev_region(cdev_one->dev_num, 2);
error1:
kfree(cdev_one);
error0:
return ret;
}
static void __exit linux26_device_exit(void)
{
// 1. 注销 cdev 结构
cdev_del(&cdev_one->cdev);
// 2. 释放设备号
unregister_chrdev_region(cdev_one->dev_num, 2);
// 3. 释放 cdev 结构空间
kfree(cdev_one);
}
module_init(linux26_device_init);
module_exit(linux26_device_exit);
MODULE_LICENSE("GPL");
代码中申请了 2 个次设备号,且次设备号的开始编号从0开始(代码中设置)。这里我们通过使用命令来创建设备节点文件。由于需要知道主设备号,因此在代码中使用分解设备号的宏来获取并打印出主设备号。使用dmesg
命令查看。
由于我们将模块注册到内核以后,并不会在/dev/
目录下生成设备节点,所以需要我们手动创建设备节点文件。这里由于我们申请了两个次设备号范围,则我们可以选择次设备号0或1。具体使用命令:sudo mknod /dev/QJLdev_test c 246 0
,或使用sudo mknod /dev/QJLdev_test c 246 1
。这里的次设备号不能使用超过我们申请的设备号范围!否则在操作设备节点时会报错!
3. 具体代码(自动创建设备节点文件)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#define DEVICE_NAME "QJLdev_test"
struct cdev_obj {
dev_t dev_num; // 设备号
struct cdev cdev; // 字符设备结构体
struct class *myclass; // 设备类
struct device *devfile; // 设备文件
};
struct cdev_obj *cdev_one; // 初始化封装的结构体
const struct file_operations my_fops = {
.read = NULL,
.write = NULL,
.open = NULL,
};
static int __init linux26_device_init(void)
{
int ret;
// 1. 使用kzalloc分配 cdev_obj 结构空间
cdev_one = kzalloc(sizeof(struct cdev_obj), GFP_KERNEL);
if (cdev_one == NULL) {
printk(KERN_ERR "kzalloc error\n");
ret = -ENOMEM;
goto error0;
}
// 2. 申请设备号: 动态 。次设备号的范围为0~2
ret = alloc_chrdev_region(&cdev_one->dev_num, 0, 2, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region error\n");
goto error1;
}
// 3. 初始化 cdev 结构,2个次设备号
cdev_init(&cdev_one->cdev, &my_fops);
// 4. 注册已经初始化好的 cdev 结构
ret = cdev_add(&cdev_one->cdev, cdev_one->dev_num, 2);
if (ret < 0) {
printk(KERN_ERR "cdev_add error\n");
goto error2;
}
printk(KERN_INFO "主设备号为:%d\n", MAJOR(cdev_one->dev_num));
// 5. 创建设备类
cdev_one->myclass = class_create(THIS_MODULE, "class-qjl");
if (IS_ERR(cdev_one->myclass)) {
printk(KERN_ERR "class_create error\n");
ret = PTR_ERR(cdev_one->myclass);
goto error3;
}
// 6. 创建设备节点。
cdev_one->devfile = device_create(cdev_one->myclass, NULL, cdev_one->dev_num, NULL, DEVICE_NAME);
if (IS_ERR(cdev_one->devfile)) {
printk(KERN_ERR "device_create error\n");
ret = PTR_ERR(cdev_one->devfile);
goto error4;
}
return 0;
error4:
class_destroy(cdev_one->myclass);
error3:
cdev_del(&cdev_one->cdev);
error2:
unregister_chrdev_region(cdev_one->dev_num, 2);
error1:
kfree(cdev_one);
error0:
return ret;
}
static void __exit linux26_device_exit(void)
{
// 1. 注销设备节点
device_destroy(cdev_one->myclass, cdev_one->dev_num);
// 2. 注销设备类
class_destroy(cdev_one->myclass);
// 3. 注销 cdev 结构
cdev_del(&cdev_one->cdev);
// 4. 释放设备号
unregister_chrdev_region(cdev_one->dev_num, 2);
// 5. 释放 cdev 结构空间
kfree(cdev_one);
}
module_init(linux26_device_init);
module_exit(linux26_device_exit);
MODULE_LICENSE("GPL");
在/dev/
目录下生成的设备节点。
在/sys/class/
目录下生成的设备类。
四、涉及c语言知识点–使用指针变量前必须为其分配内存空间
使用结构体来举例:如果你在代码中定义了一个结构体变量(普通变量),例如 struct MyStruct myStruct;
,那么内存会在定义时自动为这个结构体分配。你不需要手动为它分配内存。如果你定义了一个结构体指针(指针变量),例如 struct MyStruct *pMyStruct;
,那么指针本身只占用指针的内存空间(通常是4或8字节,取决于系统)。但是,结构体本身的内存并没有自动分配。你需要通过 malloc 等方式为这个指针分配内存,例如 pMyStruct = (struct MyStruct*)malloc(sizeof(struct MyStruct));
。
知识点:当我们在结构体内部定义指针变量时,如果我们要使用该指针,也必须为其申请内存空间!因为在为结构体分配内存时,只会为结构体本身的成员分配内存,但不会为结构体中的指针指向的内存分配空间。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 需要包含这个头文件以使用 strcpy
struct data {
int a;
char *b;
};
struct data *my_data;
int main()
{
my_data = (struct data *)malloc(sizeof(struct data)); // 为结构体分配内存
my_data->a = 13; //直接操作普通变量
my_data->b = (char *)malloc(sizeof(char) * 4); //操作指针,则需要分配内存,
strcpy(my_data->b, "hhh"); // 使用 strcpy 将字符串复制到分配的内存中
printf("%d, %s\n", my_data->a, my_data->b);
// 释放分配的内存
free(my_data->b); // 先释放内部指针
free(my_data); // 再释放结构体本身
return 0;
}
如果函数返回值为指针类型,通常意味着函数内部已经为该指针分配了内存。这种情况下,无需手动分配内存。在某些框架或库中,可能会提供专门的API函数用于释放内存。这种情况下,需要根据API的文档来确定是否需要手动调用释放函数。