在 Linux内核输入子系统框架_Bin Watson的博客-优快云博客 这篇文章中,我们详细分析了输入子系统。了解到了 dev 和 handler 分层的思想。而在 jz2440_输入子系统驱动程序_Bin Watson的博客-优快云博客 这篇文章中,我们实际操作编写了一个基于输入子系统的按键驱动程序。
而这种分层思想,在内核中是非常常见的。我们也可以实现我们自己的分层驱动程序,这种驱动模型称为 bus-drv-dev 模式,总线驱动设备模型。
总线驱动程序与具体设备的驱动程序相比,总线驱动程序与核心内核代码的工作要密切得多。另外,总线驱动程序向相关的设备驱动程序提供功能和选项的方式,也不存在标准的接口。这是因为,不同的总线系统之间,使用的硬件技术可能差异很大。但这并不意味着,负责管理不同总线的代码没有共同点。相似的总线采用相似的概念,还引入了通用驱动程序模型,在一个主要数据结构的集合中管理所有系统总线,采用最小公分母的方式,尽可能降低不同总线驱动程序之间的差异。
内核支持大量总线,可能涉及多种硬件平台,也有可能只涉及一种平台。所以我不可能详细地讨论所有版本,这里我们只会仔细讨论 PCI 总线。因为其设计相对现代,而且具备一种强大的系统总线所应有的所有共同和关键要素。此外,在Linux支持的大多数体系结构上都使用了 PCI 总线。我还会讨论广泛使用、系统无关的的 USB 总线,该总线用于外设。
1. 总线驱动设备框架
总线设备驱动模式由三个部分组成:
- 虚拟总线 bus;
- 设备 device;
- 驱动 driver;
bus:总线是处理器和设备之间的通道。总线有多种类型,每种总线可以挂载多个设备。
在设备模型中,所有的设备都通过总线相连,以总线来管理设备和驱动函数。总线在内核中由 bus_type 结构体表示。
device:设备就是连接在总线上的物理实体。设备是有功能之分的。具有相同功能的设备被归到一个类,如输入设备(鼠标,键盘,游戏杆等)。Linux 系统中每个设备都用一个 device 结构体的表示。
当添加一个新的 device 时,会执行下列操作:
- 把 device 放入 bus 的 dev 链表上;
- 从 bus 的 drv 链表取出每个 driver,用 bus 的 .match 函数判断 drv 能否支持 dev;
- 若可以支持,则调用 drv 的 .probe 函数。
driver:驱动程序是在 CPU 运行时,提供操作的软件接口。所有的设备必须有与之配套驱动程序才能正常工作。一个驱动程序可以驱动多个类似或者完全不同的设备。
当添加一个新的 driver 时,会执行下列操作:
- 把 driver 放入 bus 的 drv 链表上;
- 从 bus 的 dev 链表取出每个 device,用 bus 的 .match 函数判断 drv 能否支持 dev;
- 若可以支持,则调用 drv 的 .probe 函数。
总线 bus 上有两条链表,分别挂载当前总线上所有的 device 和所有的 driver。当注册(添加 device_register/driver_register)一个新的 device/driver 到总线上时,会触发总线上的 .match 函数被调用,进行该 device/driver 去和另一个链表 driver/device 上的设备进行匹配,若匹配成功就会调用 driver 中的 .probe 函数。
注意:.probe 函数是提供一种将 device 和 driver 连接起来的机制。而具体 .probe 函数如何实现,是否要记录 device 的信息?这些操作都由实现者去决定,并不是强制的。
2. 通用总线模型
2.1 数据结构定义
device
<device.h>
struct device {
struct klist klist_children;
struct klist_node knode_parent; /* 兄弟结点链表中的结点 */
struct klist_node knode_driver;
struct klist_node knode_bus;
struct device * parent;
struct kobject kobj;
char bus_id[BUS_ID_SIZE]; /* 在父总线上的位置 */
/*...省略...*/
struct bus_type * bus; /* 所在总线设备的类型 */
struct device_driver *driver; /* 分配当前device实例的驱动程序 */
void *driver_data; /* 驱动程序的私有数据 */
void *platform_data; /* 特定于平台的数据,设备模型代码不会访问 */
/*...省略...*/
void (*release)(struct device * dev);
};
-
klist 和 klist_node 数据结构是我们熟悉的 list_head 数据结构的增强版,其中增加了与锁和引用计数相关的成员。
klist 是一个表头,而 klist_node 是一个链表元素。这种类型的链表只用于通用设备模型,内核的其余部分不会使用。
-
嵌入的 kobject 控制通用对象属性。
-
有一些成员用于建立设备之间的层次关系。
klist_children 是一个链表的表头,该链表包含了指定设备的所有子设备。如果设备包含于父设备的 klist_children 链表中,则将 knode_parent 用作链表元素。
parent 指向父结点的 device 实例。
因为一个设备驱动程序能够服务多个设备(例如,系统中安装了两个相同的扩展卡),
-
knode_driver 用作链表元素,用于将所有被同一驱动程序管理的设备连接到一个链表中。
-
driver 指向控制该设备的设备驱动程序的数据结构(下面的成员包括更多相关信息)。
-
bus_id 唯一指定了该设备在宿主总线上的位置(不同总线类型使用的格式也会有所不同)。例如,设备在 PCI 总线上的位置由一个具有以下格式的字符串唯一地定义:<总线编号>:<插槽编号>.<功能编号>。
-
bus 是一个指针,指向该设备所在总线(更多信息见下文)的数据结构的实例。
-
driver_data 是驱动程序的私有成员,不能由通用代码修改。它可用于指向与设备协作必需、但又无法融入到通用方案的特定数据。platform_data 和 firmware_data 也是私有成员,可用于将特定于体系结构的数据和固件信息关联到设备。通用驱动程序模型也不会访问这些数据。
-
release 是一个析构函数,用于在设备(或device实例)不再使用时,将分配的资源释放回内核。
内核提供了标准函数 device_register,用于将一个新设备添加到内核的数据结构。该函数在下文讨论。device_get 和 device_put 一对函数用于引用计数。
driver
<driver.h>
struct device_driver {
const char * name;
struct bus_type * bus;
struct kobject kobj;
struct klist klist_devices;
struct klist_node knode_bus;
/*...省略...*/
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);
};
各个成员的语义如下。
-
name 指向一个正文串,用于唯一标识该驱动程序。
-
bus 指向一个表示总线的对象,并提供特定于总线的操作(更详细的内容,请参见下文)。
-
klist_devices 是一个标准链表的表头,其中包括了该驱动程序控制的所有设备的 device 实例。链表中的各个设备通过 device->knode_driver 彼此连接。
-
knode_bus 用于连接一条公共总线上的所有设备。
-
probe 是一个函数,用于检测系统中是否存在能够用该设备驱动程序处理的设备。
-
删除系统中的设备时会调用 remove。
-
shutdown、suspend 和 resume 用于电源管理。
驱动程序使用内核的标准函数 driver_register 注册到系统中,该函数在下文讨论。
通用驱动程序模型不仅表示了设备,还用另一个数据结构表示了总线,定义如下:
bus
<device.h>
struct bus_type {
const char * name;
/*...省略...*/
struct kset subsys;
struct kset drivers;
struct kset devices;
struct klist klist_devices;
struct klist klist_drivers;
/*...省略...*/
int (*match)(struct device * dev, struct device_driver * drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
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);
/*...省略...*/
};
-
name 是总线的文本名称。特别地,它用于在 sysfs 文件系统中标识该总线。
-
与总线关联的所有设备和驱动程序,使用 drivers 和 devices 成员,作为集合进行管理。
-
内核创建两个链表(klist_devices 和 klist_drivers )来保存相同的数据。这些链表使内核能够快速扫描所有资源(设备和驱动程序)。
-
subsys 提供与总线子系统的关联。对应的总线出现在 /sys/bus/busname。
-
match 指向一个函数,试图查找与给定设备匹配的驱动程序。
-
add 用于通知总线新设备已经添加到系统。
-
在有必要将驱动程序关联到设备时,会调用 probe。该函数检测设备在系统中是否真正存在。
-
remove 删除驱动程序和设备之间的关联。例如,在将可热插拔的设备从系统中移除时,会调用该函数。
-
shutdown、suspend 和 resume 函数用于电源管理。
2.2 注册
注册总线
在可以注册设备及其驱动程序之前,需要有总线。因此我们从 bus_register 开始,该函数向系统添加一个新总线。
首先,通过嵌入的 kset 类型成员 subsys,将新总线添加到总线子系统:
drivers/base/bus.c
int bus_register(struct bus_type * bus)
{
int retval;
retval = kobject_set_name(&bus->subsys.kobj, "%s", bus->name);
bus->subsys.kobj.kset = &bus_subsys;
retval = subsystem_register(&bus->subsys);
/*...省略...*/
};
总线需要了解相关设备及其驱动程序的所有有关信息,因此总线对二者注册了 kset。
两个 kset 分别是 drivers 和 devices,都将总线作为父结点:
drivers/base/bus.c
int bus_register(struct bus_type * bus)
{
/*...省略...*/
kobject_set_name(&bus->devices.kobj, "devices");
bus->devices.kobj.parent = &bus->subsys.kobj;
retval = kset_register(&bus->devices);
kobject_set_name(&bus->drivers.kobj, "drivers");
bus->drivers.kobj.parent = &bus->subsys.kobj;
bus->drivers.ktype = &driver_ktype;
retval = kset_register(&bus->drivers);
/*...省略...*/
}
注册设备
注册设备包括两个独立的步骤,如图6-22所示。具体是:初始化设备的数据结构,并将其加入到数据结构的网络中。
-
device_initialize主 要通过 kobj_set_kset_s(dev, devices_subsys) 将新设备添加到设备子系统。
-
device_add 首先,将通过 device->parent 指定的父子关系转变为一般的内核对象层次结构:
int device_add(struct device *dev) { struct device *parent = NULL; /*...省略...*/ parent = get_device(dev->parent); kobj_parent = get_device_parent(dev, parent); dev->kobj.parent = kobj_parent; /*...省略...*/ }
在设备子系统中注册该设备只需要调用一次 kobject_add,因为在 device_initialize 中已经将该设备设置为子系统的成员了。
int device_add(struct device *dev) { /*...省略...*/ kobject_set_name(&dev->kobj, "%s", dev->bus_id); error = kobject_add(&dev->kobj); /*...省略...*/ }
然后调用 bus_add_device 在 sysfs 中添加两个链接:一个在总线目录下指向设备,另一个在设备的目录下指向总线子系统。 bus_attach_device 试图自动探测设备。如果能够找到适当的驱动程序,则将设备添加到 bus->klist_devices。设备还需要添加到父结点的子结点链表中(此前,设备知道其父结点,但父结点不知道该子结点的存在)。
int device_add(struct device *dev) { /*...省略...*/ error = bus_add_device(dev); bus_attach_device(dev); if (parent) klist_add_tail(&dev->knode_parent, &parent->klist_children); /*...省略...*/ }
注册设备驱动程序
在进行一些检查和初始化工作之后,driver_register 调用 bus_add_driver 将一个新驱动程序添加到一个总线。同样,驱动程序首先要有名字,然后注册到通用数据结构的框架中:
drivers/base/bus.c
int bus_add_driver(struct device_driver *drv)
{
struct bus_type * bus = bus_get(drv->bus);
int error = 0;
/*...省略...*/
error = kobject_set_name(&drv->kobj, "%s", drv->name);
drv->kobj.kset = &bus->drivers;
error = kobject_register(&drv->kobj);
/*...省略...*/
}
如果总线支持自动探测,则调用 driver_attach。该函数迭代总线上的所有设备,使用驱动程序的 match 函数进行检测,确定是否有某些设备可使用该驱动程序管理。
最后,将该驱动程序添加到总线上注册的所有驱动程序的链表中。
drivers/base/bus.
int bus_add_driver(struct device_driver *drv)
{
/*...省略...*/
if (drv->bus->drivers_autoprobe)
/* 该函数迭代总线上的所有设备,
* 使用驱动程序的 match 函数进行检测,
* 确定是否有某些设备可使用该驱动程序管理
*/
error = driver_attach(drv);
/*...省略...*/
/* 将驱动程序添加到总线上注册的所有驱动程序的链表中 */
klist_add_tail(&drv->knode_bus, &bus->klist_drivers);
/*...省略...*/
}
3. 平台设备
在 Linux 内核中有多种设备类型,如:PCI、USB 等,其中一种为平台设备。
平台设备的总线是 platform_bus_type,是基于通用总线 bus_type 实现的一种内核中已经定义好的了的总线。
3.1 平台设备结构
总线:
platform_bus_type 定义如下:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_attrs = platform_dev_attrs,
.match = platform_match,
.uevent = platform_uevent,
.suspend = platform_suspend,
.suspend_late = platform_suspend_late,
.resume_early = platform_resume_early,
.resume = platform_resume,
};
设备:平台设备的数据结构是 platform_device,对 device 进行了扩展:
struct platform_device {
const char * name;
u32 id;
struct device dev;
u32 num_resources; /* resource这数组的长度 */
struct resource * resource; /* 资源数组,用于记录该设备所具有的硬件资源 */
};
-
resource:用于记录设备的资源
struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; struct resource *parent, *sibling, *child; };
一个独立的挂接在 CPU 总线上的设备单元,一般都需要一段线性的地址空间来描述设备自身。
Linux 采用 struct resource 结构体来描述一个挂接在 CPU 总线上的设备实体(32位 CPU 的总线地址范围是 0~4G):- start:描述设备实体在 CPU 总线上的线性起始物理地址;
- end:描述设备实体在 CPU 总线上的线性结尾物理地址;
- name:描述这个设备(资源)实体的名称。这个名字开发人员可以随意起,但最好贴切;
- flag:描述这个设备(资源)实体的一些共性和特性的标志位;
驱动:平台设备的数据结构是 platform_driver,对 device_driver 进行了扩展:
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*suspend_late)(struct platform_device *, pm_message_t state);
int (*resume_early)(struct platform_device *);
int (*resume)(struct platform_device *);
struct device_driver driver;
};
3.2 驱动注册函数
platform_driver_register 函数分析
我们以 gpio_keys.c 为例:
在其 init 函数中,通过 platform_driver_register 函数注册一个平台设备的驱动:
static int __init gpio_keys_init(void)
{
return platform_driver_register(&gpio_keys_device_driver);
}
-
gpio_keys_device_driver 的实现如下:
struct platform_driver gpio_keys_device_driver = { .probe = gpio_keys_probe, .remove = __devexit_p(gpio_keys_remove), .driver = { .name = "gpio-keys", } };
platform_driver_register 的定义如下:
int platform_driver_register(struct platform_driver *drv)
{
drv->driver.bus = &platform_bus_type; /* 将drv总线类型设置为platform_bus_type */
/* 设置默认的操作函数 */
if (drv->probe)
drv->driver.probe = platform_drv_probe;
if (drv->remove)
drv->driver.remove = platform_drv_remove;
if (drv->shutdown)
drv->driver.shutdown = platform_drv_shutdown;
if (drv->suspend)
drv->driver.suspend = platform_drv_suspend;
if (drv->resume)
drv->driver.resume = platform_drv_resume;
return driver_register(&drv->driver); /* 注册驱动到总线 */
}
driver_register 的执行流程分析
driver_register 的执行流程如下:
driver_register(&drv->driver);
|-> error = driver_attach(drv);
|-> bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
| while ((dev = next_device(&i)) && !error)
| error = __driver_attach(dev, data);
|-> __driver_attach(dev, data);
| if (!dev->driver)
| driver_probe_device(drv, dev);
|-> driver_probe_device(drv, dev);
| if (drv->bus->match && !drv->bus->match(dev, drv))
| goto done;
| ret = really_probe(dev, drv);
|-> really_probe(dev, drv);
| if (dev->bus->probe) {
| ret = dev->bus->probe(dev);
| if (ret)
| goto probe_failed;
| } else if (drv->probe) {
| ret = drv->probe(dev);
| if (ret)
| goto probe_failed;
| }
-
首先
driver_register
会调用driver_attach
函数,后者会遍历 bus 上的每一个 dev,对每一个 dev 都执行__driver_attach
; -
__driver_attach
会调用driver_probe_device
函数。后者首先会使用 bus 的 match 函数,进行 driver 和 device 的匹配,若 bus 没有实现 match 函数,则会使用 driver 的 match 函数进行匹配。
-
match 匹配成功后会调用
really_probe
函数。该函数最终会调用 bus 的 probe 函数,如果 bus 没有实现 probe 函数,则会调用 driver 的 probe 函数。
而前面我们知道 platform_bus_type 中是定义了 .match = platform_match
函数的。
platform_match
platform_match 的函数实现如下:
static int platform_match(struct device * dev, struct device_driver * drv)
{
struct platform_device *pdev = container_of(dev, struct platform_device, dev);
return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
}
平台设备的匹配规则很简单,使用 strncmp()
比对 pdev->name 和 drv->name,如果一样就说明它们可以匹配。
那么就会调用 .probe 函数,由于 platform_bus_type 没有实现 .probe 函数,因此最终是调用 driver 的 .probe 函数。
我们以这个LED驱动程序(jz2440_基于平台设备的LED驱动程序_Bin Watson的博客-优快云博客)为例:
static int leds_device_probe(struct platform_device *pdev)
{
struct resource *res, *pin;
int error;
/*...省略...*/
/* 根据platform_device的资源,进行ioremap操作 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
pin = platform_get_resource(pdev, IORESOURCE_IO, 0);
if (!res || !pin) {
printk(DRIVER_NAME "leds driver get platform resource [%s] failed.\n",
(res == NULL ? "IORESOURCE_MEM" : "IORESOURCE_IO"));
error = -1;
goto fail3;
}
gpio_con = (volatile unsigned long*)ioremap(res->start, res->end - res->start + 1);
gpio_dat = gpio_con + 1;
led_pin = pin->start;
/*...省略...*/
}
在 driver 的 .probe 函数中调用了 platform_get_resource 来获取设备中的资源,也就是 platform_device 中指定的 resource 资源。
3.3 平台设备注册
platform_device_register 函数分析
我们以 jz2440_基于平台设备的LED驱动程序_Bin Watson的博客-优快云博客 这个程序为例分析:入口函数调用了 platform_device_register 进行注册。
static int __init leds_dev_init(void)
{
platform_device_register(&leds_device);
printk(KERN_INFO "leds device register.\n");
return 0;
}
-
leds_device 是一个 platform_device 设备,其定义如下:
static struct platform_device leds_device = { .name = DEVICE_NAME, .id = -1, .num_resources = ARRAY_SIZE(leds_resource), .resource = leds_resource, .dev = { .release = leds_dev_release, }, };
主要是 leds_resource,记录了该设备所具有的硬件资源,其定义如下:
static struct resource leds_resource[] = { [0] = { /* 配置和数据 寄存器 */ .start = S3C2440_GPFCON, .end = S3C2440_GPFCON + 8 - 1, .flags = IORESOURCE_MEM, }, [1] = { .start = 4, /* 第4位 */ .end = 4, .flags = IORESOURCE_IO, .name = "led" }, };
详细的成员分析参考 Linux内核 struct resource 结构体_Bin Watson的博客-优快云博客
在前面我们分析了平台驱动的 .probe 函数会通过 platform_get_resource 来获取平台设备的资源。通过这种操作,我们就可以将设备的资源与驱动程序进行分离,提高了驱动程序的通用性。
platform_device_register 的定义如下:
int platform_device_register(struct platform_device * pdev)
{
device_initialize(&pdev->dev);
return platform_device_add(pdev);
}
device_initialize 完成通用设备的初始化工作。
详细看 platform_device_add 的定义如下:
int platform_device_add(struct platform_device *pdev)
{
/*...省略...*/
pdev->dev.bus = &platform_bus_type; /* 总线类型设置为platform_bus_type */
/*...省略...*/
for (i = 0; i < pdev->num_resources; i++) {
struct resource *p, *r = &pdev->resource[i];
if (r->name == NULL) /* 如果资源没有指定名字,则设置名字为设备id */
r->name = pdev->dev.bus_id;
p = r->parent;
if (!p) {
if (r->flags & IORESOURCE_MEM)
p = &iomem_resource;
else if (r->flags & IORESOURCE_IO)
p = &ioport_resource;
}
/* 将 IORESOURCE_MEM 类型的资源加入到 iomem_resource 类型的资源树上
* 将 IORESOURCE_IO 类型的资源加入到 ioport_resource 类型的资源树上
* 这么做的目的是防止资源冲突,多个设备使用了相同的资源。
*/
if (p && insert_resource(p, r)) {
printk(KERN_ERR
"%s: failed to claim resource %d\n",
pdev->dev.bus_id, i);
ret = -EBUSY;
goto failed;
}
}
/*...省略...*/
ret = device_add(&pdev->dev); /* 将设备添加到内核中,这个与通用设备的相同。 */
if (ret == 0)
return ret;
/*...省略...*/
}
4. 其它设备模型
Linux 内核中除了平台设备之外,还有其它类型的设备,如:
USB设备,其定义如下:
struct bus_type usb_bus_type = {
.name = "usb",
.match = usb_device_match,
.uevent = usb_uevent,
.suspend = usb_suspend,
.resume = usb_resume,
};
PCI设备,其定义如下:
struct bus_type pci_bus_type = {
.name = "pci",
.match = pci_bus_match,
.uevent = pci_uevent,
.probe = pci_device_probe,
.remove = pci_device_remove,
.suspend = pci_device_suspend,
.suspend_late = pci_device_suspend_late,
.resume_early = pci_device_resume_early,
.resume = pci_device_resume,
.shutdown = pci_device_shutdown,
.dev_attrs = pci_dev_attrs,
};
这些类型将在其它后续文章中进行分析。
参考
《深入Linux内核架构》第6章第7节。
韦东山《高级驱动第二期》。