8. 字符设备驱动¶
上几章节我们了解到什么是内核模块,模块的加载/卸载详细过程以及内核模块的使用等内容。
本章,我们将学习字符设备使用、字符设备驱动相关的概念,理解字符设备驱动程序的基本框架,并从源码上分析字符设备驱动实现和管理等。
8.1. 字符设备抽象¶
Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象。
cdev记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations)。
在我们想要添加一个字符设备时,就是 将这个对象注册到内核 中,通过 创建一个文件(设备节点)绑定对象的cdev ,当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
注意
C语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性,为上层提供了统一接口,使得管理和操作设备变得很容易。
在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发,将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现file_operations结构体。
在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号; 在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器。
实际上,在Linux上写驱动程序,都是做一些“填空题”。因为Linux给我们提供了一个基本的框架,我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。有句成语工欲善其事,必先利其器,在理解这个框架之前我们得花点时间来学习字符设备驱动相关概念及数据结构。
8.2. 字符设备相关概念¶
在linux中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。
注意
cdev结构体被内核用来记录设备号,而在使用设备时,我们通常会打开设备节点,通过设备节点的inode结构体、file结构体最终找到file_operations结构体,并从file_operations结构体中得到操作设备的具体方法。
8.2.1. 设备号¶
对于字符的访问是通过文件系统的名称进行的,这些名称被称为 特殊文件 、 设备文件 ,或者简单称为 文件系统树的节点 。
Linux根目录下有 /dev
这个文件夹,专门用来存放设备中的驱动程序,我们可以使用 ls -l
以列表的形式列出系统中的所有设备。其中,每一行表示一个设备,每一行的第一个字符表示设备的类型。
一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。
注解
‘c’用来标识字符设备,’b’用来标识块设备。
如 autofs 是一个字符设备c, 它的主设备号是10,次设备号是235;
loop0 是一个块设备,它的主设备号是7,次设备号为0,同时可以看到loop0-loop7共用一个主设备号,次设备号由0开始递增。
I2C-2、I2C-4、I2C-2属于不同设备但是共用一套驱动程序
8.2.1.1. 内核中设备编号的含义¶
在内核中, dev_t用来表示设备编号,dev_t是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号 。也就是理论上主设备号取值范围:0-2^12,次设备号0-2^20。
实际上在内核源码中__register_chrdev_region(…)函数中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一个宏,值是512。
在kdev_t中,设备编号通过移位操作最终得到主/次设备号码,同样主/次设备号也可以通过位运算变成dev_t类型的设备编号,具体实现参看下面代码MAJOR(dev)、MINOR(dev)和MKDEV(ma,mi)。
1 2 3 | typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
|
1 2 3 4 5 6 | #define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
|
第4行:宏定义“MAJOR”,可以根据设备的设备号来获取设备的主设备号。
第5行:宏定义“MINOR”,可以根据设备的设备号来获取设备的次设备号。
第6行:宏定义“MKDEV”,用于将主设备号和次设备号合成一个设备号。
注意
可以查阅内核源码的Documentation/devices.txt文件,从而了解主设备号和次设备号是怎么定义的。
下面截取了Documentation/devices.txt文件的部分内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ……
4 char TTY devices
0 = /dev/tty0 Current virtual console
1 = /dev/tty1 First virtual console
...
63 = /dev/tty63 63rd virtual console
64 = /dev/ttyS0 First UART serial port
...
255 = /dev/ttyS191 192nd UART serial port
UART serial ports refer to 8250/16450/16550 series devices.
Older versions of the Linux kernel used this major
number for BSD PTY devices. As of Linux 2.1.115, this
is no longer supported. Use major numbers 2 and 3.
……
89 char I2C bus interface
0 = /dev/i2c-0 First I2C adapter
1 = /dev/i2c-1 Second I2C adapter
...
……
|
8.2.1.2. cdev结构体¶
内核通过一个散列表(哈希表)来记录设备编号。
哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。
以主设备号为cdev_map编号,使用哈希函数f(major)=major%255来计算组数下标(使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率);主设备号冲突,则以次设备号为比较值来排序链表节点。
如下图所示,内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备。
1 2 3 4 5 6 7 8 | struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
|
struct kobject kobj: 内嵌的内核对象,通过它将设备统一加入到“Linux设备驱动模型”中管理(如对象的引用计数、电源管理、热插拔、生命周期、与用户通信等)。
struct module *owner: 字符设备驱动程序所在的内核模块对象的指针。
const struct file_operations *ops: 文件操作,是字符设备驱动中非常重要的数据结构,在应用程序通过文件系统(VFS)呼叫到设备设备驱动程序中实现的文件操作类函数过程中,ops起着桥梁纽带作用,VFS与文件系统及设备文件之间的接口是file_operations结构体成员函数,这个结构体包含了对文件进行打开、关闭、读写、控制等一系列成员函数。
struct list_head list: 用于将系统中的字符设备形成链表(这是个内核链表的一个链接因子,可以再内核很多结构体中看到这种结构的身影)。
dev_t dev: 字符设备的设备号,由主设备和次设备号构成。
unsigned int count: 属于同一主设备好的次设备号的个数,用于表示设备驱动程序控制的实际同类设备的数量。
8.2.2. 设备节点¶
设备节点(设备文件):Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux中称为设备文件。
注意
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。因此,在Linux中,设备节点也可称为设备文件。
设备节点被创建在/dev下,是连接内核与用户层的枢纽。 相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息在Linux中。
所有设备都以文件的形式存放在/dev目录下,都是通过文件的方式进行访问,设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。
应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。
8.3. 字符设备数据结构¶
在驱动开发过程中,不可避免要涉及到三个重要的内核数据结构,分别是文件操作结构体(file_operations)、文件描述结构体(file)以及inode结构体。
注意
Linux设备文件三大结构:inode、file、file_operations。
8.3.1. file_operations结构体¶
file_operation就是把系统调用和驱动程序关联起来的关键数据结构。
这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数指针指向的函数,从而完成了Linux设备驱动程序的工作。
以下代码中只列出本章使用到的部分函数。
1 2 3 4 5 6 7 8 9 | struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
} __randomize_layout;
|
8.3.1.1. llseek¶
用于修改文件的当前读写位置,并返回偏移后的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /******************************************************************************
* @brief llseek
* @note 用于修改文件的当前读写位置,并返回偏移后的位置。
* @param filp 传入的文件指针
* @param offset 指定偏移量的大小
* @param whence 指定新位置(SEEK_SET、SEEK_CUR、SEEK_END)
* @return 非负数,当前文件的指针位置;负数,函数调用失败。
* @Sample usage: 参数file传入了对应的文件指针,参数loff_t指定偏移量的大小;
* 参数int是用于指定新位置指定成从文件的某个位置进行偏移,SEEK_SET表示从文件起始处
* 开始偏移,SEEK_CUR表示从当前位置开始偏移,SEEK_END表示从文件结尾开始偏移。
*****************************************************************************/
loff_t (*llseek) (struct file *, loff_t, int);
// 下面是自行加了参数信息的,方便进行参数分析。
loff_t (*llseek) (struct file *filp, loff_t offset, int whence);
|
8.3.1.2. read¶
用于读取设备中的数据,并返回成功读取的字节数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /********************************************************************************
* @brief read
* @note 用于读取设备中的数据,并返回成功读取的字节数。
* @param filp 传入的文件指针
* @param buffer 数据缓冲区指针
* @param size 指定读取的数据大小
* @param loff 读的位置相对于文件开头的偏移(该参数一般不设置)
* @return 非负数,成功读取到的字节数;负数,函数调用失败。
* @Sample usage: 该函数指针被设置为NULL时,会导致系统调用read函数报错,提示“非法
* 参数”。设备驱动的read 函数,file是文件结构体指针,buffer是用户空间的内存地址,size 是
* 要读的长度,loff是读的位置相对于文件开头的偏移。
*******************************************************************************/
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
// 下面是自行加了参数信息的,方便进行参数分析。
ssize_t (*read) (struct file *filp, char __user *buffer, size_t size, loff_t *loff);
// 应用层
ssize_t read(int fd, void *buf, size_t count);
|
8.3.1.3. write¶
用于向设备写入数据,并返回成功写入的字节数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /********************************************************************************
* @brief write
* @note 用于向设备写入数据,并返回成功写入的字节数。
* @param filp 传入的文件指针
* @param buffer 数据缓冲区指针
* @param size 指定写入的数据大小
* @param loff 写的位置相对于文件开头的偏移(该参数一般不设置)
* @return 非负数,成功写入的字节数;负数,函数调用失败。
* @Sample usage: 设备驱动的write函数,filp是文件结构体指针,buffer是用户空间的内存地址,size 是要写的长度,loff表示写的位置相对于开头的偏移。
*******************************************************************************/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
// 下面是自行加了参数信息的,方便进行参数分析。
ssize_t (*write) (struct file *filp, const char __user *buffer, size_t size, loff_t *loff);
// 应用层
ssize_t write(int fd, const void *buf, size_t count);
|
8.3.1.4. unlocked_ioctl¶
提供设备执行相关控制命令的实现方法,它对应于应用程序的fcntl函数以及ioctl函数。在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针。
1 2 3 4 5 6 7 8 9 10 11 12 | /********************************************************************************
* @brief unlocked_ioctl
* @note 通过指定的命令来实现对应的操作,在无大内核锁(BKL)的情况下调用
* @param filp 传入的文件指针
* @param cmd 指令
* @param add 应用层传递给驱动层的数据或者是接收数据用到的缓存地址
*******************************************************************************/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
// 下面是自行加了参数信息的,方便进行参数分析。
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long add);
// 应用层
int ioctl(int fd, int cmd, ...) ;
|
8.3.1.5. open¶
设备驱动第一个被执行的函数,一般用于硬件的初始化。如果该成员被设置为NULL,则表示这个设备的打开操作永远成功。
注意
open主要的工作:
检查设备特定的错误(如设备未就绪或硬件问题)
如果设备是首次打开,则进行初始化(如果维护打开计数)
如有必要,更新f_op指针
分配并填写置于filp->private_data里的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 | /********************************************************************************
* @brief open
* @param inode 传入的inode参数指针
* @param filp 传入的文件指针
* @Sample usage: 两个参数inode和filp由内核根据我们打开的设备节点信息
* (如主次设备号,字符设备等)来填充,这样我们可以通过这两个参数来找到我们要操作
* 的目标设备.这对于一个驱动对应多个设备的情况非常实用
*******************************************************************************/
int (*open) (struct inode *, struct file *);
// 下面是自行加了参数信息的,方便进行参数分析。
int (*open)(struct inode *inode,struct file *filp);
// 应用层
int open(const char *pathname, int flags);
|
8.3.1.6. release¶
当file结构体被释放时,将会调用该函数。与open函数相反,该函数可以用于释放上面,我们提到read和write函数时,需要使用copy_to_user函数以及copy_from_user函数来进行数据访问,写入/读取成功函数返回0,失败则会返回未被拷贝的字节数。
注意
release主要的工作:
释放由open分配的、保存在file->private_data中的数据
在最后一次关闭操作时关闭设备(如果维护打开计数)
1 2 3 4 5 6 7 8 9 10 11 12 13 | /********************************************************************************
* @brief release
* @param inode 传入的inode参数指针
* @param filp 传入的文件指针
* @Sample usage: 两个参数inode和filp由内核根据我们打开的设备节点信息
* (如主次设备号,字符设备等)来填充,这样我们可以通过这两个参数来找到我们要操作
* 的目标设备。这对于一个驱动对应多个设备的情况非常实用。
*******************************************************************************/
int (*release) (struct inode *, struct file *);
// 下面是自行加了参数信息的,方便进行参数分析。
int (*release) (struct inode *inode, struct file *filp);
// 应用层
int close(int fd);
|
8.3.1.7. copy_to_user和copy_from_user¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
|
函数参数和返回值如下:
参数
to:指定目标地址,也就是数据存放的地址,
from:指定源地址,也就是数据的来源。
n:指定写入/读取数据的字节数。
返回值
写入/读取数据的字节数
8.3.2. file结构体¶
内核中用file结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量f_op,当文件所有实例被关闭后,内核会释放这个结构体。
如下代码中,只列出了我们本章需要了解的成员变量。
1 2 3 4 5 6 7 8 9 10 11 | struct file {
......
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
......
/* needed for tty driver, and maybe others */
void *private_data;
......
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
|
f_op:存放与文件操作相关的一系列函数指针,如open、read、wirte等函数。
private_data:该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体。
8.3.3. inode结构体¶
VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。 它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file文件结构表示同一个文件的多个文件描述符,但此时, 所有的这些file文件结构全部都必须只能指向一个inode结构体。
inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct inode {
......
dev_t i_rdev;
......
union {
struct pipe_inode_info *i_pipe; /* linux内核管道 */
struct block_device *i_bdev; /* 如果这是块设备,则设置并使用 */
struct cdev *i_cdev; /* 如果这是字符设备,则设置并使用 */
char *i_link;
unsigned i_dir_seq;
};
......
} __randomize_layout;
/********************************************************************************
* dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
* struct cdev *i_cdev: struct cdev是内核的一个内部结构,它是用来表示字符设备的,
* 当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
*******************************************************************************/
|
8.4. 字符设备驱动程序框架¶
讲了很多次字符设备驱动程序框架,那到底什么是字符文件程序框架呢?我们可以从下面的思维导图来解读内核源码。
8.4.1. 驱动初始化和注销¶
8.4.1.1. 设备号的申请和归还¶
Linux内核提供了两种方式来定义字符设备。
1 2 3 4 | //第一种方式,就是我们常见的变量定义
static struct cdev chrdev;
//第二种方式,是内核提供的动态分配方式,调用该函数之后,会返回一个struct cdev类型的指针,用于描述字符设备。
struct cdev *cdev_alloc(void);
|
从内核中移除某个字符设备,则需要调用cdev_del函数。
1 2 3 4 5 6 7 | /********************************************************************************
* @brief cdev_del
* @note 将我们的字符设备结构体的地址作为实参传递进去,就可以从内核中移除该字符设备了。
* @param p 字符设备结构体的地址
* @return 无
*******************************************************************************/
void cdev_del(struct cdev *p)
|
register_chrdev_region 函数用于静态地为一个字符设备申请一个或多个设备编号。
函数原型如下所示。
1 2 3 4 5 6 7 8 9 10 | /********************************************************************************
* @brief register_chrdev_region
* @note 用于静态地为一个字符设备申请一个或多个设备编号。
* @param from dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的
* 设备号已经被其他的设备注册了,那么就会导致注册失败。
* @param count 指定申请的设备号个数,该值不宜过大,否则会与下一个主设备号重叠。
* @param name 用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
* @return 返回0表示申请成功,失败则返回错误码
*******************************************************************************/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
|
重要
使用register_chrdev_region函数时,都需要去查阅内核源码的Documentation/devices.txt文件,这就十分不方便。因此,内核提供了一种能够动态分配设备编号的方式:alloc_chrdev_region。
调用 alloc_chrdev_region 函数,内核会自动分配给我们一个尚未使用的主设备号。
我们可以通过命令 cat /proc/devices
查询内核分配的主设备号。
1 2 3 4 5 6 7 8 9 10 | /********************************************************************************
* @brief alloc_chrdev_region
* @note 自动分配一个尚未使用的主设备号。
* @param dev dev_t类型数据的指针变量,用于存放分配到的设备编号的起始值。
* @param baseminor 次设备号的起始值,通常情况下,设置为0。
* @param count 同register_chrdev_region类型,指定分配的设备号个数
* @param name 同register_chrdev_region类型,用于指定该设备的名称
* @return 返回0表示申请成功,失败则返回错误码
*******************************************************************************/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
|
当我们删除字符设备时候,我们需要把分配的设备编号交还给内核,对于使用register_chrdev_region函数 以及alloc_chrdev_region函数分配得到的设备编号,可以使用 unregister_chrdev_region 函数实现该功能。
1 2 3 4 5 6 7 8 | /**********************************************************************************
* @brief unregister_chrdev_region
* @note 注销设备号(用于register_chrdev_region/alloc_chrdev_region注册的设备号)
* @param from 指定需要注销的字符设备的设备编号起始值
* @param count 指定需要注销的字符设备编号的个数,该值应与申请函数的count值相等
* @return 无
*********************************************************************************/
void unregister_chrdev_region(dev_t from, unsigned count)
|
除了上述的两种,内核还提供了 register_chrdev 函数用于分配设备号。
注解
该函数是一个内联函数,它不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /*************************************************************************************
* @brief register_chrdev
* @note 分配设备号,支持静态申请设备号和动态申请设备号
* @param major 用于指定要申请的字符设备的主设备号,等价于register_chrdev_region
* 函数,当设置为0时,内核会自动分配一个未使用的主设备号。
* @param name 用于指定字符设备的名称。
* @param fops 用于操作该设备的函数接口指针。
* @return 主设备号
*************************************************************************************/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
|
注意
我们从以上代码中可以看到,使用register_chrdev函数向内核申请设备号,同一类字符设备(即主设备号相同),会在内核中申请了256个,通常情况下,我们不需要用到这么多个设备,这就造成了极大的资源浪费。
使用register_chrdev函数申请的设备号,则应该使用 unregister_chrdev 函数进行注销。
1 2 3 4 5 6 7 8 9 10 11 12 | /***************************************************************************
* @brief unregister_chrdev
* @note 注销设备号(用于register_chrdev函数申请的设备号)
* @param major 指定需要释放的字符设备的主设备号,一般使用
* register_chrdev函数的返回值作为实参。
* @param name 执行需要释放的字符设备的名称。
* @return 无
***************************************************************************/
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
|
8.4.1.2. 初始化cdev¶
前面我们已经提到过了,编写一个字符设备最重要的事情,就是要实现file_operations这个结构体中的函数。 实现之后,如何将该结构体与我们的字符设备结构体相关联呢?内核提供了cdev_init函数,来实现这个过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /***************************************************************************
* @brief cdev_init
* @note cdev初始化
* @param cdev struct cdev类型的指针变量,指向需要关联的字符设备结构体。
* @param fops file_operations类型的结构体指针变量。
* @return 无
***************************************************************************/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
|
8.4.2. 设备注册和注销¶
cdev_add函数用于向内核的cdev_map散列表添加一个新的字符设备。
1 2 3 4 5 6 7 8 9 | /***************************************************************************
* @brief cdev_add
* @note 添加字符设备
* @param p struct cdev类型的指针,用于指定需要添加的字符设备。
* @param dev dev_t类型变量,用于指定设备的起始编号。
* @param count 指定注册的设备个数。
* @return 错误码
***************************************************************************/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
|
cdev_del函数用于删除字符设备。
注意
从系统中删除cdev,cdev设备将无法再打开,但任何已经打开的cdev将保持不变,即使在cdev_del返回后,它们的FOP仍然可以调用。
1 2 3 4 5 6 7 | /***************************************************************************
* @brief cdev_del
* @note 删除字符设备
* @param p struct cdev类型的指针,用于指定需要删除的字符设备。
* @return 无
***************************************************************************/
void cdev_del(struct cdev *p)
|
8.4.3. 设备节点的创建和销毁¶
创建一个设备并将其注册到文件系统
1 2 3 4 5 6 7 8 9 10 11 12 | /***************************************************************************
* @brief device_create
* @note 创建一个设备并将其注册到文件系统
* @param class 指向这个设备应该注册到的struct类的指针。
* @param parent 指向此新设备的父结构设备(如果有)的指针。
* @param devt 要添加的char设备的设备号。
* @param drvdata 要添加到设备进行回调的数据。
* @param fmt 输入设备名称。
* @return 成功时返回 struct device 结构体指针, 错误时返回ERR_PTR()
***************************************************************************/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
|
删除使用device_create函数创建的设备:
1 2 3 4 5 6 7 8 | /***************************************************************************
* @brief device_destroy
* @note 删除使用device_create函数创建的设备
* @param class 指向注册此设备的struct类的指针。
* @param devt 以前注册的设备的设备号。
* @return 无
***************************************************************************/
void device_destroy(struct class *class, dev_t devt)
|
除了使用代码创建设备节点,还可以使用mknod命令创建设备节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 用法
mknod 设备名 设备类型 主设备号 次设备号
# 当设备类型为"p"时可不指定主设备号和次设备号,否则它们是必须指定的。
# 如果主设备号和次设备号以"0x"或"0X"开头,它们会被视作十六进制数来解析;
# 如果主设备号和次设备号以"0"开头,则被视作八进制数;
# 其余情况下被视作十进制数。可用的类型包括:
# - b 创建(有缓冲的)区块特殊文件
# - c, u 创建(没有缓冲的)字符特殊文件
# - p 创建先进先出(FIFO)特殊文件
# 例子:创建一个字符设备/dev/test,其主设备号为2,次设备号为0
mkmod /dev/test c 2 0
|
当我们使用上述命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点inode结构体, 并且将该设备的设备编号记录在成员i_rdev,将成员f_op指针指向了def_chr_fops结构体。 这就是mknod负责的工作内容,具体代码见如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #ifdef CONFIG_SHMEM
......
static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,
umode_t mode, dev_t dev, unsigned long flags)
{
struct inode *inode;
......
inode = new_inode(sb);
if (inode) {
......
switch (mode & S_IFMT) {
default:
inode->i_op = &shmem_special_inode_operations;
init_special_inode(inode, mode, dev);
break;
......
}
} else
shmem_free_inode(sb);
return inode;
}
#else /* !CONFIG_SHMEM */
......
|
第14行:mknod命令最终执行init_special_inode函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n", mode, inode->i_sb->s_id,
inode->i_ino);
}
|
第4-17行:判断文件的inode类型,如果是字符设备类型,则把def_chr_fops作为该文件的操作接口,并把设备号记录在inode->i_rdev。
注意
inode上的file_operation并不是自己构造的file_operation,而是字符设备通用的def_chr_fops,那么自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上。接下来我们再看open函数到底做了什么。
8.4.4. open函数到底做了什么¶
注意
使用设备之前我们通常都需要调用open函数,这个函数一般用于设备专有数据的初始化,申请相关资源及进行设备的初始化等工作。
对于简单的设备而言,open函数可以不做具体的工作,你在应用层通过系统调用open打开设备时, 如果打开正常,就会得到该设备的文件描述符,之后,我们就可以通过该描述符对设备进行read和write等操作。
open函数到底做了些什么工作?下图中列出了open函数执行的大致过程。
用户空间使用open()系统调用函数打开一个字符设备时( int fd = open("dev/xxx", O_RDWR)
)大致有以下过程:
在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点
遍历散列表cdev_map,根据inod节点中的 cdev_t设备号找到cdev对象
创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件秒速符作为数组下标标识了一个设备对象)
初始化struct file对象,将 struct file对象中的 file_operations成员指向 struct cdev对象中的 file_operations成员(file->fops = cdev->fops)
回调file->fops->open函数
我们使用的open函数在内核中对应的是sys_open函数,sys_open函数又会调用do_sys_open函数。
在do_sys_open函数中,首先调用函数get_unused_fd_flags来获取一个未被使用的文件描述符fd,该文件描述符就是我们最终通过open函数得到的值。
紧接着,又调用了do_filp_open函数,该函数通过调用函数get_empty_filp得到一个新的file结构体,之后的代码做了许多复杂的工作,如解析文件路径,查找该文件的文件节点inode等,直接来到了函数do_dentry_open函数,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
......
f->f_op = fops_get(inode->i_fop);
......
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
......
}
|
第6行:使用fops_get函数来获取该文件节点inode的成员变量i_fop,在上图中我们使用mknod创建字符设备文件时,将def_chr_fops结构体赋值给了该设备文件inode的i_fop成员。
第9行:到了这里,我们新建的file结构体的成员f_op就指向了def_chr_fops。
1 2 3 4 | const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
|
最终,会执行def_chr_fops中的open函数,也就是chrdev_open函数,可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号,找到相应的字符设备,从而得到操作该设备的方法,代码实现如下。
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 46 47 48 49 50 51 52 | static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
|
在Linux内核中,使用结构体cdev来描述一个字符设备。
第9行:inode->i_rdev中保存了字符设备的设备编号,
第14行:通过函数kobj_lookup函数便可以找到该设备文件cdev结构体的kobj成员,
第17行:再通过函数container_of便可以得到该字符设备对应的结构体cdev。函数container_of的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。同时,将cdev结构体记录到文件节点inode中的i_cdev,便于下次打开该文件。
第40-45行:函数chrdev_open最终将该文件结构体file的成员f_op替换成了cdev对应的ops成员,并执行ops结构体中的open函数。
注解
最后,调用上图的fd_install函数,完成文件描述符和文件结构体file的关联,之后我们使用对该文件描述符fd调用read、write函数,最终都会调用file结构体对应的函数,实际上也就是调用cdev结构体中ops结构体内的相关函数。
总结一下整个过程,当我们使用open函数,打开设备文件时,会根据该设备的文件的设备号找到相应的设备结构体,从而得到了操作该设备的方法。也就是说如果我们要添加一个新设备的话,我们需要提供一个设备号,一个设备结构体以及操作该设备的方法(file_operations结构体)。
8.5. 杂项设备(misc device)¶
8.5.1. 杂项设备基本概念¶
字符设备是嵌入式Linux中常见的设备。misc device中文翻译称为“杂项设备”,杂项设备本质就是字符设备。
嵌入式硬件上存在各类设备,如ADC、DAC、按键、蜂鸣器等,一方面不便于单独分类,另一方面驱动设备号分配有限,因此Linux系统引入“杂项设备”,驱动工程师将这些没有明显区分的设备统一归类为杂项设备。
注解
通俗来讲,可以将杂项设备理解成无法归类的五花八门的设备(混杂设备)
8.5.2. 杂项设备的特点和优点¶
杂项设备的特点有:
杂项设备是字符设备的一个子类,是最简单的字符设备。
杂项设备的主设备号(MISC_MAJOR)为10,次设备号不同(0~255)。
Linux内核提供杂项设备注册、释放框架,简化字符设备注册过程。
杂项设备的优点有:
便于实现,简化设备驱动实现过程。
节约系统主设备号资源。
8.5.3. 查看系统杂项设备¶
在查看系统杂项设备之前,我们先看一下内核源码的Documentation/devices.txt文件。
注解
下面仅演示了文件的部分内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 10 char Non-serial mice, misc features
0 = /dev/logibm Logitech bus mouse
1 = /dev/psaux PS/2-style mouse port
2 = /dev/inportbm Microsoft Inport bus mouse
3 = /dev/atibm ATI XL bus mouse
4 = /dev/jbm J-mouse
4 = /dev/amigamouse Amiga mouse (68k/Amiga)
5 = /dev/atarimouse Atari mouse
6 = /dev/sunmouse Sun mouse
7 = /dev/amigamouse1 Second Amiga mouse
8 = /dev/smouse Simple serial mouse driver
......
237 = /dev/loop-control Loopback control device
238 = /dev/vhost-net Host kernel accelerator for virtio net
239 = /dev/uhid User-space I/O driver support for HID subsystem
240 = /dev/userio Serio driver testing device
241 = /dev/vhost-vsock Host kernel driver for virtio vsock
242-254 Reserved for local use
255 Reserved for MISC_DYNAMIC_MINOR
|
查看系统的杂项设备:
1 2 3 4 | # 查询系统的杂项设备
cat /proc/misc
# 查看其中一个设备的主设备号和次设备号
ls -l /dev/cpu_dma_latency
|
8.6. 杂项设备驱动程序框架¶
杂项设备将字符设备进一步封装一层,有专门的驱动框架,对于驱动工程师来说可以简化字符设备注册过程。
8.6.1. miscdevice结构体¶
Linux内核把杂项设备抽象为一个结构体,位于“include/linux/miscdevice.h”中。
1 2 3 4 5 6 7 8 9 10 11 | struct miscdevice {
int minor; //次设备号,需要用户设置。
const char *name; //设备节点名称,“/dev”目录下显示。
const struct file_operations *fops; //设备文件操作接口,open/read/write等。
struct list_head list; //misc设备链表节点。
struct device *parent; //当前设备父设备,一般为NULL。
struct device *this_device; //当前设备,即是linux基本设备驱动框架。
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
|
8.6.2. 预定义的杂项设备次设备号¶
Linux内核有一些预定义的misc 设备的次设备号。
1 2 3 4 5 6 7 8 9 10 11 12 | #define PSMOUSE_MINOR 1
#define MS_BUSMOUSE_MINOR 2 /* unused */
#define ATIXL_BUSMOUSE_MINOR 3 /* unused */
/*#define AMIGAMOUSE_MINOR 4 FIXME OBSOLETE */
#define ATARIMOUSE_MINOR 5 /* unused */
#define SUN_MOUSE_MINOR 6 /* unused */
#define APOLLO_MOUSE_MINOR 7 /* unused */
#define PC110PAD_MINOR 9 /* unused */
......
#define VHOST_VSOCK_MINOR 241
#define RFKILL_MINOR 242
#define MISC_DYNAMIC_MINOR 255
|
注意
设置次设备号时可以从这些预定义的次设备号中选择,也可以自定义,但不要重复使用其他设备的次设备号(特别是“cat /proc/misc”查看到的系统已使用的)。
8.6.3. 杂项设备的注册和注销¶
当创建好 miscdevice 结构体后,使用 misc_register 函数向系统中注册一个 misc 设备。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | /***************************************************************************
* @brief misc_register
* @note 注册杂项设备
* @param misc 创建好的 miscdevice 结构体
* @return 返回0表示注册成功,失败返回负数
***************************************************************************/
int misc_register(struct miscdevice *misc)
{
dev_t dev;
int err = 0; //错误码
bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR); //查看是否需要动态分配次设备号
/*初始化misc_list链表*/
INIT_LIST_HEAD(&misc->list);
/*获取互斥锁*/
mutex_lock(&misc_mtx);
if (is_dynamic) { //写入失败,采用动态分配
//查找位图内第一个为 0 的比特位
int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);
//判断是否找到
if (i >= DYNAMIC_MINORS) {
err = -EBUSY;
goto out;//没找到
}
//找到了,写入这个次设备号
misc->minor = DYNAMIC_MINORS - i - 1;
set_bit(i, misc_minors);
} else {
struct miscdevice *c; //注册一个新的miscdevice结构体
/*遍历misc_list链表,看这个次设备号以前有没有被用过,如果次设备号已被占有则退出*/
list_for_each_entry(c, &misc_list, list) {
if (c->minor == misc->minor) {
err = -EBUSY;
goto out;
}
}
}
/*计算出设备号*/
dev = MKDEV(MISC_MAJOR, misc->minor);
/*在/dev下创建设备节点,这就是有些驱动程序没有显式调用device_create,却出现了设备节点的原因*/
misc->this_device =
device_create_with_groups(misc_class, misc->parent, dev,
misc, misc->groups, "%s", misc->name);
if (IS_ERR(misc->this_device)) {
if (is_dynamic) {
int i = DYNAMIC_MINORS - misc->minor - 1;
if (i < DYNAMIC_MINORS && i >= 0)
clear_bit(i, misc_minors);
misc->minor = MISC_DYNAMIC_MINOR;
}
err = PTR_ERR(misc->this_device);
goto out;
}
/*将这个miscdevice添加到misc_list链表中*/
list_add(&misc->list, &misc_list);
out:
mutex_unlock(&misc_mtx);//释放互斥锁
return err;
}
|
我们可以使用 misc_deregister 函数来注销掉 misc 设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /***************************************************************************
* @brief misc_deregister
* @note 注销杂项设备
* @param misc 要注销的 miscdevice 结构体
* @return 无
***************************************************************************/
void misc_deregister(struct miscdevice *misc)
{
int i = DYNAMIC_MINORS - misc->minor - 1;
if (WARN_ON(list_empty(&misc->list)))
return;
/*获取互斥锁*/
mutex_lock(&misc_mtx);
/*在misc_list链表中删除miscdevice设备*/
list_del(&misc->list);
/*删除设备节点*/
device_destroy(misc_class, MKDEV(MISC_MAJOR, misc->minor));
if (i < DYNAMIC_MINORS && i >= 0)
clear_bit(i, misc_minors);
mutex_unlock(&misc_mtx);/*释放互斥锁*/
}
|
8.7. 杂项设备实验¶
本实验示例代码目录为:linux_driver/chardev_test/misc_chardev
8.7.1. 实验代码分析¶
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 46 47 48 | #include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h> //杂项设备相关头文件
#include <linux/fs.h> //文件系统头文件
//宏定义杂项设备名称
#define DEV_NAME "misc_dev"
/*定义file_operations结构体(文件操作集)*/
struct file_operations misc_char_fops = {
.owner = THIS_MODULE, //表示该文件的操作结构体所属的模块是当前的模块(所有者是这个模块)
};
/*杂项设备结构体*/
struct miscdevice misc_char_dev = {
.minor = MISC_DYNAMIC_MINOR, //动态申请的次设备号
.name = DEV_NAME, //杂项设备名称
.fops = &misc_char_fops, //文件操作集
};
/*入口函数功能实现杂项字符设备初始化*/
static int __init misc_chardev_init(void)
{
int ret = 0;
printk("misc_chrdev init start\n");
/*注册杂项设备*/
ret = misc_register(&misc_char_dev);
if(ret < 0){
printk("fail to alloc misc_chrdev\n");
goto alloc_err;
}
printk("misc_chrdev init succeed\n");
return 0;
alloc_err:
return ret;
}
module_init(misc_chardev_init);
/*出口函数功能实现*/
static void __exit misc_chardev_exit(void)
{
/*注销杂项设备*/
misc_deregister(&misc_char_dev);
printk("misc_chrdev exit\n");
}
module_exit(misc_chardev_exit);
MODULE_LICENSE("GPL v2");
|
注解
第6行:定义杂项设备名称(设备文件名称)。
第9行:定义文件操作结构体。
第13行:定义杂项设备结构体。
第26行:注册杂项设备。
第43行:注销杂项设备。
8.7.2. 实验准备¶
获取内核模块源码,将配套代码 /linux_driver/chardev_test/misc_chardev 解压到内核代码同级目录。
8.7.2.1. Makefile说明¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | KERNEL_DIR=../../../kernel/
# 指定工具链并导出环境变量
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
# 编译成模块的目标文件名。
obj-m := misc_chardev.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|
注解
第7行:指定编译模块的目标文件名
第10行:交叉编译工具链编译模块。
8.7.2.2. 编译¶
make
#编译成功后,实验目录下会生成一个名为"misc_chardev.ko"驱动模块文件。
8.7.3. 程序运行结果¶
注意
编译结果需要传输的板卡上运行,可以通过SCP传输,也可以挂载NFS。
#SCP传输文件到鲁班猫板卡
scp misc_chardev.ko cat@192.168.103.108:/home/cat
在板卡上运行:
1 2 3 4 5 6 7 8 | # 挂载模块
insmod misc_chardev.ko
# 查看杂项设备的主设备名和次设备名
ls -l /dev/misc_dev
# 尝试打开杂项设备设备节点文件
cat /dev/misc_dev
# 卸载模块
rmmod misc_chardev
|
演示如下:
注意
注册设备后发现文件无法打开,这是因为我们没有在文件操作结构体(file_operations结构体)中添加相应的操作指令,这部分留到下章节实验进行演示。