Linux设备驱动模型之字符设备
前面我们有介绍到Linux的设备树,这一节我们来介绍一下字符设备驱动。字符设备是在IO传输过程中以字符为单位进行传输的设备,而字符设备驱动则是一段可以驱动字符设备驱动的代码,当前Linux中,字符设备驱动是怎样的呢,下面一起来探讨学习一下。
基础小知识
字符设备框架
如果让你来设计字符设备框架,你会怎么设计呢?不同的开发者会有不同的需求,但是每个人都需要注册字符设备,需要有个地方来保存、管理这些设备驱动信息,在代码中如何保存会更灵活,更合适呢?
注册字符设备
当前Linux内核是通过主设备号与次设备号来定义某一个驱动,其中主设备号从0到CHRDEV_MAJOR_MAX(512) - 1共512个主设备号,而次设备号则是从0到220 - 1(MINORMASK定义)。所以,字符设备驱动第一步就是先向内核注册一个设备号:
#define DAO_NAME "dao"
static dev_t dao_devt;
// 注册字符设备函数调用
alloc_chrdev_region(&dao_devt, 0, MINORMASK + 1, DAO_NAME); //注册字符设备号
/** 注册字符设备函数声明,通过下面我们可以知道,
* dev 是保存主设备号,
* baseminor 则是代表可以从该索引开始查找可使用的次设备号
* count 代表次设备号可搜索的范围
* name 则是该字符设备的名称
*/
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
而 alloc_chrdev_region() 是怎么来注册字符设备号的呢?下面来看看它的实现。
/*
* Register a single major with a specified minor range.
*
* If major == 0 this function will dynamically allocate an unused major.
* If major > 0 this function will attempt to reserve the range of minors
* with given major.
*
*/
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, *curr, *prev = NULL;
int ret;
int i;
/* 先检查需要申请的主设备号是否在合理范围之内:0 ~ CHRDEV_MAJOR_MAX(512) - 1 */
if (major >= CHRDEV_MAJOR_MAX) {
pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
name, major, CHRDEV_MAJOR_MAX-1);
return ERR_PTR(-EINVAL);
}
/* 再检查次设备号是否在合理范围之内:0 ~ MINORMASK */
if (minorct > MINORMASK + 1 - baseminor) {
pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",
name, baseminor, baseminor + minorct - 1, 0, MINORMASK);
return ERR_PTR(-EINVAL);
}
/* 申请一块内存,该内存将是保存字符设备信息的,内核通过结构体(struct char_device_struct)来存储该信息*/
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* 如果调用该函数时传递进来的主设备号为0,则代表内核动态分配主设备号,
* 此时通过 find_dynamic_major() 搜索可用的主设备号
*/
if (major == 0) {
ret = find_dynamic_major(); /* 动态搜索可用的主设备号 */
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;
}
major = ret; /* 保存搜索到的主设备号到major */
}
ret = -EBUSY;
i = major_to_index(major);
/* 确认主、次设备号是否可用 */
for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {
if (curr->major < major)
continue;
if (curr->major > major)
break;
if (curr->baseminor + curr->minorct <= baseminor)
continue;
if (curr->baseminor >= baseminor + minorct)
break;
goto out;
}
/* 保存字符设备的主、次设备号、设备名称 */
cd->major = major;
cd->baseminor = baseminor;
cd->
= minorct;
strlcpy(cd->name, name, sizeof(cd->name));
/* 将新注册的字符设备添加到chrdevs */
if (!prev) {
cd->next = curr;
chrdevs[i] = cd;
} else {
cd->next = prev->next;
prev->next = cd;
}
mutex_unlock(&chrdevs_lock);
/* 返回配置了设备号的char_device_struct */
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
在alloc_chrdev_region中有搜索可用的主设备号以及将设备信息保存到chrdevs,它们究竟是什么呢?
上面我们就问,如果是你来设计这个字符设备驱动框架,你会怎么来设计,那么内核是怎么设计的呢?
// fs/char_dev.c
#define CHRDEV_MAJOR_HASH_SIZE 255
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
内核定义了一个叫chrdevs
的指针数组来保存字符设备信息,这个指针指向的是struct char_device_struct
结构体。chrdevs一共255个成员,它们是通过主设备号major来进行排序的。在find_dynamic_major()
中,搜索可用主设备号先从chrdevs数组的末端开始查找到CHRDEV_MAJOR_DYN_END(234),如果有元素为空的,则直接返回索引,找到有效的主设备号。简单说就是从 234254,从后往前找,如果数组元素为空,则返回数组索引作为主设备号。如果动态分配(234254)的都找不到了,则从314~511开始查找,注意到chrdevs只有255个元素,所以内核通过major_to_index
将主设备号转为cherdev的索引,实际上就是一个chrdevs的元素分别被两个字符设备共用,比如主设备号为59的设备与主设备号为314的公用一个chrdevs元素chrdevs[59]。此时检查chrdevs的元素是否为空,为空则直接返回,如果元素中的主设备已经被填充了,则查找下一次索引的chrdevs元素。
static int find_dynamic_major(void)
{
int i;
struct char_device_struct *cd;
/* 234~254是否为空闲的,为空闲则返回 */
for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {
if (chrdevs[i] == NULL)
return i;
}
/*234~254已经被占用了,从314~511开始查找 */
for (i = CHRDEV_MAJOR_DYN_EXT_START;
i >= CHRDEV_MAJOR_DYN_EXT_END; i--) {
for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)
/* 确认chrdevs元素对应的大于255的主设备号已经被占用了,
* 则从再下一个主设备开始查找,该设备号已经被占用
*/
if (cd->major == i)
break;
if (cd == NULL)
return i;
}
return -EBUSY;
}
上述也就是说,每个chrdevs元素可以代表两个主设备号,一个是元素的索引i,一个是i+255,如果两个都被用了,则只能从下一个元素继续查找了。
总结:
内核字符设备通过全局静态指针数组chrdevs来保存字符设备信息,数组一共255个元素,每个元素可保存两个不同主设备号的字符设备信息,所以内核一个可申请512个不同的主设备号字符设备。
当驱动调用alloc_chrdev_region()
注册设备号之后,终端可以看到以下的信息:
root@root:/# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
153 spi
180 usb
189 usb_device
247 ubi0
248 ttyS
249 hidraw
250 rpmb
251 dao // 这里就是我们注册的字符设备,可以看到它的主设备号是251,名字叫dao
252 watchdog
253 rtc
254 gpiochip
Block devices:
8 sd
31 mtdblock
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
254 ubiblock
259 blkext
但是此时我们并没有给这个字符设备增加什么操作,下面我们继续看如何添加操作。
字符设备初始化
上面我们向内核申请注册了一个字符设备号,但是该设备号我们还没有将其与字符设备建立关系,那么,又是怎么建立的呢?
static dev_t dao_devt; // 上面注册得到的主设备号
static struct cdev dao_cdev; // 字符设备结构体
static const struct file_operations dao_fops = { // 字符设备操作集
}
cdev_init(&dao_cdev, &dao_fops); // 初始化字符设备结构体,并更新其文件字符操作集
cdev_add(&dao_cdev, dao_devt, MINORMASK + 1); // 将主设备号devt与字符设备 cdev 建立关系,实际上就是更新cdev中的主设备号成员信息。同时在cdev_add中,cdev信息保存到指针数组cdev_map中。
struct file_operations
file_operations 是干什么的呢?Linux中任意一个设备都是文件,针对文件,我们会有各种各样的操作,比如打开、读、写、关闭等,不同的设备它们上述的操作都会不一样,所以设备驱动中需要自定义好设备的操作集,方便应用层可正确使用该设备。
经过上面的操作之后,我们的字符设备已经完成了基本的初始化,在/proc/devices
中可以看到以注册的字符设备号,而针对该设备操作的file_operations也已经填充,此时用户空间已经可以对该设备进行操作。
如果完成上述进行操作时,会发现/dev目录下并没有相关的字符节点,此时只能通过mknod /dev/dao c 251 0
这样的命令来完成创建/dev/dao
节点,执行cat /dev/dao
将会看到依次调用到 file_operations 的 open、read、release函数。
那么我们需要如何操作才会在/dev目录下完成节点的注册呢?
创建设备节点
在介绍创建节点之前,我们先来了解class。在内核中,经常会看到xxx_class,内核将设备分为字符设备、块设备、网络设备,同时也会分class。有相同特性的设备为同一个class,class可以自己创建,设备会属于某一个class。那么,上面我们完成字符设备的初始化,但我们并完成将其与某个class绑定在一起。所以,创建设备节点,我们先创建一个class:
#define DAO_CLASS_NAME "dao"
static struct class *dao_class;
/* 创建一个class,此时在/sys/class目录下看到一个叫dao的文件夹,这个就是我们注册的class */
dao_class = class_create(THIS_MODULE, DAO_CLASS_NAME);
接着创建设备:
static struct device *dao_dev;
dao_dev = device_create(dao_class, NULL, dao_devt, NULL, DAO_NAME);
函数声明:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);
通过device_create,我们将dao_devt这个字符设备号与dao_class绑定在一起并创建一个设备。通过device_create函数声明我们可以知道,创建一个设备时可传入该设备的class、父设备、设备号、设备私有数据以及设备名。
device_create主要完成下面的操作:
- 完成
struct device
结构体的初始化; - 通知平台其他总线,系统有新设备加入;
- 创建设备uevent节点;
- 将设备与class建立链接;
- 增加其他设备节点信息;
- 如果bus则加入bus;
- 创建设备attr_dev;
- sys目录下创建设备节点;
- dev目录下创建设备节点;
- 触发bus的probe;
完成上述操作之后,设备完成相应的注册,用户空间可正常的操作该设备。
框架架构图
例程
// SPDX-License-Identifier: GPL-2.0+
/*
* dao char device test code
*
* Copyright (c) 2022, dao. All rights reserved.
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/cdev.h>
#include <linux/sysfs.h>
#include <linux/fs.h>
#define DAO_NAME "dao"
#define DAO_CLASS_NAME "dao"
static dev_t dao_devt;
static struct class *dao_class;
static struct cdev dao_cdev;
static struct device *dao_dev;
static int dao_dev_open(struct inode *inode, struct file *file)
{
pr_info("%s\n", __func__);
return 0;
}
static int dao_dev_release(struct inode *inode, struct file *file)
{
pr_info("%s\n", __func__);
return 0;
}
static ssize_t dao_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ptr)
{
pr_info("%s\n", __func__);
return 0;
}
static long dao_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
pr_info("%s\n", __func__);
return 0;
}
static const struct file_operations dao_fops = {
.owner = THIS_MODULE,
.open = dao_dev_open,
.release = dao_dev_release,
.read = dao_dev_read,
.unlocked_ioctl = dao_dev_ioctl,
};
static int __init dao_dev_init(void)
{
int ret = 0;
ret = alloc_chrdev_region(&dao_devt, 0, MINORMASK + 1, DAO_NAME);
if (ret < 0) {
pr_err("Error: failed to register dao_dev, err: %d\n", ret);
return ret;
}
cdev_init(&dao_cdev, &dao_fops);
cdev_add(&dao_cdev, dao_devt, MINORMASK + 1);
pr_info("%s: major %d\n", __func__, MAJOR(dao_devt));
dao_class = class_create(THIS_MODULE, DAO_CLASS_NAME);
if (IS_ERR(dao_class)) {
pr_err("Error: failed to register dao_dev class\n");
ret = PTR_ERR(dao_class);
goto failed1;
}
dao_dev = device_create(dao_class, NULL, dao_devt, NULL, DAO_NAME);
if (!dao_dev)
goto failed2;
return 0;
failed2:
class_destroy(dao_class);
failed1:
cdev_del(&dao_cdev);
unregister_chrdev_region(dao_devt, MINORMASK + 1);
return ret;
}
static void __exit dao_dev_exit(void)
{
device_destroy(dao_class, dao_devt);
class_destroy(dao_class);
cdev_del(&dao_cdev);
unregister_chrdev_region(dao_devt, MINORMASK + 1);
}
module_init(dao_dev_init)
module_exit(dao_dev_exit)
驱动注册
上面介绍了设备的注册,但是驱动的注册流程又是怎样的呢?下面介绍来介绍一下,驱动注册通过driver_register()函数完成,而它主要进行下面几个事情:
- 驱动都是挂载在总线上的,同时驱动也是使用名字进行区分的,所以需要确认该总线上没有同样名字的驱动;
- 将该驱动挂载到总线上,将驱动添加到klist_drivers这个链表,然后和device进行匹配操作;
- 创建驱动的属性配置节点;
具体的device与driver匹配操作我们下一章节进行介绍。