----------------------------------------------------------------------------------------------------------------------------内核版本:linux 5.2.8根文件系统:busybox 1.25.0u-boot:2016.05----------------------------------------------------------------------------------------------------------------------------
前面几节内容我们介绍了设备树的结构,以及在linux内核中移植设备树。这一节将对linux内核源码进行介绍,分析内核对设备的处理逻辑。
一、汇编阶段
1.1 uboot引导内核启动
在linux设备树-linux内核设备树移植(一)中我们介绍了uboot引导linux内核的启动流程。uboot会为内核设置启动参数,最终并跳转到内核地址、启动内核。
uboot在启动内核时会设置三个参数,这三个参数会依次赋值给寄存器r0、r1、r2:
- r0一般设置为0;
- r1一般设置为machine id,在使用设备树的时候,这个参数就没有意义了;
- r2一般设置为tag的开始地址,或者设置为dtb文件的开始地址;
当我们使用了设备树的时候,我们主要关注r2参数即可。
1.2 head.S
内核启动,首先执行的是arch/arm/kernel/head.S处的汇编代码,内核head.S主要做了以下工作:
- 执行子程序__lookup_processor_type,使用汇编指令读取CPU ID, 根据该ID找到对应的proc_info_list结构体(里面含有这类CPU的初始化函数、信息);
- 执行子程序__vet_atags,判断是否存在可用的tag或dtb;
- 执行子程序__create_page_tables:创建页表, 即创建虚拟地址和物理地址的映射关系;
- 执行子程序__enable_mmu:使能MMU, 以后就要使用虚拟地址了;
- 执行子程序__mmap_switched:__enable_mmu内部会调转到这里执行:
__mmap_switched子程序定义在是arch/arm/kernel/head-common.S文件中,该段子程序会调用C函数start_kernel。
以上的内核大概了解一下即可,这里不去分析具体源码了。head.S和head-common.S最终效果:
- 把uboot传来的r1值,赋给了C变量 __machine_arch_type;
- 把uboot传来的r2值,赋给了C变量__atags_pointer;
- 然后执行C函数start_kernel。
二、内核启动
start_kernel函数位于init/main.c文件:


