3.3 PCI核心初始化

要了解PCI子系统的初始化过程,必须先透彻分析Linux初始化调用机制是如何工作的。不失一般性,我们从PCI子系统初始化必须调用的函数pcibus_class_init开始,其代码如程序3-1所示。

程序3-1 函数pcibus_class_init()代码(摘自文件drivers/pci/probe.c)

pcibus_class_init()

101static int __init pcibus_class_init(void)
102{
103    return class_register(&pcibus_class);
104}
105postcore_initcall(pcibus_class_init);

这段代码逻辑的关键在于两个宏定义:__init和postcore_initcall,逐个分析。首先看来自文件include/linux/init.h的__init宏。

#define __init __section(.init.text) __cold notrace

__cold宏定义在文件include/linux/compiler-gcc4.h,__section宏和notrace宏定义在文件include/linux/ compiler.h。扩展之后,可得:

#define __init __attribute__ ((__section__(“.init.text”))) __attribute__((__cold__))
__attribute__((no_instrument_function))

这表明,使用__init定义函数时,会在函数声明中加入一些GCC属性。__cold__属性的作用是将函数标记为较少使用,这样编译器可以考虑其长度优化,而不是速度优化。no_instrument_function属性表示不要在这个函数入口和出口生成用于分析的检测代码。这里我们更感兴趣的是__section属性,它要求编译器将这段代码放在一个独立的节(Section)中,节名为“.init.text”。这样做的目的是在一个ELF节中聚集存放所有的初始化代码,以便在初始化结束之后,释放整个节。

因此,回到pcibus_class_init函数,第101~104行定义了实现代码,并将这段代码放在“.init.text”节的位置。

再看postcore_initcall宏,同样在文件include/linux/init.h中,它直接从__define_initcall宏定义而来。

#define __define_initcall(level,fn,id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" level ".init"))) = fn
#define postcore_initcall(fn)      __define_initcall("2",fn,2)

因此,pcibus_class_init函数第105行扩展后就是:

static initcall_t __initcall_pcibus_class_init2 __attribute__((__used__)) __attribute__((__section 
__(".initcall2.init"))) = pcibus_class_init

这行代码实际上在“.initcall2.init”节定义了一个函数指针,具有唯一的名字,指向实现代码放在“.init.text”节的pcibus_class_init函数。

实际上,Linux初始化过程中调用的所有函数都是用类似的机制来实现的,最终构成如图3-11所示的布局。在右边,是将函数代码放在对应节或者定义函数指针的宏,左边是链接过程(参见文件include/ asm-generic/vmlinux.lds.h)在节前后添加的一些标签/符号。

在构造好上面的初始化布局后,Linux内核会依次调用parse_early_param解析.init.setup节的内核参数,调用do_pre_smp_initcalls执行.initearly.init节的早期初始化调用代码,然后调用do_initcalls顺序执行.initcall#.init(包括initcallrootfs.init)节的初始化代码。

因此,分析PCI子系统初始化流程,需要找出用###_initcall系列宏定义的函数。

对于具有同样限定符的函数,调用顺序取决于编译顺序。因此,通过根目录下的makefile文件可以知道:arch/x86/pci下的函数将在/drivers/pci的函数前面执行。而通过/drivers/pci下的makefile文件可以知道:drivers/pci/probe.c中的函数将在drivers/pci/pci-driver.c的函数前面执行。

Linux系统启动过程中,将根据内核编译情况,顺序调用各个初始化函数。这些顺序是通过函数声明限定符指定的PCI核心的初始化顺序,如表3-4所示。

图3-11 Linux初始化代码使用的某些内存节

表3-4 PCI核心的初始化顺序

PCI子系统的初始化主要完成以下工作。

• 初始化总线类

pcibus_class_init函数的代码在前面已给出,它的唯一目的是初始化PCI总线类。它调用Linux驱动模型中的class_register注册一个名字叫pci_bus的class。这将在sys/class/下创建一个pci_bus目录。后续的PCI核心代码将在该目录下为每条PCI总线创建一个子目录,目录名格式为####:##,对应总线域编号和总线编号。

• 初始化总线类型

作为Linux驱动模型的一个主要实例,PCI核心定义了一个名字为pci的总线类型。pci_driver_init函数(参见文件drivers/pci/pci-driver.c)调用Linux驱动模型中的bus_register注册该总线类型。正是由于PCI总线类型的存在,才会有PCI设备的链表和PCI驱动的链表,才会有PCI设备和PCI驱动之间的“绑定”。

注意:这里的pci_driver_init的函数名对应pci总线类型的驱动,和PCI设备的驱动是不同的。此外,精确理解PCI总线类型和PCI总线的含义,对理解PCI核心代码也有着很重要的作用。

• 配置访问方法

这里所谓的访问方法,是指对配置空间的访问。Linux支持多种配置访问方法,必须在PCI子系统初始化早期根据用户要求和系统状况选择一种,入口函数为pci_arch_init。对PCI配置访问方法的分析参见本章后面部分。

• PCI总线扫描

PCI总线扫描使得Linux了解整个硬件系统的“家底”,为每个设备构建内存中数据结构,这是以后对该设备进行操作的基础。入口函数是pcibios_scan_root,在pci_subsys_init函数调用的pci_legacy_ init函数中调用。对PCI总线扫描流程的分析参见本章后面部分。

• PCI中断路由

PCI中断路由的基本出发点是建立PCI硬件中断引脚与操作系统中断号之间的关联,使得在引脚上出现中断时,可以找到操作系统预设置的中断处理函数。对于x86,PCI中断路由的入口函数是pcibios_irq_init,在pci_subsys_init函数中被调用。对PCI中断路由流程的分析参见本章后面部分。

• PCI资源分配

PCI资源分配的目的是将PCI设备的I/O地址空间和内存地址空间映射到Linux系统的总线空间,这样以后对于总线空间范围内的访问会被自动“重定向”到PCI设备。PCI资源分配有两种可能:(1)PCI设备的I/O空间和内存空间已经被配置好。这时候,PCI核心只需要“验证”配置的正确性,入口函数为pcibios_resource_survey,从pci_subsys_init函数调用的pcibios_init函数中调用;(2)PCI设备的I/O空间和内存空间还没有被配置。这时候,PCI核心结构就需要为它们分配资源,入口函数为pcibios_assign_resources。对PCI资源分配的分析参见本章后面部分。

• 初始化proc文件系统

精确地说,是初始化proc文件系统中的与PCI有关的目录项。在文件drivers/pci/proc中,入口函数为pci_proc_init。

• 初始化sysfs文件系统

在PCI扫描过程中已经为PCI总线和PCI设备在sysfs文件系统中创建了目录项和一些基本的属性文件,PCI子系统初始化过程的最后,还调用pci_sysfs_init函数(参见文件drivers/pci/pci-sysfs.c)为扫描到的PCI设备创建更多的属性文件。

需要指出的是,本书在分析时,将主要关注点集中在逻辑流程部分。Linux是一个兼容性极好的操作系统,它不仅支持各种平台,还支持各个厂商的硬件产品,甚至是“有点小毛病”的硬件产品。例如,PCI子系统中,有很多与quirk有关的代码,它们的处理也很有意思。不过这里不打算讨论它们,请有兴趣的读者自行研读。