设备模型

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, DeviceDevice Driver的概念

下图是嵌入式系统常见的硬件拓扑的一个示例:

硬件拓扑描述Linux设备模型中四个重要概念中三个:Bus, ClassDevice(第四个为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, 有的目录是以其他形式创建的.

KobjectLinux设备模型的基石:

Linux设备模型的核心是使用BusClassDeviceDriver四个核心数据结构, 将大量的、不同功能的硬件设备(以及驱动该硬件设备的方法), 以树状结构的形式, 进行归纳、抽象, 从而方便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/某个目录下的一个文件, 这个文件叫属性文件.

AttributeKobject的关系也很明显, 文件是存在于某个目录下的, 所以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 */

struct  kobject

Comment

const char *name

Kobject的名称, 同时也是sysfs中的目录名称

struct list_head    entry

用于将Kobject加入到Kset中的list_head   (KobjectKset的关系下章详解)

struct kobject    *parent

指向parent kobject, 以此形成层次结构(在sysfs就表现为目录结构)

struct kset     *kset;

kobject属于的Kset. 可以为NULL. 如果存在, 且没有指定parent, 则会把KsetKobject作为parent因为Kset是一个特殊的Kobject

struct kobj_type   *ktype;

Kobject属于的kobj_type. 每个Kobject必须有一个ktype, 否则Kernel会提示错误

struct sysfs_dirent *sd;

Kobjectsysfs中的表示

struct kref     kref;

"struct kref类型(在include/linux/kref.h中定义)的变量, 为一个可用于原子操作的引用计数

unsigned int state_initialized:1

指示该Kobject是否已经初始化, 以在KobjectInit, 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  attribute

Comment

const char *name

Attribute的名称, 同时也是sysfs中的文件名称

umode_tmode

/写权限

#ifdef CONFIG_DEBUG_LOCK_ALLOC

boolignore_lockdep:1;

struct lock_class_key*key;

struct lock_class_keyskey;

#endif

 

 

CONFIG_DEBUG_LOCK_ALLOC相关, 这里不关注

 

struct bin_attribute为二进制属性, struct attribute的基础上, 增加了readwrite等函数,它所生成的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

void (*release)(struct kobject *kobj)

通过该回调函数, 可以将包含该种类型kobject的数据结构的内存空间释放掉

const struct sysfs_ops *sysfs_ops

种类型的Kobjectsysfs文件系统接口, 也就是属性文件的读/写函数

struct attribute **default_attrs

该种类型的Kobjectatrribute列表(所谓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

kobject API

Comment

void kobject_init(struct kobject *kobj, struct kobj_type *ktype)

我们可以用kmalloc自行分配一个Kobject结构体, 然后用API初始化此结构体.

注意在该API需要传递一个kobj_type类型的参数, 该参数是必须的, 不能为空, 印证了我们上面提到的, 每个Kobject都需要有一个Ktype

如果我们调用kmalloc自行分配了Kobject, 那么我们也必须自行分配并初始化Ktype, 并将初始化之后的Ktype传给Kobject

int kobject_add(struct kobject *kobj, struct kobject *parent,

const char *fmt, ...)

将初始化完成的kobject添加到kernel, 参数包括需要添加的kobject、该kobjectparent(用于形成层次结构, 可以为空)、用于提供kobject name的格式化字符串.

int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...)

是上面两个接口的组合

extern void kobject_del(struct kobject *kobj);

add对应的反函数, kernel中移除kobject

 

 

 

extern struct kobject * __must_check kobject_create(void);

API会分配一个Kobject内存空间, 然后调用kobject_init初始化Kobject. 初始化时, 传递的Ktype是系统自己定义的一个叫dynamic_kobj_ktypeKtype.

如果我们使用这个API, 那么我们不需要自己去分配Kobject空间, 也不需要去自己去弄个Ktype, 系统会自己处理好

API会返回一个初始化好的Kobject, 接下来我们就可以调用kobject_add将它添加到kernel

extern struct kobject * __must_check kobject_create_and_add(const char *name, struct kobject *parent);

Create之后自动add

 

 

 

kobject_set_name_vargs

设置kobjectname

kobject_set_name

对上个函数的封装, 用于设置kobjectname

kobject_rename

修改kobject的名字

 

 

 

kobject_get

调用kref_get(&kobj->kref), 来增加kobject的引用计数. 一般我们在用kobject_add将某个kobject添加到内核时, 如果它的parent不为NULL, 则会把parent指向的kobject的引用计数加一.

kobject_put

调用kref_put(&kobj->kref, kobject_release)来减少kobject的引用计数. 当计数到0, 会执行传进去的kobject_release函数释放资源.

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, nameNULL, 所有的属性文件都会存放在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、该kobjectparent(用于形成层次结构, 可以为空)、用于提供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增加该kobjectparent的引用计数(如果存在parent的话)

        如果存在kset(即kobj->kset不为空), 则调用kobj_kset_join接口加入kset. 同时, 如果该kobject没有parent, 却存在kset, 做两件事: kobjectparent设为ksetkobjectkset是一个特殊的kobject, 并增加kset->kobjecct引用计数

NOTE: 该章节暂不考虑kset, ksetNULL, 下一章专门介绍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中的注册

          调用该kobjectktyperelease接口, 释放内存空间

          释放该kobjectname所占用的内存空间

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_createkobject_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读写属性文件的时候, 首先调用的肯定是sysfsopen函数, 然后是sysfsread/write函数. 然后呢? read/write是如何操作到具体的属性文件的呢? struct attribute数据结构很简单, 没有看见什么接口函数去响应sysfsread/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, 如手动创建KobjectDEMO演示的那样;

如果是系统定义的ktype, 则上述流程还会增加一环, (ktype->sysfs_ops) -> (kobj_attribute->show/store) , 如自动创建KobjectDEMO演示的那样.

 

还有一个问题需要思考:

通过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会指向ksetkobject. 这种情况下, 我们可以把kset简单看做父目录, 而属于ksetkobject都是子目录. 就像下面这张图一样:

多说一句, 注意是大多数情况下, 就算某个kobject属于某个kset, 也不一定代表着该kobject->parent就一定指向ksetkobject. 相应的, 这种情况下就不存在父/子目录的关系.

 

Kset主要是跟Uevent事件上报相关的, Uevent会在下一章会详解介绍.

上文提到kset用来集合相似的kobject, 这个相似是指kset可以决定它下属的kobject可以上报哪些uevent事件; 有哪些uevent信息是下属所有kobject都要统一上报的.

5.2 主要数据结构

Kset

/* include/linux/kobject.h, line 159 */

struct  kset

Comment

struct list_head list

用于链接kset下所有的kobject链表

spinlock_t list_lock

, 用于互斥往链表中增/kobject

struct kobject kobj

kset自己的kobjectkset是一个特殊的kobject, 也会在sysfs中以目录的形式体现)