- asmlinkage __visible void __init start_kernel(void)
- {
- char *command_line;
- char *after_dashes;
-
- set_task_stack_end_magic(&init_task);
- smp_setup_processor_id();
- debug_objects_early_init();
-
- cgroup_init_early();
-
- local_irq_disable();
- early_boot_irqs_disabled = true;
-
- /*
- * Interrupts are still disabled. Do necessary setups, then
- * enable them.
- */
- boot_cpu_init();
- page_address_init();
- pr_notice("%s", linux_banner);
- setup_arch(&command_line);
- mm_init_cpumask(&init_mm);
- setup_command_line(command_line);
- setup_nr_cpu_ids();
- setup_per_cpu_areas();
- smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
- boot_cpu_hotplug_init();
-
- build_all_zonelists(NULL);
- page_alloc_init();
-
- pr_notice("Kernel command line: %s\n", boot_command_line);
- /* parameters may set static keys */
- jump_label_init();
- parse_early_param();
- after_dashes = parse_args("Booting kernel",
- static_command_line, __start___param,
- __stop___param - __start___param,
- -1, -1, NULL, &unknown_bootoption);
- if (!IS_ERR_OR_NULL(after_dashes))
- parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
- NULL, set_init_arg);
-
- /*
- * These use large bootmem allocations and must precede
- * kmem_cache_init()
- */
- setup_log_buf(0);
- vfs_caches_init_early();
- sort_main_extable();
- trap_init();
- mm_init();
-
- ftrace_init();
-
- /* trace_printk can be enabled here */
- early_trace_init();
-
- /*
- * Set up the scheduler prior starting any interrupts (such as the
- * timer interrupt). Full topology setup happens at smp_init()
- * time - but meanwhile we still have a functioning scheduler.
- */
- sched_init();
- /*
- * Disable preemption - early bootup scheduling is extremely
- * fragile until we cpu_idle() for the first time.
- */
- preempt_disable();
- if (WARN(!irqs_disabled(),
- "Interrupts were enabled *very* early, fixing it\n"))
- local_irq_disable();
- radix_tree_init();
-
- /*
- * Set up housekeeping before setting up workqueues to allow the unbound
- * workqueue to take non-housekeeping into account.
- */
- housekeeping_init();
-
- /*
- * Allow workqueue creation and work item queueing/cancelling
- * early. Work item execution depends on kthreads and starts after
- * workqueue_init().
- */
- workqueue_init_early();
-
- rcu_init();
-
- /* Trace events are available after this */
- trace_init();
-
- if (initcall_debug)
- initcall_debug_enable();
-
- context_tracking_init();
- /* init some links before init_ISA_irqs() */
- early_irq_init();
- init_IRQ();
- tick_init();
- rcu_init_nohz();
- init_timers();
- hrtimers_init();
- softirq_init();
- timekeeping_init();
-
- /*
- * For best initial stack canary entropy, prepare it after:
- * - setup_arch() for any UEFI RNG entropy and boot cmdline access
- * - timekeeping_init() for ktime entropy used in rand_initialize()
- * - rand_initialize() to get any arch-specific entropy like RDRAND
- * - add_latent_entropy() to get any latent entropy
- * - adding command line entropy
- */
- rand_initialize();
- add_latent_entropy();
- add_device_randomness(command_line, strlen(command_line));
- boot_init_stack_canary();
-
- time_init();
- printk_safe_init();
- perf_event_init();
- profile_init();
- call_function_init();
- WARN(!irqs_disabled(), "Interrupts were enabled early\n");
-
- early_boot_irqs_disabled = false;
- local_irq_enable();
-
- kmem_cache_init_late();
-
- /*
- * HACK ALERT! This is early. We're enabling the console before
- * we've done PCI setups etc, and console_init() must be aware of
- * this. But we do want output early, in case something goes wrong.
- */
- console_init();
- if (panic_later)
- panic("Too many boot %s vars at `%s'", panic_later,
- panic_param);
-
- lockdep_init();
-
- /*
- * Need to run this when irqs are enabled, because it wants
- * to self-test [hard/soft]-irqs on/off lock inversion bugs
- * too:
- */
- locking_selftest();
-
- /*
- * This needs to be called before any devices perform DMA
- * operations that might use the SWIOTLB bounce buffers. It will
- * mark the bounce buffers as decrypted so that their usage will
- * not cause "plain-text" data to be decrypted when accessed.
- */
- mem_encrypt_init();
-
- #ifdef CONFIG_BLK_DEV_INITRD
- if (initrd_start && !initrd_below_start_ok &&
- page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
- pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
- page_to_pfn(virt_to_page((void *)initrd_start)),
- min_low_pfn);
- initrd_start = 0;
- }
- #endif
- kmemleak_init();
- setup_per_cpu_pageset();
- numa_policy_init();
- acpi_early_init();
- if (late_time_init)
- late_time_init();
- sched_clock_init();
- calibrate_delay();
- pid_idr_init();
- anon_vma_init();
- #ifdef CONFIG_X86
- if (efi_enabled(EFI_RUNTIME_SERVICES))
- efi_enter_virtual_mode();
- #endif
- thread_stack_cache_init();
- cred_init();
- fork_init();
- proc_caches_init();
- uts_ns_init();
- buffer_init();
- key_init();
- security_init();
- dbg_late_init();
- vfs_caches_init();
- pagecache_init();
- signals_init();
- seq_file_init();
- proc_root_init();
- nsfs_init();
- cpuset_init();
- cgroup_init();
- taskstats_init_early();
- delayacct_init();
-
- poking_init();
- check_bugs();
-
- acpi_subsystem_init();
- arch_post_acpi_subsys_init();
- sfi_init_late();
-
- /* Do the rest non-__init'ed, we're now alive */
- arch_call_rest_init();
- }
可以看出,start_kernel函数是整个Linux内核启动过程中的一个关键函数,它负责调用其他函数来逐步初始化各个子系统,最终启动用户空间。
start_kernel的调用过程如下:
2.1 setup_arch
start_kernel内部调用setup_arch函数,用于在内核启动期间对硬件进行初始化,包括设置 CPU、内存、设备树等等。该函数的实现是针对特定架构的,因此不同的架构会有不同的 setup_arch实现。在内核启动期间,setup_arch函数是被显式调用的,它的返回值是 0 或者一个错误码,用于指示初始化是否成功。
- setup_arch(&command_line); // arch/arm/kernel/setup.c
- mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c
- early_init_dt_verify(phys_to_virt(dt_phys) // 判断是否有效的dtb文件, drivers/of/fdt.c
- initial_boot_params = params;
- mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); // 找到最匹配的machine_desc, drivers/of/fdt.c
- early_init_dt_scan_nodes();
- machine_desc = mdesc;
- ......
2.1.1 DT_MACHINE_START
一个编译成uImage的内核镜像文件,可以支持多个单板,比如支持SMDK2440、SMDK2443、以及我们在linux设备树-linux内核设备树移植中加入的MINI2440 using devicetree。
这些板子的配置稍有不同,需要做一些单独的初始化,在内核里面,对于这些单板,都构造了一个machine_desc结构体。
比如我们新增的arch/arm/mach-s3c24xx/mach-smdk2440-dt.c文件:
- static const char *const s3c2440_dt_compat[] __initconst = {
- "samsung,s3c2440",
- "samsung,mini2440",
- NULL
- };
-
- DT_MACHINE_START(S3C2440_DT, "Samsung S3C2440 (Flattened Device Tree)")
- /* Maintainer: Heiko Stuebner <heiko@sntech.de> */
- .dt_compat = s3c2440_dt_compat,
- .map_io = s3c2440_dt_map_io,
- .init_irq = irqchip_init,
- .init_machine = s3c2440_dt_machine_init,
- MACHINE_END
宏DT_MACHINE_START定义在arch/arm/include/asm/mach/arch.h,如下:
- #define DT_MACHINE_START(_name, _namestr) \
- static const struct machine_desc __mach_desc_##_name \
- __used \
- __attribute__((__section__(".arch.info.init"))) = { \
- .nr = ~0, \
- .name = _namestr,
-
- #endif
这里attribute((section(“.arch.info.init”)))就是利用了编译器的特性,把machine_desc放到了.arch.info.init段。
2.1.2 of_flat_dt_match_machine
当我们的uboot不使用tag传参数,而使用dtb文件时,那么这时内核是如何选择对应的machine_desc呢?
在设备树文件的根节点里,有如下两行:
- model = "Samsung S3C2440 SoC";
- compatible = "samsung,s3c2440","samsung,mini2440";
这里的compatible属性声明想要什么machine_desc,属性值可以是一系列字符串,依次与machine_desc匹配。
内核最好支持samsung,s3c2440,如果不支持,再尝试是否支持samsung,mini2440。
of_flat_dt_match_machine函数就是通过遍历所有的machine_desc,找到与设备树中compatible最匹配的一个machine_desc,如果没有找到匹配的机器,则返回默认的机器指针;函数定义在drivers/of/fdt.c中;
- /**
- * of_flat_dt_match_machine - Iterate match tables to find matching machine.
- *
- * @default_match: A machine specific ptr to return in case of no match.
- * @get_next_compat: callback function to return next compatible match table.
- *
- * Iterate through machine match tables to find the best match for the machine
- * compatible string in the FDT.
- */
- const void * __init of_flat_dt_match_machine(const void *default_match,
- const void * (*get_next_compat)(const char * const**))
- {
- const void *data = NULL;
- const void *best_data = default_match;
- const char *const *compat;
- unsigned long dt_root;
- unsigned int best_score = ~1, score = 0;
-
- dt_root = of_get_flat_dt_root();
- while ((data = get_next_compat(&compat))) {
- score = of_flat_dt_match(dt_root, compat);
- if (score > 0 && score < best_score) {
- best_data = data;
- best_score = score;
- }
- }
- if (!best_data) {
- const char *prop;
- int size;
-
- pr_err("\n unrecognized device tree list:\n[ ");
-
- prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
- if (prop) {
- while (size > 0) {
- printk("'%s' ", prop);
- size -= strlen(prop) + 1;
- prop += strlen(prop) + 1;
- }
- }
- printk("]\n\n");
- return NULL;
- }
-
- pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
-
- return best_data;
- }
总结如下:
- 设备树根节点的compatible属性列出了一系列的字符串,表示它兼容的单板名,从"最兼容"到次之;
- 内核中有多个machine_desc,其中有dt_compat成员,,它指向一个字符串数组,,里面表示该machine_desc支持哪些单板;
2.1.3 early_init_dt_scan_nodes
early_init_dt_scan_nodes函数位于drivers/of/fdt.c文件:
- void __init early_init_dt_scan_nodes(void)
- {
- int rc = 0;
-
- /* Retrieve various information from the /chosen node */
- rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
- if (!rc)
- pr_warn("No chosen node found, continuing without\n");
-
- /* Initialize {size,address}-cells info */
- of_scan_flat_dt(early_init_dt_scan_root, NULL);
-
- /* Setup memory, calling early_init_dt_add_memory_arch */
- of_scan_flat_dt(early_init_dt_scan_memory, NULL);
- }
里面主要对三种类型的信息进行处理,分别是:
- /chosen节点中bootargs属性:/chosen节点中bootargs属性就是内核启动的命令行参数,它里面可以指定根文件系统在哪里,第一个运行的应用程序是哪一个,指定内核的打印信息从哪个设备里打印出来;这里将 /chosen节点中bootargs属性的值, 存入全局变量boot_command_line
- 根节点的 #address-cells 和 #size-cells属性:根节点的#address-cells和#size-cells属性指定属性参数的位数;存入全局变量: dt_root_addr_cells, dt_root_size_cells;
- /memory中的reg属性:/memory中的reg属性指定了不同板子内存的大小和起始地址;
2.1.4 设备节点转换为device_node
我们先想一个问题,我们的uboot把设备树dtb文件随便放到内存的某一个地方就可以使用,为什么内核运行中,他不会去覆盖dtb所占用的那块内存呢?
在前面我们讲解设备树结构时,我们知道,在设备树文件中,可以使用/memreserve/指定一块内存,这块内存就是保留的内存,内核不会占用它。即使你没有指定这块内存,当我们内核启动时,他也会把设备树所占用的区域保留下来。
如下就是函数调用过程:
- start_kernel // init/main.c
- setup_arch(&command_line); // arch/arm/kernel/setup.c
- arm_memblock_init(mdesc); // arch/arm/kernel/setup.c
- early_init_fdt_reserve_self();
- /* Reserve the dtb region */
- // 把dtb所占区域保留下来, 即调用: memblock_reserve
- early_init_dt_reserve_memory_arch(__pa(initial_boot_params),
- fdt_totalsize(initial_boot_params),
- 0);
- early_init_fdt_scan_reserved_mem(); // 根据dtb中的memreserve信息, 调用memblock_reserve
-
- unflatten_device_tree(); // arch/arm/kernel/setup.c
- __unflatten_device_tree(initial_boot_params, NULL, &of_root,
- early_init_dt_alloc_memory_arch, false); // drivers/of/fdt.c
-
- /* First pass, scan for size */
- size = unflatten_dt_nodes(blob, NULL, dad, NULL);
-
- /* Allocate memory for the expanded device tree */
- mem = dt_alloc(size + 4, __alignof__(struct device_node));
-
- /* Second pass, do actual unflattening */
- unflatten_dt_nodes(blob, mem, dad, mynodes);
- populate_node
- np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
- __alignof__(struct device_node));
-
- np->full_name = fn = ((char *)np) + sizeof(*np);
-
- populate_properties
- pp = unflatten_dt_alloc(mem, sizeof(struct property),
- __alignof__(struct property));
-
- pp->name = (char *)pname;
- pp->length = sz;
- pp->value = (__be32 *)val;
可以看到,先把dtb中的memreserve信息告诉内核,把这块内存区域保留下来,不占用它。
然后解析dtb文件,转换成节点是device_node的树状结构,这里涉及两个结构体:
- struct device_node:Linux内核使用device_node结构体来描述一个设备节点,此结构体定义在文件 include/linux/of.h 中;
- struct property: Linux内核中使用结构体property表示节点属性,此结构体同样定义在文件include/linux/of.h中;
在dts文件里,每个大括号{}代表一个节点,比如根节点里有个大括号,对应一个device_node结构体。节点里面有各种属性,也可能里面还有子节点,所以它们还有一些父子关系。
根节点下的memory、chosen、uart、rtc等节点是并列关系,兄弟关系。对于父子关系、兄弟关系,在device_node结构体里有成员来描述这些关系。
2.2 arch_call_rest_init
start_kernel函数最后调用arch_call_rest_init函数,arch_call_rest_init函数是定义在init/main.c中的一个函数,用于调用没有被内核启动代码显式调用的初始化函数。
在内核启动过程中,当调用完所有的显式初始化函数后,arch_call_rest_init函数会被调用,以便调用其它的初始化函数,例如驱动程序注册、文件系统初始化、网络初始化等等。这些函数都由模块或子系统定义,并且它们的调用顺序可以在模块或子系统的 Makefile文件中进行配置。
需要注意的是,setup_arch主要负责硬件初始化,而arch_call_rest_init则主要负责软件初始化。它们的调用时机和作用不同,但都是Linux内核启动过程中不可或缺的环节。
在具体实现中,arch_call_rest_init函数会调用名为rest_init 的函数。而在 rest_init 函数中,会使用 fork系统调用创建两个新进程,分别为进程 1 和进程 2。其中:
- 进程1会执行内核的初始化工作,比如初始化文件系统、内存管理等子系统;
- 进程2则会负责启动 idle 进程并运行调度器;
在内核初始化完毕后,进程 1 会退出,而进程 2 则会成为系统中唯一的用户进程。
rest_init 函数位于init/main.c文件:


- noinline void __ref rest_init(void)
- {
- struct task_struct *tsk;
- int pid;
-
- rcu_scheduler_starting();
- /*
- * We need to spawn init first so that it obtains pid 1, however
- * the init task will end up wanting to create kthreads, which, if
- * we schedule it before we create kthreadd, will OOPS.
- */
- pid = kernel_thread(kernel_init, NULL, CLONE_FS);
- /*
- * Pin init on the boot CPU. Task migration is not properly working
- * until sched_init_smp() has been run. It will set the allowed
- * CPUs for init to the non isolated CPUs.
- */
- rcu_read_lock();
- tsk = find_task_by_pid_ns(pid, &init_pid_ns);
- set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
- rcu_read_unlock();
-
- numa_default_policy();
- pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
- rcu_read_lock();
- kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
- rcu_read_unlock();
-
- /*
- * Enable might_sleep() and smp_processor_id() checks.
- * They cannot be enabled earlier because with CONFIG_PREEMPT=y
- * kernel_thread() would trigger might_sleep() splats. With
- * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
- * already, but it's stuck on the kthreadd_done completion.
- */
- system_state = SYSTEM_SCHEDULING;
- complete(&kthreadd_done);
- /*
- * The boot idle thread must execute schedule()
- * at least once to get things moving:
- */
- schedule_preempt_disabled();
- /* Call into cpu_idle with preempt disabled */
- cpu_startup_entry(CPUHP_ONLINE);
- }
2.2.1 kernel_init
进程1执行的是kernel_init函数,kernel_init函数位于init/main.c文件:


- static int __ref kernel_init(void *unused)
- {
- int ret;
-
- kernel_init_freeable();
- /* need to finish all async __init code before freeing the memory */
- async_synchronize_full();
- ftrace_free_init_mem();
- free_initmem();
- mark_readonly();
-
- /*
- * Kernel mappings are now finalized - update the userspace page-table
- * to finalize PTI.
- */
- pti_finalize();
-
- system_state = SYSTEM_RUNNING;
- numa_default_policy();
-
- rcu_end_inkernel_boot();
-
- if (ramdisk_execute_command) {
- ret = run_init_process(ramdisk_execute_command);
- if (!ret)
- return 0;
- pr_err("Failed to execute %s (error %d)\n",
- ramdisk_execute_command, ret);
- }
-
- /*
- * We try each of these until one succeeds.
- *
- * The Bourne shell can be used instead of init if we are
- * trying to recover a really broken machine.
- */
- if (execute_command) {
- ret = run_init_process(execute_command);
- if (!ret)
- return 0;
- panic("Requested init %s failed (error %d).",
- execute_command, ret);
- }
- if (!try_to_run_init_process("/sbin/init") ||
- !try_to_run_init_process("/etc/init") ||
- !try_to_run_init_process("/bin/init") ||
- !try_to_run_init_process("/bin/sh"))
- return 0;
-
- panic("No working init found. Try passing init= option to kernel. "
- "See Linux Documentation/admin-guide/init.rst for guidance.");
- }
函数调用流程:
- kernel_init
- kernel_init_freeable();
- do_basic_setup();
- do_initcalls();
- for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
- do_initcall_level(level); // 比如 do_initcall_level(3)
- for (fn = initcall_levels[3]; fn < initcall_levels[3+1]; fn++)
- do_one_initcall(initcall_from_entry(fn)); // 就是调用"arch_initcall_sync(fn)"中定义的fn函数
do_initcall_level 是 Linux 内核中负责执行注册的初始化函数的函数之一。在内核启动过程中,各个子系统需要进行初始化,而这些初始化函数是通过调用 module_init、subsys_initcall、device_initcall 等宏定义的函数实现的。
这些宏实际上是向内核注册相关的初始化函数,然后将其存放到一个特定的段中,例如 __initcall_start 到 __initcall_end。
在内核初始化期间,do_initcall_level 函数按照预设的顺序(由参数 level 指定)执行这些初始化函数:


- leveltatic 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 const 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));
- }
该函数会检查 __initcall_start 到 __initcall_end 这个段中的所有函数,并将标记了 __initcall 的函数按照指定的级别(level)依次执行。
每个级别都以 __initcall[0-7]_start 标记开头,并且在 System.map 文件中可以看到相应的条目,比如:
- c06e687c T __initcall0_start
- c06e687c t __initcall_ipc_ns_init0
- c06e6880 t __initcall_init_mmap_min_addr0
- c06e6884 t __initcall_net_ns_init0
- c06e6888 T __initcall1_start
- c06e6888 t __initcall_ptrace_break_init1
- c06e688c t __initcall_s3c2442_core_init1
- c06e6890 t __initcall_s3c2440_core_init1
- c06e6894 t __initcall_samsung_gpiolib_init1
- c06e6898 t __initcall_wq_sysfs_init1
- c06e689c t __initcall_ksysfs_init1
- ......
- c06e6940 t __initcall_of_platform_default_populate_init3s
- c06e6944 T __initcall4_start
- c06e6944 t __initcall_topology_init4
- ......
System.map 是在 Linux 内核编译过程中生成的一个文件。它是内核的一个符号表,用于记录内核二进制映像中各个符号(变量、函数等)的地址,以便调试时能够快速定位到这些符号。通过 System.map 文件,开发人员可以查询内核二进制映像中每个符号的地址,以及符号所属的模块或函数名等信息。
2..2.2 of_platform_default_populate_init
在System.map中可以找到__initcall_of_platform_default_populate_init3s,也就是在执行do_initcall_level(3)时,会执行到of_platform_default_populate_init函数。
of_platform_default_populate_init函数定义在drivers/of/platform.c:


- static int __init of_platform_default_populate_init(void)
- {
- struct device_node *node;
-
- if (!of_have_populated_dt())
- return -ENODEV;
-
- /*
- * Handle certain compatibles explicitly, since we don't want to create
- * platform_devices for every node in /reserved-memory with a
- * "compatible",
- */
- for_each_matching_node(node, reserved_mem_matches)
- of_platform_device_create(node, NULL, NULL);
-
- node = of_find_node_by_path("/firmware");
- if (node) {
- of_platform_populate(node, NULL, NULL, NULL);
- of_node_put(node);
- }
-
- /* Populate everything else. */
- of_platform_default_populate(NULL, NULL, NULL);
-
- return 0;
- }
- arch_initcall_sync(of_platform_default_populate_init);
可以看到of_platform_default_populate_init初始化函数是通过arch_initcall宏定义的实现的。
在这个函数内部,它会遍历设备节点树,检查设备节点上是否存在compatible属性,如果存在,就会为这个设备节点分配一个platform_device结构体,并将这些platform_device注册到Linux内核中,以便与其它驱动程序进行匹配和绑定。
并非所有的device_node都会转换为platform_device,只有以下的device_node会转换;
(1) 该节点必须含有compatible属性;根节点是例外的,生成platfrom_device时,即使有compatible属性也不会处理。
(2) 根节点的子节点(节点必须含有compatible属性);
(3) 含有特殊compatible属性的节点的子节点(子节点必须含有compatible属性),这些特殊的compatible属性为:
"simple-bus","simple-mfd","isa","arm,amba-bus "
比如dts文件配置如下:
- / {
- mytest {
- compatible = "mytest", "simple-bus";
- mytest@0 {
- compatible = "mytest_0";
- };
- };
- i2c {
- compatible = "samsung,i2c";
- at24c02 {
- compatible = "at24c02";
- };
- };
- spi {
- compatible = "samsung,spi";
- flash@0 {
- compatible = "winbond,w25q32dw";
- spi-max-frequency = <25000000>;
- reg = <0>;
- };
- };
- };
其中:
- /mytest会被转换为platform_device, 因为它兼容"simple-bus",它的子节点/mytest/mytest@0 也会被转换为platform_device;
-
/i2c节点一般表示I2C控制器,,它会被转换为platform_device, 在内核中有对应的platform_driver;/i2c/at24c02节点不会被转换为platform_device,它被如何处理完全由父节点的platform_driver决定,一般是被创建为一个i2c_client;
-
类似的也有/spi节点,,它一般也是用来表示SPI控制器,它会被转换为platform_device,在内核中有对应的platform_driver;/spi/flash@0节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个spi_device;
参考文章
[1]linux设备树学习笔记(二、linux内核对设备树的处理)(部分转载)