05 Linux 内核启动流程

时间:2024-06-11 18:33:53

1、阅读 Linux 内核源码

学习 Linux 有两种路线:

1)按照 Linux 启动流程,梳理每个子系统。
2)把 Linux 所有用到的子系统学会,再组合起来。

博主选择第一种方式,可以快速上手,知道自己在学什么东西,在什么阶段起作用。

阅读 Linux 和 Android 源码:

https://elixir.bootlin.com/linux/latest/source
http://aospxref.com/

2、Makefile 与 Kconfig

1)Makefile

  • Makefile 是一种被广泛用于管理源代码的工具,特别是针对程序的编译和构建。它包含了一系列规则,指定了编译器如何编译源文件、链接器如何链接目标文件,以及如何清理生成的文件等操作。
  • Makefile 的作用:通过 Makefile,开发人员可以指定项目中源文件的依赖关系,使得只有受影响的文件被编译,而不是每次都编译整个项目,节省时间和资源。
  • Makefile 结构:典型的 Makefile 包含了变量定义、目标规则、依赖关系和命令等内容。

2)Kconfig

  • Kconfig 是 Linux 内核用于配置内核选项的工具,允许用户在编译内核时选择不同的配置选项以定制内核的功能。
  • Kconfig 文件:Kconfig 文件包含了内核配置选项的描述,格式为类似于菜单的层次结构,用户可以通过命令行或图形界面交互式地选择选项,以配置内核的特性和行为。
  • Kconfig 的作用:Kconfig 允许用户对内核进行高度定制,以适应不同的硬件平台、应用需求或性能要求,而无需重新编译整个内核。

3)总结

  • Makefile 是用于管理源代码编译和构建的工具,利用规则来指导建立整个项目。
  • Kconfig 是 Linux 内核的配置工具,用于选择编译内核时的不同配置选项,定制内核的功能和行为。

3、内核裁剪与内核移植

1)内核裁剪

make menuconfig 命令在 Linux 内核源码根目录执行,执行后效果如图:

[图片]

  • “方向按键”中的“左右”可以选择你需要的操作。“”表示进入选择的配置界面,“”表示返回,“< Help >”可以阅读帮助文档。
  • 输入“/”,可以进入搜索界面。

make menuconfig 是 Linux 内核构建过程中的一个命令,用于交互式地配置内核选项。通过运行 make menuconfig 命令,用户可以在终端中打开一个基于文本界面的菜单系统,以便于选择和配置 Linux 内核的各种功能选项、驱动程序和特性。

具体来说,make menuconfig 命令的作用包括以下几点:

  1. 配置内核选项:通过 make menuconfig,用户可以浏览各种内核选项,例如驱动程序、文件系统支持、网络配置等,并可以根据需求进行启用、禁用、选择或设置。
  2. 交互式选择:使用键盘进行上下左右移动,空格键选择选项,以及对选项进行开启或关闭操作。
  3. 依赖关系:make menuconfig 会显示选项之间的依赖关系,使得用户可以清晰地了解选择一个选项可能会影响到哪些其他选项。
  4. 保存配置:按 ESC 可以退出这个界面,会提示你是否要保存。配置的修改可以保存并生成 .config 文件,该文件包含了用户的配置选择,可用于后续编译内核。
  5. 定制内核:通过调整不同的配置选项,用户可以定制适合特定需求的内核,例如精简内核以提高性能,或者增加特定的功能支持。
    总的来说,make menuconfig 提供了一种方便、交互式的方式来配置 Linux 内核,使得用户可以根据自己的需求定制内核的功能和特性。

2)内核移植

[图片]

4、Linux 启动流程详解

建议大家深入学习 Linux kernel 某个子系统之前,先学习 Linux kernel 启动流程,这样能有一个框架,你学的子系统就不是孤立存在于你的知识系统中,你知道它是在哪个时刻被调用的。

从启动引导程序 bootloader(uboot)跳转到 Linux kernel 后,Linux 内核开始启动, 我们先看一下 Linux 内核启动入口。

linux4.14/arch/arm/kernel/vmlinux.lds.S

这里可以看到链接时候 Linux 入口是 stext 段,这里是 uboot 跳转过来的第一段 Linux kernel 代码:

OUTPUT_ARCH(arm)
ENTRY(stext)

1)Linux 入口地址

我们先看一下入口地址的确定,同一文件中找到 SECTIONS