const struct kset_uevent_ops *uevent_ops

ksetuevent操作函数集. 当任何Kobject需要上报uevent, 都要调用它所从属的ksetuevent_ops, 添加环境变量, 或者过滤eventkset可以决定哪些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

kset  API

Comment

extern void kset_init(struct kset *kset)

该接口用于初始化已分配的kset, 主要包括调用kobject_init_internal初始化其kobject, 然后初始化kset链表和锁.

需要注意的, 如果使用此接口, 上层软件必须提供该kset中的kobjectktype

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释放ksetkobject. 当其kobject的引用计数为0, 即调用ktyperelease接口释放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是采用手动的方式(只能用手动方式, 如果你去看kobjectAPI, 就会发现, 只有通过手动方式, 才能指定kobjectkset).

 

建议你阅读到这里时, 自己尝试用自动创建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的类型, 包括:

enum kobject_action

Comment

KOBJ_ADD

kobject或上层数据结构(内嵌了kobject)的添加事件

KOBJ_REMOVE

kobject或上层数据结构(内嵌了kobject)的移除事件

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简单理解成一大块内存, 这块内存的size2048. 每当需要上报一个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

char *envp[UEVENT_NUM_ENVP]

指针数组

一个事件会有多个信息, 每个信息都是一个字符串(见本节上文), 所有的信息都依次存放在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统一管理下属kobjectuevent事件.

