目录
2. struct cdev cdev_test:设备的 “操作手册”
3. struct file_operations cdev_test_ops:操作手册的 “具体内容”
4. struct class *class:设备的 “分类文件夹”
5. struct device *device:设备的 “详细档案”
4. 创建设备类和设备节点:class_create + device_create
引言
在 Linux 内核开发中,字符设备驱动是非常基础且重要的一部分。本文将以一个简单的字符设备驱动基本框架代码为例,利用类比推理及流程图梳理的方式帮助初学者理解基本字符设备驱动其中的结构体和接口调用,并提供相应的流程图,方便大家深入理解Linux 字符设备驱动基本框架。
首先给出代码示例:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;//定义 dev_t 类型(32 位大小) 的变量 dev_num,用来存放设备号
struct cdev cdev_test;//定义 cdev 结构体类型的变量 cdev_test,描述一个字符设备
struct class *class;//定义指向class的指针,这个class是代表设备类,其用途是对系统内的设备进行分类与管理
struct device* device;// struct device 结构体来表示一个设备。这个结构体定义于 <linux/device.h> 头文件,
// 它涵盖了设备的众多属性和操作方法,像设备的名称、父设备、设备类型、设备状态等
static int cdev_test_open (struct inode *inode, struct file *file)
{
printk("This is cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read (struct file *file, char __user *buf, size_t size, loff_t *loff)
{
printk("This is cdev_test_read\n");
return 0;
}
static ssize_t cdev_test_write (struct file *file, const char __user *buf, size_t size, loff_t *loff)
{
printk("This is cdev_test_write\n");
return 0;
}
int cdev_test_release (struct inode *inode, struct file *file)
{
printk("This is dev_test_release\n");
return 0;
}
struct file_operations cdev_test_ops = {//定义 file_operations 结构体类型的变量 cdev_test_ops
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int modulecdev_init(void)
{
int ret; // 定义ret来接受申请的设备号的结果
//
ret = alloc_chrdev_region(&dev_num, 0, 1, "chrdev_num");//动态申请设备号
if (ret < 0)
{
printk("register is error\n");
}
int major = MAJOR(dev_num);
int minor = MINOR(dev_num);
printk("major = %d\n", major);
printk("minor = %d\n", minor);
printk("register is ok\n");
cdev_test.owner = THIS_MODULE;//将 owner 字段指向本模块, 可以避免在模块的操作正在被使用时卸载该模块
cdev_init(&cdev_test, &cdev_test_ops); // //使用 cdev_init()函数初始化 cdev_test 结构体, 并链接到cdev_test_ops 结构体
cdev_add(&cdev_test,dev_num,1);
class = class_create(THIS_MODULE,"test");//创建设备类
device = device_create(class,NULL,dev_num,NULL,"test");//自动创建设备节点
return 0;
}
static int modulecdev_exit(void)
{
unregister_chrdev_region(dev_num, 1); // 释放字符驱动设备号
cdev_del(&cdev_test);
device_destroy(class,dev_num);
class_destroy(class);
printk("dev_t exit\n");
}
module_init(modulecdev_init);
module_exit(modulecdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR(" ");
MODULE_VERSION("V1.0");
结构体和接口调用的类比理解
上述字符设备驱动框架中有很多结构体与接口调用,接下来用类比推理的方式来逐步讲解各项内容
结构体理解
dev_t
与dev_num
:可以把dev_t
类型的dev_num
类比为设备的 “身份证号码”。在现实生活中,每个人都有唯一的身份证号码来标识自己,在 Linux 系统里,每个设备也需要一个唯一的 “身份标识”,dev_num
就是用来存放这个设备号的,系统通过它来准确识别不同的设备。struct cdev
(cdev_test
):struct cdev
就像是设备的 “操作手册”。想象一下,我们购买一台电器,会附带一本操作手册,上面详细说明了如何打开、关闭、使用这台电器。cdev_test
这个结构体就是字符设备的 “操作手册”,它里面记录了字符设备的各种操作方法,方便系统对设备进行操作。struct class
(class
):struct class
可以类比为设备的 “分类文件夹”。在整理文件时,我们会把相似的文件放在同一个文件夹里,方便管理。在 Linux 系统中,class
就是用来对设备进行分类的,把具有相似功能或特性的设备归为一类,例如所有的 USB 设备可以放在一个 “USB 设备类文件夹” 里。struct device
(device
):struct device
好比是设备的 “详细档案”。一份详细的档案会记录一个人的基本信息、经历、状态等。device
这个结构体就记录了设备的众多属性和操作方法,如设备的名称、父设备、设备类型、设备状态等,系统通过这个 “档案” 来管理和操作设备。struct file_operations
(cdev_test_ops
):struct file_operations
类似于设备的 “功能说明书”。它定义了对设备进行打开、读取、写入、关闭等操作的具体方法,就像我们使用电器时,功能说明书会告诉我们如何操作各个按钮来实现不同的功能。
接口调用理解
alloc_chrdev_region
:这就像是去 “设备管理处” 申请一个 “身份证号码”(设备号)。系统会动态地分配一个未被使用的设备号给我们的设备,返回值ret
就像是申请结果的 “回执单”,如果小于 0 说明申请失败。cdev_init
:类似于把 “功能说明书”(struct file_operations
)和 “操作手册”(struct cdev
)关联起来。这样当系统对设备进行操作时,就知道该调用哪些具体的操作方法了。cdev_add
:可以想象成把 “操作手册” 正式提交到 “设备管理系统” 中,让系统知道有这个设备存在,并且可以开始对它进行管理。class_create
:就像是创建一个新的 “分类文件夹”,把具有相似特性的设备放到这个文件夹里,方便统一管理。device_create
:好比是在对应的 “分类文件夹” 里创建一份 “详细档案”,并且自动为这个设备创建一个 “访问入口”(设备节点),这样用户空间的程序就可以通过这个节点来访问设备了。unregister_chrdev_region
:类似于去 “设备管理处” 注销 “身份证号码”(设备号),释放这个设备号,以便其他设备可以使用。cdev_del
:就像是从 “设备管理系统” 中删除 “操作手册”,表示这个设备不再被系统管理。device_destroy
:如同销毁 “详细档案”,同时删除对应的 “访问入口”(设备节点)。class_destroy
:类似于删除 “分类文件夹”。
深入理解 Linux 字符设备驱动基本框架(代码逐行解析)
当然对于上述每个结构体与接口调用讲解相对空洞,先将每个部分与代码相结合讲解
(1)代码中的核心结构体与变量
先看代码中定义的四个核心元素,它们是驱动的 “骨架”:
static dev_t dev_num; // 设备号(类比:设备的“身份证”)
struct cdev cdev_test; // 字符设备描述符(类比:设备的“操作手册”)
struct class *class; // 设备类指针(类比:设备的“分类文件夹”)
struct device *device; // 设备实体指针(类比:设备的“详细档案”)
(2)结构体类比:代码中的 “角色分工”
1. dev_t dev_num
:设备的 “身份证号”
- 代码对应:
-
static dev_t dev_num;
- 类比:每个人有唯一的身份证号,每个设备在内核中也需要唯一的 “身份证号”(设备号)。
- 主设备号(
MAJOR(dev_num)
):相当于 “省份代码”,标识设备类型(如所有串口设备可能共享同一个主设备号)。 - 次设备号(
MINOR(dev_num)
):相当于 “个人编号”,区分同一类型下的不同设备(如第 1 个串口、第 2 个串口)。
- 主设备号(
- 代码作用:通过
alloc_chrdev_region(&dev_num, 0, 1, "chrdev_num");
动态申请设备号,申请成功后,dev_num
存储内核分配的主 / 次设备号。
2. struct cdev cdev_test
:设备的 “操作手册”
- 代码对应:
-
struct cdev cdev_test;
- 类比:买电器时附带的 “操作手册”,记录了 “如何打开 / 关闭设备”“如何读写数据” 等操作方法。
- 核心成员与代码关联:
owner = THIS_MODULE
:代码中cdev_test.owner = THIS_MODULE;
,表示这本 “手册” 属于当前驱动模块,防止模块被提前卸载。ops
:通过cdev_init(&cdev_test, &cdev_test_ops);
关联到cdev_test_ops
结构体,而cdev_test_ops
定义了open/read/write/release
等具体操作函数(即 “手册” 的内容)。
- 代码作用:通过
cdev_add(&cdev_test, dev_num, 1);
将 “操作手册” 注册到内核,告诉内核 “这个设备号对应的操作方法在这里”。
3. struct file_operations cdev_test_ops
:操作手册的 “具体内容”
- 代码对应:
-
struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, // 打开设备时调用 .read = cdev_test_read, // 读取数据时调用 .write = cdev_test_write, // 写入数据时调用 .release = cdev_test_release, // 关闭设备时调用 };
- 类比:“操作手册” 中的具体页码,比如 “第 5 页:如何打开设备”“第 10 页:如何读取数据”。
- 代码作用:用户空间程序通过
open("/dev/test", O_RDWR)
操作设备时,内核会根据设备号找到cdev_test
,再调用cdev_test_ops.open
对应的cdev_test_open
函数(代码中打印日志)。
4. struct class *class
:设备的 “分类文件夹”
- 代码对应:
-
class = class_create(THIS_MODULE, "test");
- 类比:电脑里的文件夹,比如 “USB 设备” 文件夹里放所有 USB 设备的信息。
- 代码作用:
- 在
/sys/class/
目录下创建一个名为 “test” 的文件夹,用于归类管理该驱动的设备。 - 后续
device_create
会基于这个 “文件夹” 创建设备节点(类比:在文件夹里新建一个文件)。
- 在
5. struct device *device
:设备的 “详细档案”
- 代码对应:
-
device = device_create(class, NULL, dev_num, NULL, "test");
- 类比:学生档案包含姓名、班级、联系方式等信息,
struct device
包含设备名称、设备号、所属类等属性。 - 代码作用:
- 在内核设备模型中创建一个设备实体,关联到前面创建的
class
(“分类文件夹”)。 - 触发
udev
机制,自动在/dev/
目录下生成名为 “test” 的设备节点(用户空间通过这个节点访问设备)。
- 在内核设备模型中创建一个设备实体,关联到前面创建的
(3)接口调用类比:代码中的 “操作流程”
1. 申请设备号:alloc_chrdev_region
- 代码对应:
-
ret = alloc_chrdev_region(&dev_num, 0, 1, "chrdev_num"); if (ret < 0) { printk("register is error\n"); }
- 类比:去派出所申请身份证,
&dev_num
是 “空白身份证”,"chrdev_num"
是 “申请人姓名”。 - 参数解析:
0
:次设备号起始值(从 0 开始分配)。1
:需要分配的设备数量(这里只申请 1 个设备)。
2. 初始化设备操作手册:cdev_init
- 代码对应:
-
cdev_init(&cdev_test, &cdev_test_ops);
- 类比:把 “操作手册” 的目录(
cdev_test
)和具体内容(cdev_test_ops
)装订在一起。 - 作用:让
cdev_test
知道 “我的操作方法在cdev_test_ops
里”,后续内核调用设备操作时,能找到对应的函数(如open
对应cdev_test_open
)。
3. 注册设备到内核:cdev_add
- 代码对应:
-
cdev_add(&cdev_test, dev_num, 1);
- 类比:把 “操作手册” 提交到图书馆(内核设备管理系统),让所有人(内核其他模块)都能查到。
- 作用:告诉内核 “设备号
dev_num
对应的操作手册是cdev_test
,可以管理 1 个设备”。
4. 创建设备类和设备节点:class_create
+ device_create
- 代码对应:
-
class = class_create(THIS_MODULE, "test"); // 创建“test”分类文件夹 device = device_create(class, NULL, dev_num, NULL, "test"); // 在文件夹里创建“test”设备档案
- 类比:
class_create
:在/sys/class/
里新建一个文件夹 “test”,用于存放同类设备的信息。device_create
:在 “test” 文件夹里新建一个文件 “test”(即/dev/test
设备节点),用户通过这个文件操作设备。
5. 卸载驱动时的清理:反向操作
- 代码对应:
unregister_chrdev_region(dev_num, 1); // 注销设备号(回收身份证)
cdev_del(&cdev_test); // 从图书馆删除操作手册
device_destroy(class, dev_num); // 删除设备档案(删除/dev/test节点)
class_destroy(class); // 删除分类文件夹(删除/sys/class/test)
- 类比:就像退房时要归还房卡、清理行李,卸载驱动时也要按顺序释放资源,避免内存泄漏。
流程图
最后用流程图来梳理整体框架
总结
通过以上的类比推理和流程图,相信大家对这个简单的字符设备驱动基本框架有了更深入的理解。在学习 Linux 内核开发的过程中,理解这些基础的结构体和接口调用是非常重要的。希望本文能帮助大家更好地掌握字符设备驱动开发,也欢迎大家在评论区分享自己的学习心得和疑问。