I.MX6ULL_Linux_系统篇(21) kernel启动流程

Posted Absorbed_w

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I.MX6ULL_Linux_系统篇(21) kernel启动流程相关的知识,希望对你有一定的参考价值。

链接脚本 vmlinux.lds

要分析 Linux 启动流程,同样需要先编译一下 Linux 源码,因为有很多文件是需要编译才会生成的。首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以
找到 Linux 内核的第一行程序是从哪里执行的。 vmlinux.lds 中有如下代码:

492 OUTPUT_ARCH(arm)
493 ENTRY(stext)
494 jiffies = jiffies_64;
495 SECTIONS
496 
497 /*
498 * XXX: The linker does not define how output sections are
499 * assigned to input sections when there are multiple statements
500 * matching the same input section name. There is no documented
501 * order of matching.
502 *
503 * unwind exit sections must be discarded before the rest of the
504 * unwind sections get included.
505 */
506 /DISCARD/ : 
507 *(.ARM.exidx.exit.text)
508 *(.ARM.extab.exit.text)
509
......
645 

第 493 行的 ENTRY 指明了了 Linux 内核入口,入口为 stext, stext 定义在文件arch/arm/kernel/head.S中,因此要分析Linux内核的启动流程 , 就得先从文件arch/arm/kernel/head.S 的 stext 处开始分析。

Linux 内核启动流程

内核入口 stext

stext 是 Linux 内核的入口地址,在文件 arch/arm/kernel/head.S 中有如下所示提示内容:

根据示例代码中的注释, Linux 内核启动之前要求如下:
①、关闭 MMU。
②、关闭 D-cache。
③、 I-Cache 无所谓。
④、 r0=0。
⑤、 r1=machine nr(也就是机器 ID)。
⑥、 r2=atags 或者设备树(dtb)首地址。
Linux 内核的入口点 stext 其实相当于内核的入口函数, stext 函数内容如下:

 

第 92 行,调用函数 safe_svcmode_maskall 确保 CPU 处于 SVC 模式,并且关闭了所有的中断。 safe_svcmode_maskall 定义在文件 arch/arm/include/asm/assembler.h 中。
第 94 行,读处理器 ID, ID 值保存在 r9 寄存器中。
第 95 行,调用函数__lookup_processor_type 检查当前系统是否支持此 CPU,如果支持就获取 procinfo 信 息 。 procinfo 是 proc_info_list 类 型 的 结 构 体 , proc_info_list 在 文 件
arch/arm/include/asm/procinfo.h 中的定义如下:

struct proc_info_list 
    unsigned int cpu_val;
    unsigned int cpu_mask;
    unsigned long __cpu_mm_mmu_flags; /* used by head.S */
    unsigned long __cpu_io_mmu_flags; /* used by head.S */
    unsigned long __cpu_flush; /* used by head.S */
    const char *arch_name;
    const char *elf_name;
    unsigned int elf_hwcap;
    const char *cpu_name;
    struct processor *proc;
    struct cpu_tlb_fns *tlb;
    struct cpu_user_fns *user;
    struct cpu_cache_fns *cache;
;

Linux 内核将每种处理器都抽象为一个 proc_info_list 结构体,每种处理器都对应一个procinfo。因此可以通过处理器 ID 来找到对应的 procinfo 结构, __lookup_processor_type 函数找到对应处理器的 procinfo 以后会将其保存到 r5 寄存器中。
继续回到示例代码中,第 121 行,调用函数__vet_atags 验证 atags 或设备树(dtb)的合法性。函数__vet_atags 定义在文件 arch/arm/kernel/head-common.S 中。
第 128 行,调用函数__create_page_tables 创建页表。
第 137 行,将函数__mmap_switched 的地址保存到 r13 寄存器中。 __mmap_switched 定义在文件 arch/arm/kernel/head-common.S, __mmap_switched 最终会调用 start_kernel 函数。
第 144 行 , 调 用 __enable_mmu 函 数 使 能 MMU , __enable_mmu 定 义 在 文 件arch/arm/kernel/head.S 中。 __enable_mmu 最终会通过调用__turn_mmu_on 来打开 MMU,
__turn_mmu_on 最后会执行 r13 里面保存的__mmap_switched 函数。

__mmap_switched 函数

__mmap_switched 函数定义在文件 arch/arm/kernel/head-common.S 中,函数代码如下:

第 104 行最终调用 start_kernel 来启动 Linux 内核, start_kernel 函数定义在文件 init/main.c中。

asmlinkage __visible void __init start_kernel(void)

	char *command_line;
	char *after_dashes;

	/*
	 * Need to run as early as possible, to initialize the
	 * lockdep hash:
	 */
	lockdep_init();
	set_task_stack_end_magic(&init_task);
	smp_setup_processor_id();
	debug_objects_early_init();

	/*
	 * Set up the the initial canary ASAP:
	 */
	boot_init_stack_canary();

	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 */

	build_all_zonelists(NULL, NULL);
	page_alloc_init();

	pr_notice("Kernel command line: %s\\n", boot_command_line);
	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, &unknown_bootoption);
	if (!IS_ERR_OR_NULL(after_dashes))
		parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
			   set_init_arg);

	jump_label_init();

	/*
	 * These use large bootmem allocations and must precede
	 * kmem_cache_init()
	 */
	setup_log_buf(0);
	pidhash_init();
	vfs_caches_init_early();
	sort_main_extable();
	trap_init();
	mm_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();
	idr_init_cache();
	rcu_init();

	/* trace_printk() and trace points may be used after this */
	trace_init();

	context_tracking_init();
	radix_tree_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();
	time_init();
	sched_clock_postinit();
	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_info();

	/*
	 * 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();

#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
	page_ext_init();
	debug_objects_mem_init();
	kmemleak_init();
	setup_per_cpu_pageset();
	numa_policy_init();
	if (late_time_init)
		late_time_init();
	sched_clock_init();
	calibrate_delay();
	pidmap_init();
	anon_vma_init();
	acpi_early_init();
#ifdef CONFIG_X86
	if (efi_enabled(EFI_RUNTIME_SERVICES))
		efi_enter_virtual_mode();
#endif
#ifdef CONFIG_X86_ESPFIX64
	/* Should be run before the first non-init thread is created */
	init_espfix_bsp();
