大家好,我是王鸽,这篇文章介绍Linux内核启动流程,其实ARM-Linux的启动全过程:内核自解压阶段——>内核引导阶段——>内核初始化阶段——>BusyBox初始化阶段(会启动“init”进程,并执行一系列关键的用户空间初始化脚本)。Linux 编译
编译内核的时候 make 的过程是将各个子目录下的built-in.o 和*.a等文件链接在一起,最终生成vmlinux 这个ELF格式的可执行文件, 链接脚本是为arch/arm/kernel/vmlinux.lds, 链接过程是由shell脚本 scripts/link-vmlinux.S来完成。通过linux/arch/arm/boot/compressed目录下的Makefile寻找到vmlinux文件的链接脚本(vmlinux.lds),从中查找系统启动入口函数。$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o \ $(addprefix$(obj)/, $(OBJS)) $(lib1funcs) $(ashldi3) \ $(bswapsdi2) $(efi-obj-y) FORCE @$(check_for_multiple_zreladdr) $(call if_changed,ld) @$(check_for_bad_syms)
vmlinux.lds链接脚本
通过该脚本可以知道内核的第一行程序是从哪里执行的?OUTPUT_ARCH(arm) ENTRY(stext) jiffies = jiffies_64; SECTIONS { /DISCARD/ : { *(.ARM.exidx.exit.text) *(.ARM.extab.exit.text) *(.ARM.exidx.text.exit) *(.ARM.extab.text.exit) *(.exitcall.exit) *(.discard) *(.discard.*) *(.modinfo) *(.gnu.version*) } . = ((0xC0000000)) + 0x00008000; .head.text : { .......
ENTRY(stext) 入口为stext,定义在文件arch/arm/kernel/head.S中内核入口stext
linux/arch/arm/kernel/head.S 中stext 是内核入口的地址,/* * Kernel startup entry point. * --------------------------- * * This is normally called from the decompressor code. The requirements* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags or dtb pointer. * * This code is mostly position independent, so if you link the kernel at * 0xc0008000, you call this at __pa(0xc0008000). * * See linux/arch/arm/tools/mach-types for the complete list of machine * numbers for r1. * * We're trying to keep crap to a minimum; DO NOT add any machine specific * crap here - that's what the boot loader (or in extreme, well justified * circumstances, zImage) is for. */ .arm根据注释linux 启动内核之前需要做些工作:MMU = off, D-cache = off, I-cache = dont care, r0 = 0,r1 = machine nr, r2 = atags or dtb pointer. 关闭MMU,关闭 D-cache ,I-Cache 无所谓, r0 =0 r1=machine nr(也就是机器ID) r2=atags或者是设备树(dtb)首地址。入口点stext其实相当于内核的入口函数,stext函数内容__HEADENTRY(stext) //内核的入口函数 ARM_BE8(setend be ) @ ensure we are in BE8 mode THUMB( badr r9, 1f ) @ Kernel is always entered in ARM. THUMB( bx r9 ) @ If this is a Thumb-2 kernel, THUMB( .thumb ) @ switch to Thumb now. THUMB(1: )#ifdef CONFIG_ARM_VIRT_EXT //如果有配置(ARM虚拟化扩展),则跳转到__hyp_stub_install, bl __hyp_stub_install //该定义在arch/arm/kernel/hyp-stub.S 文件中。#endif @ ensure svc mode and all interrupts masked safe_svcmode_maskall r9 //确保cpu 处于SVC模式,并且关闭所有的中断,//safe_svcmode_maskall 定义在文件 arch/arm/include/asm/assembler.h 中。 .macro safe_svcmode_maskall reg:req mrc p15, 0, r9, c0, c0 @ get processor id //读处理器 ID, ID 值保存在 r9 寄存器中 bl __lookup_processor_type @ r5=procinfo r9=cpuid //调用函数__lookup_processor_type 检查当前系统是否支持此 CPU,如果支持的就获取 procinfo 信息。 movs r10, r5 @ invalid processor (r5=0)? THUMB( it eq ) @ force fixup-able long branch encoding beq __error_p @ yes, error 'p'#ifdef CONFIG_ARM_LPAE mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0 and r3, r3, #0xf @ extract VMSA support cmp r3, #5 @ long-descriptor translation table format? THUMB( it lo ) @ force fixup-able long branch encoding blo __error_lpae @ only classic page table format#endif#ifndef CONFIG_XIP_KERNEL adr r3, 2f ldmia r3, {r4, r8} sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) add r8, r8, r4 @ PHYS_OFFSET#else ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case#endif....#ifndef CONFIG_XIP_KERNEL adr r3, 2f ldmia r3, {r4, r8} sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) add r8, r8, r4 @ PHYS_OFFSET#else ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case#endif/* * r1 = machine no, r2 = atags or dtb, * r8 = phys_offset, r9 = cpuid, r10 = procinfo */ bl __vet_atags //调用函数__vet_atags 验证 atags 或设备树(dtb)的合法性。函数__vet_atags 定义在文件 //arch/arm/kernel/head-common.S 中。#ifdef CONFIG_SMP_ON_UP bl __fixup_smp#endif#ifdef CONFIG_ARM_PATCH_PHYS_VIRT bl __fixup_pv_table#endif bl __create_page_tables //调用函数__create_page_tables 创建页表。 /* * The following calls CPU specific code in a position independent * manner. See arch/arm/mm/proc-*.S for details. r10 = base of * xxx_proc_info structure selected by __lookup_processor_type * above. * * The processor init function will be called with: * r1 - machine type * r2 - boot data (atags/dt) pointer * r4 - translation table base (low word) * r5 - translation table base (high word, if LPAE) * r8 - translation table base 1 (pfn if LPAE) * r9 - cpuid * r13 - virtual address for __enable_mmu -> __turn_mmu_on * * On return, the CPU will be ready for the MMU to be turned on, * r0 will hold the CPU control register value, r1, r2, r4, and * r9 will be preserved. r5 will also be preserved if LPAE. *///将函数__mmap_switched 的地址保存到 r13 寄存器中。 __mmap_switched 定义//在文件 arch/arm/kernel/head-common.S, __mmap_switched 最终会调用 start_kernel 函数。 ldr r13, =__mmap_switched @ address to jump to after @ mmu has been enabled badr lr, 1f @ return (PIC) address#ifdef CONFIG_ARM_LPAE mov r5, #0 @ high TTBR0 mov r8, r4, lsr #12 @ TTBR1 is swapper_pg_dir pfn#else mov r8, r4 @ set TTBR1 to swapper_pg_dir#endif ldr r12, [r10, #PROCINFO_INITFUNC] add r12, r12, r10 ret r121: b __enable_mmu//使 能 MMU , __enable_mmu 定 义 在 文 件arch/arm/kernel/head.S 中。 __enable_mmu 最终会通过调用__turn_mmu_on 来打开 MMU,__turn_mmu_on 最后会执行 r13 里面保存的__mmap_switched 函数。ENDPROC(stext)
__mmap_switched
__mmap_switched 函数定义在文件 arch/arm/kernel/head-common.S 中,函数代码如下:__INIT__mmap_switched: mov r7, r1 mov r8, r2 mov r10, r0 adr r4, __mmap_switched_data mov fp, #0#if defined(CONFIG_XIP_DEFLATED_DATA) ARM( ldr sp, [r4], #4 ) THUMB( ldr sp, [r4] ) THUMB( add r4, #4 ) bl __inflate_kernel_data @ decompress .data to RAM teq r0, #0 bne __error#elif defined(CONFIG_XIP_KERNEL) ARM( ldmia r4!, {r0, r1, r2, sp} ) THUMB( ldmia r4!, {r0, r1, r2, r3} ) THUMB( mov sp, r3 ) subr2, r2, r1 bl memcpy @ copy .data to RAM#endif ARM( ldmia r4!, {r0, r1, sp} ) THUMB( ldmia r4!, {r0, r1, r3} ) THUMB( mov sp, r3 ) subr2, r1, r0 mov r1, #0 bl memset @ clear .bss ldmia r4, {r0, r1, r2, r3} str r9, [r0] @ Save processor ID str r7, [r1] @ Save machine type str r8, [r2] @ Save atags pointer cmp r3, #0 strne r10, [r3] @ Save control register values mov lr, #0 b start_kernelENDPROC(__mmap_switched)
上面代码中最重要的是start_kernel,千呼万唤始出来了。调用 start_kernel 来启动 Linux 内核, start_kernel 函数定义在文件init/main.c 中。init/main.c中start_kernel 函数
start_kernel 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作,start_kernel 里面调用了大量的函数,每一个函数都是一个庞大的知识点,如果想要学习 Linux 内核,那么这些函数就需要去详细的研究。有时间我会单独拉出来研究一下。asmlinkage __visible void __init __no_sanitize_address start_kernel(void){ char *command_line; char *after_dashes; set_task_stack_end_magic(&init_task);//设置任务栈结束魔术数,用于栈溢出检测 smp_setup_processor_id();//跟 SMP 有关(多核处理器),设置处理器 ID。* 有很多资料说 ARM 架构下因此函数为空函数,那是因为他们用的老版本 Linux,而那时候 ARM 还没有多核处理器。 debug_objects_early_init();/* 做一些和 debug 有关的初始化 */ cgroup_init_early();/* cgroup 初始化, cgroup 用于控制 Linux 系统资源*/ local_irq_disable();/* 关闭当前 CPU 中断 */ early_boot_irqs_disabled = true; /* * Interrupts are still disabled. Do necessary setups, then * enable them. 中断关闭期间做一些重要的操作,然后打开中断 */ boot_cpu_init(); /* 跟 CPU 有关的初始化 */ page_address_init();/* 页地址相关的初始化 */ pr_notice("%s", linux_banner);/* 打印 Linux 版本号、编译时间等信息 */ early_security_init(); setup_arch(&command_line);//架构相关的初始化,此函数会解析传递进来的ATAGS 或者设备树(DTB)文件。会根据设备树里面的 model 和 compatible 这两个属性值来查找Linux 是否支持这个单板。此函数也会获取设备树中 chosen 节点下的 bootargs 属性值来得到命令行参数,也就是 uboot 中的 bootargs 环境变量的值,获取到的命令行参数会保存到command_line 中。 setup_boot_config(command_line);//设置bootconfig 参数这块 setup_command_line(command_line); //存储命令行参数 setup_nr_cpu_ids();//如果只是 SMP(多核 CPU)的话,此函数用于获取CPU 核心数量, CPU 数量保存在变量nr_cpu_ids 中。 setup_per_cpu_areas();//在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ boot_cpu_hotplug_init(); build_all_zonelists(NULL);//建立系统内存页区(zone)链表 page_alloc_init();//处理用于热插拔 CPU 的页 pr_notice("Kernel command line: %s\n", saved_command_line);/* 打印命令行信息 */ /* parameters may set static keys */ jump_label_init(); parse_early_param();/* 解析命令行中的 console 参数 */ 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); if (extra_init_args) parse_args("Setting extra init args", extra_init_args, NULL, 0, -1, -1, NULL, set_init_arg); /* * These use large bootmem allocations and must precede * kmem_cache_init() */ setup_log_buf(0);/* 设置 log 使用的缓冲区*/ vfs_caches_init_early();/* 预先初始化 vfs(虚拟文件系统)的目录项和索引节点缓存*/ sort_main_extable();/* 定义内核异常列表 */ trap_init();/* 完成对系统保留中断向量的初始化 */ mm_init();/* 内存管理初始化 */ ftrace_init(); /* trace_printk can be enabled here */ early_trace_init(); 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(); /*允许及早创建工作队列和工作项排队/取消。工作项的执行取决于 kthread,并在 workqueue_init()之后开始。 */ rcu_init();/* 初始化 RCU, RCU 全称为 Read Copy Update(读-拷贝修改) */ /* 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();//初始中断相关初始化,主要是注册 irq_desc 结构体变量,因为 Linux 内核使用 irq_desc 来描述一个中断。 init_IRQ();//中断初始化 tick_init();//tick 初始化 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();//初始化系统时间 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();//slab 初始化, slab 是 Linux 内存分配器
不得不说,start_kernel(void)里面太多函数,继续读吧。
console_init();//初始化控制台,之前 printk 打印的信息都存放缓冲区中,并没有打印出来。只有调用此函数初始化控制台以后才能在控制台上打印信息。if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_init();/* 如果定义了宏 CONFIG_LOCKDEP,那么此函数打印一些信息。 */ /* * 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 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();/* 生成 anon_vma slab 缓存 */#ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode();#endif thread_stack_cache_init(); cred_init();/* 为对象的每个用于赋予资格(凭证)*/ fork_init();/* 初始化一些结构体以使用 fork 函数*/ proc_caches_init();//给各种资源管理结构分配缓存 uts_ns_init(); buffer_init();//初始化缓冲缓存 key_init();//初始化密钥 security_init();//安全相关初始化 dbg_late_init(); vfs_caches_init();//为 VFS 创建缓存 pagecache_init(); signals_init();// 初始化信号 seq_file_init(); proc_root_init();//注册并挂载 proc 文件系统 nsfs_init(); cpuset_init();/* 初始化 cpuset, cpuset 是将 CPU 和内存资源以逻辑性和层次性集成的一种机制,是 cgroup 使用的子系统之一*/ cgroup_init();/* 初始化 cgroup */ taskstats_init_early();/* 进程状态初始化 */ delayacct_init(); poking_init(); check_bugs();/* 检查写缓冲一致性 */ acpi_subsystem_init(); arch_post_acpi_subsys_init(); sfi_init_late(); kcsan_init(); /* Do the rest non-__init'ed, we're now alive */ arch_call_rest_init(); //调用rest_init 函数 prevent_tail_call_optimization();}
start_kernel 函数最后调用了 rest_init,接下来简单看一下 rest_init 函数。
rest_init(void)
void __init __weak arch_call_rest_init(void) |---rest_init(); |---rcu_scheduler_starting(); |----pid = kernel_thread(kernel_init, NULL, CLONE_FS);//调用函数 kernel_thread 创建 kernel_init 线程,也就是大名鼎鼎的 init 内核进程,init 进程的 PID 为 1。 init 进程一开始是内核进程(也就是运行在内核态),后面 init进程会在根文件系统中查找名为“ init”这个程序,这个“ init”程序处于用户态,通过运行这个“ init”程序, init 进程就会实现从内核态到用户态的转变。 |----int __ref kernel_init(void *unused) //回调 |---kernel_init_freeable();//用于完成 init 进程的一些其他初始化工作,稍后再来具体看一下此函数。 |---run_init_process(ramdisk_execute_command); ramdisk_execute_command 是一个全局的 char 指针变量,此变量值为“ /init”,也就是根目录下的 init 程序。 ramdisk_execute_command 也可以通过 uboot 传递,在 bootargs中使用“ rdinit=xxx”即可, xxx 为具体的 init 程序名字。如果存在“ /init”程序的话就通过函数 run_init_process 来运行此程序。 |---if (execute_command) { ret = run_init_process(execute_command); 如果 ramdisk_execute_command 为空的话就看 execute_command 是否为空,反正不管如何一定要在根文件系统中找到一个可运行的 init 程序。 execute_command 的值是通过 uboot 传递,在 bootargs 中使用“ init=xxxx”就可以了,比如“ init=/linuxrc”表示根文件系统中的 linuxrc 就是要执行的用户空间 init 程序。 |----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; 如果 ramdisk_execute_command 和 execute_command 都为空,那么就依次查找“ /sbin/init”、“ /etc/init”、“ /bin/init”和“ /bin/sh”,这四个相当于备用init 程序,如果这四个也不存在,那么 Linux 启动失败。如果以上步骤都没有找到用户空间的 init 程序,那么就提示错误发生。 |---pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。kthread 进程负责所有内核进程的调度和管理。 |--cpu_startup_entry(CPUHP_ONLINE);//进入 idle 进程, cpu_startup_entry 会调用cpu_idle_loop, cpu_idle_loop 是个 while 循环,也就是 idle 进程代码。 |--arch_cpu_idle_prepare(); |---cpuhp_online_idle(state); |--- while (1) |---do_idle(); idle 进程的PID 为 0, idle 进程叫做空闲进程,当 CPU 没有事情做的时候就在 idle 空闲进程里面“瞎逛游”,反正就是给 CPU 找点事做。当其他进程要工作的时候就会抢占 idle 进程,从而夺取 CPU使用权。其实可以看到 idle 进程并没有使用 kernel_thread 或者 fork 函数来创建,因为它是由主进程演变而来的。
输入 ps -A命令打印出当前系统中的所有进程,其中就能看到 init进程和 kthreadd 进程,init 进程的 PID 为 1, kthreadd 进程的 PID 为 2。之所以上图中没有显示 PID 为 0 的 idle 进程,那是因为 idle 进程是内核进程。
PID TTY TIME CMD 1 ? 00:00:01 init 2 ? 00:00:00 kthreadd 3 ? 00:00:00 rcu_gp 4 ? 00:00:00 rcu_par_gp 7 ? 00:00:00 kworker/u4:0-events_unbound 8 ? 00:00:00 mm_percpu_wq 9 ? 00:00:00 ksoftirqd/0 10 ? 00:00:00 rcu_preempt 11 ? 00:00:00 migration/0 12 ? 00:00:00 cpuhp/0
kernel_init_freeable()阅读
|---asmlinkage __visible void __init __no_sanitize_address start_kernel(void) |--void __init __weak arch_call_rest_init(void) |---noinline void __ref rest_init(void) |----pid = kernel_thread(kernel_init, NULL, CLONE_FS); |----static int __refkernel_init(void *unused) |---kernel_init_freeable();
其中kernel_init_freeable();函数的具体描述static noinline void __init kernel_init_freeable(void){ /* * Wait until kthreadd is all set-up. */ wait_for_completion(&kthreadd_done); /* Now the scheduler is fully set up and can do blocking allocations */ gfp_allowed_mask = __GFP_BITS_MASK; /* * init can allocate pages on any node */ set_mems_allowed(node_states[N_MEMORY]); cad_pid = task_pid(current); smp_prepare_cpus(setup_max_cpus); workqueue_init(); init_mm_internals(); do_pre_smp_initcalls(); lockup_detector_init(); smp_init();/* SMP 初始化 */ sched_init_smp();/* 多核(SMP)调度初始化 */ padata_init(); page_alloc_init_late(); /* Initialize page ext after all struct pages are initialized. */ page_ext_init(); do_basic_setup(); //do_basic_setup 函数用于完成 Linux 下设备驱动初始化工作。 do_basic_setup//会调用 driver_init 函数完成 Linux 下驱动模型子系统的初始化。 kunit_run_all_tests(); console_on_root//该函数中打开设备“ /dev/console”,在 Linux 中一切皆为文件。 因此“ /dev/console”也 是 一 个 文 件 , 此文件 为 控 制 台 设 备 。每个 文 件 都 有 一 个 文件描 述 符 , 此 处 打 开 的“ /dev/console”文件描述符为 0,作为标准输入(0)。 /* * check if there is an early userspace init. If yes, let it do all * the work */ if (init_eaccess(ramdisk_execute_command) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); |————void __init prepare_namespace(void) 分区的挂载 调用函数 prepare_namespace 来挂载根文件系统。跟文件系统也是由命令行 参数指定的,也就是 uboot 的 bootargs 环境变量。比如“ root=/dev/mmcblk1p2 rootwait rw” 就表示根文件系统在/dev/mmcblk1p2 中,也就是 EMMC 的分区 2 中。 } /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. * * rootfs is available now, try loading the public keys * and default modules */ integrity_load_keys();}
Linux 内核启动流程就分析到这里, Linux 内核最终是需要和根文件系统打交道的,需要挂载根文件系统,并且执行根文件系统中的 init 程序,以此来进入用户态。当内核被引导并进行初始化后,内核启动了自己的第一个用户空间应用程序_init,这是调用的第一个使用标准C库编译的程序,其进程编号时钟为1._init负责出发其他必须的进程,以使系统进入整体可用的状态。