由于Linux支持世界上几乎所有的、不同功能的硬件设备(这是Linux的优点),导致Linux内核中有一半的代码是设备驱动,而且随着硬件的快速升级换代,设备驱动的代码量也在快速增长。为了降低设备多样性带来的Linux驱动开发的复杂度,以及设备热拔插处理、电源管理等,Linux内核提出了设备模型(也称作Driver Model)的概念。设备模型将硬件设备归纳、分类,然后抽象出一套标准的数据结构和接口。驱动的开发,就简化为对内核所规定的数据结构的填充和实现。
Linux设备模型的目的是:为内核建立起一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。
Linux设备模型主要支持的任务有:
- 电源管理和系统关机
完成这些工作需要对系统结构的理解,设备模型使OS能以正确顺序遍历系统硬件。 - 与用户空间的通讯
sysfs 虚拟文件系统的实现与设备模型的紧密相关, 并向外界展示它所表述的结构。向用户空间提供系统信息、改变操作参数的接口正越来越多地通过 sysfs , 也就是设备模型来完成。 - 热插拔设备
内核中的热插拔机制可以处理热插拔设备,特别时能够与用户空间进行关于插拔设备的通信,而这种机制也是通过设备模型管理的。 - 设备类型
设备模型包括了将设备分类的机制,在一个更高的功能层上描述这些设备, 并使设备对用户空间可见。 - 对象生命周期
设备模型的实现需要创建一系列机制来处理对象的生命周期、对象间的关系和对象在用户空间的表示。
总结:Linux 设备模型是一个复杂的数据结构。但对模型的大部分来说, Linux 设备模型代码会处理好这些关系, 而不是把他们强加于驱动作者。模型隐藏于交互的背后,与设备模型的直接交互通常由总线级的逻辑和其他的内核子系统处理。所以许多驱动作者可完全忽略设备模型, 并相信设备模型能处理好他所负责的事。
kobject简介
kobject
Kobject是组成设备模型的基本结构。最初它只被理解为一个简单的引用计数,但是慢慢的它的任务越来越多,因此有了许多成员。现在kobject结构能处理的任务有:
- 对象的引用计数
跟踪对象生命周期的一种方法是使用引用计数。当内核中没有代码支持该对象的引用时, 该对象将结束自己的有效生命期并可被删除。 - Sysfs表述
在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述。 - 数据结构串联
整体来看, 设备模型是一个极端复杂的数据结构,通过其间的大量链接而构成一个多层次的体系结构。kobject 实现了该结构并将其聚合在一起。 - 热插事件拔处理
kobject 子系统将产生的热插拔事件通知用户空间。
kobject是一种数据结构,他定义在<linux/kobject.h>中:
struct kobject {
const char * k_name; /*kobject 的名字数组(sysfs 入口使用的名字)指针;如果名字数组大小小于KOBJ_NAME_LEN,它指向本数组的name,否则指向另外分配的一个名字数组空间 */
char name[KOBJ_NAME_LEN]; /*kobject 的名字数组,若名字数组大小不小于KOBJ_NAME_LEN,只储存前KOBJ_NAME_LEN个字符*/
struct kref kref; /*kobject 的引用计数*/
struct list_head entry; /*kobject 之间的双向链表,与所属的kset形成环形链表*/
struct kobject * parent; /*在sysfs分层结构中定位对象,指向上一级kset中的struct kobject kobj*/
struct kset * kset; /*指向所属的kset*/
struct kobj_type *ktype; /*负责对该kobject类型进行跟踪的struct kobj_type的指针*/
struct dentry * dentry; /*sysfs文件系统中与该对象对应的文件节点路径指针*/
wait_queue_head_t poll;
};
一个kobject对自身并不感兴趣,它存在的意义在于把高级对象连接到设备模型上。因此内核代码很少(甚至不知道)创建一个单独的 kobject;而kobject 被用来控制对大型域(domain)相关对象的访问,所以kobject 被嵌入到其他结构中。
kobject 可被看作一个最顶层的基类,其他类都它的派生产物。 kobject 实现了一系列方法,对自身并没有特殊作用,而对其他对象却非常有效。
对于给定的kobject指针,可使用container_of宏得到包含它的结构体的指针。
kobject的初始化
kobject的初始化较为复杂,但是必须的步骤如下:
- 将整个kobject清零,通常使用memset函数。
- 调用kobject_init()函数,设置结构内部一些成员。所做的一件事情是设置kobject的引用计数为1。具体的源码如下:
void kobject_init(struct kobject * kobj)/*inkobject.c*/
{
if (!kobj)
return;
kref_init(&kobj->kref); /*设置引用计数为1*/
INIT_LIST_HEAD(&kobj->entry); /*初始化kobject 之间的双向链表*/
init_waitqueue_head(&kobj->poll); /*初始化等待队列头*/
kobj->kset = kset_get(kobj->kset); /*增加所属kset的引用计数(若没有所属的kset,则返回NULL)*/
}
void kref_init(struct kref *kref)/*in kobject.c*/
{
atomic_set(&kref->refcount,1);
smp_mb();
}
static inline struct kset * to_kset(struct kobject * kobj)
{
return kobj ? container_of(kobj,struct kset,kobj) : NULL;
}
static inline struct kset * kset_get(struct kset * k)
{
return k ? to_kset(kobject_get(&k->kobj)) : NULL; /*增加引用计数*/
}
-
设置kobject的名字
int kobject_set_name(struct kobject * kobj, const char * fmt, ...);
-
直接或间接设置其它成员:ktype、kset和parent。
对引用计数的操作
kobject 的一个重要函数是为包含它的结构设置引用计数。只要对这个对象的引用计数存在, 这个对象( 和支持它的代码) 必须继续存在。底层控制 kobject 的引用计数的函数有:
struct kobject *kobject_get(struct kobject *kobj);
/*若成功,递增 kobject 的引用计数并返回一个指向 kobject 的指针,否则返回 NULL。
必须始终测试返回值以免产生竞态*/
void kobject_put(struct kobject *kobj);
/*递减引用计数并在可能的情况下释放这个对象*/
注意:kobject _init 设置这个引用计数为 1,因此创建一个 kobject时, 当这个初始化引用不再需要,应当确保采取 kobject_put 调用。
release 函数和 kobject 类型
引用计数不由创建 kobject 的代码直接控制,当 kobject 的最后引用计数消失时,必须异步通知,而后kobject中ktype所指向的kobj_type结构体包含的release函数会被调用。通常原型如下:
void my_object_release(struct kobject *kobj)
{
struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* 其它的清除函数写在下面 */
kfree(mine);
}
每个 kobject 必须有一个release函数, 并且这个 kobject 必须在release函数被调用前保持不变( 稳定状态 ) 。这样,每一个 kobject 需要有一个关联的 kobj_type 结构,指向这个结构的指针能在 2 个不同的地方找到:
- kobject 结构自身包含一个成员(ktype)指向kobj_type ;
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
- 如果这个 kobject 是一个 kset 的成员, kset 会提供kobj_type 指针。
struct kset {
struct kobj_type * ktype; /*指向该kset对象类型的指针*/
struct list_head list; /*用于连接该kset中所有kobject以形成环形链表的链表头*/
spinlock_t list_lock; /*用于避免竞态的自旋锁*/
struct kobject kobj; /*嵌入的kobject*/
struct kset_uevent_ops * uevent_ops;
/*原有的struct kset_hotplug_ops * hotplug_ops;已经不存在,被kset_uevent_ops 结构体替换,在热插拔操作中会介绍*/
};
以下宏用以查找指定kobject的kobj_type 指针:
struct kobj_type *get_ktype(struct kobject *kobj);
这个函数其实就是从以上提到的这两个地方返回kobj_type指针,源码如下:
static inline struct kobj_type * get_ktype(struct kobject * k)
{
if (k->kset && k->kset->ktype)
return k->kset->ktype;
else
return k->ktype;
}
关于新版本内核的kobj_type :
在新版本的内核中已经在struct kset中去除了struct kobj_type * ktype;
也就是说只有struct kobject中存在 kobj_type ,所以get_ktype也相应变为了:
static inline struct kobj_type * get_ktype(struct kobject * kobj)
{
return kobj->ktype;
}
kobject、kset和子系统
内核通常用kobject 结构将各个对象连接起来组成一个分层的结构体系,从而与模型化的子系统相匹配。有 2 个独立的机制用于连接: parent 指针和 kset。
parent 是指向另外一个kobject 结构(分层结构中上一层的节点)的指针,主要用途是在 sysfs 层次中定位对象。这里主要介绍kset。
kset
kset是kobj_type 结构的扩展; 一个 kset 是嵌入到相同类型结构的 kobject 的集合。但 struct kobj_type 关注的是对象的类型,而struct kset 关心的是对象的聚合和集合,其主要功能是包容,可认为是kobjects 的顶层容器类。
- 每个 kset 在内部包含自己的 kobject, 并可以用多种处理kobject 的方法处理kset;
- ksets 总是在 sysfs 中出现; 一旦设置了 kset 并把它添加到系统中, 将在 sysfs 中创建一个目录;
- kobjects 不必在 sysfs 中表示, 但kset中的每一个 kobject 成员都要在sysfs中表述。
增加 kobject 到 kset 中去,通常是在kobject 创建时完成,其过程分为3步:
- 完成kobject的初始化,特别注意mane和parent和初始化。
- 把kobject 的 kset 成员指向目标kset。
- 将kobject 传递给下面的函数:
int kobject_add(struct kobject *kobj); /*函数可能失败(返回一个负错误码),程序应作出相应地反应*/
内核提供了一个组合函数:
extern int kobject_register(struct kobject *kobj); /*仅仅是一个 kobject_init 和 kobject_add 的结合,其他成员的初始化必须在之前手动完成*/
当把一个kobject从kset中删除以清除引用时使用:
void kobject_unregister(struct kobject *kobj); /*是 kobject_del 和 kobject_put 的结合*/
但是在新的内核中已经没有"register"和"unregister"类似的函数了,取而代之的是类似
extern int __must_check kobject_init_and_add(struct kobject *kobj,
struct kobj_type *ktype,
struct kobject *parent,
const char *fmt, ...);
kset 在一个标准的内核链表中保存了它的子节点,在大部分情况下, 被包含的 kobjects 在它们的 parent 成员中保存指向 kset内嵌的 kobject的指针。
kset上的函数操作
ksets 有类似于kobjects初始化和设置接口:
void kset_init(struct kset *kset);
int kset_add(struct kset *kset);
int kset_register(struct kset *kset);
void kset_unregister(struct kset *kset);
/*管理 ksets 的引用计数:*/
struct kset *kset_get(struct kset *kset);
void kset_put(struct kset *kset);
/* kset 也有一个名字,存储于嵌入的 kobject,因此设置它的名字用:*/
kobject_set_name(&my_set->kobj, "The name");
ksets 还有一个指针指向 kobj_type 结构来描述它包含的 kobject,这个类型优先于 kobject 自身中的 ktype 。因此在典型的应用中, 在 struct kobject 中的 ktype 成员被设为 NULL, 而 kset 中的ktype是实际被使用的。
在新的内核里,kset不再包含一个子系统指针struct subsystem * subsys, 而且subsystem已经被kset取代。
子系统
子系统是对整个内核中一些高级部分的表述。子系统通常(但不一定)出现在 sysfs分层结构中的顶层,内核子系统包括 block_subsys(/sys/block 块设备)、 devices_subsys(/sys/devices 核心设备层)以及内核已知的用于各种总线的特定子系统。
一个子系统的简单结构:
struct subsystem {
struct kset kset;
struct rw_semaphore rwsem;
};
一个子系统其实就是对kset和一个信号量的封装。
但是新的内核已经不再有subsystem数据结构了,用kset代替了。每个 kset 必须属于一个子系统,子系统成员帮助内核在分层结构中定位 kset 。
在新的内核里subsystem的函数都已经被取消了,完全被kset取代了。其原因可能是因为原来的subsystem的函数根本就是kset函数的简单包装,而在Linux世界里简单就是美,多余的东西被剔出了。
sysfs底层操作
kobject 是在 sysfs 虚拟文件系统后的机制。对每个在 sysfs 中的目录, 在内核中都会有一个 kobject 与之对应。每个 kobject 都输出一个或多个属性, 它在 kobject 的 sysfs 目录中以文件的形式出现, 其中的内容由内核产生。 <linux/sysfs.h> 包含 sysfs 的工作代码。
在 sysfs 中创建kobject的入口是kobject_add的工作的一部分,只要调用 kobject_add 就会在sysfs 中显示,下面是一些相关知识:
- kobjects 的 sysfs 入口始终为目录, kobject_add 的调用将在sysfs 中创建一个目录,这个目录包含一个或多个属性(文件);
- 分配给 kobject 的名字(用 kobject_set_name ) 是 sysfs 中的目录名,出现在 sysfs 层次的相同部分的 kobjects 必须有唯一的名字. 分配给 kobjects 的名字也应当是合法的文件名字: 它们不能包含非法字符(如:斜线)且不推荐使用空白。
- sysfs 入口位置对应 kobject 的 parent 指针。若 parent 是 NULL ,则它被设置为嵌入到新 kobject 的 kset 中的 kobject;若 parent 和 kset 都是 NULL, 则sysfs 入口目录在顶层,通常不推荐。
默认属性
当创建kobject 时, 每个 kobject 都被给定一系列默认属性。这些属性保存在 kobj_type 结构中:
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops *sysfs_ops; /*提供实现下面属性的方法*/
struct attribute **default_attrs; /*用于保存类型属性列表(指针的指针) */
};
上面的attribute结构体表示:
struct attribute {
char *name; /*属性的名字( 在 kobject 的 sysfs 目录中显示)*/
struct module *owner; /*指向模块的指针(如果有), 此模块负责实现这个属性*/
mode_t mode; /*属性的保护位,modes 的宏定义在 <linux/stat.h>:例如S_IRUGO 为只读属性等等*/
}; /*default_attrs列表中的最后一个元素必须用 0 填充*/
sysfs 读写这些属性是由 kobj_type->sysfs_ops 成员中的函数完成的:
struct sysfs_ops {
ssize_t (*show)(struct kobject *kobj, struct attribute *attr, char *buffer);
ssize_t (*store)(struct kobject *kobj, struct attribute *attr, const char *buffer, size_t size);
};
当用户空间读取一个属性时,内核会使用指向 kobject 的指针(kobj)和正确的属性结构(*attr)来调用show 方法,该方法将给定属性值编码进缓冲(buffer)(注意不要越界( PAGE_SIZE 字节)), 并返回实际数据长度。sysfs 的约定要求每个属性应当包含一个单个人眼可读值; 若返回大量信息,需将它分为多个属性。
store 方法将存在缓冲(buffer)的数据( size 为数据的长度,不能超过 PAGE_SIZE )解码并保存新值到属性(*attr), 返回实际解码的字节数。store 方法只在拥有属性的写权限时才能被调用。此时注意:接收来自用户空间的数据一定要验证其合法性。如果到数据不匹配, 返回一个负的错误值。
非默认属性
虽然 kobject 类型的 default_attrs 成员描述了所有的 kobject 会拥有的属性,倘若想添加新属性到 kobject 的 sysfs 目录属性只需简单地填充一个attribute结构并传递到以下函数:
int sysfs_create_file(struct kobject *kobj, struct attribute *attr);
/*若成功,文件以attribute结构中的名字创建并返回 0; 否则, 返回负错误码*/
/*注意:内核会调用相同的 show() 和 store() 函数来实现对新属性的操作,
所以在添加一个新非默认属性前,应采取必要的步骤确保这些函数知道如何实现这个属性*/
若要删除属性,调用:
int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
调用后, 这个属性不再出现在 kobject 的 sysfs 入口。若一个用户空间进程可能有一个打开的那个属性的文件描述符,在这个属性已经被删除后,show 和 store 仍然可能被调用
二进制属性
sysfs 通常要求所有属性都只包含一个可读文本格式的值,很少需要创建能够处理大量二进制数据的属性。但当在用户空间和设备间传递不可改变的数据时(如上传固件到设备)就需要这个特性。二进制属性使用一个 bin_attribute 结构来描述:
struct bin_attribute {
struct attribute attr; /*属性结构体*/
size_t size; /*这个二进制属性的最大大小(若无最大值则为0)*/
void *private;
ssize_t (*read)(struct kobject *, char *, loff_t, size_t);
ssize_t (*write)(struct kobject *, char *, loff_t, size_t);
/*read 和 write 方法类似字符驱动的读写方法;,在一次加载中可被多次调用,每次调用最大操作一页数据,且必须能以其他方式判断操作数据的末尾*/
int (*mmap)(struct kobject *, struct bin_attribute *attr,
struct vm_area_struct *vma);
};
/*二进制属性必须显式创建,不能以默认属性被创建,创建一个二进制属性调用:*/
int sysfs_create_bin_file(struct kobject *kobj, struct bin_attribute *attr);
/*删除二进制属性调用:*/
int sysfs_remove_bin_file(struct kobject *kobj, struct bin_attribute *attr);
符号链接
sysfs 文件系统具有树型结构, 反映 kobject之间的组织层次关系。为了表示驱动程序和所管理的设备间的关系,需要额外的指针,其在 sysfs 中通过符号链接实现。
/*在 sysfs 创建一个符号链接:*/
int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);
/*函数创建一个链接(name)指向target的 sysfs 入口作为 kobj 的一个属性,是一个相对连接,与它在sysfs 系统中的位置无关*/
/*删除符号连接调用:*/
void sysfs_remove_link(struct kobject *kobj, char *name);
总线、设备和驱动程序
总线、设备和驱动程序的关系如下图:
总线
总线是处理器与一个或多个设备之间的通道,在设备模型中,所有的设备都通过总线相连,甚至是那些内部的虚拟“平台”总线。设备模型展示了总线和它们所控制的设备之间的连接。
在Linux设备模型中,用bus_type结构表示总线,他定义在<linux/device.h>中,结构如下:
struct bus_type {
char * name; \\总线类型的名称
struct subsystem subsys; \\与该总线相关的subsystem
struct kset drivers; \\所有与该总线相关的驱动程序集合
struct kset devices; \\所有挂接在该总线上的设备集合
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 (*hotplug) (struct device *dev, char **envp, int num_envp, char *buffer, int buffer_size);
int (*suspend)(struct device * dev, u32 state);
int (*resume)(struct device * dev);
};
总线的注册
只有非常少的bus_type成员需要初始化,它们中的大多数都是由设备模型核心所控制,但是我们必须为总线指定名字以及其它一些必要的方法。
对于新的总线,我们必须调用bus_register进行注册,如下:
ret = bus_register(&ldd_bus_type);
if (ret)
return ret;
这个调用可能失败, 当然, 因此返回值必须一直检查. 如果它成功, 新总线子系统已被添加到系统; 在 sysfs 中 /sys/bus 的下面可以见到, 并且可能启动添加设备.
如果有必要从系统中去除一个总线(当关联模块被去除, 例如), 调用 bus_unregister方法:
void bus_unregister(struct bus_type *bus);
总线的方法
有几个给 bus_type 结构定义的方法; 它们允许总线代码作为一个设备核心和单独驱动之间的中介. 在 2.6.10 内核中定义的方法是:
int (*match)(struct device *device, struct device_driver *driver);
/*当一个新设备或者驱动被添加给这个总线,会一次或多次调用这个函数,如果指定的驱动程序能够处理指定的设备的话,
这个函数返回非0值。这个函数必须在总线层上使用, 因为那里存在合适的;
核心内核不能知道如何匹配每个可能总线类型的设备和驱动。*/
int (*hotplug) (struct device *device, char **envp, int num_envp, char *buffer, int buffer_size);
/*在为用户空间产生热插拔事件前,这个方法允许总线添加环境变量,它和kset的hotplug方法参数相同。*/
对设备和驱动程序的迭代
如果要对注册到总线上的设备或驱动程序执行某些操作时,为了保证可以操作到各个设备可以使用以下方法:
int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *));
这个函数迭代了总线上的每个设备, 将相关的device结构传递给 fn, 同时传递data值。 如果 start 是 NULL, 将从总线的第一个设备开始迭代; 否则迭代从 start 之后的第一个设备开始. 如果 fn 返回一个非零值, 迭代停止并且那个值也会从 bus_for_each_dev 返回。
要迭代驱动程序的话,方法:bus_for_each_drv(),参数和上面一样,不过device结构变成device_driver结构。
总线属性
几乎 Linux 驱动模型中的每一层都提供一个添加属性的接口, 并且总线层不例外。
bus_attribute 类型定义在 <linux/device.h> 如下:
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);
};
在默认属性中,我们看过attribute结构,bus_attribute 类型也包含 2 个方法来显示和设置属性值. 大部分在 kobject 之上的设备模型层都是以这种方式工作. 在编译时创建和初始化 bus_attribute 结构:
BUS_ATTR(name, mode, show, store);
这个宏声明一个结构, 它将 bus_attr_ 作为给定name的前缀来创建总线的真正名称。
创建属于总线的任何属性,都需要调用 bus_create_file 来创建:
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
属性也可被去除, 使用:
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);
设备
系统中的任一设备在设备模型中都由一个device对象描述,其对应的数据结构struct device定义为:
struct device {
struct list_head g_list;
struct list_head node;
struct list_head bus_list;
struct list_head driver_list;
struct list_head children;
struct device *parent;
struct kobject kobj;
char bus_id[BUS_ID_SIZE];
struct bus_type *bus;
struct device_driver *driver;
void *driver_data;
/* Several fields omitted */
};
- g_list 将该device对象挂接到全局设备链表中,所有的device对象都包含在devices_subsys中,并组织成层次结构;
- Node域将该对象挂接 到其兄弟对象的链表中;
- bus_list则用于将连接到相同总线上的设备组织成链表;
- driver_list则将同一驱动程序管理的所有设备组织为链表;
- children域指向该device对象子对象链表头;
- parent域则指向父对象;
- Device对象还内嵌一个kobject对象,用于引 用计数管理并通过它实现设备层次结构;
- Driver域指向管理该设备的驱动程序对象;
- driver_data则是提供给驱动程序的数据;
- Bus域描述设 备所连接的总线类型。
内核提供了相应的函数用于操作device对象。其中Device_register()函数将一个新的device对象插 入设备模型,并自动在/sys/devices下创建一个对应的目录。Device_unregister()完成相反的操作,注销设备对象。
Get_device()和put_device()分别增加与减少设备对象的引用计数。通常device结构不单独使用,而是包含在更大的结构中作为一 个子结构使用,比如描述PCI设备的struct pci_dev,其中的dev域就是一个device对象。
设备属性
sysfs 中的设备入口可有属性. 相关的结构是:
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, char *buf);
ssize_t (*store)(struct device *dev, const char *buf,
size_t count);
};
这些属性结构可在编译时建立, 使用这些宏:
DEVICE_ATTR(name, mode, show, store);
它将 bus_attr_ 作为给定name的前缀来创建总线的真正名称 用以下两个函数来实现对属性文件的操作:
int device_create_file(struct device *device, struct device_attribute *entry);
void device_remove_file(struct device *dev, struct device_attribute *attr);
struct bus_type 的 dev_attrs 成员指向一个缺省的属性列表, 这些属性给添加到总线的每个设备创建。
设备驱动程序
系统中的每个驱动程序由一个device_driver对象描述,对应的数据结构定义为:
struct device_driver {
char *name; \\设备驱动程序的名称
struct bus_type *bus; \\该驱动所管理的设备挂接的总线类型
struct kobject kobj; \\内嵌kobject对象
struct list_head devices; \\该驱动所管理的设备链表头
int (*probe)(struct device *dev); \\指向设备探测函数,用于探测设备是否可以被该驱动程序管理
int (*remove)(struct device *dev); \\用于删除设备的函数
};
与device 结构类似,device_driver对象依靠内嵌的kobject对象实现引用计数管理和层次结构组织。内核提供类似的函数用于操作 device_driver对象,如get_driver()增加引用计数,driver_register()用于向设备模型插入新的driver对 象,同时在sysfs文件系统中创建对应的目录。Device_driver()结构还包括几个函数,用于处理热拔插、即插即用和电源管理事件。
类
类也是设备模型的概念,类是一个设备的高层视图,它抽象出了低层的实现细节。类允许用户空间使用设备所提供的功能,而不关心设备是怎么连接的,以及它们是怎么工作的。
几乎所有的类都显示在/sys/class目录下,比如网络接口,/sys/class/net下,输入设备在/sys/class/input下,例外的是块设备,它在/sys/block下。
类成员通常被上层代码所控制,而不需要来自驱动程序的明确支持。
创建类接口
为了管理类,驱动程序核心导出了两个不同的接口。一个简单的类接口:class_simple例程提供一种比较简单的方法来向系统中添加新的类。另一种是正规的类接口,比较复杂,但功能更多。
- class_simple接口
class_simple接口非常易于使用,甚至用不着担心导出包含已分配设备号属性这样的信息。这个接口只是一些简单的函数调用。
创建类本身,调用class_simple_create 方法创建:
struct class_simple *class_simple_create(struct module *owner, char *name); /*这个函数使用给定的名子创建一个类. 这个操作可能失败,因此需要检查它的返回值。*/
销毁一个简单的类, 使用:
void class_simple_destroy(struct class_simple *cs);
创建一个类的真实目的是为它添加设备,我们可以使用下面的函数添加设备:
struct class_device *class_simple_device_add(struct class_simple *cs, dev_t devnum,
struct device *device, const char *fmt, ...);
- cs 是之前创建的简单类;
- devnum 是分配的设备号;
- device 是代表这个设备的device结构;
- 剩下的参数是用来创建设备名称的, printk-风格 的格式串和参数。该调用向包含设备号属性(dev)的类中添加了一个入口,如果device参数是非 NULL, 一个符号连接( 称为 device )指向在 /sys/devices 下的设备的入口。
在插拔设备时,类会产生热插拔事件. 如果驱动程序需要为用户空间处理程序添加环境变量时,可以建立一个热插拔回调, 使用:
int class_simple_set_hotplug(struct class_simple *cs,
int (*hotplug)(struct class_device *dev,
char **envp, int num_envp,
char *buffer, int buffer_size));
当拔除设备时, 类入口应当被去除, 使用:
void class_simple_device_remove(dev_t dev);
注意, 由 class_simple_device_add 返回的 class_device 结构这里不需要; 设备号(它当然应当是唯一的)足够了。
正规的类接口
class_simple接口能够满足许多需求,但有时候需要更强的灵活性,下面就是基于class_simple的完整类机制。
管理类
一个类由一个 struct class 的实例来定义:
struct class {
char *name; //类名
struct class_attribute *class_attrs; //类的属性
struct class_device_attribute *class_dev_attrs; //类的缺省属性
int (*hotplug)(struct class_device *dev, char **envp, //热插拔函数
int num_envp, char *buffer, int buffer_size);
void (*release)(struct class_device *dev);
void (*class_release)(struct class *class);
};
每个类需要一个唯一的名字, 它是这个类如何在 /sys/class 中出现,当这个类被注册, 由 class_attrs 所指向的数组中列出的所有属性被创建. 还有一套缺省属性给每个添加到类中的设备, class_dev_attrs 指向它们;有通常的热插拔函数来添加变量到环境中, 当事件产生时. 还有 2 个释放方法: release 在无论何时从类中去除一个设备时被调用, 而 class_release 在类自己被释放时调用。
注册函数是:
int class_register(struct class *cls);
void class_unregister(struct class *cls);
处理属性的接口:
struct class_attribute {
struct attribute attr;
ssize_t (*show)(struct class *cls, char *buf);
ssize_t (*store)(struct class *cls, const char *buf, size_t count);
};
CLASS_ATTR(name, mode, show, store);
int class_create_file(struct class *cls, const struct class_attribute *attr);
void class_remove_file(struct class *cls, const struct class_attribute *attr);
类设备
一个类的真正目的是作为一个是该类成员的设备的容器. 一个成员由class_device 结构来表示:
struct class_device {
struct kobject kobj;
struct class *class;
struct device *dev;
void *class_data;
char class_id[BUS_ID_SIZE];
};
- class_id 成员持有设备名字, 如同它在 sysfs 中的一样;
- class 指针应当指向持有这个设备的类;
- dev 应当指向关联的设备结构,设置 dev 是可选的,如果它是非 NULL, 它用来创建一个符号连接从类入口到对应的在 /sys/devices 下的入口, 使得易于在用户空间找到设备入口.
- class_data 用来保存私有数据指针。
常用的注册函数:
int class_device_register(struct class_device *cd);
void class_device_unregister(struct class_device *cd);
类设备还允许已经注册过的入口项改名:
int class_device_rename(struct class_device *cd, char *new_name);
类设备入口有属性:
struct class_device_attribute {
struct attribute attr;
ssize_t (*show)(struct class_device *cls, char *buf);
ssize_t (*store)(struct class_device *cls, const char *buf,
size_t count);
};
CLASS_DEVICE_ATTR(name, mode, show, store);
int class_device_create_file(struct class_device *cls,
const struct class_device_attribute *attr);
void class_device_remove_file(struct class_device *cls,
const struct class_device_attribute *attr);
在类的class_dev_attrs成员中保存了默认的属性,在注册类设备的时候,就会创建这些属性。 class_device_create_file用来创建其它的属性,属性也能被添加到class_simple接口创建的设备中。
类接口
struct class_interface {
struct class *class; //一个类
int (*add) (struct class_device *cd); //添加类设备
void (*remove) (struct class_device *cd); //移除类设备
};
接口可被注册或注销, 使用:
int class_interface_register(struct class_interface *intf);
void class_interface_unregister(struct class_interface *intf);
一个接口的功能是简单明了的. 无论何时一个类设备被加入到在 class_interface 结构中指定的类时, 接口的 add 函数被调用. 这个函数可进行任何额外的这个设备需要的设置; 这个设置常常采取增加更多属性的形式, 但是其他的应用都可能. 当设备被从类中去除, remove 方法被调用来进行任何需要的清理。一个类可以注册多个接口。
热插拔事件
在Linux系统中,当系统配置发生变化时,如:添加kset到系统;移动kobject, 一个通知会从内核空间发送到用户空间,这就是热插拔事件。热插拔事件会导致用户空间中相应的处理程序(如udev,mdev)被调用, 这些处理程序会通过加载驱动程序, 创建设备节点等来响应热插拔事件。
操作集合
Struct kset_uevent_ops {
int (*filter)(struct kset *kset, struct kobject *kobj);
const char *(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
}
/*
当该kset所管理的kobject和kset状态发生变化时(如被加入,移动),这三个函数将被调用。
Filter:决定是否将事件传递到用户空间。如果filter返回0,将不传递事件。
Name:负责将相应的字符串传递给用户空间的热插拔处理程序。
Uevent:将用户空间需要的参数添加到环境变量中。
*/
int (*uevent)(struct kset *kset,
struct kobject *kobj, /*产生事件的目标对象*/
char **envp, /*一个保存其他环境变量定义(通常为NAME=value的格式)的数组*/
int num_envp, /*环境变量数组中包含的变量数(数组大小)*/
char *buffer, int buffer_size/*环境变量被放入的缓冲区的指针和字节数*/
);/*返回值正常时是,若返回非零值将终止热插拔事件的产生*/
知识总结
设备驱动模型主要结构分为kset、kobject、ktype。
- kset是同类型kobject对象的集合,可以说是一个容器。
- kobject是总线、驱动、设备的三种对象的一个基类,实现公共接口。
- ktype,记录了kobject对象的一些属性。
设备驱动模型的核心即是kobject,是为了管理日益增多的设备,使得设备在底层都具体统一的接口。他与sysfs文件系统紧密相连,每个注册的kobject都对应sysfs文件系统中的一个目录。为了直观管理,统一存放的路径,使用了kset。但是仅仅有这些目录没有意义,这两个结构体只能表示出设备的层次关系,所以基本不单独使用,会嵌入到更大的结构体中,(如希望在驱动目录下能看到挂在该总线上的各种驱动,而在设备目录下能看到挂在该总线的各种设备,就将kobject嵌入到描述设备以及驱动的结构体中,这样每次注册设备或驱动,都会在sys目录下有描述)。
在使用kobject、kset、ktype结构,就在sysfs虚拟文件系统下创建(通过kset_create_and_add和kobject_init_and_add函数)了一些子目录(kobject_test)和属性文件。 kset和kobject都可以创建出目录,但是kset的目录下存放kobject目录,kobject下存放属性文件(可以对属性文件进行读写操作,而且kobject目录下也可以存放kobject目录,只需parent指向它即可)。
如图,通过pci设备的创建过程我们可以了解到一个设备怎么添加到总线上,以及怎么匹配对应的驱动程序:
Pci设备创建过程
设备添加和卸载( 通过pci设备来介绍)
PCI子系统声明一个单个 struct bus_type 称为 pci_bus_type, 它使用下列值初始化:
struct bus_type pci_bus_type = {
.name = "pci",
.match = pci_bus_match,
.hotplug = pci_hotplug,
.suspend = pci_device_suspend,
.resume = pci_device_resume,
.dev_attrs = pci_dev_attrs, };
- 第一步:在将pci子系统装载到内核时,通过调用bus_register,该Pci_bus_type变量将向驱动核心中注册。当这个发生时, 驱动核心创建一个 sysfs 目录在 /sys/bus/pci 里, 它包含 2 个目录: devices 和 drivers.
- 第二步:所有的 PCI 驱动必须定义一个 struct pci_driver 变量, 它定义了这个 PCI 驱动能够实现的不同的功能。这个结构包含一个 struct device_driver, 在注册PCI驱动程序时,这个结构将会初始化:
drv->driver.name = drv->name;
drv->driver.bus = &pci_bus_type;
drv->driver.probe = pci_device_probe;
drv->driver.remove = pci_device_remove;
drv->driver.kobj.ktype = &pci_driver_kobj_type;
这个代码为用来为驱动程序指定总线。它将驱动程序的总线指向 pci_bus_type ,并且使 probe 和 remove 函数来指向 PCI 核心内的相关函数. 为使 PCI 驱动程序的属性文件正常工作,将驱动程序的 kobject 的 ktype 被设置为变量 pci_driver_kobj_type, 然后将 PCI 核心向驱动程序核心注册PCI驱动程序:
/* register with core */
error = driver_register(&drv->driver);
现在驱动程序可与其所支持的任何PCI设备绑定。
- 第三步: 在能与PCI总线交互的特定体系架构代码的帮助下,PCI核心 开始探测 PCI 地址空间, 查找所有的 PCI 设备。当一个 PCI 设备被发现, PCI 核心在内存中创建一个 pci_dev 类型的结构变量.。struct pci_dev 结构的一部分看来如下:
struct pci_dev {
/* ... */
unsigned int devfn;
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class;
struct pci_driver *driver;
struct device dev;
/* ... */
};
这个 PCI 设备与总线相关的成员将被 PCI 核心初始化( devfn, vendor, device, 和其他成员), 并且 device 结构变量的 parent 变量被设置为这个 PCI 设备所在的 PCI 总线设备. bus 变量被设置指向 pci_bus_type 结构。接下来 name 和 bus_id 变量被设置为该 PCI 设备的 name 和 ID.
在 PCI 设备结构被初始化之后, 设备被注册到驱动核心, 使用:
device_register(&dev->dev);
在 device_register 函数中, 驱动核心初始化设备的许多成员, 注册设备的 kobject 到 kobject 核心( 它导致一个热插拔事件产生), 接着添加设备到驱动的 parent 所持有的设备列表中. 完成这些工作使得所有的设备可被以正确的顺序浏览, 并且知道每一个设备挂在层次结构的哪一点上。
- 第四步:接着设备被添加到与总线相关的所有设备链表中,在本例中, 是pci_bus_type 链表。这个链表包含了所有向总线注册的设备,遍历这个链表,并且为每个驱动程序调用该总线的match函数,同时指定该设备。对于 pci_bus_type 总线来说, pci核心在把设备提交给驱动程序核心前,将match函数指向 pci_bus_match函数。
pci_bus_match 函数将把驱动程序核心传递过来的device结构转换为pci_dev结构; 它还把 device_driver 结构转换为一个 pci_driver 结构, 并且查看设备和驱动程序中的PCI设备相关信息, 以确定驱动程序是否支持这类设备。
如果匹配不成功, 函数返回 0 给驱动核心, 并且驱动核心移向链表中的下一个驱动程序。
如果匹配成功, 函数返回 1 给驱动核心. 驱动核心将设置 device 结构中的device指针指向这个驱动程序, 并且接着调用在 struct device_driver 中特定的 probe 函数。
在 PCI 驱动注册到驱动核心之前, probe 变量被设为指向 pci_device_probe 函数. 这个函数转换(又一次) device结构为一个 pci_dev结构, 并且把device中设置的 driver结构转换为一个 pci_driver结构。它也将检测这个驱动程序的状态以确保能支持某个设备( 这意味着一个重复的额外检查, 某些未知的原因), 增加设备的引用计数, 然后用绑定的pci_dev结构指针为参数,调用PCI程序的prob函数。
如果这个 PCI 驱动的 probe 函数由于某些原因, 不能处理这个设备,它返回一个负的错误值给驱动程序核心,这将导致驱动程序核心继续在驱动程序列表中搜索,以匹配这个设备。如果这个 probe 函数探测到了设备, 它将做所有的初始化工作,接着它返回 0 给驱动核心. 这使驱动核心来添加该设备到与此驱动程序绑定的设备链表中, 并且在sysfs中的drivers目录到当前控制设备之间建立符号链接,这个符号链接使得用户知道哪个驱动程序被绑定到了哪个设备上。
去除一个设备
被调用一个 PCI 可用多个不同的方法被从系统中去除。要删除一个设备,有以下的要求步骤:
- 调用pci_remove_bus_device 函数。
这个函数做一些 PCI相关的清理工作,然后使用一个指向 struct pci_dev 的 struct device 成员的指针调用 device_unregister 函数。 - 在 device_unregister 函数中, 驱动核心只是删除了从绑定绑定设备的驱动程序到sysfa文件的符号链接,从内部设备链表删除了该设备, 并且以device结构中的kobject结构指针为参数,调用kobject_del函数。这个函数引起了用户空间的hotplug 调用,表明该 kobject 现在被从系统中去除, 然后删除所有的和 kobject 关联的 sysfs 文件和 sysfs 目录。
kobject_del 函数还删除了设备的 kobject 引用。如果那个引用是最后一个( 意味着没有用户空间文件为这个 sysfs 的设备入口而打开 )。 - 接着是调用 PCI 设备自身的 release 函数——pci_release_dev,这个函数只释放 struct pci_dev 占用的内存。
此后, 所有的和这个设备关联的 sysfs 入口被去除, 并且和这个设备关联的内存被释放. PCI 设备现在完全从系统中被去除。
设备驱动程序的添加和卸载
当它调用 pci_register_driver 函数时,一个 PCI 驱动被添加到 PCI 核心, 如同之前在关于添加设备的一节中提过的,这个函数只初始化 struct device_driver 结构, 这个结构包含在 struct pci_driver 结构里面, 接着 PCI 核心使用指向包含在 struct pci_driver 结构中的 sturct device_driver 结构的指针调用在驱动核心的driver_register 函数。
driver_register 函数初始化在 struct device_driver 结构中的几个锁, 并且接着调用 bus_add_driver 函数. 这个函数进行下面的步骤:
- 查找驱动要被关联的总线. 如果没有找到这个总线, 函数立刻返回。
- 根据驱动程序的名字以及相关的总线,创建驱动程序的sysfs目录。
- 总线的内部锁被获取, 接着所有的已经注册到总线的设备被检查, 匹配函数为它们被调用, 就象当一个新设备被添加时. 如果那个匹配函数成功, 接着剩下的绑定过程发生, 如同在前面章节描述过的。
删除设备驱动程序:
对于一个 PCI 驱动, 驱动调用 pci_unregister_driver 函数. 这个函数使用一个指向传递给它的 struct pci_driver 的 struct devie_driver 的指针调用驱动核心函数 driver_unregister。
deiver_unregister 函数处理一些基本的日常工作, 通过清理某些在 sysfs 树中连接到这个驱动入口的 sysfs 属性. 它接着列举所有的连接到这个驱动的设备并且为它调用 release 函数. 发生这个恰好象前面提过的 release 函数, 当一个设备从系统中去除时。