ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 14.4. 总线, 设备, 和驱动 至今, 我们已经看到大量低级框架和一个相对少的例子. 我们试图在本章剩下部分中补充, 随着我们进入 Linux 设备模型的更高级. 为此, 我们介绍一个新的虚拟总线, 我们称为 lddbus, [[46](#)]并且修改 scullp 驱动来 "接入" 到这个总线. 再一次, 许多驱动作者将不会需要这里涉及的材料. 这个水平的细节通常在总线级别处理, 并且很少作者需要添加一个新总线类型. 这个信息是有用的, 但是, 对任何人好奇在 PCI, USB 等层面的里面发生了什么或者谁需要在那个级别做改变. ### 14.4.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); /* Some fields omitted */ }; ~~~ name 成员是总线的名子, 有些同 pci. 你可从这个结构中见到每个总线是它自己的子系统; 这个子系统不位于 sysfs 的顶层, 但是. 相反, 它们在总线子系统下面. 一个总线包含 2 个 ksets, 代表已知的总线的驱动和所有插入总线的设备. 所以, 有一套方法我们马上将涉及. #### 14.4.1.1. 总线注册 如同我们提过的, 例子源码包含一个虚拟总线实现称为 lddbus. 这个总线建立它的 bus_type 结构, 如下: ~~~ struct bus_type ldd_bus_type = { .name = "ldd", .match = ldd_match, .hotplug = ldd_hotplug, }; ~~~ 注意很少 bus_type 成员要求初始化; 大部分由设备模型核心处理. 但是, 我们确实不得不指定总线的名子, 以及任何伴随它的方法. 不可避免地, 一个新总线必须注册到系统, 通过一个对 bus_register 的调用. lddbus 代码这样做以这样的方式: ~~~ ret = bus_register(&ldd_bus_type); if (ret) return ret; ~~~ 这个调用可能失败, 当然, 因此返回值必须一直检查. 如果它成功, 新总线子系统已被添加到系统; 在 sysfs 中 /sys/bus 的下面可以见到, 并且可能启动添加设备. 如果有必要从系统中去除一个总线(当关联模块被去除, 例如), 调用调用 bus_unregister: ~~~ void bus_unregister(struct bus_type *bus); ~~~ #### 14.4.1.2. 总线方法 有几个给 bus_type 结构定义的方法; 它们允许总线代码作为一个设备核心和单独驱动之间的中介. 在 2.6.10 内核中定义的方法是: int (*match)(struct device *device, struct device_driver *driver); 这个方法被调用, 大概多次, 无论何时一个新设备或者驱动被添加给这个总线. 它应当返回一个非零值如果给定的设备可被给定的驱动处理. (我们马上进入设备和 device_driver 结构的细节). 这个函数必须在总线级别处理, 因为那是合适的逻辑存在的地方; 核心内核不能知道如何匹配每个可能总线类型的设备和驱动. int (*hotplug) (struct device *device, char **envp, int num_envp, char *buffer, int buffer_size); 这个模块允许总线添加变量到环境中, 在产生一个热插拔事件在用户空间之前. 参数和 kset 热插拔方法相同( 在前面的 "热插拔事件产生" 一节中描述 ). lddbus 驱动有一个非常简单的匹配函数, 它仅仅比较驱动和设备的名子: ~~~ static int ldd_match(struct device *dev, struct device_driver *driver) { return !strncmp(dev->bus_id, driver->name, strlen(driver->name)); } ~~~ 当涉及到真实硬件, match 函数常常在有设备自身提供的硬件 ID 和驱动提供的 ID 之间, 做一些比较. lddbus 热插拔方法看来象这样: ~~~ 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; } ~~~ 这里, 我们加入 lddbus 源码的当前版本号, 只是以防有人好奇. #### 14.4.1.3. 列举设备和驱动 如果你在编写总线级别的代码, 你可能不得不对所有已经注册到你的总线的设备或驱动进行一些操作. 它可能会诱惑人直接进入 bus_type 结构中的各种结构, 但是最好使用已经提供的帮助函数. 为操作每个对总线已知的设备, 使用: ~~~ int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *)); ~~~ 这个函数列举总线上的每个设备, 传递关联的设备结构给 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 *)); ~~~ 这个函数就像 buf_for_each_dev, 除了, 当然, 它替之作用于驱动. 应当注意, 这 2 个函数持有总线子系统的读者/写者旗标在工作期间. 因此试图一起使用这 2 个会死锁 -- 每个将试图获取同一个旗标. 修改总线的操作( 例如注销设备 )也将锁住. 因此, 小心使用 bus_for_each 函数. #### 14.4.1.4. 总线属性 几乎 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); }; ~~~ 我们已经见到 struct attribute 在 "缺省属性" 一节. bus_attribute 类型也包含 2 个方法来显示和设置属性值. 大部分在 kobject 之上的设备模型层以这种方式工作. 已经提供了一个方便的宏为在编译时间创建和初始化 bus_attribute 结构: ~~~ BUS_ATTR(name, mode, show, store); ~~~ 这个宏声明一个结构, 产生它的名子通过前缀字符串 bus_attr_ 到给定的名子. 任何属于一个总线的属性应当明确使用 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"); ~~~ 这个调用创建一个属性文件(/sys/busldd/version) 包含 lddbus 代码的版本号. ### 14.4.2. 设备 在最低层, Linux 系统中的每个设备由一个 struct device 代表: 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); /* Several fields omitted */}; 有许多其他的 struct device 成员只对设备核心代码感兴趣. 但是, 这些成员值得了解: struct device *parent 设备的 "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; 管理这个设备的驱动; 我们查看 struct device_driver 在下一节. void *driver_data; 一个可能被设备驱动使用的私有数据成员. void (*release)(struct device *dev); 当对这个设备的最后引用被去除时调用的方法; 它从被嵌入的 kobject 的 release 方法被调用. 注册到核心的所有的设备结构必须有一个 release 方法, 否则内核打印出慌乱的抱怨. 最少, parent, bus_id, bus, 和 release 成员必须在设备结构被注册前设置. #### 14.4.2.1. 设备注册 通常的注册和注销函数在: ~~~ int device_register(struct device *dev); void device_unregister(struct device *dev); ~~~ 我们已经见到 lddbus 代码如何注册它的总线类型. 但是, 一个实际的总线是一个设备并且必须单独注册. 为简单起见, 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. 我们有一个简单的, no-op release 方法, 并且, 作为第一个(并且唯一)总线, 它的名子时 ldd0. 这个总线设备被注册, 使用: ~~~ ret = device_register(&ldd_bus); if (ret) printk(KERN_NOTICE "Unable to register ldd0\n"); ~~~ 一旦调用完成, 新总线可在 sysfs 中 /sys/devices 下面见到. 任何加到这个总线的设备接着在 /sys/devices/ldd0 下显示. #### 14.4.2.2. 设备属性 sysfs 中的设备入口可有属性. 相关的结构是: ~~~ struct device_attribute { struct attribute attr; ssize_t (*show)(struct device *dev, char *buf); ssize_t (*store)(struct device *dev, const char *buf, size_t count); }; ~~~ 这些属性结构可在编译时建立, 使用这些宏: ~~~ DEVICE_ATTR(name, mode, show, store); ~~~ 结果结构通过前缀 dev_attr_ 到给定名子上来命名. 属性文件的实际管理使用通常的函数对来处理: ~~~ int device_create_file(struct device *device, struct device_attribute *entry); void device_remove_file(struct device *dev, struct device_attribute *attr); ~~~ struct bus_type 的 dev_attrs 成员指向一个缺省的属性列表, 这些属性给添加到总线的每个设备创建. #### 14.4.2.3. 设备结构嵌入 设备结构包含设备模型核心需要的来模型化系统的信息. 大部分子系统, 但是, 跟踪关于它们驻留的设备的额外信息. 结果, 对设备很少由空设备结构所代表; 相反, 这个结构, 如同 kobject 结构, 常常是嵌入一个更高级的设备表示中. 如果你查看 struct pci_dev 的定义或者 struct usb_device 的定义, 你会发现一个 struct device 埋在其中. 常常地, 低层驱动甚至不知道 struct device, 但是有例外. lddbus 驱动创建它自己的设备类型( struct 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); ~~~ 这个结构允许驱动提供一个实际的名子给设备( 这可以清楚地不同于它的总线 ID, 存储于设备结构) 以及一个这些驱动信息的指针. 给真实设备的结构常常还包含关于供应者信息, 设备型号, 设备配置, 使用的资源, 等等. 可以在 struct pci_dev (<linux/pci.h>) 或者 struct usb_device (<linux/usb.h>) 中找到好的例子. 一个方便的宏( to_ldd_device ) 也为 struct ldd_device 定义, 使得容易转换指向被嵌入的结构的指针为 ldd_device 指针. lddbus 输出的注册接口看来如此: ~~~ 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); ~~~ 这里, 我们简单地填充一些嵌入的设备结构成员( 单个驱动不应当需要知道这个 ), 并且注册这个设备到驱动核心. 如果我们想添加总线特定的属性到设备, 我们可在这里做. 为显示这个接口如何使用, 我们介绍另一个例子驱动, 我们称为 sculld. 它是在第 8 章介绍的 scullp 驱动上的另一个变体. 它实现通用的内存区设备, 但是 sculld 也使用 Linux 设备模型, 通过 lddbus 接口. 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 成员来存储指向我们自己的内部的设备结构的指针. ### 14.4.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 结构的函数的形式, 现在应当看来是类似的(因此我们快速涵盖它们). 注册函数是: ~~~ 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 ) 指向一套缺省属性, 对所有关联到这个总线的驱动都创建. #### 14.4.3.1. 驱动结构嵌入 如同大部分驱动核心结构的情形, 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 输出这个版本字串为它知道的每个驱动. 总线特定的驱动注册函数是: ~~~ 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 结构必须手工填充. 注意我们设定属性的拥有者为驱动模块, 不是 lddbus 模块; 这样做的理由是可以在为这个属性的 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); } ~~~ 有人可能认为属性拥有者应当是 lddbus 模块, 因为实现这个属性的函数在那里定义. 这个函数, 但是, 是使用驱动自身所创建的 ldd_driver 结构. 如果那个结构在一个用户空间进程试图读取版本号时要消失, 事情会变得麻烦. 指定驱动模块作为属性的拥有者阻止了模块被卸载, 在用户空间保持属性文件打开时. 因为每个驱动模块创建一个对 lddbus 模块的引用, 我们能确信 lddbus 不会在一个不合适的时间被卸载. 为完整起见, sculld 创建它的 ldd_driver 结构如下: ~~~ static struct ldd_driver sculld_driver = { .version = "$Revision: 1.1 $", .module = THIS_MODULE, .driver = { .name = "sculld", }, }; ~~~ 一个简单的对 register_ldd_driver 的调用添加它到系统中. 一旦完成初始化, 驱动信息可在 sysfs 中见到: ~~~ $ tree /sys/bus/ldd/drivers /sys/bus/ldd/drivers `-- sculld |-- sculld0 -> ../../../../devices/ldd0/sculld0 |-- sculld1 -> ../../../../devices/ldd0/sculld1 |-- sculld2 -> ../../../../devices/ldd0/sculld2 |-- sculld3 -> ../../../../devices/ldd0/sculld3 `-- version ~~~ [[46](#)] 这个总线的逻辑名子, 当然, 应当是"sbus", 但是这个名子已经被一个真实的, 物理总线采用.