PCIE内核下的扫描流程

内核下PCIE初始化流程

1. 找到函数入口

在大多数linux系统中, /boot下通常含有一个名为System.map*的文件, 其中记录了内核的符号表, 如果没有的话也可以通过命令生成:

TODO

然后查找相应函数的初始化等级, 以pci为例:

bash 复制代码
loongson@loongson-pc:~$ cat /boot/System.map-4.19.0-19-loongson-3 | grep pci | grep initcall
900000000150c5f0 t __initcall_pci_realloc_setup_params0
900000000150c788 t __initcall_pcibus_class_init2
900000000150c790 t __initcall_pci_driver_init2
900000000150c818 t __initcall_set_pcie_wakeup3
900000000150c880 t __initcall_acpi_pci_init3
900000000150ca10 t __initcall_pci_slot_init4
900000000150caf8 t __initcall_pcibios_init4
900000000150cd00 t __initcall_pci_apply_final_quirks5s
900000000150d0b0 t __initcall_pci_proc_init6
900000000150d0b8 t __initcall_pcie_portdrv_init6
900000000150d0c0 t __initcall_pci_hotplug_init6
900000000150d0d0 t __initcall_loongson_pci_driver_init6
900000000150d0d8 t __initcall_loongson_ppci_driver_init6
900000000150d120 t __initcall_serial_pci_driver_init6
900000000150d1b0 t __initcall_mvumi_pci_driver_init6
900000000150d1d0 t __initcall_ahci_pci_driver_init6
900000000150d208 t __initcall_stmmac_pci_driver_init6
900000000150d3b0 t __initcall_pci_resource_alignment_sysfs_init7
900000000150d3b8 t __initcall_pci_sysfs_init7

其中后缀的数字表示initcall的等级, 数字越小, 等级越高, 任意以一个函数为例, 如__initcall_pci_realloc_setup_params0, 可以通过关键字样搜索:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ grep pci_realloc_setup_params -rnI drivers/pci/
drivers/pci/pci.c:6224:static int __init pci_realloc_setup_params(void)
drivers/pci/pci.c:6230:pure_initcall(pci_realloc_setup_params);

可以看到pure_initcall就是设定初始化等级的函数, 跳转后发现其他的初始化等级定义:

C 复制代码
#define pure_initcall(fn)       __define_initcall(fn, 0)

#define core_initcall(fn)       __define_initcall(fn, 1)
#define core_initcall_sync(fn)      __define_initcall(fn, 1s)
#define postcore_initcall(fn)       __define_initcall(fn, 2)
#define postcore_initcall_sync(fn)  __define_initcall(fn, 2s)
#define arch_initcall(fn)       __define_initcall(fn, 3)
#define arch_initcall_sync(fn)      __define_initcall(fn, 3s)
#define subsys_initcall(fn)     __define_initcall(fn, 4)
#define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
#define fs_initcall(fn)         __define_initcall(fn, 5)
#define fs_initcall_sync(fn)        __define_initcall(fn, 5s)
#define rootfs_initcall(fn)     __define_initcall(fn, rootfs)
#define device_initcall(fn)     __define_initcall(fn, 6)
#define device_initcall_sync(fn)    __define_initcall(fn, 6s)
#define late_initcall(fn)       __define_initcall(fn, 7)     
#define late_initcall_sync(fn)      __define_initcall(fn, 7s)

由此, 根据上面的符号表信息可知, PCI初始化的流程大致是:

diff 复制代码
|__initcall_pci_realloc_setup_params0
----|__initcall_pcibus_class_init2
----|__initcall_pci_driver_init2
--------|__initcall_set_pcie_wakeup3
--------|__initcall_acpi_pci_init3
------------|__initcall_pci_slot_init4
------------|__initcall_pcibios_init4
----------------|__initcall_pci_apply_final_quirks5s
--------------------|__initcall_pci_proc_init6
--------------------|__initcall_pcie_portdrv_init6
--------------------|__initcall_pci_hotplug_init6
--------------------|__initcall_loongson_pci_driver_init6
--------------------|__initcall_loongson_ppci_driver_init6
--------------------|__initcall_serial_pci_driver_init6
--------------------|__initcall_mvumi_pci_driver_init6
--------------------|__initcall_ahci_pci_driver_init6
--------------------|__initcall_stmmac_pci_driver_init6
------------------------|__initcall_pci_resource_alignment_sysfs_init7
------------------------|__initcall_pci_sysfs_init7

2. pci_realloc_setup_params函数

函数内容很少, 如下:

C 复制代码
static int __init pci_realloc_setup_params(void)
{
    disable_acs_redir_param = kstrdup(disable_acs_redir_param, GFP_KERNEL);
    return 0;
}

可以看出仅仅是赋值了一个全局变量, 根据函数的注释(内核中的函数注释很详尽, 不在文中展出, 有兴趣自行查看源代码), 这个全局变量是在pci_setup()函数中初始化的, 继续追代码, 发现, pci_setup()的声明是:

C 复制代码
early_param("pci", pci_setup);

在Linux内核中,early_param用于注册一个特殊的内核参数解析处理函数,这个函数可以在内核引导的早期阶段被调用,即在内存管理子系统和其他大部分内核子系统初始化之前。这意味着early_param可以用于设置那些需要在内核初始化早期阶段就生效的参数,比如影响内核如何初始化内存、CPU或其他硬件相关的设置。

使用early_param注册的参数解析函数具有以下特点:

  1. 早期执行early_param函数在内核初始化的非常早的阶段被执行,早于大多数内核子系统的设置,这对于需要在某些关键子系统初始化前设置参数的场景非常有用。
  2. 限制使用 :由于执行时机很早,使用early_param的函数受到限制,不能依赖于复杂的内核服务,如内存分配器或锁机制,因为那时它们可能还未初始化。
  3. 用途特殊:它通常用于设置对后续引导流程有重大影响的参数,例如调试选项、内存布局调整或硬件配置等。
  4. 定义方式early_param通过宏定义来注册,一般形式为early_param(name, function),其中name是参数的名字,function是当参数被发现时调用的函数。

