linux设备树-linux内核对设备树的处理
目录
----------------------------------------------------------------------------------------------------------------------------
内核版本:linux 5.2.8
根文件系统:busybox 1.25.0
u-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); }
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了