Driving Me Nuts - 设备类

作者:Greg Kroah-Hartman

在上一篇“Driving Me Nuts”专栏文章[参见 LJ,2003 年 6 月]中,我们介绍了内核驱动模型框架,并解释了通用总线以及驱动程序和设备代码的工作原理。我们以 i2c 核心为例,展示了这些不同的子系统是如何工作的。本月,我们将介绍驱动程序类代码的工作原理,同样使用 i2c 代码来提供一些实际示例。

正如上一篇专栏文章中所讨论的,设备类不符合面向对象类的一般定义;相反,它们为用户提供单一类型的功能。例如,内核类用于 tty 设备、块设备、网络设备、SCSI 主机,以及在不久的将来用于文件系统。

在 2.5.69 内核中,驱动程序类支持进行了彻底的重写。在以前的内核版本中,类支持与驱动程序和设备支持紧密相连。类会在注册到驱动程序的同时绑定到设备。这对于许多设备和类来说是有效的,但一些现实世界的设备不太符合这种模型。现在,类支持仅与设备和驱动程序松散地联系在一起;事实上,现在甚至不需要设备或驱动程序就可以使用类代码,正如 tty 类代码所展示的那样。类代码现在被分为三种不同的结构类型:类、类设备和类接口。

内核中的类使用简单的 struct class 结构定义。是的,class 在 C 语言中不是保留字。(所有想用 C++ 编译器构建内核的人,都去批评新类代码的作者吧。)要创建类结构,只需要在 struct class 结构中定义 name 变量,使其成为有效的类。可以使用以下代码完成此操作

static struct class i2c_adapter_class = {
	.name = "i2c_adapter"
};

定义类结构后,可以通过调用 class_register 函数将其注册到驱动程序核心

if (class_register(&i2c_adapter_class) != 0)
    printk(KERN_ERR "i2c adapter class failed "
                    "to register properly\n");

在 class_register 函数返回且未报告错误后,/sys/class/i2c_adapter 目录已成功创建。稍后,当需要卸载类时,应调用 class_unregister 函数

class_unregister(&i2c_adapter_class);

类设备

类用于管理一组不同的类设备。类设备在内核中使用 struct class_device 结构定义。此结构包含驱动程序核心使用的许多变量,驱动程序编写者可以忽略它。只需设置以下变量

  • class:应指向将要管理类设备的 struct class。

  • dev:应设置为与类设备关联的 struct device 的地址(如果有)。单个 struct device 可以被多个类设备结构指向。这是以前的内核类支持与当前实现之间的主要区别。此变量不必设置内核也能正常工作。如果设置了此变量,则会在类设备的 sysfs 条目中创建一个指向 struct device 的设备符号链接。请参见下面的示例。

  • class_id:用于描述类设备的字符数组。它在分配给单个类结构的所有类设备结构中必须是唯一的。

  • class_data:用于存储指向类驱动程序想要与类设备关联的任何私有数据的指针。不应直接访问此变量,而应使用 class_set_devdata 和 class_get_devdata 函数来设置和检索此变量的值。

要注册正确设置的 struct class_device 结构,应调用 class_device_register 函数。以下代码来自 drivers/i2c/i2c-core.c 文件,其中可以看到如何初始化 struct class_device 并将其注册到驱动程序核心的示例

/* Add this adapter to the i2c_adapter class */
memset(&adap->class_dev, 0x00,
       sizeof(struct class_device));
adap->class_dev.dev = &adap->dev;
adap->class_dev.class = &i2c_adapter_class;
strncpy(adap->class_dev.class_id,
        adap->dev.bus_id, BUS_ID_SIZE);
class_device_register(&adap->class_dev);

首先,struct class_device 变量(嵌入在 struct i2c_adapter 变量中)被初始化为零。所有驱动模型结构都需要在注册之前将所有变量设置为零,以便驱动程序核心能够正确使用它们。

然后,dev 变量被设置为指向 i2c_adapter 的 struct device 变量;在本例中,相同的结构 struct i2c_adapter 同时包含 struct device 和 struct class_device。class 变量被设置为 i2c_adapter_class 变量的地址,然后 class_id 变量被设置为与设备的 bus_id 相同的值。由于 i2c_adapter 设备的 bus_id 是唯一的,因此也确保了 i2c_adapter 类设备的 class_id 是唯一的。最后,通过调用 class_device_register 函数,将类设备结构注册到内核驱动程序核心。

