Linux设备驱动程序学习笔记系列文章原作者是:Tekkaman Ninja,他博客地址:
在此向Tekkaman Ninja表示感谢,写出这么好的文章,使我少走了很多弯路。
以《LDD3》的说法:Linux设备模型这部分内容可以认为是高级教材,对于多数程序作者来说是不必要的。但是我个人 认为:对于一个嵌入式Linux的底层程序员来说,这部分内容是很重要的。 以 我学习的ARM9为例,有很多总线(如SPI、IIC、IIS等等)在Linux下已经被编写成了子系统,无需自己写驱动;而这些总线又不像PCI、 USB等在《LDD3》上有教程,有时还要自己研究它的子系统构架,甚至要自己添加一个新的总线类型。
对于这方面的学习,我推荐几个网页,这些也是我这部分文章的参考资料:
(1)《 Linux 那 些事儿 之 我是Sysfs 》来源于复旦和交大三个牛人的 Linux技术博客:http://blog.csdn.net/fudan_abc (复旦_abc)他们还分析了很多Linux的驱 动,值得珍藏!
(2)《linux设备模型详解》也是一个牛人的博客文章,博客网址:http://hi.baidu.com/csdeny/blog
(3)《s3c2410设备的注册》是一篇关于2410中linux内核实现设备模型的不可多得的好资料。网址:http://blog.chinaunix.net/u1/41638/showart_438078.html
(4)luofuchong的博客 ,此人分析了一些2410中的Linux子系统(如SPI,input等),实力不凡,值得关注。网址:http://www.cnitblog.com/luofuchong/
在这部分的学习中,将会先研究linux设备模型的每个元素,最后将其一步一步整合,至底向上地分析。一开始会比较摸不 着头脑,到了整合阶段就柳暗花明了。我之所以没有先介绍整体,再分析每个部分是因为如果不对每个元素做认真分析,看了整体也会云里雾里(我试过了,恕小生 愚钝)。所以一开始要耐着性子看,到整合阶段就会豁然开朗。
Linux设备模型的目的是:为内核建立 起一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。
现在内核使用设备模型支持多种不同的任务:
电源管理和系统关机 :这些需要对系统结构的理解,设备模型使OS能以正确顺序遍历系统硬件。
与用户空间的通讯 :sysfs 虚拟文件系统的实现与设备模型的紧密相关, 并向外界展示它所表述的结构。向用户空间提供系统信息、改变操作参数的接口正越来越多地通过 sysfs , 也就是设备模型来完成。
热插拔设备
设备类型:设备模型包括了将设备分类的机制,在一个更高的功能层上描述这些设备, 并使设备对用户空间可见。
对象生命周期 :设备模型的 实现需要创建一系列机制来处理对象的生命周期、对象间的关系和对象在用户空间的表示。
Linux 设备模型是一个复杂的数据结构。但对模型的大部分来说, Linux 设备模型代码会处理好这些关系, 而不是把他们强加于驱动作者。模型隐藏于交互的背后,与设备模型的直接交互通常由总线级的逻辑和其他的内核子系统处理。所以许多驱动作者可完全忽略设备模 型, 并相信设备模型能处理好他所负责的事。
在此之前请先了解一下 sysfs ,请看 Linux 那些事儿之我是Sysfs(1)sysfs 初探 我就不在这里废话了!这里还建议先看看 sysfs 的内核文档/Documentation/filesystems/ sysfs.txt , 我将其翻译好做成PDF,下载地址:http://blogimg.chinaunix.net/blog/upfile2/071229162826.pdf
如有错误欢迎指正!
Kobject、 Kset 和 Subsystem
Kobjects
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 所处理的任务和支持代码包括:
对象的引用计数 :跟踪对象生命周期的一种方法是使用引用计数。当没有内核代码持有该对象的引用时, 该对象将结束自己的有效生命期并可被删除。
sysfs 表述 :在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述。
数据结构关联 :整体来看, 设备模型是一个极端复杂的数据结构,通过其间的大量链接而构成一个多层次的体系结构。kobject 实现了该结构并将其聚合在一起。
热插拔事件处理 :kobject 子系统将产生的热插拔事件通知用户空间。
一个kobject对自身并不感兴趣,它存在的意义在于把高级对象连接到设备模型上。因此内核代码很少(甚至不知道)创 建一个单独的 kobject;而kobject 被用来控制对大型域(domain)相关对象的访问,所以kobject 被嵌入到其他结构中。kobject 可被看作一个最顶层的基类,其他类都它的派生产物。 kobject 实现了一系列方法,对自身并没有特殊作用,而对其他对象却非常有效。
对于给定的kobject指针,可使用container_of 宏 得到包含它的结构体的指针。
kobject 初始化
kobject的初始化较为复杂,但是必须的步骤 如 下:
(1)将整个kobject清零,通常使用memset函数。
(2)调用kobject_init()函数,设置结构内部一些成员。所做的一件事情是设置kobject的引用计数为1。具体的源码如下:
void kobject_init( struct kobject * kobj)/*in kobject.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)/*in kobject.h */ { return kobj ? container_of( kobj, struct kset, kobj) : NULL ; } static inline struct kset * kset_get( struct kset * k)/*in kobject.h */ { return k ? to_kset( kobject_get( & k- > kobj) ) : NULL ;/*增加引用计数*/ } |
(3)设置kobject的名字
int kobject_set_name( struct kobject * kobj, const char * fmt, . . . ) ; |
(4)直接或间接设置其它成员: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 调用。同理:struct cdev 的引用计数实现如下:
struct kobject * cdev_get( struct cdev * p) { struct module * owner = p- > owner; struct kobject * kobj; if ( owner & & ! try_module_get( owner) ) return NULL ; kobj = kobject_get( & p- > kobj) ; if ( ! kobj) module_put( owner) ; return kobj; } |
创建一个对 cdev 结构的引用时,还需要创建包含它的模块的引用。因此, cdev_get 使用 try_module_get 来试图递增这个模块的使引用计数。如果这个操作成功, kobject_get 被同样用来递增 kobject 的引用计数。kobject_get 可能失败, 因此这个代码检查 kobject_get 的返回值,如果调用失败,则释放它的对模块的引用计数。
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) ; /* Perform any additional cleanup on this object, then... */ kfree( mine) ; } |
每个 kobject 必须有一个release函数, 并且这个 kobject 必须在release函数被调用前保持不变( 稳定状态 ) 。这样,每一个 kobject 需要有一个关联的 kobj_type 结构,指向这个结构的指针能在 2 个不同的地方找到:
(1)kobject 结构自身包含一个成员(ktype)指向kobj_type ;
(2)如果这个 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; } |
kobject 层次结构、kset 和子系统
内核通常用kobject 结构将各个对象连接起来组成一个分层的结构体系,与模型化的子系统相匹配。有 2 个独立的机制用于连接: parent 指针和 kset
。
parent
是指向另外一个kobject 结构(分层结构中上一层的节点)的指针,主要用途是在 sysfs 层次中定位对象.
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 创建时完成,其过程分为2步:
(1)完成kobject的初始化,特别注意mane和parent和初始化。
(2)把kobject 的 kset 成员指向目标kset。
(3)将kobject 传递给下面的函数:
int kobject_add( struct kobject * kobj) ; /*函数可能失败(返回一个负错误 码),程序应作出相应地反应*/ |
内核提供了一个组合函数:
extern int kobject_register( struct kobject * kobj) ; /*仅仅是一个 kobject_init 和 kobject_add 的结合,其他成员的初始化必须在之前手动完成 */ |
当把一个
kobject从kset中删除以清除引用时使用:
void kobject_del( struct kobject * kobj) ; /*是 kobject_del 和 kobject_put 的结合*/ |
kset 在一个标准的内核链表中保存了它的子节点,在大部分情况下, 被包含的 kobjects 在它们的 parent 成员中保存指向 kset内嵌的 kobject的指针,关系如下:
图表中的所有的被包含的 kobjects 实际上被嵌入在一些其他类型中, 甚至可能其他的 kset。
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 核心设备层)以及内核已知的用于各种总线的特定子系统。
对于新的内核已经不再有 subsystem 数据结构 了,用kset代替了。每个 kset 必须属于一个子系统,子系统成员帮助内核在分层结构中定位 kset 。
/*子系统通常用以下的宏声明:*/ decl_subsys( name, struct kobj_type * type, struct kset_uevent_ops * uevent_ops) ; /*子系统的操作函数:*/ void subsystem_init( struct kset * s) ; int subsystem_register( struct kset * s) ; void subsystem_unregister( struct kset * s) ; struct subsystem * subsys_get( struct kset * s) void subsys_put( struct kset * s) ; /*这些函数基本上是kset操作 函数的包装,以实现子系统的操作*/ |
底层 sysfs操作
kobject 是在 sysfs 虚拟文件系统后的机制。对每个在 sysfs 中的目录 , 在内核中都会有一个 kobject 与之对应。每个 kobject 都输出一个或多个属性, 它在 kobject 的 sysfs 目录中以文件 的形式出现, 其中的内容由内核产生。 <linux/sysfs.h> 包含 sysfs 的工作代码。
在 sysfs 中创建kobject的入口是kobject_add的工作的一部分,只要调用 kobject_add 就会在sysfs 中显示,还有些知识值得记住:
(1)kobjects 的 sysfs 入口始终为目录, kobject_add 的调用将在sysfs 中创建一个目录,这个目录包含一个或多个属性(文件);
(2)分配给 kobject 的名字( 用 kobject_set_name ) 是 sysfs 中的目录名,出现在 sysfs 层次的相同部分的 kobjects 必须有唯一的名字. 分配给 kobjects 的名字也应当是合法的文件名字: 它们不能包含非法字符(如:斜线)且不推荐使用空白。
(3)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; /*用于保存类 型属性列表(指针的指针) */ } ; 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 的约定要求每个属性应当包含一个单个人眼可读值; 若返回大量信息,需将它分为多个属性.
也可对所有 kobject 关联的属性使用同一个 show 方法,用传递到函数的 attr 指针来判断所请求的属性。有的 show 方法包含对属性名字的检查。有的show 方法会将属性结构嵌入另一个结构, 这个结构包含需要返回属性值的信息,这时可用container_of 获得上层结构的指针以返回属性值的信息。
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) ; |
热插拔事件 产生
一个热插拔事件是一个从内核空间发送到用户空间的通知, 表明系统配置已经改变. 无论 kobject 被创建或删除,都会产生这种事件。热插拔事件会导致对 /sbin/hotplug 的调用, 它通过加载驱动程序, 创建设备节点, 挂载分区或其他正确动作响应事件。
热插拔事件的实际控制是通过一套存储于 kset_uevent_ops (《LDD3》中介绍的struct kset_hotplug_ops * hotplug_ops;在2.6.22.2中已经被kset_uevent_ops 结构体替换 ) 结构的方法完成:
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, char * * envp, int num_envp, char * buffer, int buffer_size) ; } ; |
可以在 kset 结构的uevent_ops 成员中找到指向kset_uevent_ops结构的指针。
若在 kobject 中不包含指定的 kset , 内核将通过 parent 指针在分层结构中进行搜索,直到发现一个包含有kset的 kobject ; 接着使用这个 kset 的热插拔操作。
kset_uevent_ops 结构中的三个方法作用如下:
(1) filter 函数让 kset 代码决定是否将事件传递给用户空间。如果 filter 返回 0,将不产生事件。以磁盘的 filter 函数为例,它只允许kobject产生磁盘和分区的事件,源码如下:
static int block_hotplug_filter( struct kset * kset, struct kobject * kobj) { struct kobj_type * ktype = get_ktype( kobj) ; return ( ( ktype = = & ktype_block) | | ( ktype = = & ktype_part) ) ; } |
(2) 当调用用户空间的热插拔程序时,相关子系统的名字将作为唯一的参数传递给它。name 函数负责返回合适的字符串传递给用户空间的热插拔程序。
(3)热插拔脚本想得到的任何其他参数都通过环境变量传递。uevent 函数的作用是在调用热插拔脚本之前将参数添加到环境变量中。函数原型:
int ( * uevent) ( struct kset * kset, struct kobject * kobj, /*产生事件的目标对象*/ char * * envp, /*一个保存其他环境变量定义(通常为 NAME=value 的格式)的数组*/ int num_envp, /*环境变量数 组中包含的变量个数(数组大小)*/ char * buffer, int buffer_size/*环境变量被编码后放入的缓冲区的指针和字 节数(大小)*/ /*若需要添加任何环境变量到 envp, 必须在最后的添加项后加一个 NULL 入口,使内核知道数组的结尾*/ ) ; /*返回值正常应当是 0,若返回非零值将终止热插拔事件的产生*/ |
热插拔事件的产生通常是由在总线驱动程序层的逻辑所控制。
以上是 Linux设备模型的底层原理简介,具体的细节应该参阅内核源码和《ULK3》。
文章的例子和实验使用《LDD3》所配的lddbus模块(稍作修改)。
总线
总线是处理器和一个或多个设备之间的通道,在设备模型中, 所有的设备都通过总线相连, 甚至是内部的虚拟"platform "总线。总线可以相互插入。设备模型展示了总线和它们所 控制的设备之间的实际连接。
在 Linux 设备模型中, 总线由 bus_type 结构表示, 定义在 <linux/device.h> :
struct bus_type { const char * name; /*总线类型名称*/ struct module * owner; /*指向模块的指针(如果有), 此模块负责操作这个总线*/ struct kset subsys; /*与该总线相关的子系统*/ struct kset drivers; /*总线驱动程序的kset*/ struct kset devices; /* 挂在该总线的所有设备的kset*/ struct klist klist_devices; /*与该总线相关的驱动程序链表*/ struct klist klist_drivers; /*挂接在该总线 的设备链表*/ struct blocking_notifier_head bus_notifier; struct bus_attribute * bus_attrs; /*总线属性* / struct device_attribute * dev_attrs; /*设备属性,指向为每个加入总线的设备建立的默认属性链表*/ struct driver_attribute * drv_attrs; /*驱动程序属 性*/ struct bus_attribute drivers_autoprobe_attr; /*驱动自动探测属性*/ struct bus_attribute drivers_probe_attr; /*驱动探测属性*/ int ( * match) ( struct device * dev, struct device_driver * drv) ; int ( * uevent) ( struct device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) ; 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 ( * suspend_late) ( struct device * dev, pm_message_t state) ; int ( * resume_early) ( struct device * dev) ; nt ( * resume) ( struct device * dev) ; /*处理热插拔、电源管理、探测和移除等事件的方法*/ unsigned int drivers_autoprobe: 1; } ; |
总线的注册和 删除
总线的主要注册步骤:
(1)申明和初始化 bus_type 结构体。只有很少的 bus_type 成员需要初始化,大部分都由设备模型核心控制。但必须为总线指定名字及一些必要的方法。例如:
struct bus_type ldd_bus_type = { . name = "ldd" , . match = ldd_match, . uevent = ldd_uevent, } ; |
(2)调用bus_register函数注册总线。
int bus_register( struct bus_type * bus) |
调用可能失败, 所以必须始终检查返回值。若成功,新的总线子系统将被添加进系统,并可在 sysfs 的 /sys/bus 下看到。之后可以向总线添加设备。
例如:
ret = bus_register( & ldd_bus_type) ; if ( ret) return ret; |
当必须从系统中删除一个总线时, 调用:
void bus_unregister( struct bus_type * bus) ; |
总线方法
在 bus_type 结构中定义了许多方法,它们允许总线核心作为设备核心和单独的驱动程序之间提供服务的中介,主要介绍以下两个方法:
int ( * match) ( struct device * dev, struct device_driver * drv) ; /*当一个新设备或者驱动被添加到这个总线时,这个方法会被调用一次或多次,若指定 的驱动程序能够处理指定的设备,则返回非零值。必须在总线层使用这个函数, 因为那里存在正确的逻辑,核心内核不知道如何为每个总线类型匹配设备和驱动程序*/ int ( * uevent) ( struct device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) ; /*在为用户空间产生热插拔事件之前,这个方法允许总线添加环境变量(参数和 kset 的uevent方法相同)*/ |
lddbus的match和uevent方法:
static int ldd_match( struct device * dev, struct device_driver * driver) { return ! strncmp ( dev- > bus_id, driver- > name, strlen ( driver- > name) ) ; } /*仅简单比较驱动和设备的名字*/ /*当涉及实际硬件时, match 函数常常对设备提供的硬件 ID 和驱动所支持的 ID 做比较*/ static int ldd_uevent( struct device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) { envp[ 0] = buffer; if ( snprintf( buffer, buffer_size, "LDDBUS_VERSION=%s" , Version) > = buffer_size) return - ENOMEM; envp[ 1] = NULL ; return 0; } /*在环境变量中加入 lddbus 源码的当前版本号*/ |
对设备和驱动 的迭代
若要编写总线层代码, 可能不得不对所有已经注册到总线的设备或驱动进行一些操作,这可能需要仔细研究嵌入到 bus_type 结构中的其他数据结构, 但最好使用内核提供的辅助函数:
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 * ) ) ; /*这两个函数迭代总线上的每个设备或驱动程序, 将关联的 device 或 device_driver 传递给 fn, 同时传递 data 值。若 start 为 NULL, 则从第一个设备开始; 否则从 start 之后的第一个设备开始。若 fn 返回非零值, 迭代停止并且那个值从 bus_for_each_dev 或bus_for_each_drv 返回。*/ |
总线属性
几乎 Linux 设备模型中的每一层都提供添加属性的函数, 总线层也不例外。bus_attribute 类型定义在 <linux/device.h> 如下:
struct bus_attribute { struct attribute attr; ssize_t ( * show) ( struct bus_type * , char * buf) ; ssize_t ( * store) ( struct bus_type * , const char * buf, size_t count ) ; } ; |
可以看出struct bus_attribute 和struct attribute 很相似,其实大部分在 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) ; |
例如创建一个包含源码版本号简单属性文件方法如下:
static ssize_t show_bus_version( struct bus_type * bus, char * buf) { return snprintf( buf, PAGE_SIZE, "%s/n" , Version) ; } static BUS_ATTR( version, S_IRUGO, show_bus_version, NULL ) ; /*在模块加载时创建属性文件:*/ if ( bus_create_file( & ldd_bus_type, & bus_attr_version) ) printk( KERN_NOTICE "Unable to create version attribute/n" ) ; /*这个调用创建一个包含 lddbus 代码的版本号的属性文件(/sys/bus/ldd/version)*/ |
设备
在最底层, Linux 系统中的每个设备由一个 struct device 代表:
struct device { struct klist klist_children; struct klist_node knode_parent; /* node in sibling list */ struct klist_node knode_driver; struct klist_node knode_bus; struct device * parent; /* 设备的 "父" 设备,该设备所属的设备,通常一个父设备是某种总线或者主控制器. 如果 parent 是 NULL, 则该设备是顶层设备,较少见 */ struct kobject kobj; /*代表该设备并将其连接到结构体系中的 kobject; 注意:作为通用的规则, device->kobj->parent 应等于 device->parent->kobj*/ char bus_id[ BUS_ID_SIZE] ; /*在总线上唯一标识该设备的字符串;例如: PCI 设备使用标准的 PCI ID 格式, 包含:域, 总线, 设备, 和功能号.*/ struct device_type * type; unsigned is_registered: 1; unsigned uevent_suppress: 1; struct device_attribute uevent_attr; struct device_attribute * devt_attr; struct semaphore sem; /* semaphore to synchronize calls to its driver. */ struct bus_type * bus; /*标识该设备连接在何种类型的总线上*/ struct device_driver * driver; /*管理该设备的驱动程 序*/ void * driver_data; /*该设 备驱动使用的私有数据成员*/ void * platform_data; /* Platform specific data, device core doesn't touch it */ struct dev_pm_info power; # 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 list_head dma_pools; /* dma pools (if dma'ble) */ struct dma_coherent_mem * dma_mem; /* internal for coherent mem override */ /* arch specific additions */ struct dev_archdata archdata; spinlock_t devres_lock; struct list_head devres_head; /* class_device migration path */ struct list_head node; struct class * class ; dev_t devt; /* dev_t, creates the sysfs "dev" */ struct attribute_group * * groups; /* optional groups */ void ( * release) ( struct device * dev) ; /*当这个设备的最后引用被删除时,内核调用该方法; 它从被嵌入的 kobject 的 release 方法中调用。所有注册到核心的设备结构必须有一个 release 方法, 否则内核将打印错误信息*/ } ; /*在注册 struct device 前,最少要设置parent, bus_id, bus, 和 release 成员 */ |
设备注册
设备的注册和注销函数为:
int device_register( struct device * dev) ; void device_unregister( struct device * dev) ; |
一个实际的总线也是一个设备,所以必须单独注册, 以下 为 lddbus 在编译时注册它的虚拟总线设备源码:
static void ldd_bus_release( struct device * dev) { printk( KERN_DEBUG "lddbus release/n" ) ; } struct device ldd_bus = { . bus_id = "ldd0" , . release = ldd_bus_release } ; /*这是顶层总线,parent 和 bus 成员为 NULL*/ /*作 为第一个(并且唯一)总线, 它的名字为 ldd0,这个总线设备的注册代码如下:*/ ret = device_register( & ldd_bus) ; if ( ret) printk( KERN_NOTICE "Unable to register ldd0/n" ) ; /*一旦调用完成, 新总线会在 sysfs 中 /sys/devices 下显示,任何挂到这个总线的设备会在 /sys/devices/ldd0 下显示*/ |
设备属性
sysfs 中的设备入口可有属性,相关的结构是:
/* interface for exporting device attributes 这个结构体和《LDD3》中的不同,已经被更新过了,请特别注意! */ 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( _name, _mode, _show, _store) ; /*这个宏声 明一个结构, 将 dev_attr_ 作为给定 _name 的前缀来命名设备属性 /*属性文件的实际处理使用以下函数:*/ int device_create_file( struct device * device, struct device_attribute * entry) ; void device_remove_file( struct device * dev, struct device_attribute * attr) ; |
设备结构的嵌 入
device 结构包含设备模型核心用来模拟系统的信息。但大部分子系统记录了关于它们又拥有的设备的额外信息,所以很少单纯用 device 结构代表设备,而是,通常将其嵌入一个设备的高层表示中。底层驱动几乎不知道 struct device。
lddbus 驱动创建了它自己的 device 类型,并期望每个设备驱动使用这个类型来注册它们的设备:
struct ldd_device { char * name; struct ldd_driver * driver; struct device dev; } ; # define to_ldd_device( dev) container_of( dev, struct ldd_device, dev) ; |
lddbus 导出的注册和注销接口如下:
/* * LDD devices. */ /* * For now, no references to LDDbus devices go out which are not * tracked via the module reference count, so we use a no-op * release function. */ static void ldd_dev_release( struct device * dev) { } int register_ldd_device( struct ldd_device * ldddev) { ldddev- > dev. bus = & ldd_bus_type; ldddev- > dev. parent = & ldd_bus; ldddev- > dev. release = ldd_dev_release; strncpy ( ldddev- > dev. bus_id, ldddev- > name, BUS_ID_SIZE) ; return device_register( & ldddev- > dev) ; } EXPORT_SYMBOL( register_ldd_device) ; void unregister_ldd_device( struct ldd_device * ldddev) { device_unregister( & ldddev- > dev) ; } EXPORT_SYMBOL( unregister_ldd_device) ; |
sculld 驱动添加一个自己的属性到它的设备入口,称为 dev, 仅包含关联的设备号,源码如下:
static ssize_t sculld_show_dev( struct device * ddev, struct device_attribute *attr , char * buf) { struct sculld_dev * dev = ddev- > driver_data; return print_dev_t( buf, dev- > cdev. dev) ; } static DEVICE_ATTR( dev, S_IRUGO, sculld_show_dev, NULL ) ; /*接着, 在初始化时间, 设备被注册, 并且 dev 属性通过下面的函数被创建:*/ static void sculld_register_dev( struct sculld_dev * dev, int index) { sprintf ( dev- > devname, "sculld%d" , index) ; dev- > ldev. name = dev- > devname; dev- > ldev. driver = & sculld_driver; dev- > ldev. dev. driver_data = dev; register_ldd_device( & dev- > ldev) ; if (device_create_file(&dev->ldev.dev, &dev_attr_dev)) printk( "Unable to create dev attribute ! /n"); } /*注意:程序使用 driver_data 成员来存储指向我们自己的内部的设备结构的指针。请检查 device_create_file 的返回值,否则编译时会有警告。*/ |
设备驱动程 序
设备模型跟踪所有系统已知的驱动,主要目的是使驱动程序核心能协调驱 动和新设备之间的关系。 一旦驱动在系统中是已知的对象就可能完成大量的工作。驱动程序的结构体 device_driver 定义如下:
/*定义在<linux/device.h>*/ struct device_driver { const char * name; /*驱动程序的名字( 在 sysfs 中出现 )*/ struct bus_type * bus; /*驱动程序所操作的总线类型*/ struct kobject kobj; /*内嵌的kobject对象*/ struct klist klist_devices; /*当前驱动程序能操作的设备链表*/ struct klist_node knode_bus; struct module * owner; const char * mod_name; /* used for built-in modules */ struct module_kobject * mkobj; 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) ; } ; /*注册device_driver 结构的函数是:*/ int driver_register( struct device_driver * drv) ; void driver_unregister( struct device_driver * drv) ; /*driver的属性结构在:*/ struct driver_attribute { struct attribute attr; ssize_t ( * show) ( struct device_driver * drv, char * buf) ; ssize_t ( * store) ( struct device_driver * drv, const char * buf, size_t count ) ; } ; DRIVER_ATTR( _name, _mode, _show, _store) /*属性文件创建的方法:*/ int driver_create_file( struct device_driver * drv, struct driver_attribute * attr) ; void driver_remove_file( struct device_driver * drv, struct driver_attribute * attr) ; /*bus_type 结构含有一个成员( drv_attrs ) 指向一组为属于该总线的所有设备创建的默认属性*/ |
驱动程序结构 的嵌入
对大多数驱动程序核心结构, device_driver 结构通常被嵌入到一个更高层的、总线相关的结构中。
以lddbus 子系统为例,它定义了ldd_driver 结构:
struct ldd_driver { char * version; struct module * module; struct device_driver driver; struct driver_attribute version_attr; } ; # define to_ldd_driver( drv) container_of( drv, struct ldd_driver, driver) ; |
lddbus总线中相关的驱动注册和注销函数是:
/* * Crude driver interface. */ static ssize_t show_version( struct device_driver * driver, char * buf) { struct ldd_driver * ldriver = to_ldd_driver( driver) ; sprintf ( buf, "%s/n" , ldriver- > version) ; return strlen ( buf) ; } int register_ldd_driver( struct ldd_driver * driver) { int ret; driver- > driver. bus = & ldd_bus_type; ret = driver_register( & driver- > driver) ; /*注册底层的 device_driver 结构到核心*/ if ( ret) return ret; driver- > version_attr. attr. name = "version" ; /* driver_attribute 结构必须手工填充*/ driver- > version_attr. attr. owner = driver- > module; /*注意:设定 version 属性的拥有者为驱动模块, 不是 lddbus 模块!因为 show_version 函数是使用驱动模块所创建的 ldd_driver 结构,若 ldd_driver 结构在一个用户空间进程试图读取版本号时已经注销,就会出错*/ driver- > version_attr. attr. mode = S_IRUGO; driver- > version_attr. show = show_version; driver- > version_attr. store = NULL ; return driver_create_file( & driver- > driver, & driver- > version_attr) ; /*建立版本属性,因为这个属性在运行 时被创建,所以不能使用 DRIVER_ATTR 宏*/ } void unregister_ldd_driver( struct ldd_driver * driver) { driver_unregister( & driver- > driver) ; } EXPORT_SYMBOL( register_ldd_driver) ; EXPORT_SYMBOL( unregister_ldd_driver) ; |
在sculld 中创建的 ldd_driver 结构如下:
/* Device model stuff */ static struct ldd_driver sculld_driver = { . version = "$Revision: 1.21 $" , . module = THIS_MODULE, . driver = { . name = "sculld" , } , } ; /*只要一个简单的 register_ldd_driver 调用就可添加它到系统中。一旦完成初始化, 驱动信息可在 sysfs 中显示*/ |
类 子系统
类是一个设备的高层视图, 它抽象出了底层的实现细节,从而允许用户空间使用设备所提供的功能, 而不用关心设备是如何连接和工作的。类成员通常由上层代码所控制, 而无需驱动的明确支持。但有些情况下驱动也需要直接处理类。
几乎所有的类都显示在 /sys/class 目录中。出于历史的原因,有一个例外:块设备显示在 /sys/block目录中。在许多情况, 类子系统是向用户空间导出信息的最好方法。当类子系统创建一个类时, 它将完全拥有这个类,根本不用担心哪个模块拥有那些属性,而且信息的表示也比较友好。
为了管理类,驱动程序核心导出了一些接口,其目的之一是提供包含设备 号的属性以便自动创建设备节点,所以udev的使用离不开类。 类函数和结构与设备模型的其他部分遵循相同的模式 ,所以真正崭新的概念是 很少的。
注意:class_simple 是老接口,在2.6.13中已被删除,这里不再研究。
管理类的接口
类由 struct class 的结构体来定义:
/* * device classes */ struct class { const char * name; /*每个类需要一个唯一的名字, 它将显示在 /sys/class 中*/ struct module * owner; struct kset subsys; struct list_head children; struct list_head devices; struct list_head interfaces; struct kset class_dirs; struct semaphore sem; /* locks both the children and interfaces lists */ struct class_attribute * class_attrs; /* 指向类属性的指针(以NULL结尾) */ struct class_device_attribute * class_dev_attrs; /* 指向类中每个设备的一组默认属性的指针 */ struct device_attribute * dev_attrs; int ( * uevent) ( struct class_device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) ; /* 类热插拔产生时添加环境变量的函数 */ int ( * dev_uevent) ( struct device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) ; /* 类中的设备热插拔时添加环境变量的函数 */ void ( * release) ( struct class_device * dev) ; /* 把设备从类中删除的函数 */ void ( * class_release) ( struct class * class ) ; /* 删除类本身的函数 */ void ( * dev_release) ( struct device * dev) ; int ( * suspend) ( struct device * , pm_message_t state) ; int ( * resume) ( struct device * ) ; } ; /*类注册函 数:*/ 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) ; |
类设备
类存在的真正目的是给作为类成员的各个设备提供一个容器,成员由 struct class_device 来表示:
struct class_device { struct list_head node; /*for internal use by the driver core only*/ struct kobject kobj; /*for internal use by the driver core only*/ struct class * class ; /* 指向该设备所属的类,必须*/ dev_t devt; /* dev_t, creates the sysfs "dev" ,for internal use by the driver core only*/ struct class_device_attribute * devt_attr; /*for internal use by the driver core only*/ struct class_device_attribute uevent_attr; struct device * dev; /* 指向此设备相关的 device 结构体,可选。若不为NULL,应是一个从类入口到/sys/devices 下相应入口的符号连接,以便用户空间查找设备入口*/ void * class_data; /* 私有数据指针 */ struct class_device * parent; /* parent of this child device, if there is one */ struct attribute_group * * groups; /* optional groups */ void ( * release) ( struct class_device * dev) ; int ( * uevent) ( struct class_device * dev, char * * envp, int num_envp, char * buffer, int buffer_size) ; char class_id[ BUS_ID_SIZE] ; /* 此类中的唯一的名字 */ } ; /*类设备注册函 数:*/ 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) ; /*创建和删除除struct class中设备默认属性外的属性*/ 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) ; |
类接口
类子系统有一个 Linux 设备模型的其他部分找不到的附加概念,称为“接口”, 可将它理解为一种设备加入或离开类时获得信息的触发机制,结构体如下:
struct class_interface { struct list_head node; struct class * class ; /* 指向该接口所属的类*/ int ( * add) ( struct class_device * , struct class_interface * ) ; /*当一个类设备被加入到在 class_interface 结构中指定的类时, 将调用接口的 add 函数,进行一些设备需要的额外设置,通常是添加更多属性或其他的一些工作*/ void ( * remove ) ( struct class_device * , struct class_interface * ) ; /*一个接口的功能是简单明了的. 当设备从类中删除, 将调用remove 方法来进行必要的清理*/ int ( * add_dev) ( struct device * , struct class_interface * ) ; void ( * remove_dev) ( struct device * , struct class_interface * ) ; } ; /*注册或注销接口的函数:*/ int class_interface_register( struct class_interface * class_intf) ; void class_interface_unregister( struct class_interface * class_intf) ; /*一个类可注册多个接口*/ |