内核下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
注册的参数解析函数具有以下特点:
- 早期执行 :
early_param
函数在内核初始化的非常早的阶段被执行,早于大多数内核子系统的设置,这对于需要在某些关键子系统初始化前设置参数的场景非常有用。- 限制使用 :由于执行时机很早,使用
early_param
的函数受到限制,不能依赖于复杂的内核服务,如内存分配器或锁机制,因为那时它们可能还未初始化。- 用途特殊:它通常用于设置对后续引导流程有重大影响的参数,例如调试选项、内存布局调整或硬件配置等。
- 定义方式 :
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_dev
和pcibus_groups
, 前者用于释放此pcibus
, 包括减少此pcibus
的引用和清空pcibus
中的资源及删除此总线. 后者则注册了一个结构体数组用于记录设备的一些参数.
4. pci_driver_init函数
核心代码:
C
ret = bus_register(&pci_bus_type);
在Linux内核中,
bus_register
函数用于注册一个总线(bus)对象。总线在内核中扮演着连接设备和驱动程序的桥梁角色,负责设备的探测、识别、匹配以及驱动程序的加载。当调用bus_register
时,内核会执行以下几个关键操作:
- 初始化总线结构 :确保总线结构体(通常是
struct bus_type
)的所有必要字段已经被正确填充,比如名字、匹配函数、枚举函数等。- 添加通知链:总线可能会注册一些通知链,以便在特定事件发生时通知感兴趣的子系统或模块。这有助于模块间的通信和协调。
- 设备模型集成:总线会被加入到内核的设备模型中,这意味着它现在可以参与设备的探测和匹配过程。
- 挂载总线:总线会被挂在内核的总线系统树上,这是一个描述系统中所有总线、设备和驱动程序之间关系的数据结构。
- 启动探测:根据总线的特性,可能会触发设备的探测过程,尝试找出连接到该总线上的所有设备。
- 事件通知:可能会触发一些内核事件,通知其他子系统总线已经注册,从而允许它们采取相应的行动,比如注册设备或驱动到该总线上。
通过
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
,见字如晤,pci
的root
设备添加的根本好像就在其中, 跟下去看了一下, 发现如下代码:
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.
所以, _SEG
是ACPI
规范中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
的入口, 它的下游总线便是PCIE
的0
号总线.
接着将传参进来的device
和上面得到的_SEG
的值分别赋值root
的成员, 并将PCI Root Bridge
和pci_bridge
分别赋值给device
的成员. 而device->driver_data = root
则又将root
赋值给了它自身.
然后又使用acpi_pci_root_get_mcfg_addr(handle)
获取mcfg
的值, 但是根据_CBA
查找的, 我在UEFI
和coreboot
中都没找到, 所以就假设没有吧.
之后通过pci_acpi_scan_root(root)
继续扫描Root Bridge
. 注释说必须在绑定设备之前做这件事, 因为PCI
名字空间在调用之前还不存在, 所以Root Bridge
的pci_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.start
和root->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
是阈值, 后面的00
是bus
号.
在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
域中, 访问某个设备的地址, 需要指定准确的8bit
的bus
号, 5bit
的device
号, 和3bit
的function
号, 所以一个PCIE
域中, 最多含有256
个bus
, 32
的device
, 和8
个function
.
一开始通过256
次for
循环执行pci_scan_slot(bus, devfn)
, 其中devfn
是device 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
)进行扫描。下面是对主要步骤的解释:
- 入口参数:
struct pci_bus *bus
: 指向当前正在被扫描的PCI总线的指针。
int devfn
: 设备-功能号,一个16位的值,其中高8位表示设备号,低8位表示功能号,用于唯一标识PCI总线上的一个特定设备及其功能。
- 判断是否已经扫描过整个插槽:
- 函数首先检查当前总线是否只有一个子设备(通过
only_one_child(bus)
),并且如果devfn > 0
,意味着之前已经扫描过至少一个设备。如果是这种情况,函数直接返回0,因为不需要再次扫描。
- 扫描单个设备:
- 调用
pci_scan_single_device(bus, devfn)
尝试扫描指定设备-功能号上的设备。如果找到设备,继续后续处理;如果没有找到,直接返回0。
- 计数新增设备:
- 如果设备未被系统添加(通过
pci_dev_is_added(dev)
检查),增加设备计数器nr
。这用于跟踪新发现的设备数量。
- 扫描多功能设备:
- 对于多功能设备(Multifunction Device),通过循环调用
next_fn(bus, dev, fn)
递增功能号fn
,并继续调用pci_scan_single_device
来扫描同一物理插槽上的其他功能。同样,若新发现的设备未被添加,则增加计数器nr
,并标记该设备为多功能设备(dev->multifunction = 1
)。
- PCIe ASPM初始化:
- 如果当前总线有自描述符(通过
bus->self
检查)并且找到了至少一个新的设备(nr
大于0),则调用pcie_aspm_init_link_state(bus->self)
来初始化PCI Express链路状态,这通常涉及到电源管理设置,以优化性能和功耗。
- 返回结果:
- 最后,函数返回新发现的设备数量
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
.