Linux 设备驱动程序(二)(下)

简介: Linux 设备驱动程序(二)

Linux 设备驱动程序(二)(中):https://developer.aliyun.com/article/1597444

十四、Linux设备模型

可参考 ==> 3、设备模型

1、kobject、kset和子系统

   kobject 是组成设备模型的基本结构。最初它只是被理解为一个简单的引用计数,但是随着时间的推移,它的任务越来越多、因此也有了许多成员。现在 kobject 结构所能处理的任务以及它所支持的代码包括:


对象的引用计数

通常,一个内核对象被创建时,不可能知道该对象存活的时间。跟踪此对象生命周期的一个方法是使用引用计数。当内核中没有代码持有该对象的引用时,该对象将结束自己的有效生命周期,并且可以被删除。

sysfs 表述

在 sysfs 中显示的每一个对象,都对应一个 kobject,它被用来与内核交互并创建它的可见表述。

数据结构关联

从整体上看,设备模型是一个友好而复杂的数据结构,通过在其间的大量连接而构成一个多层次的体系结构。kobject 实现了该结构并把它们聚合在一起。

热插事件拔处理

当系统中的硬件被热插拔时,在 kobject 子系统控制下,将产生事件以通知用户空间。

   从前面的介绍中,读者可能会得出 kobject 是一个复杂结构的结论。的确是这样的。每次只去理解该结构的一部分,这样对了解该结构是如何工作的更为可行。

(1)kobject 基础知识

    kobject 是一种数据结构,它定义在 <linux/kobject.h> 中。在这个文件中,还包括了与 kobject 相关结构的声明,当然还有一个用于操作 kobject 对象的函数清单。

// include/linux/kobject.h
struct kobj_type {
  void (*release)(struct kobject *kobj);
  const struct sysfs_ops *sysfs_ops;
  struct attribute **default_attrs;
};

struct kobject {
  const char    *name;
  struct list_head  entry;
  struct kobject    *parent;
  struct kset   *kset;
  struct kobj_type  *ktype;
  struct sysfs_dirent *sd;
  struct kref   kref;
  unsigned int state_initialized:1;
  unsigned int state_in_sysfs:1;
  unsigned int state_add_uevent_sent:1;
  unsigned int state_remove_uevent_sent:1;
  unsigned int uevent_suppress:1;
};
(a)嵌入的 kobject

   在深入细节研究前,花点时间了解 kobject 的工作过程是值得的。如果回头看看 kobject 所处理的函数清单,就能发现它们都是一些代表其他对象完成的服务。换句话说,一个 kobject 对自身并不感兴趣,它存在的意义在于把高级对象连接到设备模型上。


   因此,内核代码很少(甚至不知道)去创建一个单独的 kobject 对象,相反,kobject 用于控制对大型域(domain)相关对象的访问。为了达到此目的,我们会发现 kobject 对象被嵌入到其他结构中。如果读者熟悉使用面向对象的方法思考,kobject 可以被认为是最顶层的基类,其他类都是它的派生产物。kobject 实现了一系列的方法,对自身并没有特殊的作用,但是对其他对象却非常有效。在 C 语言中不允许直接描述继承关系,因此使用了诸如在一个结构中嵌入另外一个结构的技术。


   举一个例子,先回头看看在第三章遇到过的 cdev 结构。该结构在 2.6.10 内核中有着如下形式:

// include/linux/cdev.h
struct cdev {
  struct kobject kobj;
  struct module *owner;
  struct file_operations *ops;
  struct list_head list;
  dev_t dev;
  unsigned int count;
} 

   如上所见,cdev 结构中嵌入了 kobject 结构。如果使用该结构,只需要访问 kobject 成员就能获得嵌入的 kobject 对象。使用 kobject 的代码经常遇到相反的问题:对于给定的一个 kobject 指针,如何获得包含它的结构指针呢? 必须要抛弃一些想当然的想法(比如假设 kobject 处于包含结构开始的位置),此时要使用 container_of 宏(在第三章 “open 函数” 一节中有过讲述)。利用这个宏,对包含在 cdev 结构中的,名为 kp 的 kobject 结构指针进行转换的代码如下:

// include/linux/stddef.h
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

// include/linux/kernel.h
#define container_of(ptr, type, member) ({      \
  const typeof( ((type *)0)->member ) *__mptr = (ptr);  \
  (type *)( (char *)__mptr - offsetof(type,member) );})

struct cdev *device = container_of(kp, struct cdev, kobj);

    为了能通过 kobject 指针回找(back-casting)包含它的类,程序员经常要定义简单的宏完成这件事。

(b)kobject 的初始化

   为了在编译和运行时对结构进行初始化,本书提供了大量的简单机制。但是对 kobject 的初始化要复杂一些,特别是当 kobject 所有的函数都要被用到时,其初始化更加复杂。不管如何使用 kobject,有一些步骤是必须的。


   首先是将整个 kobject 设置为 0、这通常使用 memset 函数。通常在对包含 kobject 的结构清零时,使用这种初始化方法。如果忘记对 kobject 的清零初始化,则在以后使用 kobject 时,可能会发生一些奇怪的错误,因此,不能跳过这一步骤。

    之后调用 kobject_init() 函数,以便设置结构内部的一些成员:

void kobject_init(struct kobject *kobj);

   kobject_init 所做的一件事情是设置 kobject 的引用计数为 1 。然而仅仅调用 kobject_init 是不够的。kobject 的使用者必须至少设置 kobject 的名字,这是在 sysfs 入口中使用的名字。如果仔细分析内核源代码,可以发现直接将字符串拷贝到 kobject 的 name 成员的代码。但尽量别这么做,而应该使用:

int kobject_set_name(struct kobject *kobj, const char *format, ...);

   该函数使用了类似 printk 的变量参数列表。不管是否相信,它可能会导致该操作的失败(因为要分配内存),因此,严格的代码应该检查返回值,并做相应的处理。


   kobject 的创建者需要直接或者间接设置的成员有:ktype、kset 和 parent。在本章以后的部分中,将对它们进行讲解。

(c)对引用计数的操作

    kobject 的一个重要函数是为包含它的结构设置引用计数。只要对象的引用计数存在,对象(以及支持它的代码)就必须继续存在。底层控制 kobject 引用计数的函数有:

struct kobject *kobject_get (struct kobject *kobj);
void kobject_put(struct kobject *kobj);

   对 kobject_get 的成功调用将增加 kobject 的引用用计数,并返回指向 kobject 的指针。如果 kobject 已经处于被销毁的过程中,则该调用失败,kobject_get 返回 NULL。必须检查返回值,否则可能会产生麻烦的竞态。


   当引用被释放时,调用 kobject_put 减少引用计数、并在可能的情况下释放该对象。请记住 kobject_init 设置引用计数为 1,所以当创建 kobject 时,如果不再需要初始的引用,就要调用相应的 kobject_put 函数。


   请注意,在许多情况下,在 kobject 中的引用计数不足以防止竞态的产生。举例来说, kobject(以及包含它的结构)的存在需要创建它的模块继续存在。当 kobject 继续被使用时,不能卸载该模块。这就是为什么在前面看到的 cdev 结构中包含了模块指针的原因。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 的返回值。如果调用失败,则要释放对模块的引用计数。

(d)release 函数和 kobject 类型

   在上面的讨论中还漏掉了一个重要内容,就是当引用计数为 0 的时候,kobject 将采取什么操作。通常,创建 kobject 的代码无法知道这种情况会在什么时候发生。如果能知道的话,使用引用计数就毫无意义。在使用 sysfs 的时候,即使那些可预知的对象生命期也会变得更为复杂,因为用户空间程序可能在任意时间内引用 kobject 对象(比如让对应的 sysfs 文件保持打开状态)。


   最终结果是,一个被 kobject 所保护的结构,不能在驱动程序生命周期的任何可预知的、单独的时间点上被释放掉。但是当 kobject 的引用计数为 0 时,上述代码又要随时准备运行。引用计数不为创建 kobject 的代码所直接控制。因此当 kobject 的最后一个引用计数不再存在时,必须异步地通知。


   通知是使用 kobject 的 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 自身内,相反,它是与包含 kobject 的结构类型相关联的。一种称为 ktype 的 kobj_type 数据结构负责对该类型进行跟踪。下面是 kobj_type 结构的声明:

struct kobj_type {
  void (*release)(struct kobject *);
  struct sysfs_ops *sysfs_ops;
  struct attribute **default_attrs;
}

   在 kobj_type 的 release 成员中,保存的是这种 kobject 类型的 release 函数指针。在本章中还要讲解另外两个成员(sysfs_ops 和 default_attrs)。


   每个 kobject 都需要有一个相应的 kobj_type 结构。另人困惑的是,可以在两个不同的地方找到这个结构的指针。在 kobject 结构中包含了一个成员(称之为 ktype)保存了该指针。但是,如果 kobject 是 kset 的一个成员的话,kset 会提供 kobj_type 指针(在下一节中会讲到 kset)。如下的宏:

struct kobj_type *get_ktype(struct kobject *kobj);

  查找指定 kobjectkobj_type 指针。

(2)kobject 层次结构、kset 和子系统

   通常,内核用 kobject 结构将各个对象连接起来组成一个分层的结构体系,从而与模型化的子系统相匹配。有两种独立的机制用于连接:parent 指针和 kset。


   在 kobject 结构的 parent 成员中,保存了另外一个 kobject 结构的指针,这个结构表示了分层结构中上一层的节点。比如一个 kobject 结构表示了一个 USB 设备,它的 parent 指针可能指向了表示 USB 集线器的对象,而 USB 设备是插在 USB 集线器上的。


   对 parent 指针最重要的用途是在 sysfs 分层结构中定位对象。在后面的 “低层 sysfs 操作” 一节中,读者将会看到它是如何实现的。

(a)kset

  从许多角度上看,kset 像是 kobj_type 结构的扩充。一个 kset 是嵌入相同类型结构的 kobject 集合。但 kobj_type 结构关心的是对象的类型,而 kset 结构关心的是对象的聚集与集合。这两个概念是分立的。这样同种类型的对象可以出现在不同的集合中。


   因此 kset 的主要功能是包容;我们可以认为它是 kobject 的顶层容器类。实际上,在每个 kset 内部,包含了自己的 kobject,并且可以用多种处理 kobject 的方法处理 kset。需要注意的是,kset 总是在 sysfs 中出现; 一旦设置了 kset 并把它添加到系统中,将在 sysfs 中创建一个目录。kobject 不必在 sysfs 中表示,但是 kset 中的每一个 kobject 成员都将在 sysfs 中得到表述。


   创建一个对象时,通常要把一个 kobject 添加到 kset 中去。这个过程有两个步骤。先把 kobject 的 kset 成员要指向目的 kset,然后将 kobject 传递给下面的函数:

int kobject_add(struct kobject *kobj);

  和处理相似的函数一样,程序员应该意识到该函数可能会失败(如果失败,将返回一个负的错误码),并对此做出相应的动作。内核提供了一个方便使用的函数:

extern int kobject_register(struct kobject *kobj);

    该函数只是 kobject_initkobject_add 的简单组合。

    当把一个 kobject 传递给 kobject_add 时,将会增加它的引用计数。在 kset 中包含的最重要的内容是对象的引用。在某些时候,可能不得不把 kobjectkset 中删除,以清除引用;使用下面的函数达到这个目的:

void kobject_del(struct kobject *kobj);

   还有一个 kobject_unregister 函数,它是 kobject_del 和 kobject_put 的组合。 kset 在一个标准的内核链表中保存了它的子节点。在大多数情况下,所包含的 kobject 会在它们的 parent 成员中保存 kset(严格地说是其内嵌的 kobject)的指针。因此典型的情况是 ,kset 和它的 kobject 的关系与图 14-2 所示类似。请记住:


在图中所有被包含的 kobject,实际上是被嵌入到其他类型中,甚至可能是其他的 kset。

一个 kobject 的父节点不一定是包含它的 kset(这样的结构非常少见)。

(b)kset 上的操作

    kset 拥有与 kobject 相似的初始化和设置接口。下面是这些面数:

void kset_init(struct kset *kset);
int kset_add(struct kset *kset);
int kset_register(struct kset *kset);
void kset_unregister(struct kset *kset);

  在大多数情况下,这些函数只是对 kset 中的 kobject 结构调用类似前面 kobject_ 的函数。

    为了管理 kset 的引用计数,其情况也是一样的:

struct kset *kset_get (struct kset *kset);
void kset_put(struct kset *kset);

  一个 kset 也拥有名字,它保存在内嵌的 kobject 中。因此,如果我们有一个名为 my_setkset、可使用下面的函数设置它的名字:

kobject_set_name(&my_set->kobj, "The name");

   kset 中也有一个指针(在 ktype 成员中)指向 kobj_type 结构,用来描述它所包含的 kobject。该类型的使用优先于 kobject 中的 ktype。因此在典型应用中,kobject 中的 ktype 成员被设置为 NULL。因为 kset 中的 ktype 成员是实际上被使用的成员。


  最后,kset 包含了一个子系统指针(称之为 subsys)。因此现在该讲一讲子系统了。

(c)子系统

   子系统是对整个内核中一些高级部分的表述。子系统通常(但不一定)显示在 sysfs 分层结构中的顶层。内核中的子系统包括 block_subsys(对块设备来说是 /sys/block)、devices_subsys(/sys/devices,设备分层结构的核心)以及内核所知晓的用于各种总线的特定子系统。一个驱动程序的作者几乎不需要创建一个新的子系统。如果想这么做的话,请三思而后行。驱动程序作者最终要做的是添加一个新类,这如同在 “类” 一节中所讲的那样。


下面的简单结构表示了一个子系统:

struct subsystem {
  struct kset kset;
  struct rw_semaphore rwsem;
);

   一个子系统其实是对 kset 和一个信号量的封装。


   每一个 kset 都必须属于一个子系统。子系统的成员将帮助内核在分层结构中定位 kset,但更重要的是,子系统的 rwsem 信号量被用于串行访问 kset 内部的链表。在 kset 结构中,这种成员关系被表示为 subsys 指针。因此通过 kset 结构,可以找到包含 kset 的每一个子系统。但是我们无法直接从 subsystem 结构中找到子系统所包含的多个 kset。

通常使用下面的宏声明 subsystem

decl_subsys(name, struct kobj_type *type, struct kset_hotplug_ops *hotplug_ops);

  该宏使用传递给它的 name 并追加 _subsys,然后做为结构名而创建 subsystem 结构。该宏还使用了指定的 typehotplug_ops 初始化内部的 kset(在本章后面的部分将讲到热插拔操作)。

子系统拥有一个设置和销毁的通数列表:

void subsystem_init(struct subsystem *subsys);
int subsystem_register(struct subsystem *subsys);
void subsystem_unregister(struct subsystem *subsys);
struct subsystem *subsys_get(struct subsystem *subsys)
void subsys put(struct subsystem *subsys);

  这些函数中的大多数都用于对子系统中的 kset 进行操作。

2、低层 sysfs 操作

可参考 ==> Linux—驱动属性文件添加、DEVICE_ATTR宏、device_create_file()及sysfs_create_group()

   kobject 是隐藏在 sysfs 虚拟文件系统后的机制,对于 sysfs 中的每个目录,内核中都会存在一个对应的 kobject 。每一个 kobject 都输出一个或者多个属性,它们在 kobject 的 sysfs 目录中表现为文件,其中的内容由内核生成。这部分内容揭示了 kobject 和 sysfs 在底层是如何交互的。


   <linux/sysfs.h> 中包含了 sysfs 的工作代码。


   只要调用 kobject_add、就能在 sysfs 中显示 kobject。在把 kobject 添加到 kset 的时候,已经讨论过这个函数了;在 sysfs 中创建入口项也是该函数的功能之一。需要了解许多知识,才能理解是如何创建 sysfs 入口的。


kobject 在 sysfs 中的入口始终是一个目录,因此,对 kobject_add 的调用将在 sysfs 中创建一个目录。通常这个目录包含一个或多个属性,随后将讲到如何设置属性。

分配给 kobject(使用 kobject_set_name 函数)的名字是 sysfs 中的目录名。这样,处于 sysfs 分层结构相同部分中的 kobject 必须有唯一的名字。分配给 kobject 的名字必须是合法的文件名: 不能包含反斜杠,并且强烈建议不要使用空格。

sysfs 入口在目录中的位置对应于 kobject 的 parent 指针。如果调用 kobject_add 的时候,parent 是 NULL,它将被设置为嵌入到新 kobject 的 kset 中的 kobject,这样, sysfs 分层结构通常与 kset 创建的内部结构相匹配。如果 parent 和 kset 都是 NULL,则会在最高层创建 sysfs 目录,而这通常不是我们所期望的。

   使用前面介绍过的机制,可以使用 kobject 在 sysfs 中创建空目录。有时读者可能想做一些更有趣的事,因此现在需要讲述一下属性的实现方法。

(1)默认属性

    当创建 kobject 的时候,都会给每个 kobject 一系列默认属性。这些属性保存在 kobj_type 结构中。下面是该结构的成员:

// include/linux/kobject.h
struct kobj_type {
  void (*release)(struct kobject *kobj);
  const struct sysfs_ops *sysfs_ops;
  struct attribute **default_attrs;
};

    default_attrs 成员保存了属性列表,用于创建该类型的每一个 kobjectsysfs_ops 提供了实现这些属性的方法。先来研究 default_attrs,它指向了一个包含 attribute 结构数组的指针:

// include/linux/sysfs.h
struct attribute {
  const char    *name;
  struct module   *owner;
  mode_t      mode;
};  

   在这个结构中, name 是属性的名字(在 kobject 的 sysfs 目录中显示),owner 是指向模块的指针(如果有的话),该模块负责实现这些属性,mode 是应用于属性的保护位。对于只读属性,mode 通常是 S_IRUGO。如果属性是可写的,则可以使用 S_IWUSR 仅为 root 提供写权限(操作模式的宏在 <linux/stat.h> 中定义)。default_attrs 链表中的最后一个元素必须用零填充。


   default_attrs 数组说明了都有些什么属性,但是没有告诉 sysfs 如何真正实现这些属性。这个任务交给了 kobj_type->sysfs_ops 成员,它所指向的结构定义如下:

// include/linux/sysfs.h
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 的指针和正确的属性结构来调用 show 方法。该方法将把指定的值编码后放入缓冲区,然后把实际长度做为返回值返回。请注意不要越界(它有 PAGE_SIZE 个字节大)。sysfs 的约定要求每个属性都要包含一个单个的人眼可阅读的值。如果要返回大量的信息,则需要把它拆分成多个属性。


   也可以对所有的 kobject 的属性使用同一个 show 方法。传递给该函数的 attr 指针可以用来判断所请求的是哪个属性。一些 show 方法包含一系列对属性名的检查。其他的实现方法会把 attribute 结构嵌入到其他结构中,而在那些结构中包含了需要返回的属性值信息,在这种情况下,可在 show 方法中使用 container_of,以获得嵌入结构的指针。


   store 函数与此类似;它将对保存在缓冲区中的数据解码(在 size 中保存了数据的长度,该长度不能超过 PAGE_SIZE),并调用各种实用的方法保存新值,并且返回实际解码的字节数。只有当拥有属性的写权限时,才能调用 store 函数。在编写 store 函数时,不要忘记它是从用户空间取回信息,因此,在对其采取任何响应前,最好仔细检查其合法性。如果输入的数据与预期不符,就要返回一个负的错误码,而不是采取一些不可预期或无法恢复的行动。举例说明,假定我们的设备导出 self_destruct 属性,则应该要求必须将特定的字符串写入该属性才能调用相应的功能,而一个偶然的随机写操作应产生错误。

(2)非默认属性

    在许多情况下,kobject 类型的 default_attrs 成员描述了 kojbect 拥有的所有属性。但是在设计上,这不是一个严格的限制,我们可以根据需要对 kobject 内的属性进行添加和删除。如果希望在 kobjectsysfs 目录中添加新的属性,只需要填写一个 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);

    在调用之后,属性不再出现在 kobjectsysfs 入口中。需要知道的是,一个用户空间进程可能拥有一个指向属性的、打开的文件描述符,因此,在属性被删除后,showstore 函数依然可能被调用。

(3)二进制属性

   sysfs 的约定要求所有属性都只能包含一个可读的文本格式值。也就是说,对创建一个可以处理大量二进制数据属性的需求是很少发生的。但是,当我们在用户空间和设备之间传递不可改变的数据时,有可能产生这种需求。比如向设备上载固件时就需要这样的功能。如果我们在系统中遇到这样的设备,就可以运行用户空间程序(通过热插拔机制),这些程序使用二进制的 sysfs 属性将固件代码传递给内核,其过程将在 “内核固件接口” 一节中讲述。


我们可以用 bin_attribute 结构描述二进制属性:

struct bin_attribute {
  struct attribute attr;
  size_t size;
  ssize_t (*read) (struct kobject *kobj, char *buffer,
            loff_t pos, size_t size);
  ssize_t (*write) (struct kobject *kobj, char *buffer,
            loff_t pos, size_t size);
};

   这里,attr 是一个 attribute 结构,它给出了名字、所有者、二进制属性的权限。size 是二进制属性的最大长度(如果没有最大值,则设置为 0)。read 和 write 函数与子系统设备驱动程序中的相应函数工作方式类似。它们可以在一次加载过程中被调用多次。每次调用所能操作的最大数据量是一页。sysfs 中没有方法可以通知最后一个写操作已经完成,因此实现二进制属性操作的代码必须能用其他方法判断是否已经操作到数据的末尾。

    必须显式创建二进制属性,也就是说,它们不能作为默认属性被设置。调用下面的函数可创建二进制属性:

int sysfs_create_bin_file(struct kobject *kobj, struct bin_attribute *attr);

使用下面的函数删除二进制属性:

int sysfs_remove_bin_file(struct kobject *kobj, struct bin_attribute *attr);

(4)符号链接

   sysfs 文件系统具有常用的树形结构,以反映 kobject 之间的组织层次关系。通常内核中各对象之间的关系远比这复杂。比如一个 sysfs 的子树(/sys/devices)表示了所有系统知晓的设备。而其他的子树(在 /sys/bus 下)表示了设备的驱动程序。但是这些树并不能表示驱动程序及其所管理的设备之间的关系。为了表示这种关系还需要其他的指针,在 sysfs 中,通过符号链接实现了这个目的。


在 sysfs 中创建符号链接时使用下面的函数:

int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);

  该函数创建了一个链接(称为 name)指向 target 的 sysfs 入口,并作为 kobj 的一个属性。这是一个相对链接,因此与 sysfs 挂装系统中的特定位置无关。


   即使 target 已经从文件系统中删除,该链接依然存在。如果创建指向其他 kobject 的符号链接,应该有某种方法探测到 kobject 的这些变化,或者有办法保证目标 kobject 不会消失。其结果(在 sysfs 中失效的符号链接)并不致命,但是它们并不是完美的编程风格,而且可能在用户空间引起混乱。

用下面的函数删除符号链接:

void sysfs_remove_link(struct kobject *kobj, char *name);

3、热插拔事件的产生

    备注:后续内核版本已发生变化,已不使用此种模式了。使用 uevent ?

   一个热插拔事件是从内核空间发送到用户空间的通知,它表明系统配置出现了变化。无论 kobject 被创建还是被删除,都会产生这种事件。比如,当数码相机通过 USB 线缆插入到系统时,或者用户切换控制台终端时,或者当给磁盘分区时,都会产生这类事件。热插拔事件会导致对 /sbin/hotplug 程序的调用,该程序通过加载驱动程序,创建设备节点,挂装分区,或者其他正确的动作来响应事件。


   我们要讨论最后一个重要的 kobject 函数用来产生这些事件。当我们把 kobject 传递给 kobject_add 或者 kobject_del 时,才会真正产生这些事件。在事件被传递到用户空间之前,处理 kobject(或者准确一些,是 kobject 所属的 kset)的代码能够为用户空间添加信息、或者完全禁止事件的产生。

(1)热插拔操作

    对热插拔事件的实际控制,是由保存在 kset_hotplug_ops 结构中的函数完成的:

struct kset_hotplug_ops {
  int (*filter)(struct kset *kset, struct kobject *kobj);
  char * (*name)(struct kset *kset, struct kobject *kobj);
  int (*hotplug)(struct kset *kset, struct kobject *kobj,
          char **envp, int num_envp, char *buffer,
          int buffer_size);
}

   我们可以在 kset 结构的 hotplug_ops 成员中发现指向这个结构的指针。如果在 kset 中不包含一个指定的 kobject,内核将在分层结构中进行搜索(通过 parent 指针),直到找到一个包含有 kset 的 kobject 为止,然后使用这个 kset 的热插拔操作。


   无论什么时候,当内核要为指定的 kobject 产生事件时,都要调用 filter 函数。如果 filter 返回 0,将不产生事件。因此该函数给 kset 代码一个机会,用于决定是否向用户空间传递特定的事件。


   使用该函数的一个例子是块设备子系统。在该子系统中,至少使用了三种类型的 kobject,它们是磁盘、分区和请求队列。用户空间将会对磁盘或者分区的添加产生响应,但通常不会响应请求队列的变化。因此,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));
}

  这里对 kobject 的快速类型检查足以用来判断是否产生事件。


   在调用用户空间的热插拔程序时,相关子系统的名字将作为唯一的参数传递给它。 hotplug 方法负责提供这个名字,它将返回一个适合传递给用户空间的字符串。


   任何热插拔脚本所需要知道的信息将通过环境变量传递。最后一个 hotplug 方法(hotplug)会在调用脚本前,提供添加环境变量的机会。该方法的原型是:

int (*hotplug)(struct kset *kset, struct kobject *kobj,
        char **envp, int num_envp, char *buffer,
        int buffer_size);

   和先前一样,kset 和 kobject 描述了产生事件的目的对象。envp 是一个保存其他环境变量定义的数组(一般使用 NAME=value 的格式),num_envp 说明目前有多少个变量入口。变量应当在编码后放入长度为 buffer_size 的缓冲区中。如果要在 envp 中添加任何变量,请确保在最后一个新变量后加入 NULL 入口,这样内核就知道哪里是结束点了。该方法的通常返回值是 0,返回任何非 0 值将终止热插拔事件的产生。


   热插拔事件的创建(如同设备模型中的许多工作一样)通常被总线驱动程序级别上的逻辑所控制。

4、总线、设备和驱动程序

   到目前为止,读者已经看到了许多低层程序的片段和相关的例子,在本章的剩余部分,将讲述 Linux 设备模型的高级部分。为达到这个目的,我们会介绍一个新的虚拟总线,称之为 lddbus(注 1),并且修改 scullp 驱动程序来 “连接” 到这个总线。


   注 1:当然,该总线的逻辑名称应该是 “sbus”,但我们还是取了一个真正的、物理总线的名字。


   再强调一遍,这里讲述的大部分内容,对许多驱动程序作者来说是不必要的。通常这个层次的具体细节集中于总线层,只有很少的驱动程序作者需要添加一个新的总线类型。本章内容的读者主要是那些希望了解 PCI、USB 等设备的工作原理,或者是需要修改这些代码的程序员。


(1)总线

   总线是处理器与一个或者多个设备之间的通道。在设备模型中,所有的设备都通过总线相连。甚至是那些内部的虚拟 “平台” 总线。总线可以互相插入,比如一个 USB 控制器通常是一个 PCI 设备。设备模型展示了总线和它们所控制的设备之间的连接。


   在 Linux 设备模型中,用 bus_type 结构表示总线,它的定义包含在 < linux/device.h> 中。其结构如下:

struct bus_type {
  char *name;
  struct subsystem subsys;
  struct kset drivers;
  struct kset devices;
  int (*match) (struct device *dev, struct device_driver *drv);
  struct device * (*add) (struct device * parent, char * bus_id);
  int (*hotplug) (struct device *dev, char **envp,
          int num_envp, char *buffer, int buffer_size);
  /* 这里省略了一些成员 */
};

   name 成员是总线的名字,比如 pci。我们可以从这个结构中看到,每个总线都有自己的子系统。然而这些子系统并不在 sysfs 中的顶层,相反,我们会在总线子系统下面发现它们。一个总线包含两个 kset,分别代表了总线的驱动程序和插入总线的所有设备。另外还有一组方法,将在下面讲到。

(a)总线的注册

    正如读者注意到的,例子源代码包含了一个叫 lddbus 的虚拟总线的实现。这个总线用下面的代码设置 bus_type 结构:

struct bus_type ldd_bus_type = {
  .name = "ldd",
  .match = ldd_match,
  .hotplug = ldd_hotplug,
};

    请注意,只有非常少的 bus_type 成员需要初始化;它们中的大多数都由设备模型核心所控制。但是,我们必须为总线指定名字以及其他一些必要的方法。

    对于新的总线,我们必须调用 bus_register 进行注册。Iddbus 使用下面的代码完成注册:

  ret = bus_register(&ldd_bus_type);
  if (ret)
    return ret;

    当然,这个调用可能会失败,因此必须检查它的返回值。如果成功,新的总线子系统将被添加到系统中,可以在 sysfs/sys/bus 目录下看到它。然后,我们可以向这个总线添加设备。

    当有必要从系统中删除一个总线的时候 (比如相应的模块被删除), 要使用 bus_unregister 函数:

// drivers/base/bus.c
void bus_unregister(struct bus_type *bus);
(b)总线方法

    在 bus_type 结构中,定义了许多方法,这些方法允许总线核心作为中间介质,在设备核心与单独的驱动程序之间提供服务。2.6.10 内核定义的总线方法有:

int (*match)(struct device *device, struct device_driver *driver);

   当一个总线上的新设备或者新驱动程序被添加时,会一次或多次调用这个函数。如果指定的驱动程序能够处理指定的设备,该函数返回非零值(不久将详细讲述 device 和 device_driver 结构)。必须在总线层上使用该函数,因为那里存在着正确的逻辑。核心内核不知道如何为每个总线类型匹配设备和驱动程序。

int (*hotplug) (struct device *device, char **envp, int num_envp, 
        char *buffer, int buffer_size);

在为用户空间产生热插拔事件前,这个方法允许总线添加环境变量。其参数与 ksethotplug 方法相同(在前面的 “热插拔事件的产生” 一节中讲述)。hotplug 被 uevent 函数取代:

struct kobj_uevent_env {
  char *envp[UEVENT_NUM_ENVP];
  int envp_idx;
  char buf[UEVENT_BUFFER_SIZE];
  int buflen;
};

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

    Iddbus 驱动程序有一个非常简单的 match 方法,它只是简单地比较了驱动程序和设备的名字:

static int ldd_match(struct device *dev, struct device_driver *driver) {
  return !strncmp(dev->bus_id, driver->name, strlen(driver->name));
}

    在调用真实的硬件时,match 函数通常对设备提供的硬件 ID 和驱动所支持的 ID 做某种类型的比较。

下面是 Iddbushotplug 函数:

static int ldd_hotplug(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;
}


    这里,我们只是在环境变量中添加了 Iddbus 源代码的当前版本号,以便读者做相应的了解。

(c)对设备和驱动程序的迭代

    如果要编写总线层代码,可能会发现不得不对注册到总线的所有设备和驱动程序执行某些操作。这可能需要仔细研究嵌入到 bus_type 结构中的其他数据结构,但是使用内核提供的辅助函数会更好一些。

为了操作注册到总线的每个设备,可使用:

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 返回。

相似的函数也可用于驱动程序的迭代上:

int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,
          void *data, int (*fn) (struct device_driver *, void *));

   该函数的工作方式与 bus_for_each_dev 相同,只是它的工作对象是驱动程序而已。


   值得注意的是,这两个函数在工作期间,都会拥有总线子系统的读取者/写入者信号量。因此,同时使用这两个函数会发生死锁 —— 它们中的任何一个函数都试图获得相同的信号量。修改总线的操作(比如注销设备)也有同样的问题。因此使用 bus_for_each 函数要多加小心。

(d)总线属性

    几乎在 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 类型也包括了两个用来显示和设置属性值的函数。大多数在 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);

    lddbus 驱动程序创建了一个包含版本号的属性文件。其中 show 函数和 bus_attribute 结构使用下面的代码设置:

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");

    在 Iddbus 中,上面的语句创建了一个包含版本号的属性文件(/sys/bus/ldd/version)。

(2)设备

    在最底层,Linux 系统中的每一个设备都用 device 结构的一个实例来表示:

// include/linux/device.h
struct device {
  struct device *parent;
  struct kobject kobj;
  char bus_id[BUS_ID_SIZE];
  struct bus_type *bus;
  struct device_driver *driver;
  void *driver_data;
  void (*release) (struct device *dev);
  /* 省略了几个成员 */
};

   在 device 结构中还有许多包含其他结构的成员,它们只对设备核心代码起重要的作用。但是了解以下这些成员是非常值得的:


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 bus_type *bus;

标识了该设备连接在何种类型的总线上。

struct device_driver *driver;

管理该设备的驱动程序。在下一节中将介绍 device_driver 结构。

void *driver_data;

由设备驱动程序使用的私有数据成员。

void (*release) (struct device *dev);

当指向设备的最后一个引用被删除时,内核调用该方法;它将从内嵌的 kobject 的 release 方法中调用。所有向核心注册的 device 结构都必须有一个 release 方法,否则内核将打印出错误信息。

   在注册 device 结构前,至少要设置 parent、bus_id、bus 和 release 成员。

(a)设备注册

常用的注册和注销函数是:

// drivers/base/core.c
int device_register(struct device *dev);
void device_unregister(struct device *dev);

    我们已经看到了 Iddbus 代码是如何注册它的总线类型的,然而,一个实际的总线是一个设备,因此必须被单独注册。出于简化的原因,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
};

  这是一个顶层总线,因此 parentbus 成员是 NULL,而 release 方法不做任何实质性的工作。作为第一个(也是唯一一个)总线,它的名字是 ldd0。该总线用下面的函数

注册:

  ret = device_register(&ldd_bus);
  if (ret)
    printk(KERN_NOTICE "Unable to register ldd0\n");

    完成这个调用后,我们就可以在 sysfs 中的 /sys/devices 目录中看到它。任何添加到该总线的设备都会在 /sys/devices/ldd0/ 中显示。

(b)设备属性

    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);
};


我们可以在编译时刻用下面的宏构造这些 attribute 结构:

DEVICE_ATTR(name, mode, show, store);

    该结构将 dev_attr_ 作为指定名字的前缀来构造结构的名称。用下面的两个函数实现对属性文件的实际处理:

int device_create_file(struct device *device, struct device_attribute *entry);
void device_remove_file(struct device *dev, struct device_attribute *attr);

    bus_type 结构中的 dev_attrs 成员,指向一个为每个加入总线的设备建立的默认属性链表。

(c)设备结构的嵌入

   device 结构中包含了设备模型核心用来模拟系统的信息。然而,大多数子系统记录了它们所拥有设备的其他信息,因此,单纯用 device 结构表示的设备是很少见的,而是通常把类似 kobject 这样的结构内嵌在设备的高层表示之中。如果读者阅读 pci_dev 或者 usb_device 结构的定义,就会发现其中隐藏了device 结构。通常,底层驱动程序并不知道 device 结构,但是也有例外。

// include/linux/usb.h
struct usb_device {
  int   devnum;
  char    devpath [16];
  u32   route;
  enum usb_device_state state;
  enum usb_device_speed speed;

  struct usb_tt *tt;
  int   ttport;

  unsigned int toggle[2];

  struct usb_device *parent;
  struct usb_bus *bus;
  struct usb_host_endpoint ep0;

  struct device dev;
  /* ... */
};  

    Iddbus 驱动程序创建了自己的 device 类型(ldd_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);

   该结构允许驱动程序为设备提供实际的名字(它与保存在 device 结构中的总线 ID 不同),还提供一个指向驱动程序信息的指针。真实设备的结构通常包含供应商信息、设备模型、设备配置、使用的资源等其他信息。pci_dev(<linux/pci>)结构或者 usb_device(<linux/usb.h>)结构都是非常好的例子。我们为 ldd_device 定义了一个宏(to_ldd_device),以便于将嵌入的 device 结构指针转化为 ldd_device 指针。



Iddbus 导出的注册接口如下:

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

   如上所示,只是简单地填充了嵌入的 device 结构中一些成员(单独的驱动程序没有必要知道这些),并且向驱动程序核心注册设备。我们也可以在这里添加总线专有的设备属性。


   为了展示这个接口是如何被使用的,现在向读者介绍另外一个例子驱动程序,称之为 sculld。它是先前在第八章介绍的 scullp 驱动程序的另外一个版本。它实现了通常的内存区域设备,但是 sculld 可通过 Iddbus 接口利用 Linux 设备模型工作。


   sculld 驱动程序向它的设备入口添加了一个自己的属性,称之为 dev,它只包含了相关的设备编号。这个属性可以由模块装载脚本,或者热插拔子系统使用,以便在设备添加到系统中时自动创建设备节点。该属性的设置使用以下代码:

static ssize_t sculld_show_dev(struct device *ddev, 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);
  device_create_file(&dev->ldev.dev, &dev_attr_dev);
}

    请注意这里使用了 driver_data 成员保存了指向自身内部 device 结构的指针。

(3)设备驱动程序

    设备模型跟踪所有系统所知道的设备。进行跟踪的主要原因是让驱动程序核心协调驱动程序与新设备之间的关系。一旦驱动程序是系统中的已知对象,就可能完成大量的工作。例如,设备驱动程序可以导出信息和配置变量,而这些东西都是独立于任何特定设备的。

驱动程序由以下结构定义:

struct device_driver {
  char *name;
  struct bus_type *bus;
  struct kobject kobj;
  struct list_head devices;
  int (*probe)(struct device *dev);
  int (*remove)(struct device *dev);
  void (*shutdown) (struct device *dev);
};

   再次强调,结构中的许多成员已经被忽略掉了(请参看 <linux/device.h> 了解全部的成员)。其中,name 是驱动程序的名字(它将在 sysfs 中显示),bus 是该驱动程序所操作的总线类型、kobj 是必需的 kobject,devices 是当前驱动程序能操作的设备链表, probe 是用来查询特定设备是否存在的函数(以及这个驱动程序是否能操作它),当设备从系统中删除的时候要调用 remove 函数,在关机的时候调用 shutdown 函数关闭设备。


   操作 device_driver 结构的函数形式现在看起来会很熟悉(将很快讨论它们)。它的注册函数是:

// drivers/base/driver.c
int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);

常用的属性结构是:

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),它指向一组为属于该总线的所有设备创建的默认属性。

(a)驱动程序结构的嵌入

    对于大多数驱动程序核心结构来说,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);

    这里,我们要求每个驱动程序提供自己当前的软件版本号,Iddbus 为它所知道的每一个驱动动程序导出这个版本字符串。该总线特有的驱动程序注册函数如下:

int register_ldd_driver(struct ldd_driver *driver) {
  int ret;
  driver->driver.bus = &ldd_bus_type;
  ret = driver_register(&driver->driver);
  if (ret)
    return ret;
  driver->version_attr.attr.name ="version";
  driver->version_attr.attr.owner = driver->module;
  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);
}

    该函数的前半部分只是简单地向核心注册了低层的 device_driver 结构,其余部分设

置了版本号属性。由于该属性是在运行时建立的,因此不能使用 DRIVER_ATTR 宏,这样,我们必须手工填写 driver_attribute 结构。请注意要将 owner 属性设置为驱动程序模块,而不是 Iddbus 模块,这么做的原因可以在为该属性实现的 show 函数中看到:

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);
}

   也许有的读者会想,owner 属性应该是 Iddbus 模块,因为在那里定义了实现该属性的函数。然而,该函数使用驱动程序自己创建和拥有的 ldd_driver 结构。如果该结构已经不存在了,而用户空间进程又要试图读取版本号,则可能会出现麻烦。将 owner 属性设置为驱动程序模块可防止用户空间打开属性文牛时卸载模块的情况发生。由于每个驱动程序模块创建了 Iddbus 模块的引用,因此可以保证不会在不适当的时候被卸载。


考虑到完整性,sculld 用下面的代码创建自己的 ldd_driver 结构:

static struct ldd_driver sculld_driver = {
  .version = "$Revision:1.1 $",
  .module = THIS_MODULE,
  .driver = {
    .name = "sculld",
  }
};

    一个 register_ldd_driver 调用将它添加到了系统中。一旦初始化完成,就可以在 sysfs 中看到驱动程序信息:

5、类

   本章讨论的最后一个设备模型概念是类。类是一个设备的高层视图,它抽象出了低层的实现细节。驱动动程序看到的是 SCSI 磁盘和 ATA 磁盘,但是在类的层次上,它们都是磁盘而已。类允许用户空间使用设备所提供的功能,而不关心设备是如何连接的,以及它们是如何工作的。


   几乎所有的类都显示在 /sys/class 目录中。举个例子,不管网络接口的类型是什么,所有的网络接接口都集中在 /sys/class/net 下。输入设备可以在 /sys/class/input 下找到,而串行设备都集中在 /sys/class/tty 中。一个例外是块设备、出于历史的原因、它们出现在 /sys/block 下。


   类成员通常被上层代码所控制,而不需要来自驱动程序的明确支持。当 sbull 驱动程序(参看第十六章)创建一个虚拟磁盘设备时,它将自动出现在 /sys/block 中。snull 网络驱动程序(参看第十七章)也不必为 /sys/class/net 中出现的接口做任何特殊的事情。但是,有些情况下驱动程序也需要直接处理类。


   在许多情况下,类子系统是向用户空间导出信息的最好方法。当子系统创建一个类时,它将完全拥有这个类,因此根本不必担心哪个模块拥有那些属性。只要花很少的时间观察一下 sysfs 中那些面向硬件的部分,就会发现它的表示并不十分友好,我们会更愿意在 /sys/class/ 下查找信息,而不是在 /sys/devices/pci0000:00/0000:00:10.0/usb2/2-0:1.0 中查找。


   为了管理类,驱动程序核心导出了两个不同的接口。class_simple 例程提供了一种尽可能简单的方法,来向系统中添加新的类;通常这些例程的主要目的是,提供包含设备号的属性以便创建设备节点。正规的类的接口更复杂一些,但也提供了更多的功能。这里从简单的接口入手学习。

(1)class_simple 接口

    class_simple 接口非常易于使用,甚至用不着用户担心导出包含已分配设备号属性这样的信息。这个接口只是一些简单的函数调用,几乎没有使用 Linux 设备模型的样板文件。

    第一步是创建类本身。调用 class_simple_create 函数完成这一任务:

struct class_simple *class_simple_create(struct module *owner, char *name);

  该函数使用给定的名字创建类。这个操作有可能会失败,因此在进行下一步前,应始终检查它的返回值(使用第十一章 “指针和错误值” 一节中介绍的 IS_ERR)。

