前言
内核中的PCI子系统(也称PCI层)提供各种PCI设备驱动程序共同的所有通用功能。这个子系统让程序员减少了很多必须对各种设备所做的事,让驱动程序能以更简明的方式编写,使内核更易于收集和维护有关各种设备的信息,如描述信息和统计数据。
涉及的数据结构
在此列出的PCI层使用的一些关键数据结构类型。还有很多其他类型,但是下列几种是必须知道的。第一个结构定义在include/linux/mod_devicetale.h
中,而另外两个定义在include/linux/pci.h
中
-
pci_device_id
- 设备标识符。这不是Linux所使用的本地ID,而是根据PCI标准所定义的ID。后面一节会说明此ID的定义
-
pci_dev
- 每个PCI设备都会被分派一个pci_dev实例,如同网络设备都会被分派net_device实例一样。这个结构由内核使用,已引用一个PCI设备。
-
pci_driver
- 定义PCI层和设备驱动程序之间的接口。这个结构主要由函数指针组成。所有PCI设备都会使用这个结构。后面一节会进行介绍。
PCI设备驱动程序由pci_driver
结构的实例定义。以下是其主要字段说明,特别注意NIC设备的情况。函数指针会由驱动程序初始化为该驱动程序内适当的函数。
-
char *name
- 驱动程序的名字
-
const struct pci_device_id *id_table
- 这是一个ID向量,内核用于把一些设备关联到此驱动程序,”PCI NIC“驱动程序注册范例一节会展示一个实例。
-
int (*probe)(struct pci_dev *dev, const pci_device_id *id)
- 当PCI层发现它正在搜寻驱动程序的设备ID与前面所提到的id_table匹配时,就会调用该函数。此函数应该开启硬件,分配
net_device
结构,初始化并注册新设备。此函数中,驱动程序会分配正确工作所需的所有数据结构(如传输或接收时所用的缓冲区)。
- 当PCI层发现它正在搜寻驱动程序的设备ID与前面所提到的id_table匹配时,就会调用该函数。此函数应该开启硬件,分配
-
void (*remove)(struct pci_dev *dev)
- 当驱动程序从内核除名时,或者当个可热插拔设备被删除时,PCI层就会调用此函数。此函数为probe函数配对函数,用于清理任何数据结构和状态。网络设备使用此函数来释放已分配的I/O端口和I/O内存,为设备除名,释放net_device数据结构以及其他由设备驱动程序在probe函数内所分配的辅助数据结构。
-
int (*suspend)(struct pci_dev *dev, pm_message_t state)
-
int (*resume)(struct pci_dev *dev)
- 当系统进入挂起模式以及重新继续时,PCI层就会调用这些函数。后面一节介绍
-
int (*enable_wake)(struct pci_dev *dev, u32 state, int enable)
- 利用这个函数,驱动程序可以通过产生特定的电源管理事件信号,开启或关闭设备唤醒系统的能力。
-
struct pci_dynids dynids
- 动态ID
PCI NIC设备驱动程序的注册
PCI设备独一无二识别方式是通过一些参数的组合,包括开发商以及模型等。这些参数由内核存储在pci_device_id
类型的数据结构中,定义如下:
struct pci_device_id{
unsigned int vendor, device;
unsigned int subvendor, suddevice;
unsigned int class, class_mask;
unsigned long driver_data;
}
vendor和device通常就足以识别设备。subvendor和subdevice很少用到,通常设置为通配符(PCI_ANY_ID)。class和class_mask代表该设备所属的类,而NETWORK就是我们所要讨论的设备所属类。driver_data不是PCI ID的一部分,而是由驱动程序所使用的一个私有参数。
PCI设备驱动程序分别用pci_register_driver和pci_unregister_driver向内核注册和除名。这些函数定义在include/pci/pci.c
中。除此之外,还有一个pci_module_init
,是pci_register_driver
的别名。因此pci_register_driver
之前,旧版本内核中所用的函数名为pci_module_init
,有些驱动程序依然使用它。
pci_register_driver
需要一个pci_driver
数据结构作为自变量。借助于pci_driver
的id_table
向量,内核知道该驱动程序可以处理那些设备,此外,也因为pci_driver
中所有虚拟函数,使内核有一种机制,可以与此驱动程序的任何相关联的设备彼此交互。
PCI的优点之一是,其之处寻找IRQ和每个设备所需要的其他资源的探测方式相当优雅。模块可以加载期间接收一些输入参数,以告知该如何配置其所负责的所有设备。但是,有些时候让驱动程序自行检查系统上的设备,然后为其负责的那些设备做配置会比较简答一点。必要时,用户依然可以退回到手动配置。
/sys
文件系统输出有关系统总线(PCI,USB等等)的信息,包括各种设备及各种设备之间的关系。/sys也允许管理员为特定的设备驱动程序定义新的ID,使得除了驱动程序通过其pci_driver
结构的id_table
向量注册的静态ID之外,内核还能使用由用户所配置的参数。
这里不会说明内核根据设备ID查询驱动程序所采用的探测机制。然而值得一提的是,探测方式有两种:
-
静态
- 给定一个设备PCI ID,内核就能根据
is_table
向量查询出正确的PCI驱动程序(也就是pci_driver实例_。)这称为静态探测方式
- 给定一个设备PCI ID,内核就能根据
-
动态
-
这种查询是根据用户手动配置ID,这种情况在实际中很少见,但是,在调试的情况下偶尔也使用。动态指的是系统管理员可以新增ID的能力,而不是指ID本身可自行变动。
-
由于动态ID是在运行中的系统上配置的,只有当内核被编译成支持热插拔才能使用。
-
电源管理和网络唤醒
PCI电源管理事件由pci_driver
数据结构的sudpend
和resume
函数处理。除了分别负责PCI状态的保存和恢复之外,这些函数遇到NIC的情况时还需要采取特殊步骤:
-
suspend
主要停止设备出口队列,使得该设备无法再传输。 -
resume
重启出口队列,使得该设备得以再次传输。
网络唤醒(Wake-on-lan,WOL)是一种功能,允许NIC在接收到一种特殊类型的帧时唤醒处于待命模式的系统。WOL通常默认是关闭的。此功能用pci_enable_wake
打开或关上。
当首次引入WOL功能时,只有一种帧可以唤醒系统:“魔术封包”这类特殊帧有两个主要特征:
-
目的MAC地址属于正在接收的NIC(无论该地址是单播、多播还是广播)。
-
帧中的某处(任何地方)会设置一段48位序列,后面再接NIC MAC地址,在一行中至少连续重复16此。
现在也有可能允许其他类型的帧唤醒系统。有少量设备可以根据在模块加载期间设置的一个参数开启或关闭WOL功能。
主要一个开启WOL功能的功能识别一个帧,其类型为可唤醒系统,就会产生一个电源管理通知信息,去做相应的工作。
PCI NIC驱动程序注册范例
用drivers/net/e100.c
中的Intel PRO/100 Ethernet驱动程序来说明驱动程序的注册:
#define INTEL_8255X_ETHERNET_DEVICE(device_id, ich){\
PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \
PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich}
static struct pci_device_id e100_id_table[] = {
INTEL_8255X_ETHERNET_DEVICE{0x1029, 0},
INTEL_8255X_ETHERNET_DEVICE{0x1030, 0},
....
}
PCI NIC设备驱动程序会把一个pci_device_id
结构体向量注册给内核,该结构向量列出了其所能处理的那些设备。例如,e100_id_table就是e100.c驱动程序所使用的结构,注意:
-
第一个字段(相当于此结构定义中的vendor)为固定值
PCI_VENDOR_ID_INTEL
,它为指定给Intel初始化的开发商ID。 -
第三个字段和第四个字段(subvendor和subdevice)通常初始化为通用值
PCI_ANY_ID
,因为前两个字段(vendor和device)足以识别那些设备。 -
许多设备都会使用那张设备表使用宏
__devinitdata
,用来标记初始化数据,不过e100_id_table没有。
此模块由module_init
宏指定的e100_init_module初始化。当此函数在引导期间或者模块加载期间由内核执行时,就会调用pci_moudule_init
。此函数就会注册该驱动程序,并间接注册所有相关的NIC,后面一节进行介绍。
下面是e100驱动程序与PCI层接口相关的关键部分:
#define NAME "e100"
static int __devinit e100_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
}
static void __devexit e100_remove(struct pci_dev *pdev)
{
...
}
#ifdef CONFIG_PM
static int e100_suspend(struct pci_dev *pdev, u32 state)
{
...
}
static int e100_resume(struct pci_dev *pdev)
{
...
}
#endif
static struct pci_driver e100_driver = {
.name = NAME,
.id_table = e100_id_table,
.probe = e100_probe,
.remove = __devexit_p(e100_remove),
#ifdef CONFIG_PM
.suspend = e100_suspend,
.resume = e100_resume,
#endif
};
static int __init e100_init_module(void)
{
...
return pci_module_init(&e100_driver);
}
static void __exit e100_cleanup_module(void)
{
pci_unregister_driver(&e100_driver);
}
module_init(e100_init_module);
module_exit(e100_cleanup_module);
此外,注意:
-
只有当内核支持电源管理时,才会对suspend和resume做初始化,所以只有当该条件为真时,
e100_suspend和e100_resume
这两个函数才会被包括在映像中。 -
pci_driver的remove
字段用宏__devexit_p标记,而e100_remove用__devexit标记。 -
e100_probe用__devinit标记。
大蓝图
我们把前几节所了解的综合来看,采用PCI总线的系统以及一些PCI设备在引导期间会发生什么事。
当系统引导时,会建立一种数据库,把每个总线都关联一份已侦测到而使用该总线的设备列表。例如,PCI总线的描述符除了其他参数外,还包括一个已侦测PCI设备的列表。如“PCI NIC设备驱动程序的注册”一节所见,每个pci设备都可由pci_device_id
结构中的一个大的字段集合唯一地识别,不过通常只需要几个字符就足够了。我们也知道PCI设备驱动程序如何定义一个pci_driver
实例,以及如何用pci_register_driver
与PCI层注册。设备驱动程序加载时,内核已建好其数据库,我们以配有三个PCI设备的图为例,说明当设备驱动程序A和B加载时会发生什么事。
当设备驱动程序A被加载时,会调用pci_register_driver
并提供pci_driver
实例而与PCI层注册。pci_driver
结构有内含一个此驱动程序能驱动的PCI设备ID的向量。接着,PCI层使用该表去查看已侦测的PCI设备列表中与那些设备匹配。于是,就会建立该驱动程序的设备列表(如上图b所示)。此外,对每个匹配的设备而言,PCI层会调用相匹配的驱动程序中的pci_driver
结构中锁提供的probe
函数。probe函数会建立并注册相关联的网络设备。就此而言,设备Dev3就会被分派给驱动程序B。图c所示就是加载此去程程序后的结果。
当驱动程序于稍后卸载时,该模块的module_exit
函数就会调用pci_unregister_driver
。接着,由于其数据库,使得PCI层能够遍历所有与该驱动程序相关联的设备,并启用该驱动程序的remove
函数。此函数就会将此网络设备除名。
通过/proc文件系统调整
/proc/pci
文件可用于倾卸有关已注册的PCI设备的信息。pciutils套件中的lspci命令也可用于打印出有关于本地PCI设备的有用信息,但其信息取自/sys
。
本章涉及的函数和变量
名称 | 描述 |
---|---|
函数和宏 | |
pci_register_driver | 注册PCI驱动程序 |
pci_unregister_driver | 除名PCI驱动程序 |
pci_module_init | 初始化PCI驱动程序 |
数据结构 | |
struct pci_driver | 定义PCI驱动程序(多数都是虚拟回调函数) |
struct pci_device_id | 存储PCI设备相关联的通用ID |
struct pci_dev | 结构代表内核空间中的PCI设备 |