1、前言
众所周知,Linux内核主要包括三种驱动模型,字符设备驱动,块设备驱动以及网络设备驱动。
其中,Linux字符设备驱动,可以说是Linux驱动开发中最常见的一种驱动模型。
我们该系列文章,主要为了帮助大家快速入门Linux驱动开发,该篇主要来了解一些字符设备驱动的框架和机制。
系列文章基于Kernel 4.19
2、关键数据结构2.1 cdev struct cdev {
结构体名称:cdev
文件位置:include/linux/cdev.h
主要作用:cdev可以理解为char device,用来抽象一个字符设备。
核心成员及含义:
kobj:表示一个内核对象。
owner:指向该模块的指针
ops:指向文件操作的指针,包括open、read、write等操作接口
list:用于将该设备加入到内核模块链表中
dev:设备号,由主设备号和次设备号构成
count:表示有多少个同类型设备,也间接表示设备号的范围
__randomize_layout:一个编译器指令,用于随机化结构体的布局,以增加安全性。
2.2 file_operations struct file_operations {
结构体名称:file_operations
文件位置:include/linux/fs.h
主要作用:正如其名,主要用来描述文件操作的各种接口,Linux一切接文件的思想,内核想要操作哪个文件,都需要通过这些接口来实现。
核心成员及含义:
open:打开文件的函数
read:读取文件的函数。
write:写入文件的函数。
release:关闭文件的函数。
flush:刷新文件的函数,通常在关闭文件时调用。
llseek:改变文件读写指针位置的函数。
fsync:将文件数据同步写入磁盘的函数。
poll:询问文件是否可被非阻塞读写
2.3 dev_t typedef u32 __kernel_dev_t;
类型名称:dev_t
文件位置:include/linux/types.h
主要作用:表示字符设备对应的设备号,其中包括主设备号和次设备号。
3、数据结构之间关系
上图绘制是对字符设备驱动程序的数据结构以及API的关系图,
有需要原始文件,可在公~号【嵌入式艺术】获取。
4、字符设备驱动整体架构4.1 加载与卸载函数
驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。
我们一般这么定义驱动的加载卸载函数:
static int __init xxx_init(void)这段代码就是实现一个通用驱动的加载与卸载,关于module_init和module_exit的实现机制,可以查看之前总结文章。
4.2 设备号管理4.2.1 设备号的概念
每一类字符设备都有一个唯一的设备号,其中设备号又分为主设备号和次设备号,那么这两个分别作用是什么呢?
主设备号:用于标识设备的类型,
次设备号:用于区分同类型的不同设备
简单来说,主设备号用于区分是IIC设备还是SPI设备,而次设备号用于区分IIC设备下,具体哪一个设备,是MPU6050还是EEPROM。
4.2.2 设备号的分配
了解了设备号的概念,Linux中设备号有那么多,那么我们该如何去使用正确的设备号呢?
设备号的分配方式有两种,一种是动态分配,另一种是静态分配,也可以理解为一种是内核自动分配,一种是手动分配。
静态分配函数:
int register_chrdev_region(dev_t from, unsigned count, const char *name);from:表示已知的一个设备号
count:表示连续设备编号的个数,(同类型的设备有多少个)
name:表示设备或者驱动的名称
函数作用:以from设备号开始,连续分配count个同类型的设备号
动态分配函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);dev:设备号的指针,用于存放分配的设备号的值
baseminor:次设备号开始分配的起始值
count:表示连续设备编号的个数,(同类型的设备有多少个)
name:表示设备或者驱动的名称
函数作用:从baseminor次设备号开始,连续分配count个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给*dev
这两个函数最大的区别在于:
register_chrdev_region:调用前,已预先定义好了主设备号和次设备号,调用该接口后,会将自定义的设备号登记加入子系统中,方便系统追踪系统设备号的使用情况。
alloc_chrdev_region:调用前,未定义主设备号和次设备号;调用后,主设备号以0来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况。
这两个函数的共同点在于:
系统维护了一个数组列表,用来登记所有的已使用的设备号信息,这两个接口归根到底也是将其设备号信息,登记到系统维护的设备号列表中,以免后续冲突使用。
在Linux中,我们可以通过cat /proc/devices命令,查看所有i登记的设备号列表。
后面有时间,我们可以详细聊设备号的自动分配机制,管理机制。
4.2.3 设备号的注销
设备号作为一种系统资源,当所对应的设备卸载时,当然也要将其所占用的设备号归还给系统,无论时静态分配,还是动态分配,最终都是调用下面函数来注销的。
void unregister_chrdev_region(dev_t from, unsigned count);from:表示已知的一个设备号
count:表示连续设备编号的个数,(同类型的设备有多少个)
函数作用:要注销from主设备号下的连续count个设备
4.2.4 设备号的获取
设备号的管理很简单,在关键数据结构中,我们看到设备号的类型是dev_t,也就是u32类型表示的一个数值。
其中主设备号和次设备号的分界线,由MINORBITS宏定义指定:
也就是主设备号占用高12bit,次设备号占用低20bit
并且,内核还提供了相关API接口,来获取主设备号和次设备号,以及生成设备号的接口,如下:
以上,通过移位操作,来实现主次设备号的获取。
4.2.4 通用代码实现
该函数实现了设备号的分配,如果主设备号为0,则采用动态配分的方式,否则采用静态分配的方式。
更多干货可见:,助力大家更上一层楼!
4.3 字符设备的管理
了解完设备号的管理之后,我们来看下字符设备是如何管理的。
4.3.1、字符设备初始化 void cdev_init(struct cdev *cdev, const struct file_operations *fops);cdev:一个字符设备对象,也就是我们创建好的字符设备
fops:该字符设备的文件处理接口
函数作用:初始化一个字符设备,并且将所对应的文件处理指针与字符设备绑定起来。
4.3.2、字符设备注册 int cdev_add(struct cdev *p, dev_t dev, unsigned count);
p:一个字符设备指针,只想待添加的字符设备对象
dev:该字符设备所负责的第一个设备编号
count:该类型设备的个数
函数作用:添加一个字符设备驱动到Linux系统中。
4.3.3、字符设备注销 void cdev_del(struct cdev *p);
p:指向字符设备对象的指针
函数作用:从系统中移除该字符设备驱动
4.4 文件操作接口的实现
因为在Linux中,一切皆文件的思想,所以每一个字符设备,也都有一个文件节点来对应。
我们在初始化字符设备的时候,会将struct file_operations的对象与字符设备进行绑定,其作用是来处理该字符设备的open、read、write等操作。
我们要做的就是去实现我们需要的函数接口,如:
static const struct file_operations global_mem_fops = {至此,我们一个基本的字符设备驱动程序的框架就基本了然于胸了
5、总结
本篇文章,旨在通俗易懂的讲解:
字符设备驱动相关数据结构
数据结构关系图
核心API接口
字符设备驱动整体框架
希望对大家有所帮助。
作者: _嵌入式艺术_, 来源:面包板社区
链接: https://mbb.eet-china.com/blog/uid-me-4040659.html
版权声明:本文为博主原创,未经本人允许,禁止转载!
文章评论(0条评论)
登录后参与讨论