具体细节已经放在上文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为环境变量, 上报一个指定actionuevent, 具体动作如下:

         查找kobj本身或者其parent是否从属于某个kset, 如果不是, 则报错返回(由此可以说明, 如果一个kobject没有加入kset, 是不允许上报uevent的)

         查看kobj->uevent_suppress是否设置, 如果设置, 则忽略所有的uevent上报并返回(由此可知, 可以通过Kobjectuevent_suppress标志, 管控Kobjectuevent的上报)

         如果所属的ksetuevent_ops->filter函数, 则调用该函数, 过滤此次上报(回头查看kset_uevent_ops数据结构, 这佐证了有关filter接口的说明, kset可以通过filter接口过滤不希望上报的event, 从而达到整体的管理效果)

         判断所属的kset是否有合法的名称(称作subsystem, 和前期的内核版本有区别),否则不允许上报uevent

         分配一个用于此次上报的、存储环境变量的buffer也就是分配一个kobj_uevent_env数据结构, 并获得该Kobjectsysfs中路径信息(用户空间程序需要依据该路径信息在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_sentkobj->state_remove_uevent_sent变量, 以记录正确的状态

         调用add_uevent_var接口, 添加格式为"SEQNUM=%llu的序列号

         如果定义了"CONFIG_NET, 则使用netlink发送该uevent

         如果定义了 CONFIG_UEVENT_HELPER, 则调用init_uevent_argv初始化kobj_uevent_envargv变量(回看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内存块操作接口, 以格式化字符的形式(类似printfprintk等), 将环境变量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, 内核系统定义了一个名为blockclass, 当把该class注册进内核时, 就会产生该目录 (阅读完Class一章后你就会明白这个目录是如何生成的).

8.   BUS

8.1 简介

总线处理器与一个或者多个设备之间的通道, 为了方便设备模型的实现, 内核规定, 系统中的每个设备都要连接在一个Bus. 这个Bus可以是一个内部Bus、虚拟Bus或者Platform Bus.

 

Bus在内核中的作用主要体现为

         Bus下面挂接devicesdrivers

         Bus定义devicedriver的匹配规则

         Bus可以定义下属devicesdrivers的默认属性

 

系统中有各种各样的总线, 这些总线的直观感受就是/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, devicedevice_driver的匹配规则

当任何属于该Busdevice或者device_driver添加到该Bus, 系统都会调用该函数, 该函数会告诉系统devicedriver是否匹配, 如果匹配成功, 该函数要返回非零值, 此时系统就会执行后续的处理

int (*uevent)(struct device *dev, struct kobj_uevent_env *env)

还记得kset的主要作用吗? 还记得kset-> uevent_ops->uevent函数的作用吗? 不记得的话回头看一下

当我们打算创建一条总线时, 需要定义该函数.

该函数的作用与ksetuevent函数的作用类似, 当任何属于该Busdevice,发生添加、移除或者其它动作时, Bus模块的核心逻辑就会调用该接口, 以便添加uevent事件的事件信息.

int (*probe)(struct device *dev)

当某个devicedriver匹配上了之后, 系统调用driver->probe来初始化driver. 但是, 如果需要probe(其实就是初始化)指定的device, 需要保证该device所在的bus是被初始化过、确保能正确工作的. 这就要就在执行device_driverprobe, 先执行它的busprobe

Note: 并不是所有的bus都需要proberemove接口的, 因为对有些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)

热插拔相关, 如果Busdevice想支持热插拔, 则本Bus必须定义online函数

int (*offline)(struct device *dev)

热插拔相关, 如果Busdevice想支持热插拔, 则本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 */

struct subsys_private

Comment

struct kset subsys

kset/sys/中以目录的形式存在, 代表本Bus, 例如/sys/bus/spi, 就代表着spi这个目录, 目录的名称是在bus_type中指定的

 

 

struct kset *devices_kset

kset, 代表/sys/bus/xxx/devices这个目录

所有属于该Busdevice, 都会在该目录下创建一个链接 (注意是链接哦, device的真实目录不是在这里的, 这里只是真实目录的一个链接, 就像我们可以在XP下为某个文件夹创建一个快捷方式)

struct klist klist_devices

链表头, Bus下的所有device都会挂载到这个链表下

struct kset *drivers_kset

kset, 代表/sys/bus/xxx/drivers这个目录

所有属于该Busdriver, 都会存放在该目录下, 注意不是链接, 是真实的目. 意味着所有Busdriver->kobjectparent都是该kset->kobject

struct klist klist_drivers

链表头, Bus下的所有drivers都会挂载到这个链表下

 

 

struct list_head interfaces

链表头, 用于挂载该Bus下所有的interfaces, interfaces的概念暂时详述

struct mutex mutex

interfaces链表头配合, 互斥

 

 

struct bus_type *bus

保存上层的Bus指针

struct class *class

或者保存上层的Class指针

 

 

struct blocking_notifier_head bus_notifier

bus_notifer通知链, 其他人需要监听该通知链时, 可以注册到该链上.

简单介绍一下内核通知链机制, 后面会有文章详述. 这种机制的做法是定义一个通知链, 然后如果别人想监听这个通知链上是否有新动作时, 就会注册到该通知链, 当这个通知链上有动作时, 系统就会通知所有注册的人.

unsigned int drivers_autoprobe:1

用于控制driver添加到该Bus, 是否开始匹配driverdevice

struct kset glue_dirs

 

从数据结构中我们可以看到, 这个结构就是集合了一些bus模块需要使用的私有数据, 例如kset啦、klist啦等等. 那为什么这个结构体名要叫subsys_private, 而不叫bus_private?

 

看看include/linux/device.h中的struct class结构(我们会在下一章中介绍class)就知道了,因为class结构中也包含了一个一模一样的struct subsys_private指针, 看来classbus很相似啊

 

想到这里, 就好理解了, 无论是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_driversprobe处理

         管理bus下的所有devicedevice_driver

 

头文件: include/linux/device.h

实现文件: drivers/base/bus.c

bus  API

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上有devicesaddition/removal, 或者当devicedriver 配对/解对成功, 通知链上的所有人都会收到通知

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

与上相反, deviceBus移除时, 调用该函数

extern void bus_probe_device(struct device *dev)

当调用device_add把某个device添加到Bus, 如果bus_set->subsys_private-> drivers_autoprobe1的话(在创建Bus, 默认就是1), 则会在Busdrivers链表下搜寻所有的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->kobjname, 例如:

         在该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函数, 则调用该Busremove函数; 否则如果device->driver所指向的driver定义了remove函数, 则调用driver->remove

         调用devres_release_all处理相关事情, 暂时不去了解细节

         device->driverdevice->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->driverNULL, 则针对该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->driverNULL, 则调用driver_probe_device. (在本节下面分析)

driver_probe_device

driver_probe_device(struct device_driver *drv, struct device *dev)

尝试绑定drvdev, 具体指向逻辑如下:

         首先, 判断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函数, 则调用Busprobe函数, 否则如果driver定义了probe函数, 则调用driverprobe函数

         调用driver_bound(dev)进行处理, 前面有介绍这个函数

8.4 关键代码分析

bus_register

bus添加到内核是由bus_register接口实现的, 该接口主要的功能就是创建了一些文件夹和一些属性文件, 比较简单, 执行逻辑如下:

         bus_typestruct subsys_private类型的指针分配空间, 并更新priv->busbus->p两个指针为正确的值

         初始化通知链 priv->bus_notifier

         接下来用手动创建kset的方式(可以回头看下手动创建ksetDEMO), 创建priv->subsys, 这个kset代表本Bus, /sys/bus/xxx中的xxx目录

         priv->subsys.kobjname初始化为bus_type->name, name就是xxx目录的目录名

         priv->subsys.kobjkset初始化为bus_kset, bus_kset是系统创建的, 代表/sys/bus这个目录(如果不记得了, 回头看下 /sys/下的顶层目录 一章), kobject的特性可知, 如果不指定priv->subsys.kobjparent, 则该kset做为它的parent. 实际上也是如此, 所以所有的Bus都会存在于/sys/bus目录下

         priv->subsys.kobjktype初始化为bus_ktype, ktype也是由系统创建的. 我们自己也定义过ktype(在手动创建kobjectDEMO), 因此知道该ktype会负责释放资源和读/priv->subsys.kobj下的属性文件(/sys/bus/xxx/下的属性文件).

         调用kset_registerpriv->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指针中的mutexklist_devicesklist_drivers等变量

         调用add_probe_files/sys/bus/xxx目录下创建2个属性文件, 这两个属性文件也是系统用BUS_ATTR这个宏定义的.

         第一个属性文件是drivers_probe, 它只有写入函数, 当用户空间往该属性文件写入一个属于该Busdevice的名称时, 最终调用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接口, 将该devicesysfs中的目录, 链接到该busdevices目录下, 例如

/sys/devices/platform/i8042device所在的真正目录, 为了方便管理, 内核在device所属Busdevices目录下创建了一个符号链接, 前面在介绍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, 以及一些链表头

         为该driverstruct 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->ktypedriver_ktype, 把该kobj添加到内核. 此步骤执行完毕之后, 就会在/sys/bus/xxx/drivers目录下生成一个driver对应的目录. 目录的名称是driver->name. 例如 /sys/bus/platform/drivers/i8042

         priv->knode_bus加入到bus->p->klist_drivers链表头, 也就是把driver挂接到Busdrivers链表头下

         如果该driverBusdrivers_autoprobe1, 则调用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, 会触发系统针对该driverkobject上报一个kobject_action类型的uevent事件.

         调用driver_add_attrs接口, sysfs的该driver的目录, 创建由bus->drv_attrs指针定义的属性组

         如果driver->suppress_bind_attrs0, 则会在driver的目录下创建两个属性文件

         bind属性文件, bus.c中用DRIVER_ATTR_WO(bind)定义, 当用户空间写入属于该Bus的某个devicename, 调用driver_probe_device, 触发系统去匹配这个driver和这个device, 如果匹配成功, 会调用driver->probe初始化.

这个功能与/sys/bus/xxx下的drivers_probe属性文件很像, 不同的是, drivers_probe写入devicename, 系统会去尝试匹配该deviceBus下的所有drivers; 而往bind写入devicename, 系统只会尝试匹配device与该driver

         unbind属性文件, bus.c中用DRIVER_ATTR_WO(unbind)定义, bind属性文件相反, 写入devicename, 调用device_release_driver, 解除devicedriver的绑定

bus_probe_device

内核提供了device_register接口用于把某个device添加到某个Bus, device_register会调用device_add, device_add会调用bus_probe_device函数.

 

bus_probe_device的处理逻辑:

         如果Busdrivers_autoprobe1, 则会调用device_attach去尝试匹配该deviceBus下的某个driver.

8.5 Demo

我们打算定义这样一个Demo, 首先创建一条总线(Bus), 在该总线下, 创建1个属性文件; 然后往该Bus添加一个device和一个driver, 观察devicedriver的匹配状况.

 

这个DEMO会分几次完成, 在本章中, 我们只会创建一条Bus, 并添加Bus下的属性文件, 在随后的章节中, 我们会往该Bus添加devicedriver. 你可以通过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就是nameblockclass.

 

类成员通常被上层代码所控制, 而不需要来自驱动程序的明确支持. 极少的情况下, 驱动程序需要直接处理类

 

快速体验class

         /sys/class : block class, 所有的class都在该目录下

         /sys/class/input : input 就是一个class

         在本class目录下, 创建每一个属于该classdevice的符号链接.

这个链接的好处是: 用户空间可以更方便的通过sysfs访问device相关的特性(也就是属性文件), 原因是路径名简短.

         如果device属于该class, device所在的目录下, 创建一个指向本class的符合链接, 链接名 subsystem, 上图没有显示, 有兴趣可以自己去看看

9.2 主要的数据结构

class

内核用struct class抽象一个class, 它的定义如下

/* include/linux/device.h, line 332 */

struct class

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

属于该classdevice的默认属性, 当调用device_register添加一个device, 如果device属于该class, 会在device的目录下创建这些属性文件

struct kobject                  *dev_kobj

表示该class下的设备在/sys/dev/下的目录, 现在一般有charblock两个,如果dev_kobjNULL, 则默认选择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)

