一:概述
Linux 设备模型(LDM)是 Linux 内核中引入的一个概念。用于管理内核对象(那些需要引用计数的对象、例如文件、设备、总线甚至驱动程序),以及描述它们之间的层次结构,以及这些内核对象之间绑定关系。Linux 设备模型引入了对象生命周期管理、引用计数、以及面向对象(OO)编程风格、 以及资源自动释放等底层特性,在此不再赘述。我们将在后续文章中详细介绍。
在本文中,我们将讨论Linux设备模型上层部分,包括设备(devices)、驱动程序(drivers)和总线(buses)、Linux内核平台抽象数据结构、以及设备和驱动程序的匹配机制。
二:Linux 内核平台抽象和数据结构
Linux 设备模型是建立在一些基本数据结构之上的,包括设备(struct device)、设备驱动程序(struct device_driver)和总线类型(struct bus_type)。第一个数据结构表示的是设备,第二个数据结构表示的是驱动程序,而最后一个数据结构表示设备与 CPU 之间的连接通道。
设备的数据结构:
设备(Devices)用于关联物理设备或虚拟设备。这是建立在struct device 数据结构之上,所以得首先介绍struct device数据结构,详见 include/linux/:
-
struct device {
-
struct device *parent;
-
struct kobject kobj;
-
struct bus_type *bus;
-
struct device_driver *driver;
-
void *platform_data;
-
void *driver_data;
-
struct dev_pm_domain *pm_domain;
-
struct device_node *of_node;
-
struct fwnode_handle *fwnode;
-
dev_t devt;
-
u32 id;
-
[...]
-
};
让我们来看看数据结构中的成员:
parent:这是设备的 "父 "设备,即该设备所挂靠的设备。大多数情况下,父设备是某种总线或主机控制器。如果父设备为NULL,则该设备为顶层设备。例如,总线控制器设备就是这种情况。
kobj:这是最底层的数据结构,用于跟踪内核对象(总线、驱动程序、设备等)。这是 LDM 的核心。我们将在后续文章Linux 设备模型简介中讨论。
bus: 指定设备所处的总线类型。它是设备与 CPU 之间的连接通道。
driver: 指定设备对应的驱动程序。
platform_data(平台数据): 该字段提供了与设备相关的特定信息。该字段在从板级文件中声明设备时自动设置。换句话说,它指向板级设置文件中的特定结构,其中描述了设备及其布线方式。它有助于减少设备驱动程序代码中的#ifdefs的使用。它包含诸如芯片信息、GPIO引脚功能和中断线等资源。
driver_data: 这是驱动程序特定信息的专用指针。总线控制器驱动程序负责提供辅助函数,即用于获取/设置该字段数据。
pm_domain: 此参数指定在系统电源状态更改(如休眠、休眠、和恢复运行)时执行的与电源管理相关的回调函数,以及子系统级和驱动程序级回调函数。
of_node: 这是与该设备相关联的设备树节点。当设备树中声明设备时,固件(OF)会自动填写此字段。你可以检查 platform_data 或 of_node 这两个字段是否已设置,以确定设备被声明。
id:设备实例。
设备通常不会以裸设备结构的形式出现,因为大多数子系统会跟踪它们所包含的设备的额外信息;相反,该数据结构通常嵌入更高级别的设备表示中。例如,struct i2c_client、struct spi_device、struct usb_device和struct platform_device结构都包含一个struct device元素(spi_device->dev、i2c_client->dev、usb_device->dev和platform_device->dev)。
驱动的数据结构:
接下来我们要介绍的数据结构是struct device_driver结构。这个结构是任何设备驱动程序的基础。在面向对象语言中,这结构相当于是基类,它将由每个设备驱动程序继承。这个数据结构在include/linux/device/中定义如下:
-
struct device_driver {
-
const char *name;
-
struct bus_type *bus;
-
struct module *owner;
-
const struct of_device_id *of_match_table;
-
const struct acpi_device_id *acpi_match_table;
-
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);
-
const struct dev_pm_ops *pm;
-
};
让我们看下数据结构中的成员:
name:这是设备驱动程序的名称。当没有显示提供设备匹配方法是,它用来和设备名称进行匹配。
bus: 它是必填字段。它表示该驱动程序的设备所在的总线。如果这个字段没有设置,驱动程序注册将失败,因为在probe方法中,将会检查bus并进行驱动程序与设备的匹配。
owner:该字段指定驱动属于哪个模块。
of_match_table: 这是固件表。是struct of_device_id 的数组,用于设备树匹配。
probe:该函数用于查询指定设备是否存在,以及驱动程序是否可以在这个设备上工作,并将驱动程序绑定到特定的设备。bus driver负责在某个时刻调用这个函数。我们将在后面讨论这个。
remove:当设备从系统中移除时,将调用此方法将其与这个驱动程序中解绑定。
shutdown:当设备即将关闭时调用此函数。
suspend:这是一个回调函数,它使设备进入睡眠模式,处于低功率状态。
resume:这也是一个回调函数,用于唤醒已进入睡眠模式的设备。
pm:表示被驱动设备的一组电源管理回调函数。
在前面的数据结构中,包括shutdown、suspend、resume和pm成员是可选的,因为它们用于电源管理。提供这些成员取决于底层设备的能力(是否可以关闭,挂起,或执行其他与电源管理相关的功能)。
驱动注册:
首先,你应该记住,注册设备就是将该设备插入由其总线驱动程序维护的设备列表中。同样,注册设备驱动程序也是将该驱动程序推入由其所在总线驱动程序维护的驱动程序列表。例如,注册一个 USB 设备驱动程序,就会将该驱动程序插入由 USB 控制器驱动程序维护的驱动程序列表。注册 SPI 设备驱动程序也是如此,它将把该驱动程序放入由 SPI 控制器驱动程序维护的驱动程序列表中。 driver_register() 是一个底层函数,用于在总线上注册设备驱动程序。它将驱动程序添加到总线的驱动程序列表中。当设备驱动程序在总线上注册后,内核会遍历总线的设备列表,并调用总线的 match() 回调,以查找是否存在驱动程序可以处理的设备。一旦出现匹配,设备和设备驱动程序就会绑定在一起。将设备与设备驱动程序关联起来的过程称为绑定。
你可能永远都不会用到 driver_register() 函数;因为总线驱动程序提供了总线专用的注册函数,该函数将是基于 driver_register() 的封装。总线专用注册函数的形式类似这样 {bus_name}_register_driver() 。例如,USB、SPI、I2C 和 PCI 驱动程序的注册函数分别是 usb_register_driver()、spi_register_driver()、i2c_register_driver() 和 pci_register_driver()。
建议在模块的 init/exit 函数中注册/注销驱动程序,这些函数分别在模块加载/卸载阶段执行。在很多情况下,注册/注销驱动程序是需要在 init/exit 函数中执行。在这种情况下,每个总线内核都会提供一个特定的辅助函数,该函数将作为模块的 init/exit 函数封装,并在内部调用总线特定的注册/取消注册函数。这些总线宏遵循 module_{bus_name}_driver(__{bus_name}_driver)的形式,其中 __{bus_name}_driver 是对应总线的驱动结构。下表列出了 Linux 支持的总线及其函数:
在驱动程序中声明所支持的设备
内核必须知道某个驱动程序支持哪些设备,以及这些设备是否存在于系统中,这样每当其中一个设备出现在系统(总线)上时,内核就能知道是哪个驱动程序负责管理,并运行其probe函数。也就是说,驱动程序的 probe() 函数只有在加载该驱动程序(这是用户空间操作)时才会运行,否则不会发生任何操作。下一节将介绍如何管理驱动程序的自动加载,以便在设备出现时自动加载其驱动程序,并调用其probe函数。
如果我们看一下每个总线特定的设备驱动程序结构(struct platform_Driver, struct i2c_driver, struct spi_driver, struct pci_driver,和Struct usb_driver),我们将看到有一个id_table字段,它的类型依赖于特定的总线。驱动程序中有id_table字段,表示驱动所支持的设备。下表显示了常用总线及其设备ID结构:
三:设备/驱动匹配和模块(自动)加载
总线是设备驱动程序和设备所依赖的基础。从硬件角度看,总线是设备与 CPU 之间的纽带,而从软件角度看,总线驱动程序是设备与其驱动程序之间的纽带。每当向系统添加/注册一个设备或驱动程序时,该设备或驱动程序就会自动添加到总线驱动程序所维护的列表中。例如,注册一个可由给定驱动程序(当然是 i2c)管理的 I2C 设备列表,将导致这些设备排队进入维护 I2C 适配器驱动程序的全局列表,以及提供一个 USB 设备表,将这些设备插入由 USB 控制器驱动程序维护的设备列表。另一个例子涉及注册一个新的 SPI 驱动程序,这将把该驱动程序插入由 SPI 控制器驱动程序维护的驱动程序列表中。如果不这样做,内核就无法知道哪个驱动程序应该处理哪个设备。
每个设备驱动程序都应该公开它所支持的设备列表,并使该列表能够被驱动(尤其是总线驱动程序)访问。该设备列表称为id_table,在驱动程序代码内部声明并填充。该表格是一个设备ID数组,每个ID的类型取决于设备的类型(I2C、SPI、USB等)。这样,每当一个设备出现在总线上时,总线驱动程序将遍历其设备驱动程序的列表,并查看每个ID表中与新设备对应的条目。包含该设备ID的任何驱动程序的probe()函数都将被运行,并将新设备作为参数传递。这个过程称为匹配循环。对于驱动程序也是如此。每当一个新的驱动程序注册到总线上时,总线驱动程序将遍历其设备的列表,并查找出现在已注册驱动程序的id_table中的设备ID。对于每个命中,将相应设备作为参数传递给驱动程序的probe()函数,该函数将被运行多次,次数与命中次数相同。
匹配循环的问题在于,只有加载了的模块才会调用其探针函数。换句话说,如果对应的模块没有加载(使用insmod或modprobe命令加载或内置),匹配循环将毫无用处。您必须手动加载模块,然后设备才会出现在总线上。解决此问题的方法是模块自动加载。由于大多数情况下模块加载是用户空间操作(当内核未使用request_module()函数请求模块本身时),内核必须找到一种方法,将驱动程序及其设备表暴露给用户空间。因此出现了一个名为MODULE_DEVICE_TABLE()的宏:
MODULE_DEVICE_TABLE(<bus_type_name>, <array_of_ids>
添加设备
设备声明不是 LDM 的一部分。它包括声明系统中存在(或不存在)的设备,有三个地方可以声明/填加设备:
- 从电路板文件或在单独模块中(较旧,现已弃用)
- 从设备树(新方法,推荐使用)
- 从高级配置和电源接口 (ACPI),在此不作讨论。
四:总线结构
最后,还有struct bus_type结构,代表内核内部总线结构(无论是物理的还是虚拟的)。总线控制器为任何层次结构的根元素。从物理上讲,总线是处理器和一个或多个设备之间的通道。从软件的角度来看,总线结构是设备(struct device)和驱动(struct device)之间的链接。如果没有这个,系统就不会附加任何东西,因为Bus (bus_type)负责匹配设备和驱动程序:
-
struct bus_type {
-
const char *name;
-
struct device *dev_root;
-
int (*match)(struct device *dev,
-
struct device_driver *drv);
-
int (*probe)(struct device *dev);
-
int (*remove)(struct device *dev);
-
/* [...] */
-
};
让我们看下 bus_type 的成员
name: 这是总线的名称,它将出现在/sys/bus/中。
match: 这是一个回调函数,当一个新的设备或驱动程序被添加到 bus 时调用。回调必须足够智能,当设备和驱动程序之间存在匹配时,将返回一个非零值。match 回调的主要目的是允许总线确定是否特定的设备可以由给定的驱动程序或其他逻辑处理,如果驱动程序支持给定的设备。大多数情况下,验证过程使用一个简单的字符串比较(设备和驱动程序名称,或表和设备)。
probe: 这是一个回调函数,当一个新的设备或驱动程序被添加到总线调用,一旦匹配发生。这个函数负责分配具体的总线设备结构,并调用给定驱动程序的probe函数。
remove:当设备从总线上移除时调用。
五:设备与驱动匹配机制介绍
设备驱动程序和设备总是在总线上注册的。在导出驱动程序支持的设备时,可以使用 driver.of_match_table、driver.of_match_table 或 <bus>_driver.id_table(针对特定设备类型;例如 i2c_device.id_table 或 platform_device.id_table)。
每个总线驱动程序都有责任提供其匹配函数,每当有新设备或设备驱动程序在该总线上注册时,内核都会运行该函数。也就是说,平台设备有三种匹配机制,全部由字符串比较组成。这些匹配机制基于 DT 表、ACPI 表、设备和驱动程序名称。