可以用下面的函数销毁一个简单类:

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 下的设备入口。


   可以向设备入口添加其他属性,这时可使用 class_device_create_file 函数,该函数和类子系统的其余部分将在下一节讲述。


   在插拔设备时,类会产生热插拔事件。如果驱动程序需要为用户空间处理程序添加环境变量的话,可以用下面的代码设置热插拔回调函数:

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 结构,提供设备号(应该是唯一的)就足够了。

(2)完整的类接口

class_simple 接口能够满足许多需求,但有时候需要有更强的灵活性。下面的讨论将描述如何使用基于 class_simple 的完整类机制。简而言之,类函数和结构与设备模型的其他部分遵从相同的模式,因此真正崭新的概念是很少的。

(a)管理类

    用 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 指向的数组(以 NULL 结尾)中的所有属性。还要添加每个设备的一组默认属性,而 class_dev_attrs 指针指向了这些属性。当热插拔事件产生时,还有一个常用的 hotplug 函数用来添加环境变量。还有另外两个 release 方法: 把设备从类中删除时,调用 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);
(b)类设备

    类存在的真正目的是,给作为类成员的各个设备提供一个容器。用 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 指向与此相关的 device 结构。对 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 接口创建的类设备中去。

(c)类接口

    类子系统具有一个在 Linux 设备模型其他部分找不到的附加概念。该机制被称为 “接口”,但是把它理解成一种设备加入或者离开类时获得信息的触发机制更为贴切些。

一个接口由下面的结构表达:

// include/linux/device.h
struct class_interface {
  struct class *class;
  int (*add) (struct class_device *cd);
  void (*remove) (struct class_device *cd);
};  

可以用下面的函数注册和注销接口:

// drivers/base/class.c
int class_interface_register(struct class_interface *intf);
void class_interface_unregister(struct class_interface *intf);

   接口函数都是望其名知其意的。无论何时把一个类设备添加到 class_interface 结构所指定的类中,都将调用接口的 add 函数。该函数能为设备做一些其他的必要设置,通常这些设置表现为添加更多的属性,但也能完成其他一些工作。在从类中删除设备时,调用 remove 函数来做必要的清理工作。


可以为单个类注册多个接口。

6、各环节的整合

    为了能更好地理解什么是驱动程序模型,现在进入内核看一看设备生命周期的各个环节。前面讲述了 PCI 子系统是如何与驱动程序模型交互的,还讲解了在系统内一个驱动程序的添加与删除,以及一个设备的添加与删除的概念。这些细节虽然是针对 PCI 核心代码讲解的,但是也适用于其他所有使用驱动程序核心管理驱动程序和设备的子系统。

    在 PCI 核心、驱动程序核心以及单独的 PCI 驱动程序之间的交互是非常复杂的,如图 14-3 所示。

(1)添加一个设备

    PCI 子系统声明了一个 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 变量将向驱动程序核心注册。此后,驱动程序将在 /sys/bus/pci 中创建一个 sysfs 目录,其中包含了两个目录: devices 和 drivers。


   所有的 PCI 驱动程序都必须定义一个 pci_driver 结构变量,在该变量包含了这个 PCI 驱动程序所提供的不同功能函数(关于 PCI 子系统,以及如何编写 PCI 驱动程序的知识,请参看第十二章)。这个结构中包含了一个 device_driver 结构,在注册 PCI 驱动程序

时,这个结构将被初始化:

  /* 初始化 driver 中常用的成员 */
  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 驱动程序:

  /* 向核心注册 */
  error = driver_register(&drv->driver);

    现在驱动程序可与其所支持的任何 PCI 设备绑定。

    在能与 PCI 总线交互的特定体系架构代码的帮助下,PCI 核心开始探测 PCI 地址空间,查找所有的 PCI 设备。当一个 PCI 设备被找到时,PCI 核心在内存中创建一个pci_dev 类型的结构变量。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 设备所在的总线设备。bus 变量被设置为指向 pci_bus_type 结构。接着设置 name 和 bus_id 变量,其值取决于从 PCI 设备中读取的名字和 ID。


   当 PCI 的 device 结构被初始化后,使用下面的代码向驱动程序核心注册设备:

// drivers/base/core.c
  device_register(&dev->dev);

   在 device_register 函数中,驱动程序核心对 device 中的许多成员进行初始化,向 kobject 核心注册设备的 kobject(这将产生一个热插拔事件,在本章后面的部分讨论),然后将该设备添加到设备列表中,该设备列表为包含该设备的父节点所拥有。完成这些工作后,所有的设备都可以通过正确的顺序访问,并且知道每个设备都挂在层次结构的哪一点上。


   接着设备将被添加到与总线相关的所有设备链表中,在这个例子中是 pci_bus_type 链表。这个链表包含了所有向总线注册的设备,遍历这个链表,并且为每个驱动程序调用该总线的 match 函数,同时指定该设备。对于 pci_bus_type 总线来说,PCI 核心在把设备提交给驱动程序核心前,将 match 函数指向 pci_bus_match 函数。

// drivers/pci/pci-driver.c
static int pci_bus_match(struct device *dev, struct device_driver *drv)
{
  struct pci_dev *pci_dev = to_pci_dev(dev);
  struct pci_driver *pci_drv = to_pci_driver(drv);
  const struct pci_device_id *found_id;

  found_id = pci_match_device(pci_drv, pci_dev);
  if (found_id)
    return 1;

  return 0;
}

static const struct pci_device_id *pci_match_device(struct pci_driver *drv,
                struct pci_dev *dev)
{
  struct pci_dynid *dynid;

  /* Look at the dynamic ids first, before the static ones */
  spin_lock(&drv->dynids.lock);
  list_for_each_entry(dynid, &drv->dynids.list, node) {
    if (pci_match_one_device(&dynid->id, dev)) {
      spin_unlock(&drv->dynids.lock);
      return &dynid->id;
    }
  }
  spin_unlock(&drv->dynids.lock);

  return pci_match_id(drv->id_table, dev);
}

// drivers/pci/pci.h
static inline const struct pci_device_id *
pci_match_one_device(const struct pci_device_id *id, const struct pci_dev *dev)
{
  if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) &&
      (id->device == PCI_ANY_ID || id->device == dev->device) &&
      (id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) &&
      (id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) &&
      !((id->class ^ dev->class) & id->class_mask))
    return id;
  return NULL;
}

// drivers/pci/pci-driver.c
const struct pci_device_id *pci_match_id(const struct pci_device_id *ids,
           struct pci_dev *dev)
{
  if (ids) {
    while (ids->vendor || ids->subvendor || ids->class_mask) {
      if (pci_match_one_device(ids, dev))
        return ids;
      ids++;
    }
  }
  return NULL;
}

   pci_bus_match 函数将把驱动程序核心传递给它的 device 结构转换为 pci_dev 结构。它还把 device_driver 结构转换为 pci_driver 结构,并且查看设备和驱动程序中的 PCI 设备相关信息,以确定驱动程序是否能支持这类设备。如果这样的匹配工作没能正确执行,该函数会向驱动程序核心返回 0,接着驱动程序核心考虑在其链表中的下一个驱动程序。


   如果匹配工作圆满完成,函数向驱动程序核心返回 1。这将导致驱动程序核心将 device 结构中的 driver 指针指向这个驱动程序,然后调用 device_driver 结构中指定的 probe 函数。


   在 PCI 驱动程序向驱动程序核心注册前,probe 变量被设设置为指向 pci_device_probe 函数。该函数将 device 结构转换为 pci_dev 结构,并且把在 device 中设置的 driver 结构转换为 pci_driver 结构。它也将检测这个驱动程序的状态,以确保其能支持这个设备(出于某种原因,这个检测有点多余),增加设备的引用计数,然后用绑定的 pci_dev 结构指针为参数,调用 PCI 驱动程序的 probe 函数。


   如果 PCI 驱动程序的 probe 函数出于某种原因,判定不能处理这个设备,其将返回负的错误值给驱动程序核心,这将导致驱动程序核心继续在驱动程序列表中搜索,以匹配这个设备。如果 probe 函数探测到了设备,为了能正常操作设备,它将做所有的初始化工作,然后向驱动程序核心返回 0。这会使驱动程序核心将该设备添加到与此驱动程序绑定的设备链表中,并且在 sysfs 中的 drivers 目录到当前控制设备之间建立符号链接。这个符号链接使得用户知道哪个驱动程序被绑定到了哪个设备上。该目录有类似以下的内容:

