手机网站智能管理系统,吉林省高等级公路建设局 网站,diywap手机微网站内容管理系统,深圳seo优化推广业务员Linux设备驱动模型之字符设备
前面我们有介绍到Linux的设备树#xff0c;这一节我们来介绍一下字符设备驱动。字符设备是在IO传输过程中以字符为单位进行传输的设备#xff0c;而字符设备驱动则是一段可以驱动字符设备驱动的代码#xff0c;当前Linux中#xff0c;字符设备…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 255static 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一个是i255如果两个都被用了则只能从下一个元素继续查找了。
总结
内核字符设备通过全局静态指针数组chrdevs来保存字符设备信息数组一共255个元素每个元素可保存两个不同主设备号的字符设备信息所以内核一个可申请512个不同的主设备号字符设备。
当驱动调用alloc_chrdev_region()注册设备号之后终端可以看到以下的信息
rootroot:/# cat /proc/devices
Character devices:1 mem4 /dev/vc/04 tty5 /dev/tty5 /dev/console5 /dev/ptmx7 vcs10 misc13 input89 i2c90 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 gpiochipBlock devices:8 sd31 mtdblock65 sd66 sd67 sd68 sd69 sd70 sd71 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。有相同特性的设备为同一个classclass可以自己创建设备会属于某一个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 daostatic 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匹配操作我们下一章节进行介绍。