Driving Me Nuts - 设备类
在上一篇“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
它对应于主设备号 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/)。希望这两篇文章能帮助您解开这个混乱的网络,展示内核中所有设备之间的真正互连性。
Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护人员。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。