1. 找到函数入口
在大多数linux
系统中, /boot
下通常含有一个名为System.map*
的文件, 其中记录了内核的符号表, 如果没有的话也可以通过命令生成:
nm -n vmlinux | grep -v '\( [aNUw] \)\|\(__crc_\)\|\( \$[adt]\)\|\( .L\)' > System.map
然后查找相应函数的初始化等级, 以pci
为例:
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
, 可以通过关键字样搜索:
[mxd@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
就是设定初始化等级的函数, 跳转后发现其他的初始化等级定义:
#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初始化的流程大致是:
|__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函数
函数内容很少, 如下:
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()
的声明是:
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函数
函数内容很少, 如下:
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函数
核心代码:
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
函数核心代码:
ret = register_acpi_bus_type(&acpi_pci_bus);
即注册了acpi_bus_type
类型的acpi_pci_bus
. 与前面类似暂不分析.
6. pci_slot_init函数
核心代码:
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
的内容:
#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
:
[mxd@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)