#endif
	thread_info_cache_init();
	cred_init();
	fork_init();
	proc_caches_init();
	buffer_init();
	key_init();
	security_init();
	dbg_late_init();
	vfs_caches_init(totalram_pages);
	signals_init();
	/* rootfs populating might need page-writeback */
	page_writeback_init();
	proc_root_init();
	nsfs_init();
	cpuset_init();
	cgroup_init();
	taskstats_init_early();
	delayacct_init();

	check_bugs();

	acpi_subsystem_init();
	sfi_init_late();

	if (efi_enabled(EFI_RUNTIME_SERVICES)) 
		efi_late_init();
		efi_free_boot_services();
	

	ftrace_init();

	/* Do the rest non-__init'ed, we're now alive */
	rest_init();

本文不做详细分析。

rest_init 函数

rest_init 函数定义在文件 init/main.c 中,函数内容如下:

第 387 行,调用函数 rcu_scheduler_starting,启动 RCU 锁调度器
第 394 行,调用函数 kernel_thread 创建 kernel_init 进程,也就是大名鼎鼎的 init 内核进程。init 进程的 PID 为 1。 init 进程一开始是内核进程(也就是运行在内核态),后面 init 进程会在根
文件系统中查找名为“init”这个程序,这个“init”程序处于用户态,通过运行这个“init”程序, init 进程就会实现从内核态到用户态的转变。
第 396 行,调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。 kthreadd进程负责所有内核进程的调度和管理。
第 409 行,最后调用函数 cpu_startup_entry 来进入 idle 进程, cpu_startup_entry 会调用cpu_idle_loop, cpu_idle_loop 是个 while 循环,也就是 idle 进程代码。 idle 进程的 PID 为 0, idle
进程叫做空闲进程,如果学过 FreeRTOS 或者 UCOS 的话应该听说过空闲任务。 idle 空闲进程就和空闲任务一样,当 CPU 没有事情做的时候就在 idle 空闲进程里面“瞎逛游”,反正就是给
CPU 找点事做。当其他进程要工作的时候就会抢占 idle 进程,从而夺取 CPU 使用权。其实大家应该可以看到 idle 进程并没有使用 kernel_thread 或者 fork 函数来创建,因为它是有主进程演
变而来的。
在 Linux 终端中输入“ps -A”就可以打印出当前系统中的所有进程,其中就能看到 init 进程和 kthreadd 进程,如图所示:

从图中可以看出, init 进程的 PID 为 1, kthreadd 进程的 PID 为 2。之所以图中没有显示 PID 为 0 的 idle 进程,那是因为 idle 进程是内核进程。 我们接下来重点看一下 init进程, kernel_init 就是 init 进程的进程函数。

第 932 行, kernel_init_freeable 函数用于完成 init 进程的一些其他初始化工作。
第 942 行, ramdisk_execute_command 是一个全局的 char 指针变量,此变量值为“/init”,也就是根目录下的 init 程序。 ramdisk_execute_command 也可以通过 uboot 传递,在 bootargs 中
使用“rdinit=xxx”即可, xxx 为具体的 init 程序名字。
第 943 行,如果存在“/init”程序的话就通过函数 run_init_process 来运行此程序。
第 956 行,如果 ramdisk_execute_command 为空的话就看 execute_command 是否为空,反正不管如何一定要在根文件系统中找到一个可运行的 init 程序。 execute_command 的值是通过
uboot 传递,在 bootargs 中使用“init=xxxx”就可以了,比如“init=/linuxrc”表示根文件系统中的 linuxrc 就是要执行的用户空间 init 程序。
第 963~966 行,如果 ramdisk_execute_command 和 execute_command 都为空,那么就依次查找“/sbin/init”、“/etc/init”、“/bin/init”和“/bin/sh”,这四个相当于备用 init 程序,如果这四个也不存在,那么 Linux 启动失败!
第 969 行,如果以上步骤都没有找到用户空间的 init 程序,那么就提示错误发生!

kernel_init_freeable功能:

1.驱动初始化

2.console设置

3.挂载根文件系统

Linux 内核启动流程就分析到这里, Linux 内核最终是需要和根文件系统打交道的,需要挂载根文件系统,并且执行根文件系统中的 init 程序,以此来进去用户态。这里就正式引出了根文件系统,根文件系统也是我们系统移植的最后一片拼图。 Linux 移植三巨头: uboot、 Linuxkernel、 rootfs(根文件系统)。关于根文件系统后面章节会详细的讲解,这里我们只需要知道 Linux内核移植完成以后还需要构建根文件系统即可。
 

以上是关于I.MX6ULL_Linux_系统篇(21) kernel启动流程的主要内容,如果未能解决你的问题,请参考以下文章

2018-04-26 《鸟哥的Linux私房菜 基础学习篇(第四版)》 第21章 软件安装_原始码与Tarball 笔记

[从零开始学习FPGA编程-21]:进阶篇 - 架构 - VerilogHDL编码规范

Linux基础篇

ruby test_not_null_database_foreign_keys_with_rspec_2.rb

ruby test_not_null_database_foreign_keys_with_rspec_1.rb

ruby test_not_null_database_foreign_keys_with_rspec_0.rb