(2)删除设备

  可以使用多种不同的方法从系统中删除 PCI 设备。所有的 CardBus 设备都是真实的 PCI 设备,只是它们具有不同的物理形态参数,内核 PCI 核心对它们不加以区分。那些允许在机器运行时添加和删除 PCI 设备的系统正日益流行,Linux 也支持这样的操作。还有一种伪 PCI 热插拔驱动程序,它允许开发者测试他们的 PCI 驱动动程序能否在系统运行时正确处理设备的删除。这个模块叫做 fakephp,它可以让内核认为 PCI 设备被拔走,但是它不允许用户从不具备这个能力的硬件系统中真正地移除该 PCI 设备。读者可参看该驱动程序的相关文档,以了解如何测试 PCI 驱动程序的更多知识。


   相对于添加设备,PCI 核心对删除设备做了很少的工作。当删除一个 PCI 设备时,要调用 pci_remove_bus_device 函数。该函数做些 PCI 相关的清理工作,然后使用指向 pci_dev 中的 device 结构的指针,调用 device_unregister 函数。


   在 device_unregister 函数中,驱动程序核心只是删除了从绑定设备的驱动程序(如果有设备绑定的话)到 sysfs 文件的符号链接,从内部设备表中删除了该设备,并且以 device 结构中的 kobject 结构指针为参数,调用 kobject_del 函数。该函数引起了用户空间的 hotplug 调用,表明 kobject 现在从系统中删除,然后删除全部与 kobject 相关的 sysfs 文件和 sysfs 目录,而这些目录和文件都是 kobject 以前创建的。


   kobject_del 函数还删除了设备的 kobject 引用。如果该引用是最后一个(这意味着在用户空间中、没有该设备的 sysfs 入口文件保持打开状态)、就要调用该 PCI 设备的 release 函数 —— pci_release_dev。该函数只是释放了 pci_dev 结构所占用的空间。


   做完这些事后,与设备相关的所有 sysfs 入口都被删除了,并且与设备相关的内存也被释放。至此,PCI 设备被完全从系统中删除。

(3)添加驱动程序

   当调用 pci_register_driver 函数时,一个 PCI 驱动程序被添加到 PCI 核心中。与前面添加设备一节中相似,该函数只是初始化了包含在 pci_driver 结构中的 device_driver 结构。PCI 核心用包含在 pci_driver 结构中的 device_driver 结构指针作为参数,在驱动程序核心内调用 driver_register 函数。


   driver_register 函数初始化了几个 device_driver 中的锁,然后调用 bus_add_driver 函数。该函数按以下步骤操作:


查找与驱动程序相关的总线。如果没有找到该总线,函数立刻返回。

根据驱动程序的名字以及相关的总线,创建驱动程序的 sysfs 目录。

获取总线内部的锁,接着遍历所有向总线注册的设备,然后如同添加新设备一样,为这些设备调用 match 函数。如果 match 函数成功,接着如前面章节所讲,开始绑定过程的剩余步骤。

(4)删除驱动程序

   删除驱动程序是一个简单的过程。对于 PCI 驱动程序来说,驱动程序调用 pci_unregister_driver 函数。该函数只是用包含在 pci_driver 结构中的 device_driver 结构作为参数,调用驱动程序核心函数 driver_unregister。


   driver_unregister 函数通过清除在 sysfs 树中属于驱动程序的 sysfs 属性,来完成一些基本管理工作。然后它遍历所有属于该驱动程序的设备,并为其调用 release 函数。这与前面将设备从系统中删除时调用 release 函数一样。


   当所有的设备与驱动程序脱离后,驱动程序代码使用了下面这两个在逻辑上有点独特的数:

  down(&drv->unload_sem);
  up(&drv->unload_sem);

在返回给调用者前执行这个操作。锁住代码是因为在函数安全返回前,代码需要等待驱动程序的所有引用计数为零。模块在被卸载的时候,几乎都要调用 driver_unregister 函数作为退出的方法。只要驱动程序正在被设备引用并且等待这个锁的解开,模块就需要保留在内存中,这样,内核就能知道什么时候可以安全地把驱动程序从内存中删除掉。

7、热插拔

    有两个不同的角度来看待热插拔。从内核角度看,热插拔是在硬件、内核、内核驱动程序之间的交互。从用户的角度看,热插拔是在内核与用户之间,通过调用 /sbin/hotplug 程序的交互。当需要通知用户内核中发生了某些类型的热插拔事件时,内核才调用该函数。

(1)动态设备

   当系统正在运行时,现在几乎所有的计算机系统都能处理设备的安装与删除,在讨论这个事实的时候,用得最多的术语就是热插拔。这与前几年的计算机系统有着非常大的不同,那时候程序员都知道,当系统启动时需要扫描所有的设备,也根本不用担心在电源关闭之前,设备会被拔走。现在随着 USB、CardBus、PCMCIA、IEEE1394 和 PCI 热插控制器的出现,要求 Linux 内核能够可靠运行,而不管运行过程中在系统中添加或者删除了什么硬件。这给设备驱动程序作者增加了很大的压力,因为他们必须要处理设备被毫无征兆地突然拔走的情况。


   每种不同的总线使用不同的方法处理设备的移除。比如当一个 PCI、CardBus 或者 PCMCIA 设备从系统删除时,在驱动程序通过它的 remove 函数被告之此事发生前,会留有一段时间。在这发生之前,所有对 PCI 总线的读操作会返回全部的字节位。这就意味着驱动动程序总是要检查它们从 PCI 总线那里读来的数据值,并且能够正确处理值 0xff。


   这样的例子可以在 drivers/usb/host/ehci-hcd.c 驱动程序中找到,它是一个 USB 2.0(高速)控制器卡的驱动。在它的主要握手循环中,通过下面的代码来检测控制器卡是否已从系统中拔掉:

  result = readl(ptr);
  if (result == ~(u32)0)  /* 拔掉了硬件卡 */
    return -ENODEV;

   对 USB 驱动程序来说,当从系统中删除绑定在 USB 驱动程序上的设备时,任何发送给设备的未完成操作都会失败,并出现错误 -ENODEV。驱动程序需要识别出该错误并正确删除那些未完成的 I/O 操作。


   热插拔设备并不仅仅局限在传统设备上,比如鼠标、键盘和网卡。现在有一些系统支持添加、删除 CPU 和内存条。幸运的是 Linux 内核能正确处理添加、删除这样核心的 “系统” 设备,所以单独的设备驱动程序就不用在这些事情上多虑了。

(2)udev

   udev 依赖于 sysfs 输出到用户空间的所有设备信息,以及当设备添加或者删时,/sbin/hotplug 对它的通知。比如为给定的设备命名等一些决策方法,可以在内核空间以外的用户空间指定。这保证了能从内核中删除命名规则,并且允许在为每个设备命名时具有很大的灵活性。


   关于如何使用和配置 udev,请参看发行版中 udev 软件包内的文档。


   为了让 udev 能够正常工作,一个设备驱动程序要做的所有事情是: 通过 sysfs 将驱动程序所控制设备的主设备号和次设备号导出到用户空间。对于那些使用子系统分配主设备号和次设备号的驱动程序,该工作已经由子系统完成,驱动程序不用做任何事。这样的子系统例子有 tty、misc、usb、input、scsi、block、i2c、network 和 frame buffer 子系统。如果驱动程序通过调用 cdev_init 函数或者是老版本的 register_chrdev 函数,自己处理获得的主设备号和次设备号,那么为了能正确使用 udev,需要对驱动程序进行修改。


   udev 在 sysfs 中的 /class/ 目录树中搜索名为 dev 的文件,这样内核通过 /sbin/hotplug 接口调用它的时候,就能获得分配给特定设备的主设备号和次设备号。一个设备驱动程序只需要为它所控制的每个设备创建该文件。class_simple 接口是完成这件工作的最简单方法。


   如同在 “class_simple 接口” 一节中提到的,使用 class_simple 接口的第一步是通过 class_simple_create 函数创建 class_simple 结构:

  static struct class_simple *foo_class;
  // ...
  foo_class = class_simple_create(THIS_MODULE, "foo");
  if (IS_ERR(foo_class)) {
    printk(KERN_ERR "Error creating foo class.\n");
    goto error;
  }

  这段代码在 sysfs 中的 /sys/class/foo 下创建一个目录。

    当驱动程序发现一个设备时,并且已经像第三章中介绍的那样分配了一个次设备号,驱动程序将调用 class_simple_device_add 函数:

class_simple_device_add(foo_class, MKDEV(FOO_MAJOR, minor), NULL, "foo%d", minor);

  这段代码在 /sys/class/foo 下创建一个子目录 fooN,这里 N 是设备的次设备号。在这个目录中创建一个文件 —— dev,有了这个文件,udev 就可以为设备创建一个设备节点。

    当设备与驱动程序脱离时,它也与分配的次设备号脱离,此时需要调用 class_simple_device_remove 函数删除该设备在 sysfs 中的入口项:

class_simple_device_remove(MKDEV(FOO_MAJOR, minor));

  然后,当驱动程序完全关闭时,需要调用 class_simple_destroy 函数删除先前由 class_simple 函数创建的类:

class_simple_destroy(foo_class);

   由 class_simple_device_add 函数创建的 dev 文件包含了主设备号和次设备号,它们被一个 “:” 分开。如果要在子系统 class 目录中提供其他文件,驱动程序将不使用 class_simple 接口,而是使用 print_dev_t 函数为指定的设备正确格式化主设备号和次设备号。

8、处理固件

(1)内核固件接口

    正确的做法是当需要的时候,从用户空间获得固件。一定不要从内核空间直接打开一个包含固件的文件。这是一个隐含错误的操作,因为它把策略(以文件名的形式)包含进了内核。相反,正确的方法是使用固件接口,这些接口就是为了这个目的而引入的:

#include <linux/firmware.h>
int request_firmware(const struct firmware **fw, char *name, struct device *device);

    request_firmware 调用要求用户空间为内核定位并提供一个固件映像文件;我们一会儿将讲解它的工作细节。name 表示需要的固件,通常 name 是供应商提供的固件文件名称。典型的文件名如 my_firmware.bin如果固件被正确加载,返回值是 0(否则将返回错误代码),fw 参数指向一个下面的结构:

struct firmware {
  size_t size;
  u8 *data;
};

  该结构包含了实际的固件,现在可以把它下载到设备上了。请注意这个固件在用户空间内并未加以检验,因此,在将正确的固件映像传递给硬件前,请对其做部分或者全部的检测。设备固件通常包含校验字符串,比如校验和,我们首先对它们进行检验,然后才能信任这些数据。

  在把固件发送到设备后,需要使用下面的函数释放内核中的结构:

void release_firmware(struct firmware *fw);

    由于 request_firmware 需要用户空间的操作,因此在返回前它将保持睡眠状态。如果当驱动程序必须要使用固件,而又不能进入睡眠状态时,可以使用下面的异步函数:

int request_firmware_nowait(struct module *module,
            char *name, struct device *device, void *context,
            void (*cont) (const struct firmware *fw, void *context));

   这里的附加参数是 module(通常该参数是 THIS_MODULE)、context(并不是固件子系统使用的私有数据指针)和 cont。如果一切正常,request_firmware_nowait 将开始固件加载过程并返回 0。过一段时间后,将使用加载的结果作为参数调用 cont。如果由于某些原因固件加载失败,则 fw 是 NULL。

(2)工作原理

    固件子系统使用 sysfs 和热插拔机制工作。当调用 request_firmware 时,在 /sys/class/firmware 下将创建一个目录,该目录使用设备名作为它的目录名。该目录包含三个属性:

  • loading

该属性由负责装载固件的用户空间进程设置为 1。当装载过程完毕时,它将被设置为 0。将 loading 设置为 -1,将终止固件装载过程。

data

data 是一个二进制属性,用来接收固件数据。在设置完 loading 后,用户空间进程将把固件写入该属性。

device

该属性是到 /sys/devices 下相应入口的符号链接。

   一旦 sysfs 入口被创建,内核将为设备产生热插拔事件。传递给热插拔处理程序的环境包括一个 FIRMWARE 变量,它将设置为提供合 request_firmware 的名字。处理程序定位固件文件,使用所提供的属性把固件文件拷贝到内核。如果不能发现固件文件,处理程序将设置 loading 属性为 -1。


   如果在 10 秒钟之内不能为固件的请求提供服务,内核将放弃努力并向驱动程序返回错误状态。这个超时值可以通过修改 sysfs 属性 /sys/class/firmware/timeout 来改变。


   request_firmware 接口允许使用驱动程序来发布设备的固件。当正确地整合进热插拔机制后,固件加载子系统允许设备不受干扰地工作。很明显这是处理该问题的最好方法。


   然而还有一点要特别注意: 不能在没有制造商许可的情况下发行设备的固件。一些制造商同意在某些条款保护下授权许可使用他们的固件,而一些制造商就不是那么配合了。无论哪种情况,没有他们的许可就拷贝和发行他们的固件,是违反版权法的,这可能会引起麻烦。

9、快速索引

    在本章中已经介绍了许多函数,这里是对它们的一个总结。

(1)kobject

#include <linux/kobject.h>
// 包含文件中包含了对 kobject 的定义,以及相关的结构和函数。

void kobject_init(struct kobject *kobj);
int kobject_set_name(struct kobject *kobj, const char *format, ...);
// kobject 的初始化函数。

struct kobject *kobject_get(struct kobject *kobj);
void kobject_put (struct kobject *kobj);
// 管理 kobject 引用计数的函数。

struct kobj_type;
struct kobj_type *get_ktype(struct kobject *kobj);
// 对包含 kobject 的结构类型的描述,使用 get_ktype 获得与指定 kobject 相关的 kobj_type。

int kobject_add(struct kobject *kobj);
extern int kobject_register(struct kobject *kobj);
void kobject_del(struct kobjecc *kobj);
void kobject_unregister(struct kobject *kobj);
// kobject_add 向系统添加 kobject,处理 kset 成员关系,sysfs 表述以及产生热插拔事
// 件。kobject_register 函数是 kobject_init 和 kobject_add 的组合。使用 kobject_del
// 删除一个 kobject,或者使用 kobject_unregister 函数,它是 kobject_del 和 kobject_put
// 的组合。

void kset_init(struct kset *kset);
int kset_add(struct kset *kset);
int kset_register(struct kset *kset);
void kset_unregister(struct kset *kset);
// kset 的初始化和注册函数。

decl_subsys(name, type, hotplug_ops);
// 使声明子系统得以简化的宏。

void subsystem_init(struct subsystem *subsys);
int subsystem_register(struct subsystem *subsys);
void subsystem_unregister(struct subsystem *subsys);
struct subsystem *subsys_get(struct subsystem *subsys)
void subsys_put(struct subsystem *subsys);
// 对子系统的操作。

(2)sysfs 操作

#include <linux/sysfs.h>
// 包含 sysfs 声明的包含文件。

int sysfs_create_file(struct kobject *kobj, struct attribute *attr);
int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
int sysfs_create_bin_file(struct kbbject *kobj, struct bin_attribute *attr);
int sysfs_remove_bin_file(struct kobject *kobj, struct bin_attribute *attr);
int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);
void sysfs_remove_link(struct kobject *kobj, char *name);
// 添加或删除与 kobject 相关属性文件的函数。

(3)总线、设备和驱动动程序

int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);
// 在设备模型中实现总线注册和注销的函数。

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 *));
// 这些函数分别遍历附属于指定总线的每个设备和驱动程序。

BUS_ATTR(name, mode, show, store);
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);
// 使用宏 BUS_ATTR 声明了一个 bus_attribute 结构,使用上面的两个函数可对
// 该结构进行添加和删除。

int device_register(struct device *dev);
void device_unregister(struct device *dev);
// 处理设备注册的函数。

DEVICE_ATTR(name, mode, show, store);
int device_create_file(struct device *device, struct device_attribute *entry);
void device_remove_file(struct device *dev, struct device_attribute *attr);
// 处理设备属性的宏和函数。

int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);
// 注册和注销设备驱动程序的函数。

DRIVER_ATTR(name, mode, show, store);
int driver_create_file(st.ruct device_driver *drv, struct driver_attribute *attr);
void driver_remove_file(struct device_driver *drv, struct driver_attribute *attr);
// 管理驱动程序属性的宏和函数。

(4)类

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, ...);
void class_simple_device_remove(dev_t_dev);
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));
// 实现 class_simple 接口的函数;它们管理了包含 dev 属性和其他内容在内的简单类入口。

int class_register(struct class *cls);
void class_unregister(struct class *cls);
// 注册和注销类。

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);
// 处理类属性的常用宏和函数。

int class_device_register(struct class_device *cd);
void class_device_unregister(scruct class_device *cd);
int class_device_rename(struct class_device *cd, char *new_name);
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);
// 实现类设备接口的函数和宏。

int class_interface_register(struct class_interface *intf);
void class_interface_unregister(struct class_interface *intf);
// 向类添加(或者删除)接口的函数。

(5)固件

#include <linux/firmware.h>
int request_firmware(const struct firmware **fw, char *name, struct device *device);
int request_firmware_nowait(struct module *module, char *name, struct device *device, 
            void *context, 
            void (*cont)(const struct firmware *fw, void *context));
void release_firmware(struct firmware *fw);
// 内核中实现固件加载的接口函数。


目录
相关文章
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
18天前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
22 6
|
18天前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
23 5
|
18天前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
18 3
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
16 3
|
18天前
|
安全 数据管理 Linux
Linux 设备驱动程序(一)(中)
Linux 设备驱动程序(一)
17 2
|
18天前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
10 1
|
18天前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
13 1
|
18天前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
15 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1