SECTIONS
{
 /DISCARD/ : {
  *(.ARM.exidx.exit.text)
  *(.ARM.extab.exit.text)
  ARM_CPU_DISCARD(*(.ARM.exidx.cpuexit.text))
  ARM_CPU_DISCARD(*(.ARM.extab.cpuexit.text))
  ARM_EXIT_DISCARD(EXIT_TEXT)
  ARM_EXIT_DISCARD(EXIT_DATA)
  EXIT_CALL
#ifndef CONFIG_MMU
  *(.text.fixup)
  *(__ex_table)
#endif
#ifndef CONFIG_SMP_ON_UP
  *(.alt.smp.init)
#endif
  *(.discard)
  *(.discard.*)
 }

 . = PAGE_OFFSET + TEXT_OFFSET;
 .head.text : {
  _text = .;
  HEAD_TEXT
 }

这个 SECTIONS 比较长,只放一部分。在这里倒数第五行有个比较重要的东西:

. = PAGE_OFFSET + TEXT_OFFSET;

这一句表示了 Linux kernel 真正的启动地址。

PAGE_OFFSET 是 Linux kernel 空间的虚拟起始地址,定义在:

linux4.14/arch/arm64/include/asm/memory.h

#define VA_BITS                        (CONFIG_ARM64_VA_BITS)
#define VA_START                (UL(0xffffffffffffffff) - \
        (UL(1) << VA_BITS) + 1)
#define PAGE_OFFSET                (UL(0xffffffffffffffff) - \
        (UL(1) << (VA_BITS - 1)) + 1)
#define KIMAGE_VADDR                (MODULES_END)
#define MODULES_END                (MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR                (VA_START + KASAN_SHADOW_SIZE)
#define MODULES_VSIZE                (SZ_128M)
#define VMEMMAP_START                (PAGE_OFFSET - VMEMMAP_SIZE)
#define PCI_IO_END                (VMEMMAP_START - SZ_2M)
#define PCI_IO_START                (PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP                (PCI_IO_START - SZ_2M)

这里的地址都很重要,很多地方会用到。当然,这里的地址可能会随着 Linux kernel 版本的不同和硬件的不同,会变化。这里没有一个具体的数,因为 VA_BITS 中的数字是可选的,大家可以根据自己的平台算一下。 VA_BITS 是你的系统真正使用的位数,如果是 32 位系统就是 32,如果是 64 位系统可能是 39 位或者其他。

TEXT_OFFSET 定义在 linux4.14/arch/arm/Makefile 中:

textofs-y        := 0x00008000
The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)

这个值一般是 0x00008000 ,算出 PAGE_OFFSET 后加上这个值就是 Linux 内核的起始地址。

修改这个偏移量就可以使 Linux 内核拷贝到不同的地址,自己修改注意内存对齐。

2)stext 段

从上面的 ENTRY(stext)可以知道,一开始是运行 stext 段,这个段内的代码是 start_kernel 函数前汇编环境的初始化。

linux4.14/arch/arm64/kernel/head.S

ENTRY(stext)
        bl        preserve_boot_args
        bl        el2_setup                        // Drop to EL1, w0=cpu_boot_mode
        adrp        x23, __PHYS_OFFSET
        and        x23, x23, MIN_KIMG_ALIGN - 1        // KASLR offset, defaults to 0
        bl        set_cpu_boot_mode_flag
        bl        __create_page_tables
        bl        __cpu_setup                        // initialise processor
        b        __primary_switch
ENDPROC(stext)
  • preserve_boot_args 保存 uboot 传递过来的参数。
  • el2_setup 是设置 Linux 启动模式是 EL2。Linux 有 EL0、EL1、EL2、EL3 四种异常启动模式,这里设置一开始是 EL2,EL2 支持虚拟内存技术,然后注释说明后面又退回 EL1,在 EL1 启动 kernel。EL3 一般是只在安全模式使用。(ARM64情况下)
  • 4、5 行:这两行设定了 offset,是一种 KASLR 技术,有了这个技术才能使得 Linux kernel 被拷贝到不同的物理地址。
  • set_cpu_boot_mode_flag 保存上面 cpu 的启动模式。
  • __create_page_tables 创建页表。
  • __cpu_setup 初始化 CPU,这里主要是初始化和 MMU 内存相关的 CPU 部分。
  • __primary_switch 这里会进行跳转。

在同一个文件中,会跳转到这里:

__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASE
        mov        x19, x0                                // preserve new SCTLR_EL1 value
        mrs        x20, sctlr_el1                        // preserve old SCTLR_EL1 value
#endif

        bl        __enable_mmu
#ifdef CONFIG_RELOCATABLE
        bl        __relocate_kernel
#ifdef CONFIG_RANDOMIZE_BASE
        ldr        x8, =__primary_switched
        adrp       x0, __PHYS_OFFSET
        blr        x8

开启了 MMU。然后最重要的是跳转到 __primary_switched 函数。先把 __primary_switched 地址放到 x8 寄存器中,再跳转到 x8,也就是跳转到 __primary_switched。

__primary_switched:
        adrp        x4, init_thread_union
        add        sp, x4, #THREAD_SIZE
        adr_l        x5, init_task
        msr        sp_el0, x5                        // Save thread_info

        adr_l        x8, vectors                        // load VBAR_EL1 with virtual
        msr        vbar_el1, x8                        // vector table address
        isb
        stp        xzr, x30, [sp, #-16]!
        mov        x29, sp

        str_l        x21, fdt_pointer, __x5                // Save FDT pointer
        ldr_l        x4, kimage_vaddr                // Save the offset between
        sub        x4, x4, x0                        // the kernel virtual and
        str_l        x4, kimage_voffset, x5                // physical mappings

        // Clear BSS
        adr_l        x0, __bss_start
        mov        x1, xzr
        adr_l        x2, __bss_stop
        sub        x2, x2, x0
        bl        __pi_memset
        dsb        ishst                                // Make zero page visible to PTW

#ifdef CONFIG_KASAN
        bl        kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASE
        tst        x23, ~(MIN_KIMG_ALIGN - 1)        // already running randomized?
        b.ne        0f
        mov        x0, x21                                // pass FDT address in x0
        bl        kaslr_early_init                // parse FDT for KASLR options
        cbz        x0, 0f                                // KASLR disabled? just proceed
        orr        x23, x23, x0                        // record KASLR offset
        ldp        x29, x30, [sp], #16                // we must enable KASLR, return
        ret                                        // to __primary_switch()
0:
#endif
        add        sp, sp, #16
        mov        x29, #0
        mov        x30, #0
        b        start_kernel
ENDPROC(__primary_switched)
  • 第一段: 初始化了 init 进程的内存信息,开辟了内存空间。
  • 第二段:设置了中断向量表。
  • 第三段: 保存了 FDT,也就是 flat device tree ,设备树。
  • 第四段:清除了BSS 段,我们知道一般是内存四区:堆区、栈区、全局区、代码区。其中全局区可以再分为 data 段和 BSS 段,BSS 段存储了未初始化的变量,这里将BSS段进行清零操作,否则内存中的值是不确定的,这是一个传统操作。
  • 最后:跳转到了我们熟悉的 start_kernel

3)start_kernel 函数

linux4.14/init/main.c,start_kernel 函数,从汇编环境跳转到了 C 环境。

