设备模型
1. 文档结构介绍
首先, 第2章会给出整个设备模型的整体框架.
然后, 第3章介绍sysfs, 了解它有助于我们理解设备模型到底在做什么.
接着, 第4, 5, 6章描述设备模型的基础概念, 驱动开发中基本上不会看到这些基础, 不过我们必须先理解它们, 才能更好的理解后续内容.
然后, 第7章简单介绍了下/sys/下的一些顶层目录是如何创建出来的, 可以浏览一下.
接着, 第8, 9, 10, 11章描述了设备模型中最重要的4个概念: Bus, Class, Device, Driver.
第12章介绍了platform系统, 它是基于Bus, Class, Device, Driver抽象出来的一个更上层的东西, 理解了前面的内容之后, 再来看platfrom系统就会非常容易了.
最后一章介绍了devres, 它很容易理解, 也很实用. 教你在驱动开发过程中, 如何省去”goto 释放resource的烦恼”.
2. 架构介绍
由于Linux支持世界上几乎所有的、不同功能的硬件设备(这是Linux的优点), 导致Linux内核中有一半的代码是设备驱动, 而且随着硬件的快速升级换代, 设备驱动的代码量也在快速增长. 为了降低设备多样性带来的Linux驱动开发的复杂度, 以及设备热拔插处理、电源管理等, Linux内核提出了设备模型(也称作Driver Model)的概念. 设备模型将硬件设备归纳、分类, 然后抽象出一套标准的数据结构和接口. 驱动的开发, 就简化为对内核所规定的数据结构的填充和实现.
2.1 设备模型的基本概念
Bus, Class, Device和Device Driver的概念
下图是嵌入式系统常见的硬件拓扑的一个示例:
硬件拓扑描述Linux设备模型中四个重要概念中三个:Bus, Class和Device(第四个为Device Driver, 后面会说):
Bus(总线)
Linux认为, 总线是CPU和一个或多个设备之间信息交互的通道. 而为了方便设备模型的抽象, 所有的设备都应连接到总线上
Class(分类)
在Linux设备模型中, Class的概念非常类似面向对象程序设计中的Class(类), 它主要是集合具有相似功能或属性的设备, 这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数. 因而从属于相同Class的设备的驱动程序, 就不再需要重复定义这些公共资源, 直接从Class中继承即可
Device(设备)
抽象系统中所有的硬件设备, 描述它的名字、属性、从属的Bus、从属的Class等信息
Device Driver(驱动)
Linux设备模型用Driver抽象硬件设备的驱动程序, 它包含设备初始化、电源管理相关的接口实现. 而Linux内核中的驱动开发, 基本都围绕该抽象进行(实现所规定的接口函数)
3. sysfs
快速体验什么是sysfs
非常感性的一个认识是, 随便找个Linux的机器或者板子, 然后看看它的根目录(/)下是不是有个叫sys的文件夹? 对, /sys/目录就是这里所说的sysfs.
sysfs有什么用
ls /sys/ , 会看见几个熟悉的文件夹, bus, class, devices. 这几个文件夹是不是与设备模型中的几个概念名字一样. 猜想sysfs会不会跟设备模型有什么关系? 没错, sysfs与设备模型关系紧密.
sysfs的作用主要体现在两个方面, 从内核与用户空间的角度来说, 这两方面体现为:
sysfs把内核的某些东西展示给用户空间: 它展示设备模块中各个组件的层次关系, 层次关系就是一个树形结构. 顶层目录, 子目录, 子目录的子目录, 属性文件等. 用户空间可以用ls读取这些目录, 或者用cat读取属性文件.
sysfs可以从用户空间得到一些信息传递给内核: sysfs的某个目录下可能会存在一些文件, 我们把这个文件称作属性文件. 用户空间可以修改这些属性文件, 文件被修改之后, sysfs能够通过某种机制通知内核.
Note: 还记得在内核模块一文, 我们曾经介绍过, 内核模块可以有模块参数, 我们也可以通过sysfs来读取/修改这些模块参数. 但是内核模块并不会收到参数被修改的通知, 模块只有主动读取参数, 与之前的值做对比, 才知道参数是否被修改.
这里会有一个疑问, 模块参数也是通过sysfs修改的,为什么没有收到修改通知? 具体原因没有去细看. 不过我们会在下面的章节中介绍sysfs是通过什么方式通知内核某个属性文件被修改了. 了解这种机制之后, 如果有兴趣追踪上面的疑问, 可以自行阅读模块参数的源码细节.
详解了解sysfs
sysfs是一种基于RAM的虚拟文件系统
简单说明一下什么是虚拟文件系统.
要说明虚拟文件系统, 首先来看一下什么是文件系统. FAT/FAT32, 这是最常见的文件系统, 我们的U盘一般就是用的这种文件系统. 除了这两种, 还有众多的文件系统: EXT2, EXT4, JIFFS2, UBIFS …
Linux内核为了给用户空间一个统一的接口, 在这些真实的文件系统之上, 封装了一层虚拟文件系统(VFS). 这样用户空间看到的就是VFS提供的统一接口, 而真实文件系统之间的差异, 则在封装层由内核屏蔽掉了, 用户空间不用去关注这些差异. 通过内核提供的register_filesystem函数, 就能向VFS注册一个虚拟的文件系统.
sysfs就是一种虚拟的文件系统, 它的代码路径在fs/sysfs. 它的初始函数为sysfs_init, 在该函数中, 会调用register_filesystem, 向VFS注册虚拟文件系统.
说它基于RAM的意思是, sysfs是构建在RAM上的, 用户空间所有对sysfs的增/删/修改操作, 重启之后都没了. 比如你在sysfs下创建了一个文件, 重启之后, 这个文件就没了.
sysfs proc devfs udev devtmpfs的区别
首先, 除了udev, 其他几个都是虚拟文件系统.
devfs, 是在Linux 2.4引入的, 它叫设备文件系统, 主要与/dev目录下的设备节点有关. 曾经被许多工程师给予高度评价. 不过在Linux 2.6中被udev替代了, 原因是devfs所做的工作被确信可以在用户态来完成, 而且devfs的维护者停止了对代码的维护.
udev, Linux 2.6中引入, 完全在用户态工作, 它是一个daemon, 利用设备加入或移除时内核所发送的热插拔事件来管理/dev目录下设备节点的创建和删除操作. 本文后面会有一个章节来专门介绍内核是如何发送热插拔事件的.
devtmpfs, 可以被理解为一个增强版的devfs, 它在设计之初经历了一些争论, 有人赞同, 有人反对, 细节参考这里. 不过最终还是在Linux 2.6.31时加入内核. 它的主要目的是优化启动时间, 某些嵌入式设备对启动时间要求较高, 而udev由于某些原因会拖慢启动时间. devtmpfs最终会被mount 到/dev目录下.
proc, 用于提供进程和状态信息的虚拟文件系统
sysfs, Linux 2.6中引入, 用来展示设备模型中的各个部分之间的层级关系, 属性.
为什么要在这里介绍这几种文件系统, 一方面是对sysfs有个对比了解, 另外一方面, 本文后面章节在讲述uevent事件的时候, 需要本小节的背景知识.
4. kobject & ktype & attribute
4.1 简介
Kobject
一个感性的认识是: kobject对应/sys/下的一个目录(文件夹)或子目录. 但不是/sys/下所有的目录都对应kobject, 有的目录是以其他形式创建的.
Kobject是Linux设备模型的基石:
Linux设备模型的核心是使用Bus、Class、Device、Driver四个核心数据结构, 将大量的、不同功能的硬件设备(以及驱动该硬件设备的方法), 以树状结构的形式, 进行归纳、抽象, 从而方便Kernel的统一管理;
而硬件设备的数量、种类是非常多的, 这就决定了Kernel中将会有大量的有关设备模型的数据结构. 这些数据结构一定有一些共同的功能, 需要抽象出来统一实现, 否则就会不可避免的产生冗余代码;
这就是Kobject诞生的2个背景, 因此Kobject的主要作用也可以归纳为2方面.
Kobject主要有2方面的作用:
以树形结构组织层次: 通过parent指针, 可以将所有Kobject以层次结构的形式组合起来
引用计数: 使用一个引用计数(reference count), 来记录Kobject被引用的次数, 并在引用次数变为0时把它释放
Note1: 在Linux中,Kobject几乎不会单独存在。它的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现
Note2: Linux driver开发者,很少会直接使用Kobject以及它提供的接口,而是使用构建在Kobject之上的设备模型接口
Attribute
一个感性的认识是: Attribute就是/sys/某个目录下的一个文件, 这个文件叫属性文件.
Attribute与Kobject的关系也很明显, 文件是存在于某个目录下的, 所以Attribute也是跟Kobject挂钩的. 我们一般说某个Kobject具有某些属性.
Attribute有两个主要参数:
name: 名字, /sys/下的文件名, 就是这个name
mode: 权限, 该文件的读写权限
ktype
没办法给Ktype一个感性的认识.
Ktype主要作用是把Kobject的一些通用的东西抽象到一起.
比如我们在上面提到, Kobject会提供引用计数的功能, 当计数到0时, 就会释放资源, 这样每个Kobject都应该有一个release的方法来释放资源
在比如上面提到的属性文件, 有可能几个Kobject都具有相同的属性, 我们就可以把这些属性定义在一个Ktype里面
Ktype的概念在内核里面比较难得理解, 我们会在本章的最后, 对Kobject, Ktype, Attribute做一个整体的说明, 以便于理解.
综合上文描述的, Ktype主要有以下几方面的作用:
release: Kobject的资源释放方法
struct attribute **default_attrs : 注意它是一个双指针, 相当于一个链表头. 每个Attribute对应一个属性文件, 链表头代表着会有多个属性文件. 当某个Kobject指向该Ktype时, 这个Kobject就会包含所有的这些属性文件.
sysfs_ops : 属性文件对应的读/写函数
4.2 主要数据结构
Kobject
/* Kobject: include/linux/kobject.h line 60 */
Comment |
|
const char *name |
该Kobject的名称, 同时也是sysfs中的目录名称 |
struct list_head entry |
用于将Kobject加入到Kset中的list_head (Kobject与Kset的关系下章详解) |
struct kobject *parent |
指向parent kobject, 以此形成层次结构(在sysfs就表现为目录结构) |
struct kset *kset; |
该kobject属于的Kset. 可以为NULL. 如果存在, 且没有指定parent, 则会把Kset的Kobject作为parent(因为Kset是一个特殊的Kobject) |
struct kobj_type *ktype; |
该Kobject属于的kobj_type. 每个Kobject必须有一个ktype, 否则Kernel会提示错误 |
struct sysfs_dirent *sd; |
该Kobject在sysfs中的表示 |
struct kref kref; |
"struct kref”类型(在include/linux/kref.h中定义)的变量, 为一个可用于原子操作的引用计数 |
unsigned int state_initialized:1 |
指示该Kobject是否已经初始化, 以在Kobject的Init, Put, Add等操作时进行异常校验 |
unsigned int state_in_sysfs:1 |
指示该Kobject是否已在sysfs中呈现, 以便在自动注销时从sysfs中移除 |
unsigned int state_add_uevent_sent:1 |
记录是否已经向用户空间发送ADD uevent |
unsigned int state_remove_uevent_sent:1 |
记录是否已经向用户空间发送REMOVE uevent, 如果已经发送了ADD uevent, 且没有发送remove uevent, 则在自动注销时, 补发REMOVE uevent, 以便让用户空间正确处理 |
unsigned int uevent_suppress |
如果该字段为1, 则表示忽略所有上报的uevent事件 |
Note: Uevent提供了“用户空间通知”的功能实现, 通过该功能, 当内核中有Kobject的增加、删除、修改等动作时, 会通知用户空间.
有关该功能的具体内容, 会在其本文后面章节详细描述。
Attribute & bin_attribute
/* attribute: include/linux/sysfs.h */
struct attribute为普通的attribute, 使用该attribute生成的sysfs文件, 只能用字符串的形式读写. 一般这种形式用于内核把某个变量暴露给用户空间, 以便用户空间修改此变量
struct bin_attribute为二进制属性, 在struct attribute的基础上, 增加了read、write等函数,它所生成的sysfs文件可以用任何方式读写. 一般这种形式用于把某块Memory暴露给用户空间, 比如把EEPROM的存储空间暴露给用户空间, 在用户空间直接读/写EEPROM.
这种属性文件本文暂不做详细分析.
struct bin_attribute |
Comment |
struct attributeattr |
封装struct attribute |
size_tsize; |
|
void*private |
|
ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *,char *, loff_t, size_t) |
读函数 |
ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *,char *, loff_t, size_t) |
写函数 |
int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr, struct vm_area_struct *vma) |
map函数 |
Kobj_attribute
/* kobj_attribute: include/linux/kobject.h line 138 */
struct kobj_attribute |
Comment |
struct attribute attr |
表明是对struct attribute结构体的封装 |
show |
属性文件读函数 是不是有点迷茫了! 属性文件的读/写操作不是在ktype中定义的吗? 这里又是怎么回事. 耐心看完, 在本章最后一小节”总结”中会阐释其中的关系 |
store |
属性文件写函数 |
Ktype
/* include/linux/kobject.h, line 108 */
struct kobj_type |
Comment |
通过该回调函数, 可以将包含该种类型kobject的数据结构的内存空间释放掉 |
|
const struct sysfs_ops *sysfs_ops |
该种类型的Kobject的sysfs文件系统接口, 也就是属性文件的读/写函数 |
struct attribute **default_attrs |
该种类型的Kobject的atrribute列表(所谓attribute, 就是sysfs文件系统中的一个文件). 将会在Kobject添加到内核时, 一并注册到sysfs中 |
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj) |
和文件系统(sysfs)的命名空间有关, 这里不再详细说明 |
const void *(*namespace)(struct kobject *kobj) |
和文件系统(sysfs)的命名空间有关, 这里不再详细说明 |
4.3 主要API说明
Kobject
头文件: include/linux/kobject.h
实现代码: lib/kobject.c
Attribute
头文件: include/linux/sysfs.h
实现代码: fs/sysfs/file.c
Attribute API |
Comment |
sysfs_create_file(struct kobject *kobj,const struct attribute *attr) |
在某个kobject下创建1个属性文件
|
sysfs_remove_file(struct kobject *kobj, const struct attribute *attr) |
删除kobject下的某个属性文件 |
|
|
sysfs_create_group(struct kobject *kobj,const struct attribute_group *grp) |
在kobject下创建多个属性文件, attribute_group相当于一个数组, 里面可以存放多个属性文件. 有意思的这个group还能有一个name, 当name为NULL时, 所有的属性文件都会存放在kobject对应的目录下; 当name不为NULL时, 会在kobject对应的目录下产生一个name目录, 所有的属性文件都存放在name目录下 |
sysfs_remove_group(struct kobject *kobj, const struct attribute_group *grp) |
删除kobject下的某个属性组
|
sysfs_create_groups(struct kobject *kobj, const struct attribute_group **groups) |
创建多个属性组 |
sysfs_remove_groups(struct kobject *kobj,const struct attribute_group **groups) |
删除多个属性组 |
Kobj_attribute & __ATTR
头文件: include/linux/sysfs.h
这个算不上API, 它是一个宏, #define __ATTR(_name, _mode, _show, _store) . 这个宏用于初始化kobj_attribute数据结构.
4.4 关键代码分析 & DEMO
想象这样一个场景: 我们想写一个内核模块, 加载到内核(还记得内核模块怎么写吧!), 用于在/sys/下创建一个目录, 然后在该目录下创建1个属性文件, 可以读/写该属性文件. 先不要看下面的内容, 想想你会怎么做?
首先, 我们需要用kobject创建一个目录, 创建目录的方式有两种:
手动创建: 手动创建的意思是用kmalloc自行分配kobject, 定义Ktype, 调用kobject_init初始化, 然后调用kobject_add添加到内核
自动创建: 自动创建的意思是用kobject_create或者kobject_create_add搞定所有事情.
然后, 我们需要在kobject目录下创建属性文件, 创建属性文件很多种方式:
如果是手动创建的kobject目录, 则
可以自行定义属性文件
也可以在Ktype里面定义默认的属性列表(参考Ktype数据结构), 当把kobject添加到内核时, 会自动出现这些属性文件.
如果是自动创建, 则
只能通过调用Attribute API来添加属性文件, 不能定义默认属性列表. 原因是在自动创建时, 系统会在内部处理Ktype, 我们没法添加默认属性列表.
手动创建Kobject
kobject_init
初始化通过kmalloc等内存分配函数获得的struct kobject指针. 主要执行逻辑为
确定参数ktype不为空, 也就是初始化kobject时, 一定要给它一个Ktype
如果该指针已经初始化过(判断kobj->state_initialized), 打印错误提示及堆栈信息(但不是致命错误, 所以还可以继续)
调用kobject_init_internal初始化kobj内部的参数, 包括引用计数、list、各种标志等
在初始化引用计数时, 调用的函数是kref_init, 该函数会把引用计数值初始化为1.
根据输入参数, 将ktype指针赋予kobj->ktype
kobject_add
将初始化完成的kobject添加到kernel中, 参数包括需要添加的kobject、该kobject的parent(用于形成层次结构, 可以为空)、用于提供kobject name的格式化字符串. 主要执行逻辑为:
确认kobj不为空, 确认kobj已经初始化, 否则错误退出
调用内部接口kobject_add_varg, 完成添加操作
kobject_add_varg, 解析格式化字符串, 将结果赋予kobj->name, 之后调用kobject_add_internal接口, 完成真正的添加操作
kobject_add_internal: 将kobject添加到kernel. 主要执行逻辑为
校验kobj以及kobj->name的合法性, 若不合法打印错误信息并退出
调用kobject_get增加该kobject的parent的引用计数(如果存在parent的话)
如果存在kset(即kobj->kset不为空), 则调用kobj_kset_join接口加入kset. 同时, 如果该kobject没有parent, 却存在kset, 则做两件事: 将kobject的parent设为kset的kobject(kset是一个特殊的kobject), 并增加kset->kobjecct的引用计数
NOTE: 该章节暂不考虑kset, kset为NULL, 下一章专门介绍kset
通过create_dir接口,调用sysfs的相关接口,在sysfs下创建该kobject对应的目录
如果创建失败,执行后续的回滚操作,否则将kobj->state_in_sysfs置为1
kobject_get
调用kref_get, 增加引用计数
kobject_put
以系统定义接口kobject_release为参数, 调用kref_put. kref模块会在引用计数为零时, 调用kobject_release
kobject_release : 通过kref结构, 获取kobject指针, 并调用kobject_cleanup接口继续
kobject_cleanup: 负责释放kobject占用的空间, 主要执行逻辑如下
检查该kobject是否有ktype, 如果没有, 打印警告信息
如果该kobject向用户空间发送了ADD uevent但没有发送REMOVE uevent, 补发REMOVE uevent
如果该kobject有在sysfs文件系统注册, 调用kobject_del接口, 删除它在sysfs中的注册
调用该kobject的ktype的release接口, 释放内存空间
释放该kobject的name所占用的内存空间
DEMO
https://gitlab.com/study-linux/linux_driver_model/blob/master/manual_kobject_attribute.c
NOTE1: 为什么要练习手动方式创建kobject
在编写真正的内核驱动时, 我们基本上不会去直接操作Kobject. 而是采用更上层的接口. 比如我们知道设备模型中有一个device的概念, 当我们需要添加一个device时, 调用的是系统提供的device_add接口, 该函数最终会去处理Kobject相关的事情.
我们可以理解成device封装了kobject, 而这种封装动作, 在处理kobject的时候, 都是用的上面提到的手动方式.
所以我们在这里练习手动方式, 为的是更好的理解后面的章节(device, device_driver, bus, class, 它们才是真正的重点).
自动创建Kobject
分析几个关键函数
kobject_create
该接口为kobj分配内存空间, 并以系统内置的dynamic_kobj_ktype为参数, 调用kobject_init接口,完成后续的初始化操作
kobject_create_and_add
kobject_create和kobject_add的组合
DEMO
https://gitlab.com/study-linux/linux_driver_model/blob/master/auto_kobject_attribute.c
4.5 sysfs读写属性文件的细节
前文我们介绍了sysfs, 知道它是一种虚拟文件系统.
在介绍Kobject时, 我们也提及了Attribute, 知道Kobject代表一个目录, 而Attribute代表该目录下的文件, 我们可以通过sysfs来读/写该属性文件.
sysfs是一种虚拟文件系统, 虚拟文件系统也是文件系统, Linux的文件系统都会提供open, read, write等接口给用户空间, 这也是Linux中一切皆文件的概念.
当我们想通过sysfs读写属性文件的时候, 首先调用的肯定是sysfs的open函数, 然后是sysfs的read/write函数. 然后呢? read/write是如何操作到具体的属性文件的呢? struct attribute数据结构很简单, 没有看见什么接口函数去响应sysfs的read/write接口啊!
接下来, 我们简单梳理一下这个流程:
所有的文件系统, 都会定义一个struct file_operations变量, 用于描述本文件系统的操作接口, sysfs也不例外. 定义在fs/kernfs/file.c .
下一步, 代码会进入到fs/sysfs/file.c 中的kernfs_ops, 有很多个kernfs_ops, 一般针对属性文件的是sysfs_file_kfops_rw(针对二进制属性的是sysfs_bin_kfops_rw, 这里不细说).
下一步sysfs_file_kfops_rw会找到kobject(每个属性文件都会对应某个kobject, 当我们尝试去open某个属性文件时, 我们就已经知道它的kobject了), 获取kobject->ktype.
下一步, ktype->sysfs_ops.
稍微总结一下, 上述流程: VFS -> file_operations -> sysfs(kernfs_ops) -> (kobject->ktype) -> (ktype->sysfs_ops).
在接下来, 如果是我们自己定义ktype, 上述流程最终就会直接调用ktype->sysfs_ops, 如手动创建Kobject的DEMO演示的那样;
如果是系统定义的ktype, 则上述流程还会增加一环, (ktype->sysfs_ops) -> (kobj_attribute->show/store) , 如自动创建Kobject的DEMO演示的那样.
还有一个问题需要思考:
通过sysfs读属性文件, 意味着你需要往用户空间的buffer写入数据,通过sysfs写属性文件, 意味着你需要从用户空间的buffer读取数据.
在内核模块一文中我们讨论过, 内核不能直接访问用户空间的buffer, 需要用copy_to_user/copy_from_user.
如果你仔细留意了DEMO中的代码, 会发现我们写的store/show函数里面并没有去处理这个buffer, 这是为什么?
原因是系统内部的file_operations这一环节帮我们处理了这些问题, 而且它还考虑了互斥的问题, 有兴趣可以看看它的代码.
4.6 kobj_attribute, device_attribute, driver_attribute, bus_attribute, class_attribute
内核中与设备模型相关的几个主要数据结构, 都存在对应的Attribute.
它们的表现形式与kobj_attribute类似, 在读写属性文件的时候, 内核系统会自动帮你处理到(xxx_attribute->show/store)这一环, 你只需要用它们提供的API(或者宏)来定义属性文件即可.
__ATTR:
#define __ATTR(_name, _mode, _show, _store) {\
.attr = {.name = __stringify(_name),\
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) },\
.show= _show,\
.store= _store,\
}
static struct kobj_attribute foo_attribute =
__ATTR(foo,0666, foo_show, foo_store);
DEVICE_ATTR
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
DRIVER_ATTR
#define DRIVER_ATTR(_name, _mode, _show, _store) \
struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define BUS_ATTR(_name, _mode, _show, _store)\
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define CLASS_ATTR(_name, _mode, _show, _store) \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)
5. kset
5.1 简介
理解完kobject, 再来看kset就轻松多了. 可以结合下一节的kset数据结构来阅读本节内容.
Kset是一个特殊的Kobject(因此它也会在/sys/中以目录的形式出现), 它相当于一个容器,下面可以挂接多个kobject. 它用来集合相似的Kobject(这些Kobject可以有相同的Ktype, 也可以有不同的Ktype).
前面说过, Kobject在/sys/表现为一个目录, 这个目录的父目录是谁, 是由kobject->parent决定的.
当某个kobject属于某个kset时, 大多数情况下, 该kobject->parent会指向kset的kobject. 这种情况下, 我们可以把kset简单看做父目录, 而属于kset的kobject都是子目录. 就像下面这张图一样:
多说一句, 注意是大多数情况下, 就算某个kobject属于某个kset, 也不一定代表着该kobject->parent就一定指向kset的kobject. 相应的, 这种情况下就不存在父/子目录的关系.
Kset主要是跟Uevent事件上报相关的, Uevent会在下一章会详解介绍.
上文提到kset用来集合相似的kobject, 这个相似是指kset可以决定它下属的kobject可以上报哪些uevent事件; 有哪些uevent信息是下属所有kobject都要统一上报的.
5.2 主要数据结构
Kset
/* include/linux/kobject.h, line 159 */
Comment |
|
struct list_head list |
用于链接该kset下所有的kobject的链表头 |
spinlock_t list_lock |
锁, 用于互斥往链表中增/删kobject |
struct kobject kobj |
kset自己的kobject(kset是一个特殊的kobject, 也会在sysfs中以目录的形式体现) |
const struct kset_uevent_ops *uevent_ops |
该kset的uevent操作函数集. 当任何Kobject需要上报uevent时, 都要调用它所从属的kset的uevent_ops, 添加环境变量, 或者过滤event(kset可以决定哪些event可以上报). 因此, 如果一个kobject不属于任何kset时, 是不允许发送uevent的 |
kset_uevent_ops
/* include/linux/kobject.h, line 123 */
struct kset_uevent_ops |
Comment |
int (* const filter)(struct kset *kset, struct kobject *kobj) |
filter, 当任何Kobject需要上报uevent时, 它所属的kset可以通过该接口过滤, 阻止不希望上报的event, 从而达到从整体上管理的目的 |
const char *(* const name)(struct kset *kset, struct kobject *kobj) |
name, 该接口可以返回kset的名称. 如果一个kset没有合法的名称, 则其下的所有Kobject将不允许上报uevent |
int (* const uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env); |
uevent, 当任何Kobject需要上报uevent时, 它所属的kset可以通过该接口统一为这些event添加环境变量. 因为很多时候上报uevent时的环境变量都是相同的, 因此可以由kset统一处理, 就不需要让每个Kobject独自添加了 |
kobj_uevent_env就是表示具体的事件, 为在Uevent一章中详解解释
5.3 主要API说明
Kset是一个特殊的kobject, 因此其初始化、注册等操作也会调用kobject的相关接口, 除此之外, 会有它特有的部分. 另外, 和Kobject一样, kset的内存分配, 可以由上层软件通过kmalloc自行分配, 也可以由kset模块负责分配, 具体如下
kset
头文件: include/linux/kobject.h, line 166
实现文件: lib/kobject.c
Comment |
|
extern void kset_init(struct kset *kset) |
该接口用于初始化已分配的kset, 主要包括调用kobject_init_internal初始化其kobject, 然后初始化kset的链表头和锁. 需要注意的是, 如果使用此接口, 上层软件必须提供该kset中的kobject的ktype |
extern int __must_check kset_register(struct kset *kset) |
先调用kset_init 然后调用kobject_add_internal将其kobject添加到kernel 然后发送一个KOBJ_ADD uevent事件 |
extern void kset_unregister(struct kset *kset) |
直接调用kobject_put释放该kset的kobject. 当其kobject的引用计数为0时, 即调用ktype的release接口释放kset占用的空间 |
extern struct kset * __must_check kset_create_and_add(const char *name, const struct kset_uevent_ops *u, struct kobject *parent_kobj) |
会调用内部接口kset_create动态创建一个kset, 并调用kset_register将其注册到kernel. kset_create是一个内部接口(static 函数), 该接口使用kzalloc分配一个kset空间, 并定义一个kset_ktype类型的ktype, 用于释放所有由它分配的kset空间 |
5.4 DEMO
想象这样一种场景: 我们想在/sys/下建1个父目录, 该父目录下会有1个属性文件, 1个子目录, 子目录下会有1个属性文件. 可以读写这2个属性文件. 想一想你会怎么做?
首先, 我们需要创建一个kset, 并把该kset添加到内核中, 这样/sys/下就会出现一个目录. 你可以用手动的方式创建(kmalloc -> kset_register), 也可以用自动的方式创建(kset_create_and_add).
然后, 自行定义一个属性文件, 添加到该目录下.
接下来, 创建一个kobject, 让kobject->parent=NULL, kobject->kset=kset. 然后把该kobject添加到内核, 这样该kobject就会自动变成kset的子目录.
最后, 调用Attribute API, 在该kobject下创建一个属性文件.
DEMO: https://gitlab.com/study-linux/linux_driver_model/blob/master/manual_kset_kobject_attribute.c
我们做了一个DEMO, kset是用手动方式创建的, kobject也是采用手动的方式(只能用手动方式, 如果你去看kobject的API, 就会发现, 只有通过手动方式, 才能指定kobject的kset).
建议你阅读到这里时, 自己尝试用自动创建kset的方式完成上述场景.
6. Uevent
6.1 简介
Uevent是针对Kobject的, 用于在Kobject状态发生改变时, 例如增加、移除等, 通知用户空间程序. 用户空间程序收到这样的事件后, 会做相应的处理.
该机制通常是用来支持热拔插设备的, 例如U盘插入后, USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject), 并告知用户空间程序, 为该U盘动态的创建/dev/目录下的设备节点, 更进一步, 可以通知其它的应用程序, 将该U盘设备mount到系统中, 从而动态的支持该设备.
下面图片描述了Uevent模块在内核中的位置
由此可知, Uevent的机制是比较简单的, 设备模型中任何设备有事件需要上报时, 会触发Uevent提供的接口.
Uevent模块准备好上报事件的格式后, 可以通过两个途径把事件上报到用户空间:一种是通过kmod模块, 直接调用用户空间的可执行文件(简称uevent helper); 另一种是通过netlink通信机制, 将事件从内核空间传递给用户空间
这两种方式可以同时存在, 也就是说只要内核同时使能了这两种方式, 一个事件会同时通过这两种方式上报.
6.2 主要数据结构
kobject_action
/* include/linux/kobject.h, line 50 */
kobject_action定义了event的类型, 包括:
Comment |
|
KOBJ_ADD |
|
KOBJ_REMOVE |
|
KOBJ_CHANGE |
kobject或上层数据结构(内嵌了kobject)的状况或内容发生变化 如果设备驱动需要上报的事件不在kobject_action定义的事件范围内, 或者是自定义的事件, 可以使用该event,并携带相应的参数 |
KOBJ_MOVE |
kobject或上层数据结构(内嵌了kobject) 更改名称或者更改Parent(意味着在sysfs中更改了目录结构) |
KOBJ_ONLINE |
|
KOBJ_OFFLINE |
|
KOBJ_MAX |
|
kobj_uevent_env
可以把kobj_uevent_env简单理解成一大块内存, 这块内存的size是2048. 每当需要上报一个uevent事件时, 就会生成这样一块内存. 然后用add_uevent_var函数向此块内存中添加此次事件的相关信息(ACTION=%s, DEVPATH=%s, SUBSYSTEM=%s). 这些信息都是一些字符串.
添加完事件信息后, 就会把事件上报用户空间, 上一节提到过, 内核发送uevent事件有两种方式:
Kmod方式直接调用用户空间的可执行程序: 这种方式下kobj_uevent_env还会保存可执行程序的路径(例如/sbin/hotplug), 以及传递给可执行程序的参数. 准备好这些内容之后就会把uevent上报.
netlink方式: 这种方式下, 会把kobj_uevent_env中保存的事件信息copy到一个叫sk_buff的缓存中, 然后通过socket机制上报.
/* include/linux/kobject.h, line 31 */
#define UEVENT_NUM_ENVP32/* number of env pointers */
#define UEVENT_BUFFER_SIZE2048/* buffer for the variables */
/* include/linux/kobject.h, line 123 */
struct kobj_uevent_env |
Comment |
char *argv[3] |
argv[0] : 保存uevent helper可执行的路径(例如/sbin/hotplug) argv[1] : 传递给uevent helper的第一个参数, 是一个字符串, 一般是kobject所属的kset的名字 argv[2] : 传递给uevent helper的第二个参数, 一般为NULL |
指针数组 一个事件会有多个信息, 每个信息都是一个字符串(见本节上文), 所有的信息都依次存放在buf[UEVENT_BUFFER_SIZE]中. envp保存每个信息的buf[UEVENT_BUFFER_SIZE]中的起始地址. 总共可以保存UEVENT_NUM_ENVP个信息的地址 |
|
int envp_idx; |
每添加一个信息, envp_idx++ 记录总共添加了多少个信息 |
char buf[UEVENT_BUFFER_SIZE] |
保存信息的buf, 大小为UEVENT_BUFFER_SIZE
|
int buflen |
表示buf中已经用去多少空间 sizeof(buf) – buflen就是剩余空间大小 |
kset_uevent_ops
kset量身订做的一个数据结构, 方便kset统一管理下属kobject的uevent事件.
具体细节已经放在上文kset的主要数据结构中介绍了
6.3 主要API说明
Uevent
头文件 : include/linux/kobject.h, line 214
实现文件: lib/kobject_uevent.c
Uevent API |
Comment |
int kobject_uevent(struct kobject *kobj, enum kobject_action action) |
针对某个kobject发送kobject_action类型的事件. 记住, 该API的参数是一个kobject, 就是说只能针对某个kobject发送uevent事件 |
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]) |
封装上一个API, 不同的是, 该API会附加一些事件信息, 事件信息存储在envp中 |
int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...); |
注意它的第一个参数, kobj_uevent_env, 就是我们在数据结构中介绍的存储事件信息的内存块. 该API可以理解成操作内存块的接口, 用于把事件信息(就是后面的参数: 字符串)存储到内存块中. |
int kobject_action_type(const char *buf, size_t count, enum kobject_action *type) |
将enum kobject_action类型的Action转换为字符串 |
6.4 关键代码分析
kobject_uevent_env : 以envp为环境变量, 上报一个指定action的uevent, 具体动作如下:
查找kobj本身或者其parent是否从属于某个kset, 如果不是, 则报错返回(由此可以说明, 如果一个kobject没有加入kset, 是不允许上报uevent的)
查看kobj->uevent_suppress是否设置, 如果设置, 则忽略所有的uevent上报并返回(由此可知, 可以通过Kobject的uevent_suppress标志, 管控Kobject的uevent的上报)
如果所属的kset有uevent_ops->filter函数, 则调用该函数, 过滤此次上报(回头查看kset_uevent_ops数据结构, 这佐证了有关filter接口的说明, kset可以通过filter接口过滤不希望上报的event, 从而达到整体的管理效果)
判断所属的kset是否有合法的名称(称作subsystem, 和前期的内核版本有区别),否则不允许上报uevent
分配一个用于此次上报的、存储环境变量的buffer(也就是分配一个kobj_uevent_env数据结构), 并获得该Kobject在sysfs中路径信息(用户空间程序需要依据该路径信息在sysfs中访问它)
调用add_uevent_var接口(下面会介绍), 将Action、路径信息、subsystem等信息, 添加到kobj_uevent_env内存块中
如果传入的envp不空, 则解析传入的环境变量中, 同样调用add_uevent_var接口,添加到kobj_uevent_env内存块中
如果所属的kset存在uevent_ops->uevent接口, 调用该接口, 添加kset统一的环境变量到kobj_uevent_env内存块中
根据ACTION的类型,设置kobj->state_add_uevent_sent和kobj->state_remove_uevent_sent变量, 以记录正确的状态
调用add_uevent_var接口, 添加格式为"SEQNUM=%llu”的序列号
如果定义了"CONFIG_NET”, 则使用netlink发送该uevent
如果定义了” CONFIG_UEVENT_HELPER”, 则调用init_uevent_argv初始化kobj_uevent_env的argv变量(回看kobj_uevent_env数据结构), 即指定uevent helper路径, 参数; 然后往kobj_uevent_env内存块中添加一些环境变量信息(HOME=/, PATH=/sbin:/bin:/usr/sbin:/usr/bin).
最后, 用kmod模块提供的call_usermodehelper函数, 上报uevent
其中uevent helper可执行程序的路径是由内核配置项CONFIG_UEVENT_HELPER_PATH(位于./drivers/base/Kconfig)决定的(可参考lib/kobject_uevent.c, line 32), 该配置项指定了一个用户空间程序(或者脚本), 用于解析上报的uevent, 例如"/sbin/hotplug”
call_usermodehelper的作用, 就是fork一个进程, 以uevent为参数, 执行uevent_helper
kobject_uevent : 和kobject_uevent_env功能一样, 只是没有指定任何的附加事件信息
add_uevent_var : kobj_uevent_env内存块操作接口, 以格式化字符的形式(类似printf、printk等), 将环境变量copy到内存块中
6.5 关于如何指定uevent helper可执行程序
上面介绍kobject_uevent_env的内部动作时, 有提到, Uevent模块通过Kmod上报Uevent时,会通过call_usermodehelper函数, 调用用户空间的可执行文件(或者脚本, 简称uevent helper )处理该event. 而该uevent helper的路径保存在uevent_helper数组中.
可以在编译内核时, 通过CONFIG_UEVENT_HELPER_PATH配置项, 静态指定uevent helper.但这种方式会为每个event fork一个进程, 随着内核支持的设备数量的增多, 这种方式在系统启动时将会是致命的(可以导致内存溢出等). 因此只有在早期的内核版本中会使用这种方式, 现在内核不再推荐使用该方式. 因此内核编译时, 需要把该配置项留空
在系统启动后, 大部分的设备已经ready, 可以根据需要, 重新指定一个uevent helper, 以便检测系统运行过程中的热拔插事件. 这可以通过把helper的路径写入到"/sys/kernel/uevent_helper”文件中实现.
实际上, 内核通过sysfs文件系统的形式, 将uevent_helper数组开放到用户空间, 供用户空间程序修改访问, 具体可参考"./kernel/ksysfs.c”中相应的代码, 这里不再详细描述
7. /sys/下的顶层目录
前面我们已经介绍了Kobjet, Kset. 知道Kobject代表/sys/下的一个目录, 用于组织层次关系; 也知道kset是特殊的Kobject, 因此也是/sys/下的一个目录, 用于集合有相似功能的Kobject(主要是Uevent事件的处理).
下面我们要介绍一下/sys/下的一些顶层目录, 你暂时不用去关心每个目录的作用, 只需要结合前面学到的知识, 了解每个目录是如何创建的即可.
/sys/devices
/sys/dev
/sys/dev/block
/sys/dev/char
File : drivers/base/core.c
Function:
int __init devices_init(void)
{
devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
if (!devices_kset)
return -ENOMEM;
dev_kobj = kobject_create_and_add("dev", NULL);
if (!dev_kobj)
goto dev_kobj_err;
sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
if (!sysfs_dev_block_kobj)
goto block_kobj_err;
sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
……
}
注意该函数用__init(内核模块一文在介绍加载函数时, 介绍过该标志)标示, 代表它是在系统初始化阶段被调用的.
/sys/bus
/sys/devices/system
File : drivers/base/bus.c
Function:
int __init buses_init(void)
{
bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
if (!bus_kset)
return -ENOMEM;
system_kset = kset_create_and_add("system", NULL, &devices_kset->kobj);
if (!system_kset)
return -ENOMEM;
return 0;
}
同样, __init标示buses_init.
system_kset在创建时指定了parent, 所以它位于/sys/devices目录下.
/sys/class
File : drivers/base/class.c
Function:
int __init classes_init(void)
{
class_kset = kset_create_and_add("class", NULL, NULL);
if (!class_kset)
return -ENOMEM;
return 0;
}
/sys/kernel
File : kernel/ksysfs.c
Function:
static int __init ksysfs_init(void)
{
int error;
kernel_kobj = kobject_create_and_add("kernel", NULL);
if (!kernel_kobj) {
error = -ENOMEM;
goto exit;
}
……
}
同样, __init标示ksysfs_init.
/sys/module
File : kernel/params.c
Function:
static int __init param_sysfs_init(void)
{
module_kset = kset_create_and_add("module", &module_uevent_ops, NULL);
if (!module_kset) {
printk(KERN_WARNING "%s (%d): error creating kset\n",
__FILE__, __LINE__);
return -ENOMEM;
}
……
}
subsys_initcall(param_sysfs_init);
同样, __init标示param_sysfs_init.
/sys/block
/sys/block其实是一个class, 内核系统定义了一个名为”block”的class, 当把该class注册进内核时, 就会产生该目录 (阅读完Class一章后你就会明白这个目录是如何生成的).
8. BUS
8.1 简介
总线是处理器与一个或者多个设备之间的通道, 为了方便设备模型的实现, 内核规定, 系统中的每个设备都要连接在一个Bus上. 这个Bus可以是一个内部Bus、虚拟Bus或者Platform Bus.
Bus在内核中的作用主要体现为
Bus下面挂接devices和drivers
Bus定义device和driver的匹配规则
Bus可以定义下属devices和drivers的默认属性
系统中有各种各样的总线, 这些总线的直观感受就是/sys/bus/下的一个目录.例如/sys/bus/spi就代表SPI Bus, /sys/bus/platform/代表 Platfrom Bus.
前一章我们介绍了/sys/bus是如何生成的, 那/sys/bus目录下的这些Bus是怎么生成的呢? Bus在内核系统中到底有什么作用呢? 带着这些疑问, 开始下面的章节.
8.2 主要的数据结构
bus_type
内核通过struct bus_type结构抽象Bus, 每一个Bus都对应一个bus_type结构体.
/* inlcude/linux/device.h, line 93 */
struct bus_type |
Comment |
const char *name |
Bus的名字 对应/sys/bus/下目录的名字. |
const char *dev_name |
总线下设备(struct device)的默认前缀名 当定义了一个设备, 在添加到总线下时, 没有指定设备的名称, 则总线就会帮它定义一个名字, 形式为: bus->dev_name+device ID <总线下可以挂载多个device, 每个device在该总线下都有一个ID, 这个ID一般是总线自动分配的> |
struct device *dev_root |
就像每个kobject都可以有一个parent, 每个device都可以有一个parent device. dev_root是该Bus下所有device的默认父设备 |
struct device_attribute *dev_attrs; |
/* use dev_groups instead */ |
const struct attribute_group **bus_groups |
在定义一个bus_type的时候, 可以指定一组”属性组”, 关于属性组的概念, 可以参考4.3节 Attribute中的API: sysfs_create_group 当bus添加到内核时, 会在/sys/bus/xxx目录下创建属性组中定义的属性文件 |
const struct attribute_group **dev_groups |
又是一个属性组 当有device添加到该Bus下时, 会为每个device创建这些属性文件 |
const struct attribute_group **drv_groups |
又是一个属性组 当有device_driver添加到该Bus下时, 会为每个driver创建这些属性文件 |
int (*match)(struct device *dev, struct device_driver *drv) |
当我们打算创建一条总线时, 我们需要定义该函数. 该函数用于定义该Bus下, device和device_driver的匹配规则 当任何属于该Bus的device或者device_driver添加到该Bus时, 系统都会调用该函数, 该函数会告诉系统device和driver是否匹配, 如果匹配成功, 该函数要返回非零值, 此时系统就会执行后续的处理 |
int (*uevent)(struct device *dev, struct kobj_uevent_env *env) |
还记得kset的主要作用吗? 还记得kset-> uevent_ops->uevent函数的作用吗? 不记得的话回头看一下 当我们打算创建一条总线时, 需要定义该函数. 该函数的作用与kset中uevent函数的作用类似, 当任何属于该Bus的device,发生添加、移除或者其它动作时, Bus模块的核心逻辑就会调用该接口, 以便添加uevent事件的事件信息. |
int (*probe)(struct device *dev) |
当某个device和driver匹配上了之后, 系统调用driver->probe来初始化driver了. 但是, 如果需要probe(其实就是初始化)指定的device话, 需要保证该device所在的bus是被初始化过、确保能正确工作的. 这就要就在执行device_driver的probe前, 先执行它的bus的probe Note: 并不是所有的bus都需要probe和remove接口的, 因为对有些bus来说(例如platform bus), 它本身就是一个虚拟的总线, 无所谓初始化, 直接就能使用, 因此这个函数可以为NULL |
int (*remove)(struct device *dev) |
逻辑与上面类似. |
void (*shutdown)(struct device *dev) |
与probe逻辑类似, 电源管理相关, 暂不说明 |
int (*suspend)(struct device *dev, pm_message_t state) |
与probe逻辑类似, 电源管理相关, 暂不说明 |
int (*resume)(struct device *dev) |
与probe逻辑类似, 电源管理相关, 暂不说明 |
int (*online)(struct device *dev) |
热插拔相关, 如果Bus的device想支持热插拔, 则本Bus必须定义online函数 |
int (*offline)(struct device *dev) |
热插拔相关, 如果Bus的device想支持热插拔, 则本Bus必须定义offline函数 |
const struct dev_pm_ops *pm |
电源管理相关的逻辑, 暂不说明 |
struct iommu_ops *iommu_ops |
暂不说明 |
struct subsys_private *p |
很重要的一个结构, 我们专门说明 |
struct lock_class_key lock_key |
|
subsys_private
这是一个很有意思的结构体, 我们先看看它的数据结构
/* drivers/base/base.h, line 28 */
从数据结构中我们可以看到, 这个结构体就是集合了一些bus模块需要使用的私有数据, 例如kset啦、klist啦等等. 那为什么这个结构体名要叫subsys_private, 而不叫bus_private呢?
看看include/linux/device.h中的struct class结构(我们会在下一章中介绍class)就知道了,因为class结构中也包含了一个一模一样的struct subsys_private指针, 看来class和bus很相似啊
想到这里, 就好理解了, 无论是bus, 还是class, 或者其它包含subsys_private的数据结构, 它们都可以理解成一个sub-system, sub-system就像一个独立的王国, 它下面会包含形形色色的devices, drivers等.
所以, 内核用subsys_private这样一个数据结构抽象了各种sub-system中的公共内容.
8.3 主要API说明
Bus
与Bus相关的API主要包括:
bus的注册和注销
本bus下有device或者device_driver注册到内核时的处理
本bus下有device或者device_driver从内核注销时的处理
device_drivers的probe处理
管理bus下的所有device和device_driver
Comment |
|
extern int __must_check bus_register(struct bus_type *bus) |
向系统添加一条新的Bus |
extern void bus_unregister(struct bus_type *bus) |
从系统移除某一条Bus
|
extern int bus_register_notifier(struct bus_type *bus, struct notifier_block *nb) |
注册到Bus的通知链 当Bus上有devices的addition/removal, 或者当device与driver 配对/解对成功时, 通知链上的所有人都会收到通知 |
extern int bus_unregister_notifier(struct bus_type *bus, struct notifier_block *nb) |
从Bus的通知链中移除 移除之后将不在收到通知 |
头文件: drivers/base/base.h
实现文件: drivers/base/bus.c
bus API |
Comment |
extern int bus_add_device(struct device *dev) |
当调用device_add把一个device添加到Bus上时, device_add内部会调用该函数, 处理device在添加到Bus上时, Bus这边相关的事情 |
extern void bus_remove_device(struct device *dev |
与上相反, device从Bus移除时, 调用该函数 |
extern void bus_probe_device(struct device *dev) |
当调用device_add把某个device添加到Bus时, 如果bus_set->subsys_private-> drivers_autoprobe为1的话(在创建Bus时, 默认就是1), 则会在Bus的drivers链表下搜寻所有的driver, 看是否有可以与device配对的driver, 如果有, 则调用driver->probe开始初始化 |
extern int bus_add_driver(struct device_driver *drv) |
当调用driver_register把某个driver添加到Bus时, driver_register内部会调用该函数, 处理driver添加到Bus上时, Bus这边相关的事情 |
extern void bus_remove_driver(struct device_driver *drv) |
与上相反, 在driver_unregister时调用该函数 |
Driver & Device绑定相关
头文件: include/linux/device.h
实现文件: drivers/base/dd.c
这一节的内容目前可以跳过阅读(现在可能还不太好理解), 在后面的章节中, 会引用到这里的相关函数, 需要了解细节的时候, 可以查阅此章节.
device_bind_driver
把某个driver绑定到一个device, 在调用该函数时, device->driver指针已经指向了某个driver.
具体执行逻辑如下:
driver_sysfs_add(struct device *dev)
如果device属于某个Bus, 则在Bus的通知链上发送消息BUS_NOTIFY_BIND_DRIVER
在device所指向的driver的目录下, 创建一个指向该device的链接, 链接的名称就是device->kobj的name, 例如:
在该device的目录下, 创建一个指向该driver的链接, 链接的名称就是”driver”
driver_bound(struct device *dev)
判断该device (dev->p->knode_driver) 有没有加入到某个driver的链表头
如果没有, 则将它加入所指向driver的链表头(dev->driver->p->klist_devices), 从这里可以看到, 一个device只能bound到一个driver, 而一个driver下可以挂接多个device
调用driver_deferred_probe_trigger处理延迟probe机制, 这里暂时不关注
如果device属于某个Bus, 则在Bus的通知链上发送消息BUS_NOTIFY_BOUND_DRIVER
device_release_driver
detach device from driver.
具体执行逻辑如下:
__device_release_driver(struct device *dev)
获取device->driver所指向的driver
调用driver_sysfs_remove删除device_bind_driver中创建的2个目录链接
如果device属于某个Bus, 则在Bus的通知链上发送消息BUS_NOTIFY_UNBIND_DRIVER
如果device属于某个Bus并且该Bus定义了remove函数, 则调用该Bus的remove函数; 否则如果device->driver所指向的driver定义了remove函数, 则调用driver->remove
调用devres_release_all处理相关事情, 暂时不去了解细节
将device->driver和device->driver_data设置为NULL
将device (dev->p->knode_driver) 从driver的链表中删除
如果device属于某个Bus, 则在Bus的通知链上发送消息BUS_NOTIFY_UNBOUND_DRIVER
device_attach
代表有某个device被添加到内核, 尝试把该device绑定到某个driver, 具体的执行逻辑如下:
如果device->driver已经指向了某个driver, 并且该device没有添加到任何driver的链表头, 则调用device_bind_driver
否则, 如果device->driver为NULL, 则针对该Bus下的每一个driver, 执行如下逻辑:
首先调用Bus中定义的match函数, 判断driver与该device是否匹配. 如果不匹配则直接返回
如果匹配, 则调用driver_probe_device (在本节下面分析)
driver_attach
代表有某个driver被添加到内核, 尝试把该driver绑定到Bus下的一个或多个device, 具体执行逻辑如下:
driver_attach(struct device_driver *drv)
针对Bus下的每个device, 执行如下逻辑:
首先调用Bus中定义的match函数, 判断driver与该device是否匹配. 如果不匹配则直接返回
如果匹配, 而且该device->driver为NULL, 则调用driver_probe_device. (在本节下面分析)
driver_probe_device
driver_probe_device(struct device_driver *drv, struct device *dev)
尝试绑定drv和dev, 具体指向逻辑如下:
首先, 判断device是否已经注册, 即dev->kobj.state_in_sysfs是否为1, 如果不为1, 则返回错误
如果为1, 则调用really_probe(dev, drv)处理后续事情 (在本节下面分析)
really_probe
really_probe(struct device *dev, struct device_driver *drv)
具体执行逻辑如下:
将dev->driver赋值为drv
调用pinctrl_bind_pins(dev), 该函数主要做的事情是初始化device相关的GPIO, 比如我们在定义了LCD device的时候, 会指定该device用到了哪些GPIO, 是什么复用模式, 这句代码就会自动配置这些GPIO为正确的状态. 这个函数依赖GPIO子系统, 我们会有一篇专门的文章来详述GPIO子系统.
调用driver_sysfs_add(dev)创建一些/sys/下的目录链接, 前面有介绍这个函数
如果该Bus定义了probe函数, 则调用Bus的probe函数, 否则如果driver定义了probe函数, 则调用driver的probe函数
调用driver_bound(dev)进行处理, 前面有介绍这个函数
8.4 关键代码分析
bus_register
bus添加到内核是由bus_register接口实现的, 该接口主要的功能就是创建了一些文件夹和一些属性文件, 比较简单, 执行逻辑如下:
为bus_type中struct subsys_private类型的指针分配空间, 并更新priv->bus和bus->p两个指针为正确的值
初始化通知链 priv->bus_notifier
接下来用手动创建kset的方式(可以回头看下手动创建kset的DEMO), 创建priv->subsys, 这个kset代表本Bus, 即/sys/bus/xxx中的xxx目录
将priv->subsys.kobj的name初始化为bus_type->name, 该name就是xxx目录的目录名
将priv->subsys.kobj的kset初始化为bus_kset, bus_kset是系统创建的, 代表/sys/bus这个目录(如果不记得了, 回头看下 “/sys/下的顶层目录” 一章), 由kobject的特性可知, 如果不指定priv->subsys.kobj的parent, 则该kset会做为它的parent. 实际上也是如此, 所以所有的Bus都会存在于/sys/bus目录下
将priv->subsys.kobj的ktype初始化为bus_ktype, 该ktype也是由系统创建的. 我们自己也定义过ktype(在手动创建kobject的DEMO中), 因此知道该ktype会负责释放资源和读/写priv->subsys.kobj下的属性文件(即/sys/bus/xxx/下的属性文件).
调用kset_register将priv->subsys注册到内核中, 该调用完毕之后, 就会在/sys/bus/下生成xxx目录
调用bus_create_file在/sys/bus/xxx下创建一个uevent属性文件.
这个属性文件是系统定义的, 用BUS_ATTR这个宏定义, 我们在4.6节描述过这个宏, 回看4.5, 4.6两节, 你应该理解用户空间通过sysfs读写该属性文件的流程.
不过系统创建的这个uevent属性文件, 只提供了写入的功能, 读函数为NULL, 也就是说用户空间只能写入该属性文件
当用户空间往该属性文件写入一个kobject_action类型的字符串时, 比如KOBJ_ADD, 属性文件的写函数会调用kobject_uevent, 针对priv->subsys.kobj这个kobject, 上报一个kobject_action类型的Uevent事件, 比如上报KOBJ_ADD事件.
至于上报这个事件有什么用, 暂时还不清楚
调用kset_create_and_add创建priv->devices_kset这个kset, devices_kset对应/sys/bus/xxx目录下名称为”devices”的目录. Bus下的所有devices, 都会在该目录下创建一个链接
调用kset_create_and_add创建priv->drivers_kset这个kset, drivers_kset对应/sys/bus/xxx目录下名称为”drivers’的目录. Bus下的所有drivers, 都会在该目录下创建自己的目录
初始化priv指针中的mutex、klist_devices和klist_drivers等变量
调用add_probe_files在/sys/bus/xxx目录下创建2个属性文件, 这两个属性文件也是系统用BUS_ATTR这个宏定义的.
第一个属性文件是drivers_probe, 它只有写入函数, 当用户空间往该属性文件写入一个属于该Bus的device的名称时, 会最终调用device_attach, 触发系统去查找该device对应的driver, 匹配上之后调用driver->probe初始化
第二个属性文件是drivers_autoprobe, 提供了读写函数, 用于读取/写入priv->drivers_autoprobe
调用bus_add_groups在/sys/bus/xxx目录下创建bus_type-> bus_groups中定义的属性文件组
bus_add_device
内核提供了device_register接口用于把某个device添加到某个Bus, device_register会调用device_add, device_add会调用bus_add_device处理Bus端相关的事情
bus_add_device的处理逻辑:
调用内部的device_add_attrs接口, 将由bus->dev_attrs指针定义的默认属性组添加到内核中, 它们会出现在device所在的目录下
调用内部的device_add_groups接口, 将由bus-> dev_groups指针定义的默认属性组添加到内核中, 它们会出现在device所在的目录下
调用sysfs_create_link接口, 将该device在sysfs中的目录, 链接到该bus的devices目录下, 例如
/sys/devices/platform/i8042为device所在的真正目录, 为了方便管理, 内核在device所属Bus的devices目录下创建了一个符号链接, 前面在介绍bus_type数据结构的时候已经提到过这一点.
调用sysfs_create_link接口, 在该设备的sysfs目录中(如/sys/devices/platform/i8042/)中, 创建一个指向该设备所在bus目录的链接, 取名为subsystem, 例如
最后, 毫无疑问, 要把该device指针挂接到bus->priv->klist_devices链表头中, 这样, 就可以通过该链表头查找Bus下所有的devices.
bus_add_driver
内核提供了driver_register接口用于把某个driver添加到某个Bus, driver_register会调用bus_add_driver处理Bus端相关的事情
bus_add_driver的处理逻辑:
处理driver-> driver_private相关的事情, struct driver_private会在介绍driver的章节详解介绍, 该结构体包含driver对应的kobject, 以及一些链表头
为该driver的struct driver_private指针(priv)分配空间
初始化priv->klist_devices链表头, 该链表用于保存driver对应的所有device, 也就是说一个driver可以与多个device匹配. 但是后面我们会知道, 一个device只能匹配一个driver
初始化priv->driver指向该driver
初始化driver->p指向该priv
将priv->kobj.kset赋值为bus->p->drivers_kset, 然后调用kobject_init_and_add设置kobj->ktype为driver_ktype, 并把该kobj添加到内核. 此步骤执行完毕之后, 就会在/sys/bus/xxx/drivers目录下生成一个driver对应的目录. 目录的名称是driver->name. 例如 “/sys/bus/platform/drivers/i8042”
将priv->knode_bus加入到bus->p->klist_drivers链表头, 也就是把driver挂接到Bus的drivers链表头下
如果该driver的Bus的drivers_autoprobe为1, 则调用driver_attach, 看看该driver能否和Bus下的device匹配上, 如果匹配上, 调用driver->probe初始化
调用driver_create_file接口, 在sysfs的该driver的目录下, 创建uevent属性文件, 这个属性文件是在bus.c中用DRIVER_ATTR_WO(uevent)定义的. 该属性文件的作用与/sys/bus/xxx下的uevent属性文件类似, 当用户往该属性文件写入某个kobject_action时, 会触发系统针对该driver的kobject上报一个kobject_action类型的uevent事件.
调用driver_add_attrs接口, 在sysfs的该driver的目录, 创建由bus->drv_attrs指针定义的属性组
如果driver->suppress_bind_attrs为0, 则会在driver的目录下创建两个属性文件
bind属性文件, 在bus.c中用DRIVER_ATTR_WO(bind)定义, 当用户空间写入属于该Bus的某个device的name时, 会调用driver_probe_device, 触发系统去匹配这个driver和这个device, 如果匹配成功, 会调用driver->probe初始化.
这个功能与/sys/bus/xxx下的drivers_probe属性文件很像, 不同的是, 往drivers_probe写入device的name时, 系统会去尝试匹配该device与Bus下的所有drivers; 而往bind写入device的name时, 系统只会尝试匹配device与该driver
unbind属性文件, 在bus.c中用DRIVER_ATTR_WO(unbind)定义, 与bind属性文件相反, 写入device的name, 会调用device_release_driver, 解除device与driver的绑定
bus_probe_device
内核提供了device_register接口用于把某个device添加到某个Bus, device_register会调用device_add, device_add会调用bus_probe_device函数.
bus_probe_device的处理逻辑:
如果Bus的drivers_autoprobe为1, 则会调用device_attach去尝试匹配该device和Bus下的某个driver.
8.5 Demo
我们打算定义这样一个Demo, 首先创建一条总线(Bus), 在该总线下, 创建1个属性文件; 然后往该Bus添加一个device和一个driver, 观察device和driver的匹配状况.
这个DEMO会分几次完成, 在本章中, 我们只会创建一条Bus, 并添加Bus下的属性文件, 在随后的章节中, 我们会往该Bus添加device和driver. 你可以通过git log了解每一次提交的细节.
https://gitlab.com/study-linux/linux_driver_model/blob/master/bus_device_driver.c
9. Class
9.1 简介
类是一个设备的高层视图, 它抽象出了设备的共性.
类的概念不太好理解, 举个例子: 一些年龄相仿、需要获取的知识相似的人, 聚在一起学习,就构成了一个班级(Class). 这个班级可以有自己的名称(如295), 但如果离开构成它的学生(device),它就没有任何存在意义. 另外, 班级存在的最大意义是什么呢? 是由老师讲授的每一个课程!因为老师只需要讲一遍, 一个班的学生都可以听到. 不然的话(例如每个学生都在家学习), 就要为每人请一个老师, 讲授一遍. 而讲的内容, 大多是一样的, 这就是极大的浪费.
设备模型中的Class所提供的功能也一样了, 例如一些相似的device(学生), 需要向用户空间提供相似的接口(课程), 如果每个设备的驱动都实现一遍的话, 就会导致内核有大量的冗余代码, 这就是极大的浪费. 所以, Class说了, 我帮你们实现吧, 你们会用就行了
几乎所有的类都显示在/sys/class目录中. 例如输入设备/sys/class/input; 串行设备/sys/class/tty.
一个例外是块设备, 出于历史原因, block class是在/sys/block.所谓block class就是name为”block”的class.
类成员通常被上层代码所控制, 而不需要来自驱动程序的明确支持. 极少的情况下, 驱动程序需要直接处理类
快速体验class
/sys/class : 除block class, 所有的class都在该目录下
/sys/class/input : input 就是一个class
在本class目录下, 创建每一个属于该class的device的符号链接.
这个链接的好处是: 用户空间可以更方便的通过sysfs访问device相关的特性(也就是属性文件), 原因是路径名简短.
如果device属于该class, 在device所在的目录下, 创建一个指向本class的符合链接, 链接名为” subsystem”, 上图没有显示, 有兴趣可以自己去看看
9.2 主要的数据结构
class
内核用struct class抽象一个class, 它的定义如下
/* include/linux/device.h, line 332 */
Comment |
|
const char *name |
Class的名字 对应/sys/class/下目录的名字. |
struct module *owner |
作用还不清楚 |
struct class_attribute *class_attrs |
该class的默认属性, 当创建class时, 会在class的目录下(/sys/class/xxx)创建这些属性文件 |
const struct attribute_group **dev_groups |
属于该class的device的默认属性, 当调用device_register添加一个device时, 如果device属于该class时, 会在device的目录下创建这些属性文件 |
struct kobject *dev_kobj |
表示该class下的设备在/sys/dev/下的目录, 现在一般有char和block两个,如果dev_kobj为NULL, 则默认选择char |
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env) |
当device被添加到内核时, 如果device属于某个class, 则会调用class->dev_uevent来添加一些事件信息. 我们会在讲述device的时候详解介绍事件上报的细节, 那里会涉及到该函数 |
char *(*devnode)(struct device *dev, umode_t *mode) |
Callback to provide the devtmpfs 获取path of device node file, 然后把得到的结果以uevent事件信息的格式上报 "DEVNAME=%s" |
void (*class_release)(struct class *class) |
用于release该class的回调函数 为什么bus_type里面没有一个类似的release bus的函数呢? 原因其实很简单, 本质上来将, 不管是Bus还是Class, 都是一个kobject, 回头看一下kobject一章, 我们说过, kobject的释放是由它的ktype完成的. Bus对应的ktype是bus_ktype (bus.c); Class对应的ktype是class_ktype(class.c), bus_ktype->release是直接调用kfree释放Bus, class_ktype->release是调用class->class_release释放Class, 所以这里才有class_release这个函数 |
用于release class内设备的回调函数. 在device_release接口中, 会检查Device所在的class, 是否注册release接口, 如果有则调用相应的release接口release设备指针 |
|
int (*suspend)(struct device *dev, pm_message_t state) |
电源管理相关 |
int (*resume)(struct device *dev) |
电源管理相关 |
const struct kobj_ns_type_operations *ns_type |
和sysfs中的名字空间有关, 这里不详述 Ktype数据结构中也有类似的一项 |
const void *(*namespace)(struct device *dev) |
和sysfs中的名字空间有关, 这里不详述 Ktype数据结构中也有类似的一项 |
const struct dev_pm_ops *pm |
电源管理相关 |
struct subsys_private *p |
还记得吗, 在Bus一章中介绍过该数据结构, 下面我们会介绍一下class用到这个数据结构中的哪些部分 |
subsys_private
我们在Bus一章全面的介绍了这个数据结构, 这里只简单列出一些与class相关的项.
/* drivers/base/base.h, line 28 */
struct subsys_private |
Comment |
struct kset subsys |
kset在/sys/中以目录的形式存在, 代表本Class, 例如/sys/class/input, 就代表着input这个目录, 目录的名称是在struct class中指定的 |
|
|
struct kset *devices_kset |
未用 |
struct klist klist_devices |
链表头, 该Class下的所有device都会挂载到这个链表下 |
struct kset *drivers_kset |
未用 |
struct klist klist_drivers |
未用 |
|
|
struct list_head interfaces |
链表头, 该Class下所有的class_interface都会挂载到这个链表下. class_interface的作用我们在下面描述 |
struct mutex mutex |
与interfaces链表头配合, 用于互斥 |
|
|
struct bus_type *bus |
未用 |
struct class *class |
保存上层的Class指针 |
|
|
struct blocking_notifier_head bus_notifier |
未用
|
unsigned int drivers_autoprobe:1 |
未用 |
class有用到, 看样子应该对应一个目录, 官方的解释是为了避免namespace conflicts, 暂时还不清楚细节 |
class_interface
class_interface的作用是: 你可以向某个class注册一个class_interface (所有注册的class_interface都会挂载在class->interface链表头下), 这样当class下有device添加或移除的时候, 会调用class_interface->add_dev 或 class_interface->remove_dev.
至于这两个函数有什么用, 就有具体的实现者决定.
/* include/linux/device.h, line 468 */
struct class_interface |
Comment |
struct list_head node |
用于挂载到class->interface链表头下 |
struct class *class |
指向所属的class |
int (*add_dev) (struct device *, struct class_interface *) |
当某个device添加到class时, 会把该device传递给add_dev函数 |
void (*remove_dev) (struct device *, struct class_interface *) |
当某个device从class移除时, 会把该device传递给remove_dev函数 |
9.3 主要API说明
Class
与Class相关的API主要包括:
class的注册和注销
向class注册class_interface, 以便监听device的添加/移除
本class下有device注册到内核时的处理
本class下有device从内核注销时的处理
头文件: include/linux/device.h
实现文件: drivers/base/class.c
class API |
Comment |
class_register(class) |
它是一个宏, 会直接调用__class_register. 用于向内核添加一个class. 调用该API需要自行分配class内存空间 |
class_unregister(struct class *class) |
从内核移除一个class |
分配class内存空间并把该class添加到内核 name就是class的名称, 也就是/sys/class/xxx的名称 |
|
class_destroy(struct class *cls) |
调用class_unregister从内核移除一个class. 该API用于销毁class_create创建的class |
class_interface_register(struct class_interface *) |
向class注册一个class_interface |
class_interface_unregister(struct class_interface *) |
从class移除一个class_interface |
9.4 关键代码分析
__class_register
class的注册, 是由__class_register接口实现的, 它的处理逻辑和bus的注册类似, 主要包括:
为class结构中的struct subsys_private类型的指针(cp)分配空间, 更新cp->class和class->p两个指针为正确的值
初始化cp-> klist_devices链表头
初始化cp-> interfaces链表头
调用kset_init初始化cp->glue_dirs这个kset, 注意只是初始化, 但未添加到内核, 所以看不到目录
初始化互斥锁cp->mutex
设置本class对应的kobject (cp->subsys.kobj)的名字为class->name
如果class-> dev_kobj为NULL, 则将其设置为sysfs_dev_char_kobj (即/sys/dev/char)
如果不是block class, 则设置cp->subsys.kobj.kset为class_kset (即/sys/class), 这样, 只要在把cp->subsys.kobj这个kobject添加到内核时, 设置其parent为NULL, 系统就会把kset的kobject做为它的parent.
结果就是: 对于block class, 目录为/sys/block, 对于其它类型的class, 目录为/sys/class/xxx
设置cp->subsys.kobj.ktype为class_ktype, 该ktype是由系统创建的 (class.c). 因此知道该ktype会负责释放资源和读/写cp->subsys.kobj下的属性文件(即/sys/class/xxx/下的属性文件)
调用kset_register初始化cp->subsys这个kset并把它添加到内核.
调用add_class_attrs把class-> class_attrs中定义的默认属性添加到/sys/class/xxx/目录下
__class_create
该函数的原型是
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
从这个原型中可以看出几个重点:
主要的参数是class的name和 module
返回结果是一个class
一句话总结: 给定一个class的名称和module参数, 它就能帮你创建一个class.
具体的逻辑如下:
首先分配一个struct class的内存空间
初始化class->name和class->module
初始化class->class_release为class_create_release, class_create_release是系统定义的(class.c), 它其实就是用kfree释放第一步分配的内存空间
调用__class_register注册该class
这里想多说几句, __class_create相当于给出一个例子, 告诉你如何手动创建一个最基础的class. 你也可以在它的基础上, 设计复杂一点的class, 比如设置class的默认属性, 设计dev_release函数, 设计suspend/resume等电源管理相关函数等.
device_add_class_symlinks
当调用device_register把某个device添加到内核时, 如果device->class不为NULL, 则会调用该函数来处理class端相关的事情.
Bus中也存在一个类似的函数bus_add_device, 不同的是bus_add_device是在bus.c中定义的, 而device_add_class_symlinks是在drivers/base/core.c中实现的.
相信你也猜到了, 该函数就是创建一个符号链接, 我们来看看具体逻辑:
首先, 在device所在目录下, 创建一个指向该class的符号链接, 链接名是”subsystem”
如果device->parent不为NULL, 并且该device不是一个block device, 则在device所在的目录下创建一个指向parent的链接, 链接名是”device”.
很奇怪, 这个符号链接与class又没什么关系, 为什么要放在这个函数里面创建呢? 我也不清楚原因
如果device所属的class不是block class, 则在class的目录下, 创建一个指向该device的符合链接, 链接名是该device的name
如果device所属的class是block class, 则不会在/sys/block下创建指向该device的符合链接. 因为对于block device, 它的真实目录就是在/sys/block下.
device_add_to_class_klist_devices
额, 怎么会有这么长名字的函数? 好吧, 我是骗你的, 其实没有这个函数.
但是这个事情还是要做的, device属于某一个class, 肯定要把它加入到class->klist_devices这个链表头.
这个事情是在device_add函数里面做的, 直接看一下代码好了
if (dev->class) {
mutex_lock(&dev->class->p->mutex);
/* tie the class to the device */
klist_add_tail(&dev->knode_class,
&dev->class->p->klist_devices);
/* notify any interfaces that the device is here */
list_for_each_entry(class_intf,
&dev->class->p->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev, class_intf);
mutex_unlock(&dev->class->p->mutex);
}
很简单, 两件事情
把device加入class->klist_devices链表头
调用class->interface上挂载的所有class_interface的add_dev函数, 告诉它们有一个device添加到本class了.
9.5 Demo
我们打算设计这样一个Demo: 创建一个新的基本的class, 所谓基本的意思, 就是仿造__class_create, 只提供一些必要的功能, 你可以在此Demo的基础上, 创建更复杂的class.
然后往该class下添加一个device.
https://gitlab.com/study-linux/linux_driver_model/blob/master/class_device.c
10. Device
终于.., 我们开始关注到跟我们的日常工作紧密相关的东西了.
device和device_driver, 在前面介绍Bus和Class的时候, 我们已经无数次提到这两个概念, 接下来我们将详细介绍这两部分
10.1 简介
device和device_driver是Linux驱动开发的基本概念, 驱动开发是思路很简单: 开发指定的软件(device_driver)来驱动指定的设备(device).
所以device就是用来描述一个设备的.
device_driver就是用来描述如何让这个设备工作的.
10.2 主要的数据结构
device
/* include/linux/device.h, line 660 */
struct device |
Comment |
struct device*parent |
该设备的父设备. parent的作用之一是组织device的层次结构, 前面我们介绍过kobject的主要功能之一是组织/sys/下的目录层次, 这个规则不变. 实际上, 经常用device->kobject->parent = device->parent->kobject的方式, 来组织device的层次结构 |
struct device_private*p |
私有数据结构指针, 从这个结构体里面我们可以看到一个device会被挂接到哪些链表下. 后面专门介绍这个结构体 |
struct kobject kobj |
本device对应的kobject, 对应sysfs中的一个目录 |
const char*init_name |
就像bus, class一样, 每个device都需要有一个name, 这个name体现为目录名, 也就是device->kobject->name. device->kobject->name有两种方式指定: 1. 就是这里的init_name, 最后会赋值给device->kobject->name 2. 如果未指定init_name, 则会把”bus->dev_name+device->id”赋值给device->kobject->name |
const struct device_type *type |
与ktype&kset之于kobject有的类似 后面专门介绍一下这个结构体 |
struct mutexmutex |
互斥锁 |
struct bus_type*bus |
本device属于哪一个Bus |
struct device_driver *driver |
本device与哪一个device_driver匹配上了 |
void*platform_data |
私有数据, device核心代码不会触碰这个数据. |
void*driver_data |
某个device_driver的实现代码, 可能会存储一些driver相关数据到这个地方. device核心代码层不会触碰这个数据. 设置的接口函数: void dev_set_drvdata(struct device *dev, void *data) 或者的接口函数: void *dev_get_drvdata(const struct device *dev) |
struct dev_pm_infopower |
电源管理相关 |
struct dev_pm_domain*pm_domain |
电源管理相关 |
struct dev_pin_info*pins |
与GPIO(PINCTRL)子系统相关的一个数据, 定义了本device用到了哪些GPIO引脚, 引脚的复用功能是怎样的. 前面我们介绍过, 当device和device_driver匹配上了之后, 在really_probe函数里面, 会调用pinctrl_bind_pins(dev)来自动配置好这些GPIO引脚 |
intnuma_node |
"NUMA”功能, 暂不描述 |
u64*dma_mask ~ struct dev_archdataarchdata |
DMA相关, 暂不描述 |
struct device_node*of_node |
device tree相关, 用于通过device tree传递一些device相关信息, 比如寄存器地址等. 我们会有专门的一篇文章来描述device tree 它也会用于device与driver的匹配 |
表示高级配置和电源管理接口, 与ACPI device相关 它也会用于device与driver的匹配 |
|
dev_tdevt |
包含主/次设备号, 当此设备号不为0时, 会在/sys/dev/*/目录下创建对应的目录, 目录名称是”主设备号:此设备号”. 同时, 还会生成对应的/dev/xxx设备节点 |
u32id |
该device的id, 可以与bus->dev_name结合生成device name 这个id可以手动指定, 也可以由某个bus自动分配 |
spinlock_tdevres_lock struct list_headdevres_head |
与device resource management相关. 用于自动释放device所占用的资源, 我们会用专门的一章来介绍device resource management |
struct klist_node knode_class |
用于把本device挂载到class->subsys_private-> klist_devices链表下 |
struct class *class |
本device属于哪一个class |
const struct attribute_group **groups |
本device的属性组 |
void (*release)(struct device *dev) |
release本device |
struct iommu_group *iommu_group |
暂不描述 |
bool offline_disabled:1 |
设置本device是否支持热插拔, 为1表示不支持热插拔 |
bool offline:1 |
热插拔相关, 标志本device是处于online状态还是offline状态 |
device_private
关于该结构体的官方解释是: Structure to hold the private to the driver core portions of the device structure.
很明显可以看到, 该结构体是struct device的一部分, 但是该结构里面的内容只会被设备模型核心代码使用. 我们在编写驱动程序时不会去触屏这个数据结构.
这也许就是从struct device中剥离出这一部分, 单独定义成一个结构体的原因把.
/* drivers/base/base.h, line 71 */
Comment |
|
struct klist klist_children |
链表头, 用于挂载本device的children device. 也就是children_device->parent = 本device |
struct klist_node knode_parent |
用于把本device挂载到parent device的klist_children链表头中 |
struct klist_node knode_driver |
用于把本device挂载到与它匹配的driver的klist_devices链表头中, 所这里可以看到, 一个diver下面可以挂载多个device. 在8.3节的device_bind_driver函数中有介绍这一挂载过程. |
struct klist_node knode_bus |
用于把本device挂载到所属Bus的bus_type->subsys_private-> klist_devices |
用于把本device加入deferred_probe_list链表中. 官方解释很清楚, 直接贴过来: entry in deferred_probe_list which is used to retry the binding of drivers which were unable to get all the resources needed by the device; typically because it depends on another driver getting probed first |
|
struct device *device |
指向本device |
总结一下一个device会被挂载到哪些地方
device的parent device链表中
device对应的driver的链表中
device所属的Bus的链表中
额, 好像不对啊, 不是还有一个class吗, 没错
device所属的class的链表中, 只不过用于挂载的entry没有放到device_private结构中, 而是在device-> knode_class
device_type
与ktype&kset之于kobject有的类似. 回想一下ktype和kset的作用, 还记得吗?
ktype的作用是释放kobject资源, 定义kobject默认的属性文件, 定义kobject下属性文件的读/写函数.
kset的作用是聚合相似的kobject, 控制这些kobject的uevent事件上报行为, 比如哪些事件不能上报(filter), 对于能上报的事件, 添加统一的事件信息.
device_type也干了类似的事情, 下面我们来看看它的数据结构
/* include/linux/device.h, line 501 */
struct device_type |
Comment |
const char *name |
本device属于哪一种类型的type, 例如"partitions" and "disks" ; "mouse" and "event" 如果name不为NULL, 在上报的uevent事件中会包含DEVTYPE这个事件信息 |
const struct attribute_group **groups |
默认的属性组 |
int (*uevent)(struct device *dev, struct kobj_uevent_env *env) |
添加统一的uevent事件的事件信息 |
char *(*devnode)(struct device *dev, umode_t *mode, kuid_t *uid, kgid_t *gid) |
获取path of device node file class数据结构中也有一个类似的函数, 它们的功能相同, 都是获取device node file的路径. 系统会优先调用device_type中的devnode, 如果device_type中没有定义, 则调用所属class的devnode |
void (*release)(struct device *dev) |
用于释放资源 |
const struct dev_pm_ops *pm |
电源管理相关 |
10.3 主要API说明
头文件: include/linux/device.h
实现文件: drivers/base/core.c
Comment |
|
extern void device_initialize(struct device *dev) |
当用kzalloc分配了一个struct device内存空间后, 调用此API进行初始化 |
extern int __must_check device_add(struct device *dev) |
将初始化之后的device添加到内核 |
extern void device_del(struct device *dev) |
与device_add的动作相反 |
extern int __must_check device_register(struct device *dev) |
先调用device_initialize, 然后在调用device_add 一般我们自己定义了一个device之后, 都会调用该API |
extern void device_unregister(struct device *dev) |
先调用device_del 然后调用put_device, 利用kref机制启动释放流程 |
|
|
device_create_groups_vargs |
它不是一个API, 是core.c定义的一个内部函数. 主要作用是: 自动分配struct device内存空间, 初始化此内存空间, 并将device添加到内核. 如果执行成功, 返回一个struct device *型数据结构 |
extern struct device *device_create_vargs(…); |
此API直接调用device_create_groups_vargs |
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) |
此API把参数(const char *fmt, ...)转换为va_list, 然后调用device_create_vargs 该API可以指定device所属的class, 一般我们想自动创建一个device时, 会调用此API |
device_create_with_groups |
此API与device_create不同之处在于, 此API可以指定device的默认属性组, 即device-> groups |
void device_destroy(struct class *class, dev_t devt) |
销毁device_create所创建的device |
10.4 关键代码分析
device_initialize
这个API很简单, 就是初始化一些链表头, 互斥锁什么的, 大致逻辑如下:
device->kobj.kset = devices_kset.
device_kset是core.c中系统创建的一个kset, 它对应/sys/devices目录.
kobject_init(&dev->kobj, &device_ktype)
device_ktype是core.c中系统定义的一个ktype
别的就是一些链表头什么的, 不过说了
device_add
非常重要的API.
下面我详细分析一下该API, 具体逻辑如下:
如果device->device_private (dev->p)为空, 则调用kzalloc为dev->p分配内存空间, 并初始化dev->p->device, dev->p->klist_children, dev->p->deferred_probe
如果定义了dev->init_name, 则把dev->init_name赋值给dev->kobj->name. 然后把dev->init_name设为NULL. 意味着device_add之后, 如果想获取device的name, 只能从dev->kobj->name获取 (通过函数dev_name(dev)), 而不能从dev->init_name获取.
如果dev_name(dev)依然为NULL, 也就是说上一步中init_name为NULL, 则检查dev->bus是否存在, 如果存在, 则把”dev->bus->dev_name+dev->id”赋值给dev->kobj->name
再次检查, 如果dev_name(dev)依然为NULL, 也就是init_name为NULL, 而且device也不属于某个Bus, 则内核不允许这种情况出现, 返回错误
调用get_device_parent获取device的parent->kobject, 用于组织目录层次
如果device属于某一个class
如果所属的class是block class
如果dev->parent不为NULL, 并且dev->parent->class也是block classs, 则返回parent->kobject, 即/sys/block/xxx/
否则, 返回block_class.p->subsys.kobj, 即/sys/block/目录
从这里可以看出, block class的device存在于/sys/block目录或其子目录下. 而blcok class这个class的目录就是/sys/block (见class章节的介绍).
如果所属的class不是block class
如果dev->parent不为NULL
如果dev->parent->class不为NULL, 并且dev->class->ns_type为NULL, 则返回dev->parent所在的目录
否则, 在dev->parent所在的目录下新建一个名为”dev->class->name”的目录, 返回此目录以做为本device的父目录
如果dev->parent为NULL, 则在/sys/devices/virtual/下新建一个名为”dev->class->name”的目录, 返回此目录以做为本device的父目录
否则, device不属于某一个class
如果dev->parent为NULL, 则看看本device是否属于某个Bus, 如果属于某个Bus, 则将dev->bus->dev_root目录做为本device的父目录. 我们在Bus一章中介绍bus_type数据结构时, 有提到dev_root是本Bus下所有device的默认父设备
如果dev->parent不为NULL, 则把parent所在的目录做为本device的父目录
否则返回NULL
如果get_device_parent获取到的目录不为NULL, 则设置dev->kobj.parent, 否则dev->kobj.parent就是NULL了,
调用kobject_add把dev->kobj添加到内核, 添加完成后, 就会在dev->kobj.parent对应的目录下创建一个”device->name”的目录, 如果dev->kobj.parent为NULL, 则会把dev->kobj->kset所在的目录做为父目录, 即/sys/devices.
如果系统中有定义platform_notify, 则调用此函数通知有一个device添加到内核了
调用device_create_file在本device所在的目录下, 创建一个叫uevent的属性文件, 该属性文件由DEVICE_ATTR_RW(uevent)定义
当往这个属性文件写入一个kobject_action类型字符串时, 会触发内核针对dev->kobj发送一个此类型的uevent事件
当读取该属性文件时, 会得到dev->kobj所属kset->uevent_ops->uevent里面定义的默认事件信息
调用device_add_class_symlinks处理class相关事情, 在class一章中已经介绍过该函数
调用device_add_attrs在本device所在的目录下创建一些属性文件
如果dev->class不为NULL, 则创建dev->class-> dev_groups定义的属性组
如果dev->device_type不为NULL, 则创建device_type-> groups定义的属性组
创建dev->groups中定义的属性组
如果dev->bus不为NULL, 且bus->offline和bus->online不为NULL, 并且dev->offline_disabled为0, 则创建一个名为online的属性文件, 该属性文件由DEVICE_ATTR_RW(online)定义.
往online属性文件写入true, 会导致内核执行device_online函数, 此函数会调用dev->bus->online, 然后针对dev->kobj发送KOBJ_ONLINE事件, 最后设置dev->offline = false
往online属性文件写入false, 会导致内核执行device_offline函数, 此函数会调用dev->bus->offline, 然后针对dev->kobj发送KOBJ_OFFLINE事件, 最后设置dev->offline = true
读取该属性文件则会返回dev->offline的值.
调用bus_add_device函数处理bus端相关事情, 该函数在Bus一章中介绍过
调用dpm_sysfs_add和device_pm_add处理PM相关事情
如果dev->devt不会0, 说明本device存在主/次设备号, 则
在本device所在的目录下创建一个名为dev的属性文件, 该属性文件由DEVICE_ATTR_RO(dev)定义, 读取该属性文件, 会获取到dev->devt的值
获取本device在/sys/dev下的目录: 如果dev->class不为NULL, 则从dev->class->dev_kobj获取(/sys/dev/char或/sys/dev/block); 否则默认就是/sys/dev/char. 然后在/sys/dev/xxx下创建一个指向本device所在目录的链接, 链接名由”dev->devt”决定
调用devtmpfs_create_node创建设备节点, 系统启动后, 会把devtmpfs挂载到/dev目录下, 这样我们就能访问到/dev/xxx了. 至于为什么要用到devtmpfs, 请参考第3章中的介绍.
如果device属于某一个Bus, 则往Bus通知链接上发送一个BUS_NOTIFY_ADD_DEVICE消息
调用kobject_uevent(&dev->kobj, KOBJ_ADD)发送uevent事件, 后面会分析uevent事件发送的详解流程
调用bus_probe_device(dev)启动device与driver的匹配, 该函数在Bus一章分析过
如果dev->parent存在, 将本device加入parent的klist_children链表
调用假想函数device_add_to_class_klist_devices, 将本device加入class相关链表, 该假想函数在class一章分析过
device_create_groups_vargs
函数原型如下
static struct device *
device_create_groups_vargs(struct class *class, struct device *parent,
dev_t devt, void *drvdata,
const struct attribute_group **groups,
const char *fmt, va_list args)
具体代码逻辑也很简单:
class参数不能为NULL, 否则返回错误. 证明该API必须要求指定device所属的class, 否则用手动定义struct device的方式来添加一个device
分配一个struct device的内存空间
调用device_initialize初始化该内存空间
dev->devt = devt
dev->class = class
dev->parent = parent
dev->groups = groups
dev->release = device_create_release, device_create_release是core.c中定义的一个函数, 直接调用kfree(dev)
将drvdata赋值给dev->driver_data
调用kobject_set_name_vargs(&dev->kobj, fmt, args)设置dev->kobj的name
调用device_add(dev)
device_create
函数原型如下:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
逻辑很简单:
把参数…转换为va_list, 然后调用device_create_groups_vargs
device_create_with_groups
函数原型如下:
struct device *device_create_with_groups(struct class *class,
struct device *parent, dev_t devt,
void *drvdata,
const struct attribute_group **groups,
const char *fmt, ...)
逻辑与device_create一样, 唯一的不同之处在于此API指定了groups.
kobject_uevent: device uevent事件上报流程
还记得uevent的事件上报流程吗, 简单点来说, 就是调用kobject_uevent针对某一个kobject上报一个uevent事件, 该kobject必须属于某个kset才允许上报uevent, kset的主要作用是过滤掉一些uevent; 针对可以上报的uevent, 添加统一的uevent事件信息. 如果不记得了, 回头看看Uevent一章.
回到device上, device是封装的kobject, 它的上报流程遵循上述规则. 所以需要先找到dev->kobj所属的kset. 在device_initialize代码分析中知道这个kset是devices_kset.
OK, 找到kset, 我们着重分析这个kset添加了哪些uevent事件信息, uevent过滤功能这里就不说了.
devices_kset是调用dev_uevent来添加事件信息的, 该函数的逻辑如下:
如果dev->devt不为0, 则添加"MAJOR=%u" , "MINOR=%u" , "DEVNAME=%s" , “DEVMODE=%#o", "DEVUID=%u", "DEVGID=%u" 这些事件信息
if (dev->type && dev->type->name), 添加("DEVTYPE=%s", dev->type->name)事件信息
if (dev->driver), 添加("DRIVER=%s", dev->driver->name)
调用of_device_uevent添加Device Tree相关事件信息
if (dev->bus && dev->bus->uevent), 调用dev->bus->uevent添加相关事件信息
if (dev->class && dev->class->dev_uevent), 调用dev->class->dev_uevent添加相关事件信息
if (dev->type && dev->type->uevent), 调用dev->type->uevent添加相关事件信息
put_device: device资源释放流程
我们知道, ktype是负责是否kobject所占用的资源的, 所以对于device也是一样, 当调用put_device是dev->kobj的引用计数为0时, dev->kob所属的ktype就会开始释放资源了.
所以要先找到dev->kobj对应的ktype, 在device_initialize代码分析中知道这个ktype是device_ktype.
device_ktype的release函数device_release的逻辑如下:
devres_release_all(dev) : 释放device所占用的所有device resource, 例如clock, 中断号等等. 还记得device数据结构中的devres_head链表头吗? 它下面挂载了本device所有的device resource. 我们会有专门的一章来分析device resource management机制
if (dev->release) dev->release(dev);
else if (dev->type && dev->type->release) dev->type->release(dev);
else if (dev->class && dev->class->dev_release) dev->class->dev_release(dev);
kfree(device-> device_private)
10.5 Demo
针对device做俩个demo:
第一个是随Bus Demo一起的, 把某个device挂载到这个Bus下, 然后观察device与driver的匹配过程. 这个Demo中, 我们手动定义一个struct device, 然后调用device_register.
https://gitlab.com/study-linux/linux_driver_model/blob/master/bus_device_driver.c
第二个是随Class Demo创建一个device, 把它挂载到这个Class下, 这个demo中, 我们可以直接调用device_create来创建这个device.
https://gitlab.com/study-linux/linux_driver_model/blob/master/class_device.c
11. Driver
11.1 简介
device就是用来描述一个设备的.
device_driver就是用来描述如何让这个设备工作的
额, 好像没别的好说的…
11.2 主要的数据结构
device_driver
/* include/linux/device.h, line 229*/
struct device_driver |
Comment |
const char *name |
本driver的name |
struct bus_type *bus |
本driver所属的Bus |
struct module *owner |
class中也有一个类似的数据结构, 暂不清楚作用 |
const char *mod_name |
used for built-in modules |
bool suppress_bind_attrs |
disables bind/unbind via sysfs 当把driver添加到内核时, 会调用bus_add_driver Bus一章中分析过此函数, 它会根据driver->suppress_bind_attrs的值来决定是否创建bind/unbind两个属性文件. 往这两个属性文件写入device的name时, 会启动device与driver的匹配/解配 |
const struct of_device_id *of_match_table |
编写驱动代码时经常用到它, 用于指定本driver可以匹配的device. 记住device与driver是否能匹配上, 是由bus->match决定的, 所以bus->match函数中一定用到了这个table |
const struct acpi_device_id *acpi_match_table |
与上类似 |
int (*probe) (struct device *dev) |
这个接口函数用于实现driver逻辑的开始, 就像一段代码的main函数. 当deive与driver匹配上之后, 第一个执行的就是driver->probe. probe函数用于硬件上的一些初始化, 为device获取一些device resource, 如果哪个步骤出错, 代表本driver不能驱动这个device |
int (*remove) (struct device *dev) |
这个接口函数用于实现driver逻辑的结束, 当设备被移除时, 就会执行对应driver的remove函数 |
void (*shutdown) (struct device *dev) |
电源管理相关 |
int (*suspend) (struct device *dev, pm_message_t state) |
电源管理相关 |
int (*resume) (struct device *dev) |
电源管理相关 |
const struct attribute_group **groups |
driver的默认属性组 |
const struct dev_pm_ops *pm |
电源管理相关 |
struct driver_private *p |
device_driver应该也是一个kobject啊, 为什么没有看见struct kobject数据结构呢? 没错, 是在driver_private中定义的. |
driver_private
/* drivers/base/base.h, line 46*/
struct driver_private |
Comment |
struct kobject kobj |
本device_driver所对应的kobject |
struct klist klist_devices |
链表头, 用于挂载本driver下所有的device |
struct klist_node knode_bus |
用于把本device_driver挂载到某个Bus下 |
struct module_kobject *mkobj |
暂时不清楚 |
struct device_driver *driver |
指向上层的device_driver |
11.3 主要API说明
头文件: include/linux/device.h
实现文件: drivers/base/driver.c
Comment |
|
extern int __must_check driver_register(struct device_driver *drv) |
将本device_driver添加到某个Bus. 额, 参数列表里面好像没有看到Bus啊, 怎么知道添加到哪个Bus呢? 没错, 你必须在调用该API之前初始化driver->bus |
将本device_driver从某个Bus移除, 该API最终会调用bus_remove_driver, 我们没有介绍这个函数, 很简单, 有兴趣可以自己阅读代码 |
11.4 关键代码分析
driver_register
该API的逻辑比较简单, 大多数重要的事情是放在bus_add_driver函数中做的, 该函数我们在Bus一章介绍过了.
该API具体逻辑如下:
首先检查device_driver(drv)->bus是否存在
然后检查以下函数是否有在bus和driver中重复定义, 一般来讲, Bus中定义了, 会优先调用bus->probe, 而不会在调用driver->probe
drv->bus->probe && drv->probe
drv->bus->remove && drv->remove
drv->bus->shutdown && drv->shutdown
检查一下Bus下是否已经注册了同名(drv->name)的driver, 如果已有, 是不允许重复注册的
调用bus_add_driver(drv), 该函数会分配driver_private空间, 初始化drv->kobj并将其添加到内核, 加入链表, 创建属性文件, 启动driver与device的匹配等等.
调用driver_add_groups创建drv->groups中定义的默认属性组
调用kobject_uevent(&drv->p->kobj, KOBJ_ADD)发送uevent事件
kobject_uevent: driver uevent事件上报流程
与device上报uevent事件的逻辑一样, 先找到drv->p->kobj的kset.
bus_add_driver中初始化了此kset. 不过它很简单, 没有kset_uevent_ops函数, 所以driver在上报uevent事件的时候, 不会添加任何额外的事件信息
device与device_driver的匹配时机汇总
这些匹配时机的细节我们在前面都介绍过, 这里只是汇总一下:
当调用device_register把某个device添加到内核时 (device_register->device_add->bus_probe_device函数中分析过)
当调用driver_register把某个driver添加到内核是(driver_register->bus_add_driver函数中分析过)
当往/sys/bus/xxx/drivers_probe属性文件写入一个device name时. (bus_register中分析过)
当往/sys/bus/xxx/drivers/xxx/bind属性文件写入一个device name时. (bus_add_driver函数中分析过)
11.5 Demo
此Demo是随Bus Demo一起的, 把driver挂载到一个Bus下, 然后观察device与driver的匹配过程. 这个Demo中, 我们手动定义一个struct device_driver, 然后调用driver_register把该driver挂载到某个Bus下.
https://gitlab.com/study-linux/linux_driver_model/blob/master/bus_device_driver.c
12. Platform系统
12.1 简介
在Linux设备模型的抽象中, 存在着一类称作”Platform Device”的设备, 这些设备有一个基本的特征:可以通过CPU bus直接寻址(例如在嵌入式系统常见的”寄存器”), 也就是我们常说的芯片内部外设, 比如RTC, UART, LCD, SPI, I2C, 等等.
因此, 由于这个共性, 内核在设备模型的基础上(device和device_driver), 对这些设备进行了更进一步的封装, 抽象出paltform bus、platform device和platform driver, 以便驱动开发人员可以方便的开发这类设备的驱动.
可以说, paltform设备对Linux驱动工程师是非常重要的, 因为我们编写的大多数设备驱动,都是为了驱动plaftom设备. 本文我们就来看看Platform设备在内核中的实现.
Platform模块的软件架构
内核中Platform设备有关的实现位于include/linux/platform_device.h和drivers/base/platform.c两个文件中, 它的软件架构如下
由图片可知, Platform设备在内核中的实现主要包括三个部分:
Platform Bus, 基于底层bus模块, 抽象出一个虚拟的Platform bus, 用于挂载Platform设备;
Platform Device, 基于底层device模块, 抽象出Platform Device, 用于表示Platform设备;
Platform Driver, 基于底层device_driver模块, 抽象出Platform Driver, 用于驱动Platform设备.
其中Platform Device和Platform Driver会为其它Driver提供封装好的AP, 具体可参考后面的描述.
12.2 主要数据结构
platform_device
/* include/linux/platform_device.h, line 22 */
Comment |
|
struct device dev |
platform_device是对struct device的封装 |
const char *name |
设备名, 用脚趾头想都知道最终会写到对应的kobject->name |
int id |
对应device->id |
bool id_auto |
表示id是不是由platform系统自动分配的 |
struct resource *resource |
resource, 该设备的资源描述, 由struct resource(include/linux/ioport.h)结构抽象 在Linux中, 系统资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型. 这些资源大多具有独占性, 不允许多个设备同时使用, 因此Linux内核提供了一些API, 用于分配、管理这些资源. 当某个设备需要使用某些资源时, 只需利用struct resource组织这些资源(如名称、类型、起始、结束地址等), 并保存在该设备的resource指针中即可.然后在设备probe时, 设备需求会调用资源管理接口, 分配、使用这些资源.而内核的资源管理逻辑, 可以判断这些资源是否已被使用、是否可被使用等等. |
u32 num_resources |
代表有多少个设备资源 |
与device与driver的匹配相关. 具体可以看platform_ bus这个Bus的match函数 |
|
char *driver_override |
Driver name to force a match |
struct mfd_cell *mfd_cell |
和MFD设备相关的内容, 暂不说明 |
struct pdev_archdata archdata |
一个奇葩的存在!!它的目的是为了保存一些architecture相关的数据, 去看看arch/arm/include/asm/device.h中struct pdev_archdata结构的定义, 就知道这种放纵的设计有多么垃圾了. 不管它了!! |
platform_driver
/* include/linux/platform_device.h, line 173 */
struct platform_driver |
Comment |
struct device_driver driver; |
platform_driver是对device_driver的封装 |
int (*probe)(struct platform_device *) |
device_driver中定义了同名函数, 很显然, 这里的函数相当于覆盖了device_driver中的函数 |
int (*remove)(struct platform_device *) |
同上 |
void (*shutdown)(struct platform_device *) |
同上 |
int (*suspend)(struct platform_device *, pm_message_t state) |
同上 |
int (*resume)(struct platform_device *) |
同上 |
const struct platform_device_id *id_table |
注意到了吗? platform_device也有一个同类型的变量, 很明显, 它是用于device和driver的匹配的. 可以查看platform_bus的match函数逻辑 |
bool prevent_deferred_probe |
延迟probe机制, 详解见struct device_private数据结构的deferred_probe这个元素的描述. 从字面上的描述来看, 应该是指禁止延迟probe功能 |
platform_bus_type
platform bus就是一个普通的struct bus_type, 没有新增数据结构
12.3 主要API说明
Platform Bus
platform bus相关的没有新增API, 全部采用Bus本身定义的API, 包括创建一个platform bus, 销毁这个bus等.
Platform Device
Platform Device主要提供设备的分配、注册等接口, 供其它driver使用, 具体包括:
头文件: include/linux/platform_device.h
实现文件: drivers/base/platform.c
Platform Driver
Platform Driver提供struct platform_driver的注册功能, 没有分配函数, 需要自己手动分配结构体空间, 具体如下:
头文件: include/linux/platform_device.h
实现文件: drivers/base/platform.c
懒人API
又是注册platform device, 又是注册platform driver, 看着挺啰嗦的. 不过内核想到了这点, 所以提供一个懒人API, 可以同时注册platform driver, 并分配一个platform device:
extern struct platform_device *platform_create_bundle(
struct platform_driver *driver, int (*probe)(struct platform_device *),
struct resource *res, unsigned int n_res,
const void *data, size_t size);
只要提供一个platform_driver(要把driver的probe接口显式的传入), 并告知该设备占用的资源信息, platform模块就会帮忙分配资源, 并执行probe操作. 对于那些不需要热拔插的设备来说, 这种方式是最省事的了.
Early platform device/driver
暂时不清楚early platform的作用和工作机制, 后面遇到了在来补充此章节, 相关API包括
extern int early_platform_driver_register(struct early_platform_driver *epdrv,
char *buf);
extern void early_platform_add_devices(struct platform_device **devs, int num);
static inline int is_early_platform_device(struct platform_device *pdev)
{
return !pdev->dev.driver;
}
extern void early_platform_driver_register_all(char *class_str);
extern int early_platform_driver_probe(char *class_str,
int nr_probe, int user_only);
extern void early_platform_cleanup(void);
12.4 关键代码分析
platform_bus_init
很明显, platform的第一步是要在内核系统中创建一条platform bus, 然后才可以向该bus添加platform_device和platform_driver
代码逻辑很简单, 如下:
early_platform_cleanup : 为early platform做些准备, 这里不详述
device_register(&platform_bus)
调用device_register注册一个device, 不要看参数的名字是platform_bus, 其实它是一个struct device类型的.
这句话的唯一作用就是在/sys/devices下创建一个名为platform的目录, 所有的platform_device都会存放于此目录下, 所有platform_device的默认父设备就是platform_bus
为什么这句话能够在/sys/devices下创建一个名为platform的目录呢? 如果你能很快明白, 说明前面的知识已经吸收了. platform_bus这个device不属于任何class, 也不属于任何bus, 也没有给定parent device, 根据前文device_add这个API的代码分析就能知道, 这个device_register就能在/sys/devices下创建platfrom目录
bus_register(&platform_bus_type)
在内核系统中创建一条platform Bus
关于bus_type和bus_register的更多细节, 在Bus一章中已经介绍过了
platform_match
platform Bus的match函数, 决定了该Bus下platform_device (pdev)与platform_driver(pdrv)的匹配规则, 还是比较重要的.
本节主要目的给出pdev与pdrv的匹配规则的先后顺序, 在阅读本节时可以回头多看看pdev与pdrv的数据结构, 方便理解. 具体逻辑如下:
如果定义了pdev->driver_override (它是一个字符串), 则比较 pdev->driver_override 与pdrv->driver->name, 相同就匹配, 不同就不匹配
Attempt an OF style match
判断pdrv->driver->of_match_table 与 pdev->dev->of_node是否能匹配上
Then try ACPI style match
判断pdrv->driver->acpi_match_table 与 pdev->dev->acpi_node是否能匹配上
如果定义了pdrv->id_table, 则比较pdrv->id_table.name与pdev->name, 相同则匹配, 不同则不匹配
最后一项, 回归普通的匹配规则, 判断pdev->name与pdrv->driver->name是否相同, 相同则匹配, 不同则不匹配
platform_device_register
用于把一个platform_device(pdev)添加进内核系统.
代码逻辑很简单:
device_initialize(&pdev->dev)
arch_setup_pdev_archdata(pdev)
return platform_device_add(pdev) : 这句话是关键
platform_device_add
从前文的描述中我们知道, device属于哪个bus是由通过指定device->bus决定的. 很明显, 所有的platform_device都应该挂载在platform bus下. 照理来说, platform系统应该自动去帮我们处理这些事情, 不需要我们手动指定device->bus了.
platform系统确实是这样做的, 具体的API就是这个platfrom_device_add, 逻辑如下:
如果pdev->dev.parent == NULL, 则将pdev->dev.parent 赋值为platform_bus. 还记得platfrom_bus吧, 这印证了本章前文所说的, 所有platform_device的默认父设备都是platform_bus
pdev->dev.bus = &platform_bus_type : OK, 所有platform_device都会挂载在platform Bus下
设置pdev->dev.kobject的name, 也就是device的name.
如果pdev->id >= 0, 则name为" pdev->name.pdev->id"
如果pdev->id == PLATFORM_DEVID_NONE (-1), 则name为" pdev->name”
如果pdev->id == PLATFORM_DEVID_AUTO (-2), 则系统会自动获取一个id赋值给pdev->id, 并把pdev->id_auto设为true. name为" pdev->name.pdev->id.auto"
处理pdev中定义的resources, 给每个resource指定name 和 parent
device_add(&pdev->dev) : 把device添加到内核
platform_driver_register
把一个platform_driver (drv) 注册进内核.
逻辑很简单, 做一些准备工作, 然后调用driver_register. 具体逻辑如下:
drv->driver.bus = &platform_bus_type : platform_driver都挂载在platform bus下
如果定义了drv->probe, 则将platform_drv_probe赋值给drv->driver.probe, platform_drv_probe是platform.c中定义的, 很明显, 当device与driver匹配上之后, 内核系统会调用driver->probe, 即这里的platform_drv_probe, 然后platform_drv_probe肯定会调用drv->probe
如果定义了drv->remove, 则将platform_drv_remove赋值给drv->driver.remove, 逻辑与上类似, 不在赘述
如果定义了drv->shutdown, 则将platform_drv_shutdown赋值给drv->driver.shutdown, 逻辑与上类似
调用driver_register将platform_driver注册进内核
platform_driver_probe
它最终会调用platform_driver_register, 不同之处在于该API是针对不可热插拔的device, 具体逻辑如下:
drv->prevent_deferred_probe = true : 禁止该platform_driver的延时probe机制
drv->driver.suppress_bind_attrs = true : 不在driver目录下生成bind/unbind两个属性文件, 从而导致不可通过写入这两个属性文件来启动device/driver的匹配/解配, 也是为了不去处理热插拔
drv->probe = probe (这个probe是本API的一个参数, 是个函数指针), 然后调用platform_driver_register. 当device与driver匹配上之后, 就会调用probe函数
接下来的逻辑主要是让这个platfrom_driver不再尝试匹配其它device
drv->probe = NULL
检查drv->driver.p->klist_devices.k_list, 如果为空, 证明这个platfrom_driver没有匹配到一个device, 则返回错误
drv->driver.probe = platform_drv_probe_fail, 这样任何其它device在尝试与该driver匹配时, 都会返回失败
12.5 Demo
Demo的构思也很简单, platform bus已经存在了, 不需要重建了. 我们可以新建一个paltform_device和一个platform_driver, 把它们添加到内核中, 观察两者的匹配过程.
https://gitlab.com/study-linux/linux_driver_model/blob/master/platform_device_driver.c
13. device resource management
终于, 终于, 设备模型总算是全部介绍完毕了…
本章讲一个小东西, 思路很简单, 相信很容易理解. 不过作为一个驱动工程师, 本节内容非常实用, 可以解答一些困惑, 可以使我们的代码变得简单、简洁.
13.1 前言
先看一个例子, 不用逐行看, 扫一眼即可
/* drivers/media/platform/soc_camera/mx1_camera.c, line 695 */
static int __init mx1_camera_probe(struct platform_device *pdev)
{
...
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
irq = platform_get_irq(pdev, 0);
if (!res || (int)irq <= 0) {
err = -ENODEV;
goto exit;
}
clk = clk_get(&pdev->dev, "csi_clk");
if (IS_ERR(clk)) {
err = PTR_ERR(clk);
goto exit;
}
pcdev = kzalloc(sizeof(*pcdev), GFP_KERNEL);
if (!pcdev) {
dev_err(&pdev->dev, "Could not allocate pcdev\n");
err = -ENOMEM;
goto exit_put_clk;
}
...
/*
* Request the regions.
*/
if (!request_mem_region(res->start, resource_size(res), DRIVER_NAME)) {
err = -EBUSY;
goto exit_kfree;
}
base = ioremap(res->start, resource_size(res));
if (!base) {
err = -ENOMEM;
goto exit_release;
}
...
/* request dma */
pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH);
if (pcdev->dma_chan < 0) {
dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n");
err = -EBUSY;
goto exit_iounmap;
}
...
/* request irq */
err = claim_fiq(&fh);
if (err) {
dev_err(&pdev->dev, "Camera interrupt register failed\n");
goto exit_free_dma;
}
...
err = soc_camera_host_register(&pcdev->soc_host);
if (err)
goto exit_free_irq;
dev_info(&pdev->dev, "MX1 Camera driver loaded\n");
return 0;
exit_free_irq:
disable_fiq(irq);
mxc_set_irq_fiq(irq, 0);
release_fiq(&fh);
exit_free_dma:
imx_dma_free(pcdev->dma_chan);
exit_iounmap:
iounmap(base);
exit_release:
release_mem_region(res->start, resource_size(res));
exit_kfree:
kfree(pcdev);
exit_put_clk:
clk_put(clk);
exit:
return err;
}
相信每一个写过Linux driver的工程师, 都在probe函数中遇到过上面的困惑: 要顺序申请多种资源(IRQ、Clock、memory、regions、ioremap、dma、等等), 只要任意一种资源申请失败, 就要回滚释放之前申请的所有资源. 于是函数的最后, 一定会出现很多的goto标签(如上面的exit_free_irq、exit_free_dma、等等), 并在申请资源出错时, 小心翼翼的goto到正确的标签上, 以便释放已申请资源.
我们在内核模块一文中提到过这个现象, 那篇文章中提出了2种方式处理这种问题 : 使用goto 或者 用状态标示, 然后统一在module_exit函数中处理.
不过, 针对device resource, 内核Linux设备模型借助device resource management(设备资源管理), 帮我们解决了这个问题. 就是: driver你只管申请就行了, 不用考虑释放, 我设备模型帮你释放. 最终, 我们的driver可以这样写:
static int __init mx1_camera_probe(struct platform_device *pdev)
{
...
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
irq = platform_get_irq(pdev, 0);
if (!res || (int)irq <= 0) {
return -ENODEV;
}
clk = devm_clk_get(&pdev->dev, "csi_clk");
if (IS_ERR(clk)) {
return PTR_ERR(clk);
}
pcdev = devm_kzalloc(&pdev->dev, sizeof(*pcdev), GFP_KERNEL);
if (!pcdev) {
dev_err(&pdev->dev, "Could not allocate pcdev\n");
return -ENOMEM;
}
...
/*
* Request the regions.
*/
if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res), DRIVER_NAME)) {
return -EBUSY;
}
base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
if (!base) {
return -ENOMEM;
}
...
/* request dma */
pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH);
if (pcdev->dma_chan < 0) {
dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n");
return -EBUSY;
}
...
/* request irq */
err = claim_fiq(&fh);
if (err) {
dev_err(&pdev->dev, "Camera interrupt register failed\n");
return err;
}
...
err = soc_camera_host_register(&pcdev->soc_host);
if (err)
return err;
dev_info(&pdev->dev, "MX1 Camera driver loaded\n");
return 0;
}
怎么做到呢?
注意上面”devm_”开头的接口, 答案就在那里. 不要再使用那些常规的资源申请接口, 用devm_xxx的接口代替.
为了保持兼容, 这些新接口和旧接口的参数保持一致, 只是名字前加了”devm_”, 并多加一个struct device指针
13.2 devm_xxx
下面列举一些常用的资源申请接口, 它们由各个framework(如clock、regulator、gpio、等等)基于device resource management实现. 使用时, 直接忽略”devm_”的前缀, 后面剩下的部分, driver工程师都很熟悉. 只需记住一点, driver可以只申请, 不释放, 设备模型会帮忙释放. 不过如果为了严谨, 在driver remove时, 可以主动释放(也有相应的接口,这里没有列出).
extern void *devm_kzalloc(struct device *dev, size_t size, gfp_t gfp);
void __iomem *devm_ioremap_resource(struct device *dev,
struct resource *res);
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
unsigned long size);
struct clk *devm_clk_get(struct device *dev, const char *id);
int devm_gpio_request(struct device *dev, unsigned gpio,
const char *label);
static inline struct pinctrl * devm_pinctrl_get_select(
struct device *dev, const char *name)
static inline struct pwm_device *devm_pwm_get(struct device *dev,
const char *consumer);
struct regulator *devm_regulator_get(struct device *dev, const char *id);
static inline int devm_request_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, unsigned long irqflags,
const char *devname, void *dev_id);
struct reset_control *devm_reset_control_get(struct device *dev,
const char *id);
13.3 什么是device resource
一个设备能工作, 需要依赖很多的外部条件, 如供电、时钟等等, 这些外部条件称作设备资源(device resouce). 对于现代计算机的体系结构, 可能的资源包括:
a)power : 供电
b)clock : 时钟
c)memory, 内存, 在kernel中一般使用kzalloc分配
d)GPIO, 用户和CPU交换简单控制、状态等信息
e)IRQ, 触发中断
f)DMA, 无CPU参与情况下进行数据传输
g)虚拟地址空间, 一般使用ioremap、request_region等分配
h)等等
而在Linux kernel的眼中, “资源”的定义更为广义, 比如PWM、RTC、Reset, 都可以抽象为资源, 供driver使用.
在较早的kernel中, 系统还不是特别复杂, 且各个framework还没有成型, 因此大多的资源都由driver自行维护. 但随着系统复杂度的增加, driver之间共用资源的情况越来越多, 同时电源管理的需求也越来越迫切. 于是kernel就将各个resource的管理权收回, 基于”device resource management”的框架, 由各个framework统一管理, 包括分配和回收
13.4 软件框架
说了这么久, device resource management的软件框架到底是怎样的? 管理机制是怎么做的, 实现代码是哪个? 接下来看看这些问题.
device resource management位于”drivers/base/devres.c”中.
它的实现非常简单, 为什么呢? 因为资源的种类有很多, 表现形式也多种多样, 而devres不可能一一知情, 也就不能进行具体的分配和回收.
因此, devres能做的(也是它的唯一功能), 就是:
提供一种机制, 将系统中某个设备的所有资源, 以链表的形式, 组织起来, 以便在driver detach(device_release_driver)的时候, 自动释放.
而更为具体的事情, 如怎么抽象某一种设备资源, 则由上层的framework负责. 这些framework包括: regulator framework(管理power资源), clock framework(管理clock资源), interrupt framework(管理中断资源)、 gpio framework(管理gpio资源), pwm framework(管理PWM), 等等.
其它的driver, 位于这些framework之上, 使用它们提供的机制和接口(devm_xxx), 开发起来就非常方便了
13.5 代码分析
相关数据结构
先从struct device开始吧! 还记得在介绍struct device数据结构时提到的devres_head链表头吗? 不记得了回头看看, 它就是用来挂载本device用到的所有resources.
那单个resource用什么表示呢?
在”drivers/base/devres.c”中, 名称为struct devres, 如下:
注意: devres有关的数据结构, 是在devres.c中定义的(是C文件哦!). 换句话说, 是对其它模块透明的. 这真是优雅的设计(尽量屏蔽细节)!
struct devres {
struct devres_node node;
/* -- 3 pointers */
unsigned long long data[]; /* guarantee ull alignment */
};
这个结构体很简单, 思路也很清晰: struct devres_node描述不同resources相同的部分; data[] 这个零长度数组用于描述每个resource的不同部分 (Programming-Basics一文中描述过零长度数组).
看一下struct devres_node:
struct devres_node {
struct list_head entry;
dr_release_t release;
#ifdef CONFIG_DEBUG_DEVRES
const char *name;
size_t size;
#endif
};
抛开debug相关的不管, 也很清晰:
一个list_head, 用于把每个resource挂载到device->devres_head链表头中.
资源的存在形式到底是什么, device resource management并不知情, 因此需要上层模块提供一个release的回调函数, 用于release资源
API for framework driver
先看一眼软件框架里的那张图, framework driver就是指具体某个resource(比如IRQ)的实现部分, 它会向某个设备驱动提供自己的API(例如: devm_request_irq, devm_free_irq).
framework driver 是基于 device resource management实现的, device resource management提供给framework driver的API主要包括:
devres_alloc/devres_free、devres_add/devres_remove
结合本章前文的描述, 估计你应该能明白是怎么回事了.
先看一个使用device resource management的例子(IRQ模块:devm_request_irq, devm_free_irq 是IRQ模块提供给设备驱动的API, 它也用到了resource management提供给它的API):
/* include/linux/interrupt.h */
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
{
return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
devname, dev_id);
}
/* kernel/irq/devres.c */
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id)
{
struct irq_devres *dr;
int rc;
dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres),
GFP_KERNEL);
if (!dr)
return -ENOMEM;
/*调用我们经常在驱动程序中见到的中断注册接口, 注册中断.该步骤和device resource management无关.*/
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
dev_id);
if (rc) {
devres_free(dr);
return rc;
}
dr->irq = irq;
dr->dev_id = dev_id;
devres_add(dev, dr);
return 0;
}
EXPORT_SYMBOL(devm_request_threaded_irq);
void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id)
{
struct irq_devres match_data = { irq, dev_id };
WARN_ON(devres_destroy(dev, devm_irq_release, devm_irq_match,
&match_data));
free_irq(irq, dev_id);
}
EXPORT_SYMBOL(devm_free_irq);
devres_alloc
framework driver使用device resource management的第一步是:
定义一个描述自己resource的结构体(例如IRQ中的 struct irq_devres), 很显然, 那个零长度数组就会指向这个结构体的启示地址:
/*
* Device resource management aware IRQ request/free implementation.
*/
struct irq_devres {
unsigned int irq;
void *dev_id;
};
定义释放自己resource的release函数, 注意这个release不是用于释放memory, 而是与resource自身相关, 例如IRQ资源在申请时要用request_irq, 释放时要用free_irq
static void devm_irq_release(struct device *dev, void *res)
{
struct irq_devres *this = res;
free_irq(this->irq, this->dev_id);
}
这个release函数会被devres模块调用, 注意devres传递过来的是void*指针(也就是零长度数组指向的地址), 各个framework driver直接把void*转换成描述自己resource的结构体指针即可. 对devres而言, 它不可能知道resource的具体形态, 所以只能用void*
然后以回调函数、resource对应的结构体的size为参数, 调用devres_alloc接口, 为resource分配空间. 该接口的定义如下
void * devres_alloc(dr_release_t release, size_t size, gfp_t gfp)
{
struct devres *dr;
dr = alloc_dr(release, size, gfp);
if (unlikely(!dr))
return NULL;
return dr->data;
}
调用alloc_dr, 分配一个struct devres类型的变量, 并返回其中的data指针(data就是零长度数组). alloc_dr的定义如下:
static __always_inline struct devres * alloc_dr(dr_release_t release,
size_t size, gfp_t gfp)
{
size_t tot_size = sizeof(struct devres) + size;
struct devres *dr;
dr = kmalloc_track_caller(tot_size, gfp);
if (unlikely(!dr))
return NULL;
memset(dr, 0, tot_size);
INIT_LIST_HEAD(&dr->node.entry);
dr->node.release = release;
return dr;
}
看第一句就可以了, 在资源size之前, 加一个struct devres的size, 就是total分配的空间. 除去struct devres的, 就是资源的(由data指针访问). 之后是初始化struct devres变量的node
devres_add
alloc成功之后, 进行一下初始化, 然后以设备指针(dev)和资源指针(dr)为参数, 调用devres_add, 将资源添加到设备的资源链表头(devres_head)中, 该接口定义如下:
void devres_add(struct device *dev, void *res)
{
struct devres *dr = container_of(res, struct devres, data);
unsigned long flags;
spin_lock_irqsave(&dev->devres_lock, flags);
add_dr(dev, &dr->node);
spin_unlock_irqrestore(&dev->devres_lock, flags);
}
static void add_dr(struct device *dev, struct devres_node *node)
{
devres_log(dev, node, "ADD");
BUG_ON(!list_empty(&node->entry));
list_add_tail(&node->entry, &dev->devres_head);
}
devres_free
可以通过devres_free接口释放资源占用的空间, 这里释放的是memory:
void devres_free(void *res)
{
if (res) {
struct devres *dr = container_of(res, struct devres, data);
BUG_ON(!list_empty(&dr->node.entry));
kfree(dr);
}
}
devres_remove
从devres_head中链表中移除一个资源.
void * devres_remove(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
struct devres *dr;
unsigned long flags;
spin_lock_irqsave(&dev->devres_lock, flags);
dr = find_dr(dev, release, match, match_data);
if (dr) {
list_del_init(&dr->node.entry);
devres_log(dev, &dr->node, "REM");
}
spin_unlock_irqrestore(&dev->devres_lock, flags);
if (dr)
return dr->data;
return NULL;
}
devres_destroy
devm_free_irq接口中, 调用了devres_destroy.
devres_destroy主要做了2个动作
调用devres_remove将devres从devres_head中移除
调用devres_free释放memory资源
devm_free_irq调用devres_destroy处理完相关事情之后, 会调用free_irq释放之前申请的IRQ资源, devm_free_irq一般是驱动主动调用的.
API for driver model
还记得我们在前言中描述的device resource management的设计初衷吗? 就是会了解决在设备初始化中头疼的goto问题.
例如, 驱动程序的probe函数中可以用request_irq去申请一个IRQ资源, 当后续的初始化过程中出现错误时, 就要用free_irq释放这个资源.
如果在申请IRQ资源的时候用devm_request_irq, 在后续初始化出错时, 可以手动用devm_free_irq释放资源(这种方式也要用goto, 与上面的做法一样); 也可以什么都不管, 直接return –XXX. driver model的核心代码在收到这个错误之后, 会自动释放资源.
devres_release_all
devres_release_all就是用来自动释放resources的. 实现如下:
int devres_release_all(struct device *dev)
{
unsigned long flags;
/* Looks like an uninitialized device structure */
if (WARN_ON(dev->devres_head.next == NULL))
return -ENODEV;
spin_lock_irqsave(&dev->devres_lock, flags);
return release_nodes(dev, dev->devres_head.next, &dev->devres_head,
flags);
}
以设备指针为参数, 直接调用release_nodes:
static int release_nodes(struct device *dev, struct list_head *first,
struct list_head *end, unsigned long flags)
__releases(&dev->devres_lock)
{
LIST_HEAD(todo);
int cnt;
struct devres *dr, *tmp;
cnt = remove_nodes(dev, first, end, &todo);
spin_unlock_irqrestore(&dev->devres_lock, flags);
/* Release. Note that both devres and devres_group are
* handled as devres in the following loop. This is safe.
*/
list_for_each_entry_safe_reverse(dr, tmp, &todo, node.entry) {
devres_log(dev, &dr->node, "REL");
dr->node.release(dev, dr->data);
kfree(dr);
}
return cnt;
}
remove_nodes就是从设备链表头中移除本资源.
node.release就是调用某个资源的release函数, 对于IRQ而言, 就是调用devm_irq_release, 这个release函数里面会调用free_irq
kfree(dr)就是释放memroy资源.
这三步动作, 其实跟devm_free_irq的效果一样.
devres_release_all的调用时机
我们知道devres_release_all是设备模型核心代码用来自动释放device所占用的resources的. 那核心代码在什么情况下会调用该函数呢? 有两个时机:
probe失败时
调用过程为(就不详细的贴代码了):
__driver_attach/__device_attach-->driver_probe_device—>really_probe, really_probe调用driver或者bus的probe接口, 如果失败(返回值非零, 可参考本文开头的例子), 则会调用devres_release_all
driver dettach时(就是driver remove时)
driver_detach / bus_remove_device-->__device_release_driver-->devres_release_all