使用上述代码以及在测试机器上加载的两个 i2c 适配器,/sys/class/i2c_adapter 树可能如下所示

$ tree /sys/class/i2c-adapter/
/sys/class/i2c-adapter/
|-- i2c-0
|   |-- device -> ../../../devices/pci0/00:07.3/i2c-0
|   `-- driver -> ../../../bus/i2c/drivers/i2c_adapter
`-- i2c-2
    |-- device -> ../../../devices/legacy/i2c-2
    `-- driver -> ../../../bus/i2c/drivers/i2c_adapter

如上面的树输出所示,驱动程序核心会自动创建设备和驱动程序符号链接,以指向 sysfs 树中表示这些值的正确位置。如果未将 dev 指针设置为指向 struct device,则不会创建这些符号链接。如果您查看 /sys/class/tty 目录,大多数类设备条目都没有相应的 struct device,因此这些符号链接不存在。

类接口

类接口只是您代码的一种方式,可以在从特定类注册或取消注册 struct class_device 时收到通知。类接口使用 struct class_interface 结构定义。此结构很简单,如下所示

struct class_interface {
    struct list_head  node;
    struct class      *class;
    int (*add)        (struct class_device *);
    void (*remove)    (struct class_device *);
};

class 变量需要设置为我们想要接收通知的类。add 和 remove 变量应设置为在从该类添加或删除任何设备时调用的函数。如果您不想收到其中一个事件的通知,则不必同时设置 add 和 remove 变量。

要向内核注册类接口,请调用 class_interface_register 函数。同样,要取消注册类接口,请调用 class_interface_unregister 函数。内核源代码树中的 kernel/cpufreq.c 文件中可以找到使用类接口的代码示例,例如 CPU 频率核心。

创建文件

如上所述,i2c-adapter 类可用于轻松确定系统中存在的所有不同 i2c 适配器及其在驱动程序树中的特定位置。但是 i2c 适配器不能被用户直接寻址。要与 i2c 适配器通信,需要加载 i2c 芯片驱动程序,或者可以使用 i2c-dev 驱动程序。i2c-dev 驱动程序为系统中存在的所有 i2c 适配器提供字符驱动程序接口。由于确定哪些 i2c-dev 设备连接到哪些 i2c 适配器非常有用,因此创建了 i2c-dev 类

static struct class i2c_dev_class = {
    .name	= "i2c-dev"
};

然后,当 i2c-dev 驱动程序找到每个 i2c 适配器时,会将新的 i2c 类设备添加到驱动程序核心。此添加在 i2c_add_class_device 函数中完成

static void
i2c_add_class_device(char *name, int minor,
                     struct i2c_adapter *adap)
{
   struct i2c_dev *i2c_dev;
   int retval;

   i2c_dev = kmalloc(sizeof(*i2c_dev), GFP_KERNEL);
   if (!i2c_dev)
       return;
   memset(i2c_dev, 0x00, sizeof(*i2c_dev));

   if (adap->dev.parent == &legacy_bus)
       i2c_dev->class_dev.dev = &adap->dev;
   else
       i2c_dev->class_dev.dev = adap->dev.parent;
   i2c_dev->class_dev.class = &i2c_dev_class;
   snprintf(i2c_dev->class_dev.class_id,
            BUS_ID_SIZE, "%s", name);
   retval =
       class_device_register(&i2c_dev->class_dev);
   if (retval)
       goto error;
   class_device_create_file (&i2c_dev->class_dev,
                             &class_device_attr_dev);
   i2c_dev->minor = minor;
   spin_lock(&i2c_dev_list_lock);
   list_add(&i2c_dev->node, &i2c_dev_list);
   spin_unlock(&i2c_dev_list_lock);
   return;
error:
   kfree(i2c_dev);
}

此函数看起来几乎与 i2c_adapter 类注册代码相同,但有两个例外。首先,class_dev.dev 字段设置为适配器的父设备或适配器的设备。这样做是因为某些 i2c 适配器在全球内核设备树中没有真正的父设备,因为它们位于尚未转换为内核驱动模型(如 ISA)的总线上,或者它们根本不位于总线上(如某些 i2c 嵌入式控制器)。当 i2c 适配器在内核设备树中没有位置时,它将被分配给传统总线。传统总线位于 /sys/devices/legacy,用于这些类型的设备。

与此类设备不同的第二件事是行

class_device_create_file (&i2c_dev->class_dev, &class_device_attr_dev);

class_device_create_file 函数用于在类设备的目录中创建文件。文件名和属性使用 CLASS_DEVICE_ATTR 宏定义,如下所示

static ssize_t
show_dev(struct class_device *class_dev, char *buf)
{
   struct i2c_dev *i2c_dev = to_i2c_dev(class_dev);
   return sprintf(buf, "%04x\n",
                  MKDEV(I2C_MAJOR, i2c_dev->minor));
}
static
CLASS_DEVICE_ATTR(dev, S_IRUGO, show_dev, NULL);

CLASS_DEVICE_ATTR 宏本身定义为

#define CLASS_DEVICE_ATTR(_name,_mode,_show,_store) \
struct class_device_attribute                       \
class_device_attr_##_name = { 	                    \
    .attr  = {.name = __stringify(_name),           \
              .mode = _mode },                      \
    .show  = _show,                                 \
    .store = _store,                                \
};

CLASS_DEVICE_ATTR 宏中的参数为

  • _name:sysfs 中要创建的文件的名称,以及描述整个属性的变量名称的一部分。

  • _mode:创建文件时使用的文件访问模式。使用标准访问宏来指定正确的值。

  • _show:指向在读取文件时调用的函数。此函数必须具有以下返回值和参数。如果文件不可读取,则不必设置此变量。

    ssize_t
    show (struct class_device *class_dev, char *buf);
    
  • _store:指向在写入文件时调用的函数。此函数必须具有以下返回值和参数。如果文件不可写入,则不必设置此变量。

    ssize_t
    store (struct device *dev, const char *buf,
           size_t count);
    

几乎所有驱动模型结构都有一个 ATTR() 宏,用于在 sysfs 树中声明文件。

在此示例中,当调用 class_device_create_file 函数时,会创建一个名为 dev 的文件。创建此文件是为了让任何用户都只读。如果从文件中读取,驱动程序核心将调用 show_dev 函数。show_dev 函数将要提供给用户的信息填充到传递给它的缓冲区中。在本例中,特定设备的主设备号和次设备号传递给用户。所有使用主设备号和次设备号的类设备都应在其 sysfs 类设备目录中包含一个 dev 文件。

class_device_remove_file 函数可用于删除 class_device_create_file 函数创建的任何文件。但是,如果要删除设备,则不必手动删除任何创建的文件。当设备从 sysfs 中删除时,sysfs 核心会自动删除在其目录中创建的所有文件。因此,当 i2c-dev 类设备从系统中删除时,只需要执行以下操作

static void
i2c_remove_class_device(int minor)
{
    struct i2c_dev *i2c_dev = NULL;
    struct list_head *tmp;
    int found = 0;

    spin_lock(&i2c_dev_list_lock);
    list_for_each (tmp, &i2c_dev_list) {
        i2c_dev = list_entry(tmp, struct i2c_dev,
                             node);
        if (i2c_dev->minor == minor) {
            found = 1;
            break;
        }
    }
    if (found) {
        list_del(&i2c_dev->node);
        spin_unlock(&i2c_dev_list_lock);
        class_device_unregister(&i2c_dev->class_dev);
        kfree(i2c_dev);
    } else {
    spin_unlock(&i2c_dev_list_lock);
    }
}

外观

加载 i2c-dev 驱动程序和两个 i2c 适配器驱动程序(i2c-piix4 和 i2c-isa 驱动程序)后,/sys/class/i2c-dev 目录可能如下所示

$ tree /sys/class/i2c-dev/
/sys/class/i2c-dev/
|-- i2c-0
|   |-- dev
|   |-- device -> ../../../devices/pci0/00:07.3
|   `-- driver -> ../../../bus/pci/drivers/piix4-smbus
`-- i2c-2
    |-- dev
    |-- device -> ../../../devices/legacy/i2c-2
    `-- driver -> ../../../bus/i2c/drivers/i2c_adapter