用于releaseclass的回调函数

为什么bus_type里面没有一个类似的release bus的函数呢?

原因其实很简单, 本质上来将, 不管是Bus还是Class, 都是一个kobject, 回头看一下kobject一章, 我们说过, kobject的释放是由它的ktype完成的.

Bus对应的ktypebus_ktype (bus.c); Class对应的ktypeclass_ktype(class.c), bus_ktype->release是直接调用kfree释放Bus, class_ktype->release是调用class->class_release释放Class, 所以这里才有class_release这个函数

void (*dev_release)(struct device *dev)

用于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

未用

struct kset glue_dirs

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 *)

当某个deviceclass移除时, 会把该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_create(owner, name)

分配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->classclass->p两个指针为正确的值

         初始化cp-> klist_devices链表头

         初始化cp-> interfaces链表头

         调用kset_init初始化cp->glue_dirs这个kset, 注意只是初始化, 但未添加到内核, 所以看不到目录

         初始化互斥锁cp->mutex

         设置本class对应的kobject (cp->subsys.kobj)的名字为class->name

         如果class-> dev_kobjNULL, 则将其设置为sysfs_dev_char_kobj (/sys/dev/char)

         如果不是block class, 则设置cp->subsys.kobj.ksetclass_kset (/sys/class), 这样, 只要在把cp->subsys.kobj这个kobject添加到内核时, 设置其parentNULL, 系统就会把ksetkobject做为它的parent.

