一、总线
总线是处理器与设备之间的通道。在linux设备模型中,所有的设备都通过总线相连,总线可能是实际的总线,比如usb总线,pci总线,也可能是虚拟的总线。1.1 数据结构
linux使用bus_type来表示总线。其定义如下:struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct bus_attribute *bus_attrs;
struct device_attribute *dev_attrs;
struct driver_attribute *drv_attrs;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
struct iommu_ops *iommu_ops;
struct subsys_private *p;
};
- name:总线的名称
- dev_name:用于枚举总线下的设备时的总线名称的名称。示例代码dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id)
- bus_attrs:总线自己的一系列属性。struct bus_attribute是一个自定义的sysfs属性,其中包括了struct attribute以及该属性的show和store方法。
- dev_attrs:总线下设备的一系列属性。类似于总线自己的属性,这也是一个自定义的sysfs属性,包括了struct attribute以及该属性的show和store方法。在bus_add_device->device_add_attrs中被加入dev目录下。
- drv_attrs:总线下驱动的一系列属性。类似于总线自己的属性和总线下的设备属性。在bus_add_driver->driver_add_attrs中被加入driver目录下。
- match:用于检测指定的设备是否可以被指定的驱动处理。在往该总线添加新的驱动或者设备时会调用该函数。如果指定的设备能够被指定的驱动处理,则应该返回一个非0值,否则返回0。
- uevent:用于处理uevent事件,典型的比如设备的热插拔。函数用于总线对uevent的环境变量添加,但在总线下设备的dev_uevent处理函数也有对它的调用。
- probe:当新的设备或者驱动被添加到一个总线,并且match返回成功时会调用该函数对新的设备进行初始化,实际上这是一个初始化函数。bus->probe和drv->probe中只会有一个生效,如果两个都存在,则使用bus->probe。
- remove:当从总线卸载设备或者驱动时,会调用该函数。类似于probe,如果bus->remove和drv->remove都存在,则使用bus->remove。
- shutdown:在所有设备都关闭时会被调用。类似于remove和probe,如果bus->shutdown和drv->shutdown都存在,则使用bus->shutdown。
- suspend:当总线上一个设备想要休眠时调用该函数。
- resume:用于唤醒总线上的一个休眠的设备。
- pm:电源管理的函数
- iommu_ops:特定于该总想的和IOMMU 操作函数,用于将IOMMU驱动的实现和总线关联起来,从而允许驱动在做IOMMU时可以进行特定于总线的操作。
- p:只能由驱动核心使用的私有数据。
struct subsys_private {
struct kset subsys;
struct kset *devices_kset;
struct list_head interfaces;
struct mutex mutex;
struct kset *drivers_kset;
struct klist klist_devices;
struct klist klist_drivers;
struct blocking_notifier_head bus_notifier;
unsigned int drivers_autoprobe:1;
struct bus_type *bus;
struct kset glue_dirs;
struct class *class;
};
该结构将总线同设备、驱动、class以及sysfs关联了起来。其各个域的含义:
- subsys:该总线在sysfs中的表示。
- devices_kset:该总线目录下的devices子目录。
- interfaces:该总线关联的subsysfs_interface接口的链表头。subsysfs_interface提供了一种向设备添加接口的方式,这种接口往往代表一种特定的功能,它不控制设备。其定义也很简单,其中包括了一个add_dev的函数指针和一个remove_dev的函数指针,具体的可以参看实际的代码。
- mutex:用于保护interfaces链表的互斥锁
- drivers_kset:该总线目录下的drivers子目录。
- klist_devices:总线上的设备链表。
- klist_drivers:总线上的驱动链表。
- bus_notifier:用于发送总线变化通告的通知链。
- driver_autoprobe:是否允许device和driver自动匹配,如果允许会在device或者driver注册时就进行匹配工作。
- bus:指向总线的bus_type类型。
- class:指向该结构关联的class。
1.2 添加、删除总线
可以调用bus_register来添加一个总线到系统中,如果要从系统中删除一个总线,需要调用bus_unregister。为了可以在sysfs中识别出来总线,在添加总线时要给它指定一个名字,添加总线可能失败,所以需要检查返回值。在注册总线时,bus_register会完成:- 创建并初始化总线的subsys_private
- 将总线的kset设置为bus_kset(即p->subsys.kobj.kset域)
- 将总线的kobj_type设置为bus_ktype(即p->subsys.kobj.ktype域)
- 默认使能驱动的自动探测(即设置p->drivers_autoprobe为1)
- 初始化总线的kset
- 在sysfs中为总线创建bus_attr_uevent文件,该属性文件用于发送uevent事件。
- 创建并初始化总线下的设备的kset和驱动的kset
- 初始化总线下的设备链表和驱动链表
- 如果支持热插拔,则在sysfs中为总线创建bus_attr_drivers_probe和bus_attr_drivers_autoprobe属性文件,其中bus_attr_drivers_probe这个文件用于在热插拔时为指定名字的设备查找并加载驱动,实际上即是加载设备(写该文件执行函数store_drivers_probe,它会试图找到指定名字的设备并找到其驱动)。而bus_attr_drivers_autoprobe用于修改总线的私有数据中的drivers_autoprobe,即是否允许自动加载设备。
- 在sysfs中为总线结构中的其它总线属性创建属性文件
总线的kset为全局定义的bus_kset,它在总线系统初始化时(buses_init)被创建和初始化。buses_init完成的工作包括:
- 在sys下创建一个名为bus的目录,其kset_uevent_ops被设置为bus_uevent_ops。bus_uevent_ops就提供了一个filter函数。
- 在sys下创建一个名为system的目录,其kset_uevent_ops被设置为NULL,并且设置其父kobject为devices_kset的kobject。
- 首先获得bus_attribute
- 检测是否有show(store)函数,如果有就调用。
1.3 总线的sysfs属性
总线属性定义如下struct bus_attribute {
struct attribute attr;
ssize_t (*show)(struct bus_type *bus, char *buf);
ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
};
该结构很简单,没什么特别之处。总线系统提供了宏BUS_ATTR用于顶一个总线属性,其原型如下:
#define BUS_ATTR(_name, _mode, _show, _store) struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
linux设备模型的各个部分都对在sysfs下添加属性文件做了封装,总线系统提供的api如下:
int __must_check bus_create_file(struct bus_type *, struct bus_attribute *);
void bus_remove_file(struct bus_type *, struct bus_attribute *);
它们分别被用于向sysfs中添加属性文件和删除属性文件,分别会调用sysfs框架的sysfs_create_file和sysfs_remove_file函数。
1.4 总线的其它操作
1.4.1 遍历设备或者驱动
为了方便使用,总线提供了两个宏,分别用于枚举总线下的设备和驱动,其原型如下:int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *))
int bus_for_each_drv(struct bus_type *bus, struct device_driver *start, void *data, int (*fn)(struct device_driver *, void *))
1.4.2 查找设备
总线框架也提供了在总线下查找设备的功能,其原型如下:struct device *bus_find_device(struct bus_type *bus, struct device *start, void *data, int (*match)(struct device *dev, void *data))
1.4.3 总线引用计数
总线的引用计数维护在总线的私有数据结构的kobject中,最终使用的是其私有数据结构中的kset的kobject中的引用计数。相关API如下:struct bus_type *bus_get(struct bus_type *bus)
void bus_put(struct bus_type *bus)
引用计数是linux内核中广泛使用的一项计数,并且实现都是类似的,这也使得我们只要了解其中一个引用计数的实现就可以了解其它引用计数的实现。这里的原理也是相同的,不过需要说明的一点是,在为总线指定kobj_type时,bus_ktype并不包括一个用于释放的函数,这可能是因为总线不支持热插拔吧:))。
1.4.4 设备链表的保护
总线下的设备链表和驱动链表都是一个klist类型的链表,这种链表可以添加get和put的保护机制,总线为其中的设备链表提供了保护函数。这些保护函数在对链表进行初始化时提供给了klist,然后在使用klist的api进行遍历操作的时候,klist的代码会自动使用相关的get和put函数。总线为设备链表提供的保护函数分别为:void klist_devices_get(struct klist_node *n)
void klist_devices_put(struct klist_node *n)
二、设备
在linux驱动模型中,struct device用于表示所有的设备,设备可能是真实的物理设备,也可能是虚拟的设备。2.1 数据结构
struct device的定义如下:
struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
const struct device_type *type;
struct mutex mutex; /* mutex to synchronize calls to
* its driver.
*/
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this
device */
void *platform_data; /* Platform specific data, device
core doesn't touch it */
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
#ifdef CONFIG_NUMA
int numa_node; /* NUMA node this device is close to */
#endif
u64 *dma_mask; /* dma mask (if dma'able device) */
u64 coherent_dma_mask;/* Like dma_mask, but for
alloc_coherent mappings as
not all hardware supports
64 bit addresses for consistent
allocations such descriptors. */
struct device_dma_parameters *dma_parms;
struct list_head dma_pools; /* dma pools (if dma'ble) */
struct dma_coherent_mem *dma_mem; /* internal for coherent mem override */
#ifdef CONFIG_CMA
struct cma *cma_area; /* contiguous memory area for dma allocations */
#endif
/* arch specific additions */
struct dev_archdata archdata;
struct device_node *of_node; /* associated device tree node */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
spinlock_t devres_lock;
struct list_head devres_head;
struct klist_node knode_class;
struct class *class;
const struct attribute_group **groups; /* optional groups */
void (*release)(struct device *dev);
};
其中的关键成员及其含义如下:
- parent:该设备的父设备,即该设备所附着到的那个设备。通常是某种总线或者控制器。如果某个设备的父设备被设置为NULL,则它为一个顶层设备。
- p: 该设备的私有数据
- kobj:代表该设备的kobject,用于将该设备添加到kobject层次结构中,通过它就将一个设备添加到了sysfs中。通常,device->kobj->parent和device->parent->kobj是相同的。
- init_name: 设备的初始化名字
- type: 设备的类型标识。类似于kobject和kobject_type的关系,它保存了某些类型的设备所共有的信息,这样就不必为每个设备指定这些共有信息,而只需要指定一份,然后将设备的type域指向相应的type即可。
- mutex:用于互斥该设备驱动的使用。
- bus:该设备位于那种总线上
- driver:管理该设备的驱动
- platform_data: 特定于平台的数据
- power:用于电源管理的数据结构
- pm_domain:用于电源管理的函数集
- numa_node:该设备使用的UMBA节点
- of_node: 该设备关联的设备树节点
- devt: 设备的devt编号,用于在sysfs中创建dev
- id: 设备的实例编号
- devres_lock: 用于保护该设备所使用资源链表的自旋锁
- devres_head: 该设备的资源链表
- knode_class: 用于将设备添加到它所属的class的链表的节点
- class: 该设备所属的class
- groups: 设备的可选属性组,包括了设备的可选属性。具体的结构定义可以查看device.h
- release:当设备的引用计数为0时,也就是不再需要改设备时用于释放该设备的函数。设备的relase函数会被device_release调用,而device_release是device_ktype的release函数。在设备模型中,每个设备在初始化时都会将device_ktype设置为自己的kobj_type,因而设备模型的引用计数也可以使用struct device中的kobject进行,在kobject进行relase时会最终调用到设备自己的release函数。
struct device_private {
struct klist klist_children;
struct klist_node knode_parent;
struct klist_node knode_driver;
struct klist_node knode_bus;
struct list_head deferred_probe;
void *driver_data;
struct device *device;
};
- klist_children:该设备的所有子设备所在的链表
- knode_parent:用于将该设备链接到它的父设备的子链表(即父设备的klist_children)中
- knode_driver:用于将该设备链接到它所使用的驱动的设备链表中
- knode_bus:用于将该设备链接到它所在总线的设备链表中
- deferred_probe:由于缺乏资源而没有加载驱动的设备链表
- driver_data:用于设备驱动的私有数据指针
- device:指向该设备的device结构
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *, struct attribute *, int);
struct attribute **attrs;
};
- name:属性组的名字
- is_visible:用于控制attrs中的某个属性是否可见,即是否为其在sysfs中创建一个文件,如果可见,它就返回创建的文件的模式,因而它控制了属性文件的可见性以及访问模式。
- attrs:属性数组
struct device_type {
const char *name;
const struct attribute_group **groups;
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, mode_t *mode);
void (*release)(struct device *dev);
const struct dev_pm_ops *pm;
};
- name:类型名字
- goups:类型属性数组,会在device_add时被添加到sysfs中
- uevent:该类型的uevent函数,用于发送uevent时准备环境变量
- devnode:获得设备的devnode
- release:该类型的释放函数
- pm:该类型设备的电源管理函数集
2.2 添加、删除设备
可以调用device_register来添加一个总线到系统中,如果要从系统中删除一个总线,需要调用device_unregister。device_register需要一个struct device的指针作为参数,在调用它之前,应该保证除了显式设置的域外其它域都为0.其完成的工作包括:- 调用device_initialize完成
- 将设备的kobj.kset设置为devices_kset
- 将设备的kobj的kobj_type设置为device_ktype,不同于总线子系统的是这里的device_ktype不仅包括了属性的读写函数还包括了包括了用于kobject释放的relase函数。
- 初始化设备的其它域
- 调用device_add完成
- 获取设备的引用计数,如果失败就返回,否则
- 如果设备的私有数据结构还没有初始化,就调用device_private_init初始化
- 设置设备的名字(可能来自于初始化名字,也可能来自于总线的dev_name,初始化名字的优先级更高)。如果没有设置初始化名字,就会尝试根据设备所在的总线为设备生成名字。如果设置名字失败就会失败返回。
- 初始化设备和其父设备的关系
- 调用kobject_add将设备添加到sysfs中
- 在sysfs中为设备创建属性文件uevent_attr
- 如果主设备号不为0,则在sysfs中为其创建属性文件devt_attr
- 在sysfs中创建相关的符号链接
- 调用device_add_attrs在sysfs中为设备添加属性文件
- 调用bus_add_device将设备添加到总线中
- 调用dpm_sysfs_add在sysfs中未设备创建电源管理相关的属性文件
- 调用device_pm_add初始化设备的电源管理信息
- 如果设备是依附于总线的,就调用blocking_notifier_call_chain发送总线变化通告
- 调用kobject_uevent发送KOBJ_ADD通知给用户空间
- 调用bus_probe_device为该设备探测驱动
- 如果该设备有父设备,就将该设备添加到其父设备的子设备链表中
- 如果设备属于某个class,则将设备添加到该class的设备链表中,并调用该class中的所有接口的add_dev函数。
- 释放设备的引用计数
如果主设备号不为0,则除了创建devt_attr属性文件外,还会做一些其它的工作:
- 调用device_create_sys_dev_entry以在/sys/dev下为设备创建相应的sysfs连接文件,该函数会首先获得该设备的kobject,然后再创建。设备真正的sysfs存在于/sys/devices下
- 调用devtmpfs_create_node在/dev目录下为设备创建设备文件。这是通过向devtmpfsd内核线程发送一个添加请求实现的。devtmpfsd是一个内核线程,用于维护基于tmpfs的/dev的内容。
删除设备基本上是添加设备的逆过程。
devices_kset在设备子系统初始化时(devices_init)创建并初始化,devices_init完成:- 创建并初始化kset devices_kset,其kset_uevent_ops被设置为device_uevent_ops,这会在/sys下创建一个devices目录。device_uevent_ops包含了对相关的kobject进行uevent处理的相关函数。device_uevent_ops中的uevent事件处理函数被设置为dev_uevent,该函数会检查并调用设备所属的总线、所属的类以及设备自身的uevent处理函数。
- 创建并初始化kobject dev_kobj,这会在/sys下创建一个dev目录。
- 创建并初始化kobject sysfs_dev_block_kobj,其父目录指定为dev_kobj,这会在/sys/dev下创建一个block目录
- 创建并初始化kobject sysfs_dev_char_kobj,其父目录指定为dev_kobj,这会在/sys/dev下创建一个char目录
static struct kobj_type device_ktype = {
.release = device_release,
.sysfs_ops = &dev_sysfs_ops,
.namespace = device_namespace,
};
在device_release中,它会依次检查设备的release成员,设备的type->release成员,设备的class->dev_release并使用第一个找到的来完成释放前的清理工作。最后再释放数据结构自身。
属性的读写函数也很简单,就是查找属性自己的读写函数,然后调用即可。namespace用于获取设备的命名空间,这是通过调用设备所属的class的命名空间函数完成的。
除了上述API之外,设备子系统还提供了一个简单的API来创建设备文件,其定义如下:
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
该函数会完成设备数据结构的创建并初始化该设备,同时将设备添加到sysfs中,这个是最简单的API了。
2.3 设备的sysfs属性
设备属性结构定义如下:struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
};
该结构很简单,没有什么特别之处。设备系统提供了宏DEVICE_ATTR用来定义一个设备属性,其原型如下:
#define DEVICE_ATTR(_name, _mode, _show, _store) struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
linux设备模型的各个部分都对在sysfs下添加属性文件做了封装,设备系统提供的api如下:
int device_create_file(struct device *device, const struct device_attribute *entry);
void device_remove_file(struct device *dev, const struct device_attribute *attr);
int __must_check device_create_bin_file(struct device *dev, const struct bin_attribute *attr);
void device_remove_bin_file(struct device *dev, const struct bin_attribute *attr);
它们分别被用于向sysfs中添加(二进制)属性文件和删除(二进制)属性文件,分别会调用sysfs框架的sysfs_create(_bin)_file和sysfs_remove(_bin)_file函数。在驱动模型之中,只有设备提供了二进制属性的封装,也就是说只有设备子系统支持二进制属性,这是因为二进制属性往往是用于固件升级的目地,否则很少有需求产生一些不可读的信息,而固件是和设备紧密相关联的,因而采用了这种设计。
2.4 设备的其它操作
2.4.1 遍历子设备或者查找子设备
为了方便使用,设备提供了查询其子设备以及遍历子设备链表的API。其原型如下:int device_for_each_child(struct device *dev, void *data, int (*fn)(struct device *dev, void *data));
extern struct device *device_find_child(struct device *dev, void *data, int (*match)(struct device *dev, void *data));
2.4.2 设备引用计数
设备的引用计数维护在设备的kobject中,最终使用的是其kobject中的引用计数。相关API如下:struct device *get_device(struct device *dev)
void put_device(struct device *dev)
这就是一个常规的引用计数,没什么特别之处。
2.4.3 子设备链表的保护
设备私有数据结构中的子设备链表是一个klist类型的链表,这种链表可以添加get和put的保护机制,设备系统为该链表提供了保护函数。这些保护函数在对链表进行初始化时提供给了klist,然后在使用klist的api进行遍历操作的时候,klist的代码会自动使用相关的get和put函数。相关保护函数为:void klist_children_get(struct klist_node *n)
void klist_children_put(struct klist_node *n)
2.4.4 重命名
如果要修改一个设备的名字,可以使用如下API:int device_rename(struct device *dev, const char *new_name);