kernel 4.14 中 start_kernel 函数一共调用了 86 个函数进行初始化 ,如下:

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*/
 debug_objects_early_init();/* 做一些和 debug 有关的初始化 */
 init_vmlinux_build_id();

 cgroup_init_early();/* cgroup 初始化,cgroup 用于控制 Linux 系统资源*/
 local_irq_disable();/* 关闭当前 CPU 中断 */
 early_boot_irqs_disabled = true;

 /*
  * 中断关闭期间做一些重要的操作,然后打开中断
  */
 boot_cpu_init();/* 跟 CPU 有关的初始化 */
 page_address_init();/* 页地址相关的初始化 */
 pr_notice("%s", linux_banner);/* 打印 Linux 版本号、编译时间等信息 */
 early_security_init();
 
 /* 系统架构相关的初始化,此函数会解析传递进来的
 * ATAGS 或者设备树(DTB)文件。会根据设备树里面
 * 的 model 和 compatible 这两个属性值来查找
 * Linux 是否支持这个单板。此函数也会获取设备树
 * 中 chosen 节点下的 bootargs 属性值来得到命令
 * 行参数,也就是 uboot 中的 bootargs 环境变量的
 * 值,获取到的命令行参数会保存到 command_line 中
 */
 setup_arch(&command_line);
 setup_boot_config();
 setup_command_line(command_line);/* 存储命令行参数 */
 
 /* 如果只是 SMP(多核 CPU)的话,此函数用于获取
 * CPU 核心数量,CPU 数量保存在变量 nr_cpu_ids 中。
 */
 setup_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);
 print_unknown_bootoptions();
 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);

 random_init_early(command_line);
 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();/* 初始化调度器,主要是初始化一些结构体 */

 if (WARN(!irqs_disabled(),"Interrupts were enabled *very* early, fixing it\n"))
  local_irq_disable();/* 检查中断是否关闭,如果没有的话就关闭中断 */
 radix_tree_init();/* 基数树相关数据结构初始化 */
 maple_tree_init();
 housekeeping_init();
 workqueue_init_early();
 rcu_init();/* 初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改) */

 /* Trace events are available after this */
 trace_init();/* 跟踪调试相关初始化 */
 if (initcall_debug)
  initcall_debug_enable();

 context_tracking_init();
 
 /* 初始中断相关初始化,主要是注册 irq_desc 结构体变
 * 量,因为 Linux 内核使用 irq_desc 来描述一个中断。
 */
 early_irq_init();
 init_IRQ();/* 中断初始化 */
 tick_init();/* tick 初始化 */
 rcu_init_nohz();
 init_timers();/* 初始化定时器 */
 srcu_init();
 hrtimers_init();/* 初始化高精度定时器 */
 softirq_init();/* 软中断初始化 */
 timekeeping_init();
 time_init();/* 初始化系统时间 */
 random_init();
 kfence_init();
 boot_init_stack_canary();
 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 内存分配器 */

 /* 初始化控制台,之前 printk 打印的信息都存放
  * 缓冲区中,并没有打印出来。只有调用此函数
  * 初始化控制台以后才能在控制台上打印信息。
  */
 console_init();
 if (panic_later)
  panic("Too many boot %s vars at `%s'", panic_later,
        panic_param);

 lockdep_init();
 locking_selftest();/* 锁自测 */ 
 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();
 /* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能
 * BogoMIPS 设置越大,说明 CPU 性能越好。
 */
 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();
 key_init();/* 初始化密钥 */
 security_init();/* 安全相关初始化 */
 dbg_late_init();
 net_ns_init();
 vfs_caches_init();/* 虚拟文件系统缓存初始化 */
 pagecache_init();
 signals_init();/* 初始化信号 */
 seq_file_init();
 proc_root_init();/* 注册并挂载 proc 文件系统 */
 nsfs_init();
 /* 初始化 cpuset,cpuset 是将 CPU 和内存资源以逻辑性
 * 和层次性集成的一种机制,是 cgroup 使用的子系统之一
 */
 cpuset_init();
 cgroup_init();/* 初始化 cgroup */
 taskstats_init_early();/* 进程状态初始化 */
 delayacct_init();

 poking_init();
 check_bugs();/* 检查写缓冲一致性 */

 acpi_subsystem_init();
 arch_post_acpi_subsys_init();
 kcsan_init();

 /* 调用 rest_init 函数 */
 /* 创建 init、kthread、idle 线程 */
 arch_call_rest_init();
 prevent_tail_call_optimization();
}

[图片]

[图片]

[图片]

其中有七个函数较为重要,分别为:

setup_arch(&command_line);
mm_init();
sched_init();
init_IRQ();
console_init();
vfs_caches_init();
rest_init();
1、setup_arch(&command_line)

此函数是系统架构初始化函数,处理 uboot 传递进来的参数,不同的架构进行不同的初始化,也就是说每个架构都会有一个 setup_arch 函数。

linux4.14/arch/arm/kernel/setup.c

void __init setup_arch(char **cmdline_p)
{
        const struct machine_desc *mdesc;

        setup_processor();
        mdesc = setup_machine_fdt(__atags_pointer);
        if (!mdesc)
                mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
        machine_desc = mdesc;
        machine_name = mdesc->name;
        dump_stack_set_arch_desc("%s", mdesc->name);

        if (mdesc->reboot_mode != REBOOT_HARD)
                reboot_mode = mdesc->reboot_mode;

        init_mm.start_code = (unsigned long) _text;
        init_mm.end_code   = (unsigned long) _etext;
        init_mm.end_data   = (unsigned long) _edata;
        init_mm.brk           = (unsigned long) _end;

        /* populate cmd_line too for later use, preserving boot_command_line */
        strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
        *cmdline_p = cmd_line;
        early_fixmap_init();
        early_ioremap_init();
        parse_early_param();

#ifdef CONFIG_MMU