结果就是: 对于block class, 目录为/sys/block, 对于其它类型的class, 目录为/sys/class/xxx

         设置cp->subsys.kobj.ktypeclass_ktype, ktype是由系统创建的 (class.c). 因此知道该ktype会负责释放资源和读/cp->subsys.kobj下的属性文件(/sys/class/xxx/下的属性文件)

         调用kset_register初始化cp->subsys这个kset并把它添加到内核.

         调用add_class_attrsclass-> class_attrs中定义的默认属性添加到/sys/class/xxx/目录下

__class_create

该函数的原型是

struct class *__class_create(struct module *owner, const char *name,

     struct lock_class_key *key)

从这个原型中可以看出几个重点:

         主要的参数是classname module

         返回结果是一个class

一句话总结: 给定一个class的名称和module参数, 它就能帮你创建一个class.

 

具体的逻辑如下:

         首先分配一个struct class的内存空间

         初始化class->nameclass->module

         初始化class->class_releaseclass_create_releaseclass_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的符合链接, 链接名是该devicename

         如果device所属的classblock  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_interfaceadd_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

终于.., 我们开始关注到跟我们的日常工作紧密相关的东西了.

devicedevice_driver, 在前面介绍BusClass的时候, 我们已经无数次提到这两个概念, 接下来我们将详细介绍这两部分

10.1 简介