/sys/class/i2c-dev/i2c-2/ 目录中的 dev 文件将包含以下字符串

$ cat /sys/class/i2c-dev/i2c-2/dev
5902

它对应于主设备号 86 和次设备号 2,这是此特定设备的字符主设备号和次设备号。

此外,加载了一些 i2c 客户端驱动程序的 /sys/bus/i2c/ 目录如下所示

$ tree /sys/bus/i2c/
/sys/bus/i2c/
|-- devices
|   |-- 0-0050 -> ../../../devices/pci0/00:07.3/i2c-0/0-0050
|   |-- 0-0051 -> ../../../devices/pci0/00:07.3/i2c-0/0-0051
|   |-- 0-0052 -> ../../../devices/pci0/00:07.3/i2c-0/0-0052
|   |-- 0-0053 -> ../../../devices/pci0/00:07.3/i2c-0/0-0053
|   `-- 2-0290 -> ../../../devices/legacy/i2c-2/2-0290
`-- drivers
    |-- dev driver
    |-- eeprom
    |   |-- 0-0050 -> ../../../../devices/pci0/00:07.3/i2c-0/0-0050
    |   |-- 0-0051 -> ../../../../devices/pci0/00:07.3/i2c-0/0-0051
    |   |-- 0-0052 -> ../../../../devices/pci0/00:07.3/i2c-0/0-0052
    |   `-- 0-0053 -> ../../../../devices/pci0/00:07.3/i2c-0/0-0053
    |-- i2c_adapter
    `-- w83781d
        `-- 2-0290 -> ../../../../devices/legacy/i2c-2/2-0290

并且,i2c 适配器的实际 /sys/devices/ 目录如下所示

$ tree /sys/devices/pci0/00:07.3
/sys/devices/pci0/00:07.3
|-- class
|-- device
|-- i2c-0
|   |-- 0-0050
|   |   |-- eeprom_00
|   |   |-- name
|   |   `-- power
|   |-- 0-0051
|   |   |-- eeprom_00
|   |   |-- name
|   |   `-- power
|   |-- 0-0052
|   |   |-- eeprom_00
|   |   |-- name
|   |   `-- power
|   |-- 0-0053
|   |   |-- eeprom_00
|   |   |-- name
|   |   `-- power
|   |-- name
|   `-- power
|-- irq
|-- name
|-- power
|-- resource
|-- subsystem_device
|-- subsystem_vendor
`-- vendor

$ tree /sys/devices/legacy/i2c-2/
/sys/devices/legacy/i2c-2/
|-- 2-0290
|   |-- alarms
|   |-- beep_enable
|   |-- beep_mask
|   |-- fan_div1
|   |-- fan_div2
|   |-- fan_div3
|   |-- fan_input1
|   |-- fan_input2
|   |-- fan_input3
|   |-- fan_min1
|   |-- fan_min2
|   |-- fan_min3
|   |-- in_input0
|   |-- in_input1
|   |-- in_input2
|   |-- in_input3
|   |-- in_input4
|   |-- in_input5
|   |-- in_input6
|   |-- in_input7
|   |-- in_input8
|   |-- in_max0
|   |-- in_max1
|   |-- in_max2
|   |-- in_max3
|   |-- in_max4
|   |-- in_max5
|   |-- in_max6
|   |-- in_max7
|   |-- in_max8
|   |-- in_min0
|   |-- in_min1
|   |-- in_min2
|   |-- in_min3
|   |-- in_min4
|   |-- in_min5
|   |-- in_min6
|   |-- in_min7
|   |-- in_min8
|   |-- name
|   |-- power
|   |-- pwm1
|   |-- pwm2
|   |-- pwm_enable2
|   |-- sensor1
|   |-- sensor2
|   |-- sensor3
|   |-- temp_input1
|   |-- temp_input2
|   |-- temp_input3
|   |-- temp_max1
|   |-- temp_max2
|   |-- temp_max3
|   |-- temp_min1
|   |-- temp_min2
|   |-- temp_min3
|   |-- vid
|   `-- vrm
|-- name
`-- power

我认为 Jonathan Corbet 对内核驱动模型使用互连的结构指针和用户表示的最佳描述是:“像蜘蛛在吸毒后编织的网络”(lwn.net/Articles/31185/)。希望这两篇文章能帮助您解开这个混乱的网络,展示内核中所有设备之间的真正互连性。

致谢

我要感谢 Pat Mochel 创建了如此强大而完整的框架,所有内核驱动程序和设备都可以轻松地向用户展示。还要非常感谢所有内核驱动子系统维护人员,他们欣然将其子系统转换为此模型;没有他们的帮助,驱动程序核心代码充其量只是一个不错的学术练习。

Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护人员。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。

加载 Disqus 评论