例如,一个典型的使用场景是在内核引导时通过命令行传递一个特定的参数来改变内存初始化的行为,或者配置一些底层硬件设置,这些操作需要在内核的其他部分开始运行之前完成。 简而言之,early_param提供了在内核引导的最初阶段传递和处理参数的能力,这对于那些对系统启动至关重要的配置项是必不可少的。

所以这个变量是用于使能或关闭一些功能的. 具体逻辑也就不需要深入分析.

3. pcibus_class_init函数

函数内容很少, 如下:

C 复制代码
static struct class pcibus_class = {
    .name       = "pci_bus",
    .dev_release    = &release_pcibus_dev,
    .dev_groups = pcibus_groups,
};

static int __init pcibus_class_init(void)
{
    return class_register(&pcibus_class); 
}

可见, 是通过调用class_register来注册设备pcibus_class, 而pcibus_class中的两个重要成员分别是release_pcibus_devpcibus_groups, 前者用于释放此pcibus, 包括减少此pcibus的引用和清空pcibus中的资源及删除此总线. 后者则注册了一个结构体数组用于记录设备的一些参数.

4. pci_driver_init函数

核心代码:

C 复制代码
ret = bus_register(&pci_bus_type);

在Linux内核中,bus_register函数用于注册一个总线(bus)对象。总线在内核中扮演着连接设备和驱动程序的桥梁角色,负责设备的探测、识别、匹配以及驱动程序的加载。当调用bus_register时,内核会执行以下几个关键操作:

  1. 初始化总线结构 :确保总线结构体(通常是struct bus_type)的所有必要字段已经被正确填充,比如名字、匹配函数、枚举函数等。
  2. 添加通知链:总线可能会注册一些通知链,以便在特定事件发生时通知感兴趣的子系统或模块。这有助于模块间的通信和协调。
  3. 设备模型集成:总线会被加入到内核的设备模型中,这意味着它现在可以参与设备的探测和匹配过程。
  4. 挂载总线:总线会被挂在内核的总线系统树上,这是一个描述系统中所有总线、设备和驱动程序之间关系的数据结构。
  5. 启动探测:根据总线的特性,可能会触发设备的探测过程,尝试找出连接到该总线上的所有设备。
  6. 事件通知:可能会触发一些内核事件,通知其他子系统总线已经注册,从而允许它们采取相应的行动,比如注册设备或驱动到该总线上。

通过bus_register注册的总线使得内核能够自动发现、配置和管理与之相连的硬件设备,以及加载适当的驱动程序来控制这些设备。这是Linux设备驱动模型的核心机制之一,对于构建灵活、可扩展的硬件支持架构至关重要。

这里注册的pci_bus_type中包含了重要的pci_device_probe成员.

5. acpi_pci_init函数

set_pcie_wakeup函数是架构相关的其他内容, 通过函数名推测是设定唤醒功能的, 与架构相关, 暂不分析.

acpi_pci_init函数核心代码:

C 复制代码
ret = register_acpi_bus_type(&acpi_pci_bus);

即注册了acpi_bus_type类型的acpi_pci_bus. 与前面类似暂不分析.

6. pci_slot_init函数

核心代码:

C 复制代码
pci_bus_kset = bus_get_kset(&pci_bus_type);
pci_slots_kset = kset_create_and_add("slots", NULL,
                    &pci_bus_kset->kobj);

其中get到的pci_bus_type是在pci_driver_init函数中注册的.

停下思考

截至目前, 所追溯的函数几乎全是在注册某个结构体变量, 并没有看到实质的初始化pci的逻辑代码.

反过来重新思考一下, 上面分析的函数, 在各个文件中几乎都是入口函数, 但是却没有某个地方直接调用(cscope可见), 所以可得这些函数都是由其宏声明, 即initcall声明调用的. 继续追initcall的内容:

C 复制代码
#define ___define_initcall(fn, id, __sec) \
    static initcall_t __initcall_##fn##id __used \
        __attribute__((__section__(#__sec ".init"))) = fn;
#endif

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

所以查一下是在哪里调用这些initcall:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ grep initcall -rnI arch/loongarch/
arch/loongarch/kernel/vmlinux.lds:60: .init.data : AT(ADDR(.init.data) - 0) { KEEP(*(SORT(___kentry+*))) *(.init.data init.data.*) . = ALIGN(8);
__start_mcount_loc = .; KEEP(*(__mcount_loc)) KEEP(*(__patchable_function_entries)) __stop_mcount_loc = .; *(.init.rodata .init.rodata.*) . = ALIGN(8); __start_ftrace_events = .; KEEP(*(_ftrace_events)) __stop_ftrace_events = .; __start_ftrace_eval_maps = .; KEEP(*(_ftrace_eval_map)) __stop_ftrace_eval_maps = .; . = ALIGN(8); __start_syscalls_metadata = .; KEEP(*(__syscalls_metadata)) __stop_syscalls_metadata = .; . = ALIGN(8); __start_kprobe_blacklist = .; KEEP(*(_kprobe_blacklist)) __stop_kprobe_blacklist = .; . = ALIGN(8); __reservedmem_of_table = .; KEEP(*(__reservedmem_of_table)) KEEP(*(__reservedmem_of_table_end)) . = ALIGN(8); __cpu_method_of_table = .; KEEP(*(__cpu_method_of_table)) KEEP(*(__cpu_method_of_table_end)) . = ALIGN(8); __cpuidle_method_of_table = .; KEEP(*(__cpuidle_method_of_table)) KEEP(*(__cpuidle_method_of_table_end)) . = ALIGN(32); __dtb_start = .; KEEP(*(.dtb.init.rodata)) __dtb_end = .; . = ALIGN(8); __irqchip_of_table = .; KEEP(*(__irqchip_of_table)) KEEP(*(__irqchip_of_table_end)) . = ALIGN(8); __irqchip_acpi_probe_table = .; KEEP(*(__irqchip_acpi_probe_table)) __irqchip_acpi_probe_table_end = .; . = ALIGN(8); __timer_acpi_probe_table = .; KEEP(*(__timer_acpi_probe_table)) __timer_acpi_probe_table_end = .; . = ALIGN(8); __earlycon_table = .; KEEP(*(__earlycon_table)) __earlycon_table_end = .; . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .; 
__initcall_start = .; KEEP(*(.initcallearly.init)) 
__initcall0_start = .; KEEP(*(.initcall0.init)) KEEP(*(.initcall0s.init)) 
__initcall1_start = .; KEEP(*(.initcall1.init)) KEEP(*(.initcall1s.init)) 
__initcall2_start = .; KEEP(*(.initcall2.init)) KEEP(*(.initcall2s.init)) 
__initcall3_start = .; KEEP(*(.initcall3.init)) KEEP(*(.initcall3s.init)) 
__initcall4_start = .; KEEP(*(.initcall4.init)) KEEP(*(.initcall4s.init)) 
__initcall5_start = .; KEEP(*(.initcall5.init)) KEEP(*(.initcall5s.init)) 
__initcallrootfs_start = .; KEEP(*(.initcallrootfs.init)) KEEP(*(.initcallrootfss.init)) 
__initcall6_start = .; KEEP(*(.initcall6.init)) KEEP(*(.initcall6s.init)) 
__initcall7_start = .; KEEP(*(.initcall7.init)) KEEP(*(.initcall7s.init)) 
__initcall_end = .; __con_initcall_start = .; KEEP(*(.con_initcall.init)) __con_initcall_end = .; __security_initcall_start = .; KEEP(*(.security_initcall.init)) __security_initcall_end = .; . = ALIGN(4); __initramfs_start = .; KEEP(*(.init.ramfs)) . = ALIGN(8); KEEP(*(.init.ramfs.info)) }

然后这个文件只有目标架构文件夹下有, 但理论上应该是内核的设计框架包含才对, 不应该跟架构强相关, 所以:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ git log -p ./arch/loongarch/kernel/vmlinux.lds
[mengxiangdong@5 linux-4.19-loongson]$ 

输出为空, 所以是文件生成的, 由于文件名跟架构无关, 所以:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ find . | grep vmlinux.lds
./include/asm-generic/vmlinux.lds.h
./arch/openrisc/kernel/vmlinux.lds.S
./arch/alpha/kernel/vmlinux.lds.S
./arch/hexagon/kernel/vmlinux.lds.S
./arch/nios2/kernel/vmlinux.lds.S
./arch/nios2/boot/compressed/vmlinux.lds.S
./arch/x86/kernel/vmlinux.lds.S
./arch/x86/boot/compressed/vmlinux.lds.S
./arch/arc/kernel/vmlinux.lds.S
./arch/xtensa/kernel/vmlinux.lds.S
./arch/um/kernel/vmlinux.lds.S
./arch/um/include/asm/vmlinux.lds.h
./arch/ia64/kernel/vmlinux.lds.S
./arch/unicore32/kernel/vmlinux.lds.S
./arch/unicore32/boot/compressed/vmlinux.lds.in
./arch/sparc/kernel/vmlinux.lds.S
./arch/parisc/kernel/vmlinux.lds.S
./arch/parisc/boot/compressed/vmlinux.lds.S
./arch/nds32/kernel/vmlinux.lds.S
./arch/mips/kernel/vmlinux.lds.S
./arch/riscv/kernel/vmlinux.lds.S
./arch/sh/kernel/vmlinux.lds.S
./arch/sh/include/asm/vmlinux.lds.h
./arch/powerpc/kernel/vmlinux.lds.S
./arch/arm64/kernel/vmlinux.lds.S
./arch/h8300/kernel/vmlinux.lds.S
./arch/h8300/boot/compressed/vmlinux.lds
./arch/s390/kernel/vmlinux.lds.S
./arch/s390/boot/compressed/vmlinux.lds.S
./arch/microblaze/kernel/vmlinux.lds.S
./arch/loongarch/kernel/.vmlinux.lds.cmd
./arch/loongarch/kernel/vmlinux.lds
./arch/loongarch/kernel/vmlinux.lds.S
./arch/c6x/kernel/vmlinux.lds.S
./arch/arm/kernel/vmlinux.lds.h
./arch/arm/kernel/vmlinux.lds.S
./arch/arm/boot/compressed/vmlinux.lds.S
./arch/m68k/kernel/vmlinux.lds.S

因此可以猜出, vmlinux.lds是由vmlinux.lds.S生成的, 然后在vmlinux.lds.S和其头文件中查找到initcall相关定义:

C 复制代码
#define INIT_CALLS_LEVEL(level)                     \ 
        __initcall##level##_start = .;              \
        KEEP(*(.initcall##level##.init))            \
        KEEP(*(.initcall##level##s.init))           \

#define INIT_CALLS                          \
        __initcall_start = .;                   \
        KEEP(*(.initcallearly.init))                \
        INIT_CALLS_LEVEL(0)                 \
        INIT_CALLS_LEVEL(1)                 \
        INIT_CALLS_LEVEL(2)                 \
        INIT_CALLS_LEVEL(3)                 \
        INIT_CALLS_LEVEL(4)                 \
        INIT_CALLS_LEVEL(5)                 \
        INIT_CALLS_LEVEL(rootfs)                \
        INIT_CALLS_LEVEL(6)                 \
        INIT_CALLS_LEVEL(7)                 \
        __initcall_end = .;

至此, 了解了其是如何被组织进内核二进制文件中的原理. 根据小学二年级就学过的ELF格式信息, 此section的调用一定是直接指定initcall调用的, 并且应该架构无关, 设备无关, 即在init文件夹里:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ grep initcall0 -rnI init/
init/main.c:905:extern initcall_entry_t __initcall0_start[];
init/main.c:916:	__initcall0_start,
init/main.c:986:	for (fn = __initcall_start; fn < __initcall0_start; fn++)

查看文件, 可以看到解析逻辑:

C 复制代码
static initcall_entry_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
    "pure",
    "core",
    "postcore",
    "arch",
    "subsys",
    "fs",
    "device",
    "late",
};

static void __init do_initcall_level(int level)
{
    initcall_entry_t *fn;

    strcpy(initcall_command_line, saved_command_line);
    parse_args(initcall_level_names[level],
           initcall_command_line, __start___param,
           __stop___param - __start___param,
           level, level,
           NULL, &repair_env_string);

    trace_initcall_level(initcall_level_names[level]);
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(initcall_from_entry(fn));
}

static void __init do_initcalls(void)                                     
{
    int level;

    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}

即通过initcall_levels数组一步步调用. 且代码中可以看到level0的调用最优先, 其他level是由do_initcalls调用.

7. 放宽条件找到pci初始化函数

根据上面分析, pci的入口函数应该也是由initcall生明的, 而且根据其等级和dmesg信息, 可以推测pci初始化是在core之后, 在fs之前:

C 复制代码
"pure",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",

而在driver/pci目录下已经没有更多信息了, 通过dmesg信息, 可以指导在pci系统初始化之前, 其他的就是bpi, acpi, fdt或者MMU等初始化, 针对ACPI系统, 在driver/acpi目录中查找:

bash 复制代码
[mengxiangdong@5 linux-4.19-loongson]$ grep -P "postcore_initcall|subsys_initcall|arch_initcall" -rnI drivers/acpi/
drivers/acpi/utils.c:718: * which happens in the subsys_initcall() subsection. Hence, do not
drivers/acpi/utils.c:719: * call from a subsys_initcall() or earlier (use acpi_get_devices()
drivers/acpi/utils.c:788: * which happens in the subsys_initcall() subsection. Hence, do not
drivers/acpi/utils.c:789: * call from a subsys_initcall() or earlier (use acpi_get_devices()
drivers/acpi/bus.c:1290:subsys_initcall(acpi_init);

为什么不在arch目录下找呢? 因为arch下是架构强相关的内容, 而pci扫描, 作为一个算法或者标准, 理论上不应该是架构相关的, 当然也不排除有可能, 不过看了一下都不是.

梳理一下信息, 只有一个acpi_init函数了:

C 复制代码
static int __init acpi_init(void)
{
    int result;
                                                                 
    if (acpi_disabled) {
        printk(KERN_INFO PREFIX "Interpreter disabled.\n");
        return -ENODEV;
    }

    acpi_kobj = kobject_create_and_add("acpi", firmware_kobj);
    if (!acpi_kobj) {
        printk(KERN_WARNING "%s: kset create error\n", __func__);
        acpi_kobj = NULL;
    }

    init_acpi_device_notify();
    result = acpi_bus_init();
    if (result) {
        disable_acpi();
        return result;
    }

    pci_mmcfg_late_init();
    acpi_iort_init();
    acpi_scan_init();
    acpi_ec_init();
    acpi_debugfs_init();
    acpi_sleep_proc_init();
    acpi_wakeup_device_init();
    acpi_debugger_init();
    acpi_setup_sb_notify_handler();
    return 0;
}

里面做了很多事, 通过展开相应函数, 发现acpi_scan_init()就是pci初始化的入口! 代码如下:

C 复制代码
int __init acpi_scan_init(void)
{      
    int result;
    acpi_status status;
    struct acpi_table_stao *stao_ptr;
       
    acpi_pci_root_init();            
    acpi_pci_link_init();
    acpi_processor_init();
    ......

8. 分析pci初始化流程

接上回, 首先是acpi_pci_root_init(), 其核心函数是:

C 复制代码
acpi_scan_add_handler_with_hotplug(&pci_root_handler, "pci_root");

即:

C 复制代码
static struct acpi_scan_handler pci_root_handler = {
    .ids = root_device_ids,
    .attach = acpi_pci_root_add,
    .detach = acpi_pci_root_remove,
    .hotplug = {
        .enabled = true,
        .scan_dependent = acpi_pci_root_scan_dependent,
    },
};

int acpi_scan_add_handler_with_hotplug(struct acpi_scan_handler *handler,   
                       const char *hotplug_profile_name)       
{   
    int error;                                                 
    error = acpi_scan_add_handler(handler);                   
    if (error)
        return error;                                            
    acpi_sysfs_add_hotplug_profile(&handler->hotplug, hotplug_profile_name);
    return 0;                                                               
}

int acpi_scan_add_handler_with_hotplug(struct acpi_scan_handler *handler,   
                       const char *hotplug_profile_name)       
{ 
    int error;                                                 
    error = acpi_scan_add_handler(handler);                   
    if (error)                                                 
        return error;                                         
    acpi_sysfs_add_hotplug_profile(&handler->hotplug, hotplug_profile_name);
    return 0; 
}

void acpi_sysfs_add_hotplug_profile(struct acpi_hotplug_profile *hotplug,
                    const char *name)                         
{
    int error;                                                 
    if (!hotplug_kobj)                                         
        goto err_out;                                         
    error = kobject_init_and_add(&hotplug->kobj,               
        &acpi_hotplug_profile_ktype, hotplug_kobj, "%s", name);          
    if (error) {                                               
        kobject_put(&hotplug->kobj);                           
        goto err_out;                                         
    } 
    kobject_uevent(&hotplug->kobj, KOBJ_ADD);                 
    return;                                                   
 err_out:                                                     
    pr_err(PREFIX "Unable to add hotplug profile '%s'\n", name);         
}

其实就是将pci_root_handler加入到acpi_scan_handlers_list中, 然后通过调用acpi_sysfs_add_hotplug_profile,内核为特定的ACPI热插拔设备或事件创建一个或多个属性文件,使得用户空间工具或脚本能够读取这些配置,或者在某些情况下修改它们,从而动态调整系统的行为。这对于实现系统的动态电源管理、设备状态监控或自定义热插拔响应行为特别有用。

其中,pci_root_handler的成员attach赋值为acpi_pci_root_add,见字如晤,pciroot设备添加的根本好像就在其中, 跟下去看了一下, 发现如下代码:

C 复制代码
......
status = acpi_evaluate_integer(handle, METHOD_NAME__SEG, NULL,
                   &segment);                                 
if (ACPI_FAILURE(status) && status != AE_NOT_FOUND) {         
    dev_err(&device->dev,  "can't evaluate _SEG\n");          
    result = -ENODEV;                                         
    goto end;                                                 
}                                                             
/* Check _CRS first, then _BBN.  If no _BBN, default to zero. */
root->secondary.flags = IORESOURCE_BUS;
status = try_get_root_bridge_busnr(handle, &root->secondary);
if (ACPI_FAILURE(status)) {
    /*
     * We need both the start and end of the downstream bus range
     * to interpret _CBA (MMCONFIG base address), so it really is
     * supposed to be in _CRS.  If we don't find it there, all we
     * can do is assume [_BBN-0xFF] or [0-0xFF].
     */
    root->secondary.end = 0xFF;
    dev_warn(&device->dev,
         FW_BUG "no secondary bus range in _CRS\n");
    status = acpi_evaluate_integer(handle, METHOD_NAME__BBN,
                       NULL, &bus);
    if (ACPI_SUCCESS(status))
        root->secondary.start = bus;
    else if (status == AE_NOT_FOUND)
        root->secondary.start = 0;
    else {
        dev_err(&device->dev, "can't evaluate _BBN\n");
        result = -ENODEV;
        goto end;
    }
}

root->device = device;
root->segment = segment & 0xFFFF;
strcpy(acpi_device_name(device), ACPI_PCI_ROOT_DEVICE_NAME);
strcpy(acpi_device_class(device), ACPI_PCI_ROOT_CLASS);
device->driver_data = root;
/*
 * TBD: Need PCI interface for enumeration/configuration of roots.
 */

/*
 * Scan the Root Bridge
 * --------------------
 * Must do this prior to any attempt to bind the root device, as the
 * PCI namespace does not get created until this call is made (and
 * thus the root bridge's pci_dev does not exist).
 */
root->bus = pci_acpi_scan_root(root);
if (!root->bus) {
    dev_err(&device->dev,
        "Bus %04x:%02x not present in PCI namespace\n",
        root->segment, (unsigned int)root->secondary.start);
    device->driver_data = NULL;
    result = -ENODEV;
    goto remove_dmar;
}

if (no_aspm)
    pcie_no_aspm();

pci_acpi_add_bus_pm_notifier(device);
device_set_wakeup_capable(root->bus->bridge, device->wakeup.flags.valid);

代码中先是通过acpi_evaluate_integer(handle, METHOD_NAME__SEG, NULL, &segment)ACPI空间中找到_SEG段, 这个_SEG段是什么呢? 看一下ACPI_Spec_6_4_Jan22.pdf:

6.5.6 _SEG (Segment)

The optional _SEG object is located under a PCI host bridge and evaluates to an integer that describes the PCI Segment Group (see PCI Firmware Specification v3.0). If _SEG does not exist, OSPM assumes that all PCI bus segments are in PCI Segment Group 0.

所以, _SEGACPI规范中PCI主桥下可选的一个对象, 倘若你有含有PCI设备初始化的固件的源代码, 可以grep一下看看, 我在UEFI的相关板卡代码中找到了, 在Coreboot中也找到了:

bash 复制代码
[mxd@5 coreboot]$ grep _SEG -rnw src/
src/soc/intel/common/block/acpi/acpi/northbridge.asl:9:Name (_SEG, 0)  // _SEG: PCI Segment
src/soc/intel/skylake/acpi/systemagent.asl:8:Name (_SEG, 0)  // _SEG: PCI Segment

其中Name(_SEG, 0)表示_SEG = 0.

所以acpi_evaluate_integer(handle, METHOD_NAME__SEG, NULL, &segment)也就是将_SEG的值返回到segment中(太分散的函数自己看啦, 也可以AI一下).

如果没有得到正常的_SEG值, 则认为没有PCIE系统, 如果得到了, 就再通过try_get_root_bridge_busnr查找_CRS, 根据注释, 这里是先查找_CRS, 接着找_BBN, 如果_BBN不为空, 则将其值作为其下游总线值, 否则认为其下游总线不存在, 从0开始. 代码中赋值的变量为:root->secondary.start. 查看root的结构:

C 复制代码
struct acpi_pci_root {
    struct acpi_device * device;
    struct pci_bus *bus;
    u16 segment;
    struct resource secondary;  /* downstream bus range */  

    u32 osc_support_set;    /* _OSC state of support bits */
    u32 osc_control_set;    /* _OSC state of control bits */
    phys_addr_t mcfg_addr;
};      

只有下游总线, 符合PCIE根节点的特性, 这个root便是内核管理pcie的入口, 它的下游总线便是PCIE0号总线.

接着将传参进来的device和上面得到的_SEG的值分别赋值root的成员, 并将PCI Root Bridgepci_bridge分别赋值给device的成员. 而device->driver_data = root则又将root赋值给了它自身.

然后又使用acpi_pci_root_get_mcfg_addr(handle)获取mcfg的值, 但是根据_CBA查找的, 我在UEFIcoreboot中都没找到, 所以就假设没有吧.

之后通过pci_acpi_scan_root(root)继续扫描Root Bridge. 注释说必须在绑定设备之前做这件事, 因为PCI名字空间在调用之前还不存在, 所以Root Bridgepci_dev设备将不存在.

所以必须先扫描Root Bridge, 添加一个pci_dev, 以防之后的桥和设备无法加入进来.

pci_acpi_scan_root与架构相关, 以下是loongarch的代码:

C 复制代码
struct pci_bus *pci_acpi_scan_root(struct acpi_pci_root *root)            
{                                                                         
......
......
    info->cfg = pci_acpi_setup_ecam_mapping(root);                        
......                                                                          
    root_ops->release_info = acpi_release_root_info;                      
    root_ops->prepare_resources = acpi_prepare_root_resources;            
    root_ops->pci_ops = &info->cfg->ops->pci_ops;                         
                                                                          
    bus = pci_find_bus(domain, busnum);                                   
    if (bus) {                                                            
        memcpy(bus->sysdata, info->cfg, sizeof(struct pci_config_window));
        kfree(info);                                                      
    } else {                                                              
        struct pci_bus *child;                                            
                                                                          
        bus = acpi_pci_root_create(root, root_ops,                        
                       &info->common, info->cfg);                         
......                                                                          
        pci_bus_size_bridges(bus);                                        
        pci_bus_assign_resources(bus);                  
        list_for_each_entry(child, &bus->children, node)
            pcie_bus_configure_settings(child);         
    }                                                   
                                                        
    return bus;                                         
}                                                       

首先还是通过pci_acpi_setup_ecam_mapping获取mcfg中给的addr, 具体是通过pci_mcfg_list查询后赋值, 而pci_mcfg_list是通过pci_mcfg_parse来的, 也就是固件传上来的ACPI表中的MCFG表的内容. 这个表在固件代码中是可以找到的:

bash 复制代码
[mengxiangdong@5 coreboot]$ grep MCFG -rinw src/mainboard/
src/mainboard/prodrive/atlas/romstage_fsp_params.c:34:	FSP_M_CONFIG *mcfg = &memupd->FspmConfig;
src/mainboard/prodrive/atlas/romstage_fsp_params.c:55:		mcfg->HyperThreading = 0;
src/mainboard/prodrive/atlas/romstage_fsp_params.c:56:		mcfg->DisPgCloseIdleTimeout = 1;

通过以上多次折腾, 终于又再次获取到了MCFG中传的地址, 将其赋值给了root->mcfg_addr. 另外在pci_acpi_setup_ecam_mapping中还注册了pcie的一些操作函数:

C 复制代码
/* ECAM ops */                                 
struct pci_ecam_ops pci_generic_ecam_ops = {   
    .bus_shift  = 20,                          
    .pci_ops    = {                            
        .map_bus    = pci_ecam_map_bus,        
        .read       = pci_generic_config_read, 
        .write      = pci_generic_config_write,
    }                                          
};                                             

PCI Express (PCIe) 中的 Enhanced Configuration Access Mechanism (ECAM) 是一种访问PCIe设备配置空间的机制。在传统的PCI系统中,配置空间可以通过两种方式访问:一是通过I/O端口(Configuration Address and Data Registers,如CONFIG_ADDRESS和CONFIG_DATA寄存器),二是采用PCI Compatibility Mechanism (CAM)。而PCIe扩展了这一机制,引入了ECAM来更高效地访问设备配置空间。
ECAM工作原理是将PCIe设备的部分配置空间映射到系统的内存空间(Memory Space),使得CPU可以直接通过内存访问指令来读写这些配置信息,而不需要通过专门的I/O操作。这样做提高了配置访问的效率,同时也简化了软件层面的处理逻辑。
具体来说,ECAM会在系统的内存映射中分配一个区域,这个区域包含了根复合体(Root Complex)中所有PCIe端口及其连接设备的基本配置空间的映射。这些映射通常包括设备的VID(Vendor ID)、DID(Device ID)、状态和控制寄存器等基本信息。通过这种方式,操作系统或驱动程序能够快速地定位并配置PCIe设备,进行资源分配、电源管理、错误处理等操作。
总结起来,ECAM是PCIe架构中为了提升配置访问效率和便利性而设计的一种机制,它通过内存映射的方式,使配置空间访问更加直接和高效。

先前root->secondary.startroot->secondary.end也就是其下游总线的范围已经设定, 是0-255, 现在通过pci_find_bus(domain, busnum)在其下游总线中继续查找总线. 倘若找到了, 则把之前root初始化的相应配置复制给该bus. 如果没找到则再通过acpi_pci_root_create创建当前root总线的资源, 并通过pci_register_host_bridge注册为一个bridge类型的设备. 由于第一次初始化时总线号通常为0, 所以和没找到效果一样, 都会再次创建root资源.

C 复制代码
struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root,
                     struct acpi_pci_root_ops *ops,             
                     struct acpi_pci_root_info *info,           
                     void *sysdata)                             
{                                                               
......
    pci_acpi_root_add_resources(info);                   
    pci_add_resource(&info->resources, &root->secondary);
    bus = pci_create_root_bus(NULL, busnum, ops->pci_ops,
                  sysdata, &info->resources);            
......
    pci_scan_child_bus(bus);                                            
    pci_set_host_bridge_release(host_bridge, acpi_pci_root_release_info,
                    info);                                              
struct pci_bus *pci_create_root_bus(struct device *parent, int bus,     
        struct pci_ops *ops, void *sysdata, struct list_head *resources)
{                                                                       
......
    bridge->dev.parent = parent;                  

    list_splice_init(resources, &bridge->windows);
    bridge->sysdata = sysdata;                    
    bridge->busnr = bus;                          
    bridge->ops = ops;                            

    error = pci_register_host_bridge(bridge);     
......
}

而在pci_register_host_bridge(bridge)过程中, 有些打印函数:

C 复制代码
pr_info("PCI host bridge to bus %s\n", name);
dev_info(&bus->dev, "root bus resource %pR%s\n", res, addr);

这一点可以在系统启动过程中查看:

bash 复制代码
root@loongson-pc:/home/loongson# dmesg  | grep "PCI host bridge to bus" 
[    0.065457] PCI host bridge to bus 0000:00
root@loongson-pc:/home/loongson# dmesg | grep "root bus resource"     
[    0.065846] pci_bus 0000:00: root bus resource [io  0x4000-0xffff window] (bus address [0x0000-0xbfff])
[    0.066753] pci_bus 0000:00: root bus resource [mem 0x40000000-0x7fffffff window]
[    0.067476] pci_bus 0000:00: root bus resource [bus 00-7f]

因此可知, 通过dev_set_name(&bridge->dev, "pci%04x:%02x", pci_domain_nr(bus),bridge->busnr)设定该设备的名称为0000:00. 其中前面的0000是阈值, 后面的00bus号.

在PCI Express (PCIe) 架构中,域(Domain)和总线号(Bus Number)是用来唯一标识和组织PCIe设备的两个关键概念,它们帮助系统识别和寻址连接在PCIe拓扑中的各个设备。下面是它们的主要区别:

Domain

  • 概念:域是一个较高层次的概念,用于区分不同的根复合体(Root Complexes)或者说是PCIe层次结构中的顶级分支。每个域代表了一组由同一个根复合体管理的PCIe总线集合。在大多数桌面和服务器系统中,只有一个域,即Domain 0,但在多主机或复杂的系统配置中,可能存在多个域来隔离不同的硬件资源或实现故障隔离。
  • 作用:域的主要目的是确保地址空间的独立性,即不同域中的设备可以拥有相同的总线、设备和功能编号,而不会引起冲突,因为它们在不同的域中是完全隔离的。

Bus Number

  • 概念:总线号是PCIe层次结构中的一部分,用于标识属于同一域内的不同PCIe总线。每个PCIe总线可以连接多个设备,每个设备又可以具有多个功能(Function)。总线号连同设备号(Device Number)和功能号(Function Number)共同构成了PCIe设备的BDF(Bus, Device, Function)地址,这是设备在系统中的唯一标识。
  • 作用:总线号帮助系统在特定域内部进一步细分和寻址不同的设备。通过总线号,系统可以定位到特定的PCIe总线上,进而访问该总线上的设备和功能。

再回到acpi_pci_root_create中, 注册完host_bridge后, 又执行了pci_scan_child_bus(bus).

9. PCIE设备枚举

pci_scan_child_bus(bus)调用的是pci_scan_child_bus_extend:

C 复制代码
static unsigned int pci_scan_child_bus_extend(struct pci_bus *bus,     
                          unsigned int available_buses)                
{                                                                      
    unsigned int used_buses, normal_bridges = 0, hotplug_bridges = 0;  
    unsigned int start = bus->busn_res.start;                          
    unsigned int devfn, fn, cmax, max = start;                         
    struct pci_dev *dev;                                               
    int nr_devs;                                                       
                                                                       
    dev_dbg(&bus->dev, "scanning bus\n");                              
                                                                       
    /* Go find them, Rover! */                                         
    for (devfn = 0; devfn < 256; devfn += 8) {                         
        nr_devs = pci_scan_slot(bus, devfn);                           
                                                                       
        /*                                                             
         * The Jailhouse hypervisor may pass individual functions of a 
         * multi-function device to a guest without passing function 0.
         * Look for them as well.                                      
         */                                                            
        if (jailhouse_paravirt() && nr_devs == 0) {                    
            for (fn = 1; fn < 8; fn++) {                               
                dev = pci_scan_single_device(bus, devfn + fn);         
                if (dev)                                               
                    dev->multifunction = 1;                            
            }                                                          
        }                                                              
    }                                                                  
    /* Reserve buses for SR-IOV capability */                         
    used_buses = pci_iov_bus_range(bus);                              
    max += used_buses;                                                
                                                                      
    /*                                                                
     * After performing arch-dependent fixup of the bus, look behind  
     * all PCI-to-PCI bridges on this bus.                            
     */                                                               
    if (!bus->is_added) {                                             
        dev_dbg(&bus->dev, "fixups for bus\n");                       
        pcibios_fixup_bus(bus);                                       
        bus->is_added = 1;                                            
    }                                                                 
                                                                      
    /*                                                                
     * Calculate how many hotplug bridges and normal bridges there    
     * are on this bus. We will distribute the additional available   
     * buses between hotplug bridges.                                 
     */                                                               
    for_each_pci_bridge(dev, bus) {                                   
        if (dev->is_hotplug_bridge)                                   
            hotplug_bridges++;                                        
        else                                                          
            normal_bridges++;                                         
    }                                                                 
                                                                      
    /*                                                                
     * Scan bridges that are already configured. We don't touch them  
     * unless they are misconfigured (which will be done in the second
     * scan below).                                                   
     */                                                               
    for_each_pci_bridge(dev, bus) {                                   
......                        
    }                                                                 
    /* Scan bridges that need to be reconfigured */              
    for_each_pci_bridge(dev, bus) {                              
......                    
    }                                                            

PCIE域中, 访问某个设备的地址, 需要指定准确的8bitbus号, 5bitdevice号, 和3bitfunction号, 所以一个PCIE域中, 最多含有256bus, 32device, 和8function.

一开始通过256for循环执行pci_scan_slot(bus, devfn), 其中devfndevice funciton的缩写, device funciton刚好总共占8bit, 最大值也是256.

关于pci_scan_slot(bus, devfn)函数:

C 复制代码
int pci_scan_slot(struct pci_bus *bus, int devfn)                        
{                                                                        
    unsigned fn, nr = 0;                                                 
    struct pci_dev *dev;                                                 
                                                                         
    if (only_one_child(bus) && (devfn > 0))                              
        return 0; /* Already scanned the entire slot */                  
                                                                         
    dev = pci_scan_single_device(bus, devfn);                            
    if (!dev)                                                            
        return 0;                                                        
    if (!pci_dev_is_added(dev))                                          
        nr++;                                                            
                                                                         
    for (fn = next_fn(bus, dev, 0); fn > 0; fn = next_fn(bus, dev, fn)) {
        dev = pci_scan_single_device(bus, devfn + fn);                   
        if (dev) {                                                       
            if (!pci_dev_is_added(dev))                                  
                nr++;                                                    
            dev->multifunction = 1;                                      
        }                                                                
    }                                                                    
                                                                         
    /* Only one slot has PCIe device */                                  
    if (bus->self && nr)                                                 
        pcie_aspm_init_link_state(bus->self);                            
                                                                         
    return nr;                                                           
}                                                                        

我让AI帮我解释了一遍:

这段代码是Linux内核中用于扫描PCI或PCI Express (PCIe) 总线上的设备的函数,特别是针对给定的总线(bus)和设备-功能号(devfn)进行扫描。下面是对主要步骤的解释:

  1. 入口参数:
  • struct pci_bus *bus: 指向当前正在被扫描的PCI总线的指针。

  • int devfn: 设备-功能号,一个16位的值,其中高8位表示设备号,低8位表示功能号,用于唯一标识PCI总线上的一个特定设备及其功能。

  1. 判断是否已经扫描过整个插槽:
  • 函数首先检查当前总线是否只有一个子设备(通过only_one_child(bus)),并且如果devfn > 0,意味着之前已经扫描过至少一个设备。如果是这种情况,函数直接返回0,因为不需要再次扫描。
  1. 扫描单个设备:
  • 调用pci_scan_single_device(bus, devfn)尝试扫描指定设备-功能号上的设备。如果找到设备,继续后续处理;如果没有找到,直接返回0。
  1. 计数新增设备:
  • 如果设备未被系统添加(通过pci_dev_is_added(dev)检查),增加设备计数器nr。这用于跟踪新发现的设备数量。
  1. 扫描多功能设备:
  • 对于多功能设备(Multifunction Device),通过循环调用next_fn(bus, dev, fn)递增功能号fn,并继续调用pci_scan_single_device来扫描同一物理插槽上的其他功能。同样,若新发现的设备未被添加,则增加计数器nr,并标记该设备为多功能设备(dev->multifunction = 1)。
  1. PCIe ASPM初始化:
  • 如果当前总线有自描述符(通过bus->self检查)并且找到了至少一个新的设备(nr大于0),则调用pcie_aspm_init_link_state(bus->self)来初始化PCI Express链路状态,这通常涉及到电源管理设置,以优化性能和功耗。
  1. 返回结果:
  • 最后,函数返回新发现的设备数量nr

其中pci_scan_single_device尝试扫描指定设备-功能号上的设备, 先在已有的bus->devices中找, 如果找到设备, 则直接返回, 如果没找到, 就按照devfn号去扫描总线查找:

C 复制代码
struct pci_dev *pci_scan_single_device(struct pci_bus *bus, int devfn)
{                                                                     
    struct pci_dev *dev;                                              
                                                                      
    dev = pci_get_slot(bus, devfn);                                   
    if (dev) {                                                        
        pci_dev_put(dev);                                             
        return dev;                                                   
    }                                                                 
                                                                      
    dev = pci_scan_device(bus, devfn);                                
    if (!dev)                                                         
        return NULL;                                                  
                                                                      
    pci_device_add(dev, bus);                                         
                                                                      
    return dev;                                                       
}                                                                     

struct pci_dev *pci_get_slot(struct pci_bus *bus, unsigned int devfn)
{                                                                    
    struct pci_dev *dev;                                             
                                                                     
    WARN_ON(in_interrupt());                                         
    down_read(&pci_bus_sem);                                         
                                                                     
    list_for_each_entry(dev, &bus->devices, bus_list) {              
        if (dev->devfn == devfn)                                     
            goto out;                                                
    }                                                                
                                                                     
    dev = NULL;                                                      
 out:                                                                
    pci_dev_get(dev);                                                
    up_read(&pci_bus_sem);                                           
    return dev;                                                      
}                                                                    

但是一开始设备不会在bus->devices列表中, 所以按devfn查找的方式才是重点:

C 复制代码
static struct pci_dev *pci_scan_device(struct pci_bus *bus, int devfn)
{                                                                     
    struct pci_dev *dev;                                              
    u32 l;                                                            
                                                                      
    if (!pci_bus_read_dev_vendor_id(bus, devfn, &l, 60*1000))         
        return NULL;                                                  
                                                                      
    dev = pci_alloc_dev(bus);                                         
    if (!dev)                                                         
        return NULL;                                                  
                                                                      
    dev->devfn = devfn;                                               
    dev->vendor = l & 0xffff;                                         
    dev->device = (l >> 16) & 0xffff;                                 
                                                                      
    pci_set_of_node(dev);                                             
                                                                      
    if (pci_setup_device(dev)) {                                      
        pci_bus_put(dev->bus);                                        
        kfree(dev);                                                   
        return NULL;                                                  
    }                                                                 
                                                                      
    return dev;                                                       
}                                                                     

首先是读取设备的vernder id, 超过60秒没读到就认为读不到. 然后便是通过pci_setup_device(dev)读取设备的配置空间并设定dev的相关成员, 加入到全局链表当中. (pci_set_of_node(dev)是从设备树节点中分析相关属性, 在ACPI传参的系统中不用设备树, 这里就不看了.)

以上便是查找第一个设备的流程, 结束后会重新回到之前的for循环当中, 直到把整个bus总线下的设备扫描完毕.

然后是pci_scan_bridge_extend()就是用于扫描pci桥和pci桥下的所有设备, 这个函数会被调用2次,第一次是处理BIOS已经配置好的pci桥, 这个是为了兼容各个架构所做的妥协。通过2次调用pci_scan_bridge_extend函数,完成所有的pci桥的处理。

至此第一根bus, 也就是host bridge下的设备和桥就全部扫描完毕了, 这也是深度优先算法的本质. 但通常, 很多系统也就只有一个host bridge.

参考文献

  1. 通义千问
  2. PCIE总线初始化
  3. PCIe初始化枚举和资源分配流程分析
  4. 内核源码
相关推荐
无为之士14 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
岑梓铭31 分钟前
(CentOs系统虚拟机)Standalone模式下安装部署“基于Python编写”的Spark框架
linux·python·spark·centos
努力学习的小廉31 分钟前
深入了解Linux —— make和makefile自动化构建工具
linux·服务器·自动化
MZWeiei35 分钟前
Zookeeper基本命令解析
大数据·linux·运维·服务器·zookeeper
7yewh1 小时前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
小张认为的测试1 小时前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
打鱼又晒网1 小时前
linux网络套接字 | 深度解析守护进程 | 实现tcp服务守护进程化
linux·网络协议·计算机网络·tcp
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
蜜獾云1 小时前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
小屁不止是运维1 小时前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构