devicedevice_driverLinux驱动开发的基本概念, 驱动开发是思路很简单: 开发指定的软件(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引脚, 引脚的复用功能是怎样的.

前面我们介绍过, devicedevice_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

它也会用于devicedriver的匹配

struct acpi_dev_nodeacpi_node

表示高级配置和电源管理接口, ACPI device相关

它也会用于devicedriver的匹配

dev_tdevt

包含主/次设备号, 当此设备号不为0, 会在/sys/dev/*/目录下创建对应的目录, 目录名称是主设备号:此设备号.

同时, 还会生成对应的/dev/xxx设备节点

u32id

deviceid, 可以与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)

releasedevice

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 */

struct device_private

Comment

struct klist klist_children

链表头, 用于挂载本devicechildren device.

也就是children_device->parent = device

struct klist_node knode_parent

用于把本device挂载到parent deviceklist_children链表头

struct klist_node knode_driver

用于把本device挂载到与它匹配的driverklist_devices链表头, 所这里可以看到, 一个diver下面可以挂载多个device.

8.3节的device_bind_driver函数中有介绍这一挂载过程.

struct klist_node knode_bus

用于把本device挂载到所属Busbus_type->subsys_private-> klist_devices

struct list_head deferred_probe

用于把本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会被挂载到哪些地方

         deviceparent device链表中

         device对应的driver的链表中

         device所属的Bus的链表中

, 好像不对啊, 不是还有一个class, 没错

         device所属的class的链表中, 只不过用于挂载的entry没有放到device_private结构中, 而是在device-> knode_class

device_type

ktype&kset之于kobject有的类似. 回想一下ktypekset的作用, 还记得吗?

ktype的作用是释放kobject资源, 定义kobject默认的属性文件, 定义kobject下属性文件的读/写函数.

kset的作用是聚合相似的kobject, 控制这些kobjectuevent事件上报行为, 比如哪些事件不能上报(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中没有定义, 则调用所属classdevnode

void (*release)(struct device *dev)

用于释放资源

const struct dev_pm_ops *pm

电源管理相关

10.3 主要API说明

头文件: include/linux/device.h

实现文件: drivers/base/core.c

device  API

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

APIdevice_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_ksetcore.c中系统创建的一个kset, 它对应/sys/devices目录.

         kobject_init(&dev->kobj, &device_ktype)

device_ktypecore.c中系统定义的一个ktype

         别的就是一些链表头什么的, 不过说了

device_add

非常重要的API.

下面我详细分析一下该API, 具体逻辑如下:

         如果device->device_private (dev->p)为空, 则调用kzallocdev->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之后, 如果想获取devicename, 只能从dev->kobj->name获取 (通过函数dev_name(dev)), 而不能从dev->init_name获取.

         如果dev_name(dev)依然为NULL, 也就是说上一步中init_nameNULL, 则检查dev->bus是否存在, 如果存在, 则把dev->bus->dev_name+dev->id赋值给dev->kobj->name

         再次检查, 如果dev_name(dev)依然为NULL, 也就是init_nameNULL, 而且device也不属于某个Bus, 则内核不允许这种情况出现, 返回错误

         调用get_device_parent获取deviceparent->kobject, 用于组织目录层次

         如果device属于某一个class

       如果所属的classblock class

         如果dev->parent不为NULL, 并且dev->parent->class也是block classs, 则返回parent->kobject, /sys/block/xxx/

         否则, 返回block_class.p->subsys.kobj, /sys/block/目录

从这里可以看出, block classdevice存在于/sys/block目录或其子目录下. blcok class这个class的目录就是/sys/block (class章节的介绍).

       如果所属的class不是block class

         如果dev->parent不为NULL

         如果dev->parent->class不为NULL, 并且dev->class->ns_typeNULL, 则返回dev->parent所在的目录

         否则, dev->parent所在的目录下新建一个名为dev->class->name的目录, 返回此目录以做为device的父目录

         如果dev->parentNULL, 则在/sys/devices/virtual/下新建一个名为dev->class->name的目录, 返回此目录以做为device的父目录

         否则, device不属于某一个class

       如果dev->parentNULL, 则看看本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_adddev->kobj添加到内核, 添加完成后, 就会在dev->kobj.parent对应的目录下创建一个device->name的目录, 如果dev->kobj.parentNULL, 则会把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->offlinebus->online不为NULL, 并且dev->offline_disabled0, 则创建一个名为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_adddevice_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)启动devicedriver的匹配, 该函数在Bus一章分析过

         如果dev->parent存在, 将本device加入parentklist_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_releasecore.c中定义的一个函数, 直接调用kfree(dev)

         drvdata赋值给dev->driver_data

         调用kobject_set_name_vargs(&dev->kobj, fmt, args)设置dev->kobjname

         调用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代码分析中知道这个ksetdevices_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_devicedev->kobj的引用计数为0, dev->kob所属的ktype就会开始释放资源了.

 

所以要先找到dev->kobj对应的ktype, device_initialize代码分析中知道这个ktypedevice_ktype.

 

device_ktyperelease函数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, 然后观察devicedriver的匹配过程. 这个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

drivername

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两个属性文件. 往这两个属性文件写入devicename, 会启动devicedriver的匹配/解配

const struct of_device_id       *of_match_table

编写驱动代码时经常用到它, 用于指定本driver可以匹配的device.

记住devicedriver是否能匹配上, 是由bus->match决定的, 所以bus->match函数中一定用到了这个table

const struct acpi_device_id     *acpi_match_table

与上类似

int (*probe) (struct device *dev)

个接口函数用于实现driver逻辑的开始, 就像段代码的main函数.

deivedriver匹配上之后, 第一个执行的就是driver->probe.

probe函数用于硬件上的一些初始化, device获取一些device resource, 如果哪个步骤出错, 代表本driver不能驱动这个device

int (*remove) (struct device *dev)

这个接口函数用于实现driver逻辑的结束, 当设备被移除时, 就会执行对应driverremove函数

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

device_driver  API

Comment

extern int __must_check driver_register(struct device_driver *drv)

将本device_driver添加到某个Bus.

, 参数列表里面好像没有看到Bus, 怎么知道添加到哪个Bus? 没错, 你必须在调用该API之前初始化driver->bus

extern void driver_unregister(struct device_driver *drv)

将本device_driver从某个Bus移除, API最终会调用bus_remove_driver, 我们没有介绍这个函数, 很简单, 有兴趣可以自己阅读代码

11.4 关键代码分析

driver_register

API的逻辑比较简单, 大多数重要的事情是放在bus_add_driver函数中做的, 该函数我们在Bus一章介绍过了.

API具体逻辑如下:

         首先检查device_driver(drv)->bus是否存在

         然后检查以下函数是否有在busdriver中重复定义, 一般来讲, 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并将其添加到内核, 加入链表, 创建属性文件, 启动driverdevice的匹配等等.

         调用driver_add_groups创建drv->groups中定义的默认属性组

         调用kobject_uevent(&drv->p->kobj, KOBJ_ADD)发送uevent事件

kobject_uevent: driver uevent事件上报流程

device上报uevent事件的逻辑一样, 先找到drv->p->kobjkset.

bus_add_driver中初始化了此kset. 不过它很简单, 没有kset_uevent_ops函数, 所以driver在上报uevent事件的时候, 不会添加任何额外的事件信息

devicedevice_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

DemoBus Demo一起的, driver挂载到一个Bus, 然后观察devicedriver的匹配过程. 这个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, 等等.

因此, 由于这个共性, 内核在设备模型的基础上(devicedevice_driver, 对这些设备进行了更进一步的封装, 抽象出paltform busplatform deviceplatform driver, 以便驱动开发人员可以方便的开发这类设备的驱动.

可以说, paltform设备对Linux驱动工程师是非常重要的, 因为我们编写的大多数设备驱动,都是为了驱动plaftom设备. 本文我们就来看看Platform设备在内核中的实现.

Platform模块的软件架构

内核中Platform设备有关的实现位于include/linux/platform_device.hdrivers/base/platform.c两个文件中, 它的软件架构如下

由图片可知, Platform设备在内核中的实现主要包括三个部分:

Platform Bus, 基于底层bus模块, 抽象出一个虚拟的Platform bus, 用于挂载Platform设备;

Platform Device, 基于底层device模块, 抽象出Platform Device, 用于表示Platform设备;

Platform Driver, 基于底层device_driver模块, 抽象出Platform Driver, 用于驱动Platform设备.

其中Platform DevicePlatform Driver会为其它Driver提供封装好的AP, 具体可参考后面的描述.

12.2 主要数据结构

platform_device

/* include/linux/platform_device.h, line 22 */

struct platform_device

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 resourceinclude/linux/ioport.h)结构抽象

Linux, 系统资源包括I/OMemoryRegisterIRQDMABus等多种类型. 这些资源大多具有独占性, 不允许多个设备同时使用, 因此Linux内核提供了一些API, 用于分配、管理这些资源.

当某个设备需要使用某些资源时, 只需利用struct resource组织这些资源(如名称、类型、起始、结束地址等), 并保存在该设备的resource指针中即可.然后在设备probe, 设备需求会调用资源管理接口, 分配、使用这些资源.而内核的资源管理逻辑, 可以判断这些资源是否已被使用、是否可被使用等等.

u32       num_resources

代表有多少个设备资源

const struct platform_device_id *id_entry

devicedriver的匹配相关. 具体可以看platform_ bus这个Busmatch函数

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.hstruct 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也有一个同类型的变量, 很明显, 它是用于devicedriver的匹配的. 可以查看platform_busmatch函数逻辑

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 device  API

Comment

extern struct platform_device *platform_device_alloc(const char *name, int id);

分配一个platform_device结构体空间, 调用device_initialize API初始化platform_device.dev , 并初始化name, id, archdata等元素.

extern void arch_setup_pdev_archdata(struct platform_device *);

初始化platform_device.archdata

上一个API在初始化archdata的时候, 就是调用的此函数

extern int platform_device_add_resources(struct platform_device *pdev,const struct resource *res,unsigned int num);

往一个platform_device里面添加一些resource

extern int platform_device_add_data(struct platform_device *pdev, const void *data, size_t size);

指定该platfrom_deviceplatform data

也就是给platfrom_device.dev.platform_data赋值

extern int platform_device_add(struct platform_device *pdev);

将一个paltform_device添加到内核系统, 毫无疑问, 最终会调用device_add函数

extern void platform_device_del(struct platform_device *pdev);

将一个platform_device从内核系统移除, 主要逻辑是调用device_del

extern void platform_device_put(struct platform_device *pdev)

调用put_device减少platform_device.dev的引用计数

 

 

extern struct platform_device *platform_device_register_full(const struct platform_device_info *pdevinfo);

总体来说, API的意思是: 给定一个platform_device_info, API自动创建一个platform_device并把该platfrom_device添加到内核系统.

它内部会调用上面提到的alloc, add_resources, add_data, addAPI

注意它的第一个参数是一个结构体platform_device_info, 这个结构主要是描述该platfrom_device的一些信息, name, id, resources等等

static inline struct platform_device *platform_device_register_resndata(struct device *parent, const char *name, int id, const struct resource *res, unsigned int num,              const void *data, size_t size)

上一个API的变种, 相当于以函数参数的形式提供name, id等信息.

API会把这些参数封装成一个platform_device_info, 然后调用platform_device_register_full

static inline struct platform_device *platform_device_register_simple(const char *name, int id,const struct resource *res, unsigned int num)

上一个API的变种, 相当于提供一些简单的参数, 然后调用platform_device_register_resndata

static inline struct platform_device *platform_device_register_data(struct device *parent, const char *name, int id,              const void *data, size_t size)

与上一个API的逻辑一样, 只不过参数不同而已, 最终会调用platform_device_register_resndata

 

 

extern int platform_device_register(struct platform_device *)

当手动分配了一个platform_device结构体后, 可以调用该API把该device添加到内核.

注意该API会先调用device_initialize初始化该device, 所以这个platform_device一般是用kzlloc分配的, 而不是用platform_device_alloc分配的, 因为platform_device_alloc里面也会调用device_initialize

extern void platform_device_unregister(struct platform_device *)

上一个API的反函数

extern int platform_add_devices(struct platform_device **, int)

注意参数是双指针, 代表一次注册多个platform_device

API内部会调用platform_device_register

 

 

extern struct resource *platform_get_resource(struct platform_device *, unsigned int, unsigned int)

获取platform_device中的resource信息.

个参数代表想要获取的resource的类型 (DMA/IRQ)

个参数代表想要获取的此种类型的resource的个数

extern int platform_get_irq(struct platform_device *, unsigned int)

与上一个API类似, 只不过这里类型已经确定了, 就是IRQ.

extern struct resource *platform_get_resource_byname(struct platform_device *, unsigned int,  const char *)

通过resource的名称获取resource

extern int platform_get_irq_byname(struct platform_device *, const char *)

通过名称获取IRQ

Platform Driver

Platform Driver提供struct platform_driver的注册功能没有分配函数, 需要自己手动分配结构体空间, 具体如下:

头文件include/linux/platform_device.h

实现文件: drivers/base/platform.c

platform_driver  API

Comment

extern int platform_driver_register(struct platform_driver *)

把一个platform_driver注册进内核, 最终会调用driver_register

extern void platform_driver_unregister(struct platform_driver *)

把一个platform_driver从内核移除, 最终会调用driver_unregister

extern int platform_driver_probe(struct platform_driver *driver,int (*probe)(struct platform_device *))

对于不可热插拔的device, 则应该用该API注册该device对应的driver.

一般芯片内部外设都是不可热插拔的, 所以芯片内部外设的驱动代码一般都是调用本API

API最终会调用platform_driver_register, platform_driver_register的区别是, 它会针对不可热插拔设备做些处理, 具体看后面的代码分析

static inline void *platform_get_drvdata(const struct platform_device *pdev)

获取platform_device.dev.driver_data的值

API直接调用dev_get_drvdata

可以回看struct device数据结构中对driver_data的描述

static inline void platform_set_drvdata(struct platform_device *pdev,void *data)

设置platform_device.dev.driver_data的值

API直接调用dev_set_drvdata

可以回看struct device数据结构中对driver_data的描述

懒人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(要把driverprobe接口显式的传入), 并告知该设备占用的资源信息, 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_deviceplatform_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_typebus_register的更多细节, Bus一章中已经介绍过了

platform_match

platform Busmatch函数, 决定了该Busplatform_device (pdev)platform_driver(pdrv)的匹配规则, 还是比较重要的.

 

本节主要目的给出pdevpdrv的匹配规则的先后顺序, 在阅读本节时可以回头多看看pdevpdrv的数据结构, 方便理解. 具体逻辑如下:

         如果定义了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.namepdev->name, 相同则匹配, 不同则不匹配

         最后一项, 回归普通的匹配规则, 判断pdev->namepdrv->driver->name是否相同, 相同则匹配, 不同则不匹配

platform_device_register

用于把一个platform_devicepdev添加进内核系统.

代码逻辑很简单:

         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.kobjectname, 也就是devicename.

          如果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_probeplatform.c中定义的, 很明显, devicedriver匹配上之后, 内核系统会调用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_registerplatform_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. devicedriver匹配上之后, 就会调用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函数中遇到过上面的困惑: 要顺序申请多种资源(IRQClockmemoryregionsioremapdma、等等), 只要任意一种资源申请失败, 就要回滚释放之前申请的所有资源. 于是函数的最后, 一定会出现很多的goto标签(如上面的exit_free_irqexit_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(如clockregulatorgpio、等等)基于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. 对于现代计算机的体系结构, 可能的资源包括:

apower : 供电

bclock : 时钟

cmemory, 内存, kernel中一般使用kzalloc分配

dGPIO, 用户和CPU交换简单控制、状态等信息

eIRQ, 触发中断

fDMA, CPU参与情况下进行数据传输

g)虚拟地址空间, 一般使用ioremaprequest_region等分配

h)等等

 

而在Linux kernel的眼中, 资源的定义更为广义, 比如PWMRTCReset, 都可以抽象为资源, 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 detachdevice_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 driverAPI主要包括:

devres_alloc/devres_freedevres_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;

};

 

定义释放自己resourcerelease函数, 注意这个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 devressize, 就是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_removedevresdevres_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或者busprobe接口, 如果失败(返回值非零, 可参考本文开头的例子), 则会调用devres_release_all

         driver dettach时(就是driver remove时)

driver_detach / bus_remove_device-->__device_release_driver-->devres_release_all

posted @ 2020-12-13 17:55  johnliuxin  阅读(787)  评论(0编辑  收藏  举报