正点原子嵌入式linux驱动开发——Linux内核启动流程

上一篇笔记学习了Linux内核的顶层Makefile,现在来看Linux内核的大致启动流程,Linux内核的启
动流程要比uboot复杂的多,涉及到的内容也更多,因此本章就大致的了解一Linux内核的启动流程

链接脚本vmlinux.lds

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

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

Linux内核启动流程分析

Linux内核入口stext

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

根据示例代码16.2.1.1中的注释,Linux内核启动之前要求如下:

  1. 关闭MMU
  2. 关闭D-cache
  3. I-Cache无所谓
  4. r0=0
  5. r1=machine nr(也就是机器ID)
  6. r2=atags或设备树(dtb)首地址

Linux内核的入口点stext其实相当于内核的入口函数,stext函数内容如下:

c 复制代码
示例代码16.2.1.2 arch/arm/kernel/head.S 代码段 
77 ENTRY(stext) 
...... 
88     @ ensure svc mode and all interrupts masked 
89     safe_svcmode_maskall r9 
90 
91     mrc p15, 0, r9, c0, c0 @ get processor id 
92     bl __lookup_processor_type @ r5=procinfo r9=cpuid 
93     movs r10, r5 @ invalid processor (r5=0)? 
94   THUMB( it eq ) @ force fixup-able long branch encoding 
95     beq __error_p @ yes, error 'p' 
...... 
105 #ifndef CONFIG_XIP_KERNEL 
...... 
110 #else 
111     ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case 
112 #endif 
113 
114     /* 
115     * r1 = machine no, r2 = atags or dtb, 
116     * r8 = phys_offset, r9 = cpuid, r10 = procinfo 
117     */ 
118     bl __vet_atags 
...... 
125     bl __create_page_tables 
...... 
146     ldr r13, =__mmap_switched @ address to jump to after 
147             @ mmu has been enabled 
148     badr lr, 1f @ return (PIC) address 
149 #ifdef CONFIG_ARM_LPAE 
150     mov r5, #0 @ high TTBR0 
151     mov r8, r4, lsr #12 @ TTBR1 is swapper_pg_dir pfn 
152 #else 
153     mov r8, r4 @ set TTBR1 to swapper_pg_dir 
154 #endif 
155     ldr r12, [r10, #PROCINFO_INITFUNC] 
156     add r12, r12, r10
157     ret r12 
158 1: b __enable_mmu 
159 ENDPROC(stext)

第89行,调用函数safe_svcmode_maskall确保CPU处于SVC模式,并且关闭了所有的中断safe_svcmode_maskall定义在文件

arch/arm/include/asm/assembler.h中。

第91行,读处理器ID,ID值保存在r9寄存器中。

第92行,调用函数__lookup_processor_type检查当前系统是否支持此CPU,如果支持的就获取procinfo信息。procinfo是proc_info_list类型的结构体,proc_info_list在文件arch/arm/include/asm/procinfo.h中的定义如下:

Linux内核将每种处理器都抽象为一个proc_info_list结构体,每种处理器都对应一个procinfo。因此可以通过处理器ID来找到对应的procinfo结构,__lookup_processor_type函数找

到对应处理器的procinfo以后会将其保存到r5寄存器中。

继续回到示例代码16.2.1.2中,第118行,调用函数__vet_atags验证atags或设备树(dtb)的合法性。函数__vet_atags定义在文件arch/arm/kernel/head-common.S中。

第120行,STM32MP157是双核A7处理器,支持MP,因此定了宏CONFIG_SMP_ON_UP,所以此处__fixup_smp函数会执行,处理多核。

第125行,调用函数__create_page_tables创建页表。

第146行,将函数__mmap_switched的地址保存到r13寄存器中。__mmap_switched定义在文件arch/arm/kernel/head-common.S,__mmap_switched最终会调用start_kernel函数。

第158行,调用__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中,函数代码如下:

c 复制代码
示例代码16.2.2.1 head-common.S 代码段 
77 __mmap_switched:
78 
79     mov r7, r1 
80     mov r8, r2 
81     mov r10, r0 
82 
83     adr r4, __mmap_switched_data 
84     mov fp, #0 
85 
86 #if defined(CONFIG_XIP_DEFLATED_DATA) 
87     ARM( ldr sp, [r4], #4 ) 
88 THUMB( ldr sp, [r4] ) 
89 THUMB( add r4, #4 ) 
90     bl __inflate_kernel_data @ decompress .data to RAM 
91     teq r0, #0 
92     bne __error 
93 #elif defined(CONFIG_XIP_KERNEL) 
94     ARM( ldmia r4!, {r0, r1, r2, sp} ) 
95 THUMB( ldmia r4!, {r0, r1, r2, r3} ) 
96 THUMB( mov sp, r3 ) 
97     sub r2, r2, r1 
98     bl memcpy @ copy .data to RAM 
99 #endif 
100 
101     ARM( ldmia r4!, {r0, r1, sp} ) 
102 THUMB( ldmia r4!, {r0, r1, r3} ) 
103 THUMB( mov sp, r3 ) 
104     sub r2, r1, r0 
105     mov r1, #0 
106     bl memset @ clear .bss 
107 
108     ldmia r4, {r0, r1, r2, r3} 
109     str r9, [r0] @ Save processor ID 
110     str r7, [r1] @ Save machine type 
111     str r8, [r2] @ Save atags pointer 
112     cmp r3, #0 
113     strne r10, [r3] @ Save control register values 
114     mov lr, #0 
115     b start_kernel 
116 ENDPROC(__mmap_switched)

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

start_kernel函数

start_kernel通过调用众多的子函数来完成Linux启动之前的一些初始化工作,由于start_kernel函数里面调用的子函数太多,而这些子函数又很复杂,因此简单来看一下一些重要的子函数。精简并添加注释后的start_kernel函数内容如下:

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

	build_all_zonelists(NULL, NULL); /* 建立系统内存页区(zone)链表 */ 
	page_alloc_init(); /* 处理用于热插拔CPU的页 */ 

	/* 打印命令行信息 */ 
	pr_notice("Kernel command line: %s\n", boot_command_line); 
	parse_early_param(); /* 解析命令行中的console参数 */ 
	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(); 

	setup_log_buf(0); /* 设置log使用的缓冲区*/ 
	pidhash_init(); /* 构建PID哈希表,Linux中每个进程都有一个ID, 
					 * 这个ID叫做PID。通过构建哈希表可以快速搜索进程 * 信息结构体。 
					 */ 
	vfs_caches_init_early(); /* 预先初始化vfs(虚拟文件系统)的目录项和索引 
							节点缓存*/ 
	sort_main_extable(); /* 定义内核异常列表 */ 
	trap_init(); /* 完成对系统保留中断向量的初始化 */ 
	mm_init(); /* 内存管理初始化 */ 

	sched_init(); /* 初始化调度器,主要是初始化一些结构体 */ 
	preempt_disable(); /* 关闭优先级抢占 */ 
	if (WARN(!irqs_disabled(),/* 检查中断是否关闭,如果没有的话就关闭中断 */ 
		"Interrupts were enabled *very* early, fixing it\n")) 
		local_irq_disable(); 
	radix_tree_init(); 
	workqueue_init_early(); /*允许及早创建工作队列和工作项排队/取消。工作项的 
							 * 执行取决于kthread,并在workqueue_init()之后 
							 开始。*/ 
	rcu_init(); /* 初始化RCU,RCU全称为Read Copy Update(读-拷贝修改) */
	trace_init(); /* 跟踪调试相关初始化 */ 
	context_tracking_init(); 
	radix_tree_init(); /* 基数树相关数据结构初始化 */ 
	early_irq_init(); /* 初始中断相关初始化,主要是注册irq_desc结构体变 
					   * 量,因为Linux内核使用irq_desc来描述一个中断。 
					   */ 
    init_IRQ(); /* 中断初始化 */ 
    tick_init(); /* tick初始化 */ 
    rcu_init_nohz(); 
    init_timers(); /* 初始化定时器 */ 
    hrtimers_init(); /* 初始化高精度定时器 */ 
    softirq_init(); /* 软中断初始化 */ 
    timekeeping_init(); 
    time_init(); /* 初始化系统时间 */ 
    sched_clock_postinit(); 
    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(); /* slab初始化,slab是Linux内存分配器 */ 
	console_init(); /* 初始化控制台,之前printk打印的信息都存放 
					 * 缓冲区中,并没有打印出来。只有调用此函数 
					 * 初始化控制台以后才能在控制台上打印信息。 
					 */ 
	if (panic_later) 
		panic("Too many boot %s vars at `%s'", panic_later, 
			panic_param); 

	lockdep_info();/* 如果定义了宏CONFIG_LOCKDEP,那么此函数打印一些信息。*/ 
	locking_selftest(); /* 锁自测 */ 
	...... 
	page_ext_init(); 
	kmemleak_init(); /* kmemleak初始化,kmemleak用于检查内存泄漏 */ 
	debug_objects_mem_init(); 
	setup_per_cpu_pageset(); 
	numa_policy_init(); 
	if (late_time_init)
		late_time_init(); 
	calibrate_delay(); /* 测定BogoMIPS值,可以通过BogoMIPS来判断CPU的性能 
						* BogoMIPS设置越大,说明CPU性能越好。 
						*/ 
	pidmap_init(); /* PID位图初始化 */ 
	anon_vma_init(); /* 生成anon_vma slab缓存 */ 
	acpi_early_init(); 
	...... 
	thread_stack_cache_init(); 
	cred_init(); /* 为对象的每个用于赋予资格(凭证) */ 
	fork_init(); /* 初始化一些结构体以使用fork函数 */ 
	proc_caches_init(); /* 给各种资源管理结构分配缓存 */ 
	buffer_init(); /* 初始化缓冲缓存 */ 
	key_init(); /* 初始化密钥 */ 
	security_init(); /* 安全相关初始化 */ 
	dbg_late_init(); 
	vfs_caches_init(totalram_pages); /* 为VFS创建缓存 */ 
	signals_init(); /* 初始化信号 */ 

	proc_root_init(); /* 注册并挂载proc文件系统 */ 
	nsfs_init(); 
	cpuset_init(); /* 初始化cpuset,cpuset是将CPU和内存资源以逻辑性 
					* 和层次性集成的一种机制,是cgroup使用的子系统之一 
					*/ 
	cgroup_init(); /* 初始化cgroup */ 
	taskstats_init_early(); /* 进程状态初始化 */ 
	delayacct_init(); 

	check_bugs(); /* 检查写缓冲一致性 */ 
	
	acpi_subsystem_init(); 
	sfi_init_late(); 
	if (efi_enabled(EFI_RUNTIME_SERVICES)) { 
		efi_free_boot_services(); 
	} 

	rest_init(); /* rest_init函数 */ 
}

start_kernel里面调用了大量的函数,每一个函数都是一个庞大的知识点,如果想要学习Linux内核,那么这些函数就需要去详细的研究 。正点原子的嵌入式linux开发指南注重于嵌入式Linux入门,因此不会去讲太多关于Linux内核的知识。start_kernel函数最后调用了rest_init,接下来简单看一下rest_init函数。

rest_init函数

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

c 复制代码
示例代码16.2.4.1 rest_init函数 
406 noinline void __ref rest_init(void)
407 { 
408     struct task_struct *tsk; 
409     int pid; 
410 
411     rcu_scheduler_starting(); 
412     /* 
413      * We need to spawn init first so that it obtains pid 1, however 
414      * the init task will end up wanting to create kthreads, which, 
415      * if we schedule it before we create kthreadd, will OOPS. 
416      */ 
417     pid = kernel_thread(kernel_init, NULL, CLONE_FS); 
418     /* 
419      * Pin init on the boot CPU. Task migration is not properly 
420      * working until sched_init_smp() has been run. It will set the 
421      * allowed CPUs for init to the non isolated CPUs. 
422     */ 
423     rcu_read_lock(); 
424     tsk = find_task_by_pid_ns(pid, &init_pid_ns); 
425     set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); 
426     rcu_read_unlock(); 
427 
428     numa_default_policy(); 
429     pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 
430     rcu_read_lock(); 
431     kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 
432     rcu_read_unlock(); 
433 
434     /* 
435      * Enable might_sleep() and smp_processor_id() checks. 
436      * They cannot be enabled earlier because with CONFIG_PREEMPTION=y 
437      * kernel_thread() would trigger might_sleep() splats. With 
438      * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled 
439      * already, but it's stuck on the kthreadd_done completion. 
440      */ 
441     system_state = SYSTEM_SCHEDULING; 
442 
443     complete(&kthreadd_done);
444 
445     /* 
446      * The boot idle thread must execute schedule() 
447      * at least once to get things moving: 
448      */ 
449     schedule_preempt_disabled(); 
450     /* Call into cpu_idle with preempt disabled */ 
451     cpu_startup_entry(CPUHP_ONLINE); 
452 }

第411行,调用函数rcu_scheduler_starting,启动 RCU锁调度器。

第417行,调用函数kernel_thread创建kernel_init进程,也就是大名鼎鼎的init内核进程。init进程的 PID为1。init进程一开始是内核进程(也就是运行在内核态),后面init进程会在根文件系统中查找名为"init"这个程序,这个"init"程序处于用户态,通过运行这个"init"程序,init进程就会实现从内核态到用户态的转变。

第429行,调用函数kernel_thread创建kthreadd内核进程,此内核进程的PID为2。kthreadd进程负责所有内核进程的调度和管理。

第451行,最后调用函数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进程的进程函数。

init进程

kernel_init函数就是init进程具体做的工作,定义在文件init/main.c中,函数内容如下:

c 复制代码
示例代码16.2.5.1 kernel_init函数 
1106 static int __ref kernel_init(void *unused) 
1107 {
1108     int ret; 
1109 
1110     kernel_init_freeable(); /* init进程的一些其他初始化工作 */ 
1111     /* need to finish all async __init code before freeing the memory */ 
1112     async_synchronize_full(); /* 等待所有的异步调用执行完成 */ 
1113     ftrace_free_init_mem(); /* 释放init段内存 */ 
1114     free_initmem(); 
1115     mark_readonly(); 
1116 
1117     /* 
1118      * Kernel mappings are now finalized - update the userspace 
1119      * page-table to finalize PTI. 
1120      */ 
1121     pti_finalize(); 
1122 
1123     system_state = SYSTEM_RUNNING; /* 标记系统正在运行 */ 
1124     numa_default_policy(); 
1125 
1126     rcu_end_inkernel_boot(); 
1127 
1128     if (ramdisk_execute_command) { 
1129         ret = run_init_process(ramdisk_execute_command); 
1130         if (!ret) 
1131             return 0; 
1132         pr_err("Failed to execute %s (error %d)\n", 
1133                 ramdisk_execute_command, ret); 
1134 } 
1135 
1136     /* 
1137      * We try each of these until one succeeds. 
1138      * 
1139      * The Bourne shell can be used instead of init if we are 
1140      * trying to recover a really broken machine. 
1141      */ 
1142     if (execute_command) { 
1143         ret = run_init_process(execute_command); 
1144         if (!ret) 
1145             return 0; 
1146         panic("Requested init %s failed (error %d).", 
1147                 execute_command, ret); 
1148 } 
1149     if (!try_to_run_init_process("/sbin/init") ||
1150         !try_to_run_init_process("/etc/init") || 
1151         !try_to_run_init_process("/bin/init") || 
1152         !try_to_run_init_process("/bin/sh")) 
1153         return 0; 
1154 
1155     panic("No working init found. Try passing init= option to kernel. " 
1156         "See Linux Documentation/admin-guide/init.rst for guidance."); 
1157 }

第1110行, kernel_init_freeable函数用于完成 init进程的一些其他初始化工作,稍后再来具体看一下此函数。

第1128行,ramdisk_execute_command是一个全局的char指针变量,此变量值为"/init"也就是根目录下的init程序。ramdisk_execute_command也可以通过uboot传递,在bootargs中使用"rdinit=xxx"即可xxx为具体的init程序名字。

第1129行,如果存在"/init"程序的话就通过函数run_init_process来运行此程序。

第1142行,如果ramdisk_execute_command为空的话就看execute_command是否为空,反正不管如何一定要在根文件系统中找到一个可运行的 init程序。execute_command的值是通过uboot传递,在bootargs中使用"init=xxxx"就可以了,比如"init=/linuxrc"表示根文件系统中的linuxrc就是要执行的用户空间init程序。

第1149-1153行,如果ramdisk_execute_command和execute_command都为空,那么就依次查找"/sbin/init"、"/etc/init"、"/bin/init"和"/bin/sh",这四个相当于备init 程序,如果这四个也不存在,那么Linux启动失败!

第1155行,如果以上步骤都没有找到用户空间的 init程序,那么就提示错误发生!

最后来简单看一下kernel_init_freeable函数,前面说了,kernel_init会调用此函数来做一些init进程初始化工作。kernel_init_freeable定义在文件init/main.c中,缩减后的函数内容如下:

c 复制代码
示例代码162.5.2 kernel_init_freeable函数 
1159 static noinline void __init kernel_init_freeable(void) 
1160 { 
1161     /* 
1162      * Wait until kthreadd is all set-up. 
1163      */ 
1164     wait_for_completion(&kthreadd_done);/* 等待kthreadd准备就绪 */ 
...... 
1185     smp_init(); /* SMP初始化 */ 
1186     sched_init_smp(); /* 多核(SMP)调度初始化 */ 
1187 
1188     page_alloc_init_late(); /* 设备初始化都在此函数中完成 */ 
1189     /* Initialize page ext after all struct pages are initialized. */ 
1190     page_ext_init(); 
1191 
1192     do_basic_setup();
1193 
1194     /* Open the /dev/console on the rootfs, this should never fail */ 
1195     if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) 
1196         pr_err("Warning: unable to open an initial console.\n"); 
1197 
1198     (void) ksys_dup(0); 
1199     (void) ksys_dup(0); 
1200     /* 
1201      * check if there is an early userspace init. If yes, let it 
1202      * do all the work 
1203      */ 
1204 
1205     if (!ramdisk_execute_command) 
1206         ramdisk_execute_command = "/init"; 
1207 
1208     if (ksys_access((const char __user *) 
1209             ramdisk_execute_command, 0) != 0) { 
1210         ramdisk_execute_command = NULL; 
1211         prepare_namespace(); 
1212     } 
1213 
1214     /* 
1215      * Ok, we have completed the initial bootup, and 
1216      * we're essentially up and running. Get rid of the 
1217      * initmem segments and start the user-mode stuff.. 
1218      * 
1219      * rootfs is available now, try loading the public keys 
1220      * and default modules 
1221      */ 
1222 
1223     integrity_load_keys(); 
1224 }

第1192行,do_basic_setup函数用于完成Linux下设备驱动初始化工作!非常重要。do_basic_setup会调用driver_init函数完成Linux下驱动模型子系统的初始化。

第1195行,打开设备"/dev/console",在Linux中一切皆为文件!因此"/dev/console"也是一个文件,此文件为控制台设备。每个文件都有一个文件描述符,此处打开的"/dev/console"文件描述符为0,作为标准输入(0)。

第1198和1199行,sys_dup函数将标准输入(0)的文件描述符复制了2次,一个作为标准输出(1),一个作为标准错误(2)。这样标准输入、输出、错误都是/dev/console了。 console通过uboot的bootargs环境变量设置,"console=ttySTM0,115200"表示将/dev/ttySTM0设置为console,也就是STM32MP1的串口 4。当然,也可以设置其他的设备为console,比如虚拟控制台tty1,设置tty1为console就可以在LCD屏幕上看到系统的提示信息。

第1211行,调用函数prepare_namespace来挂载根文件系统。根文件系统也是由命令行参数指定的,也就是uboot的bootargs环境变量。比如"root=/dev/mmcblk1p3 rootwait rw"就表示

根文件系统在/dev/mmcblk1p3中,也就是EMMC的分区3中。

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

总结

正点原子的linux驱动开发文档中,关于linux内核的讲解就是这么多,不像之前uboot一样那么详尽,因为linux内核真的非常庞大复杂!只需要关注当中截取出来的有中文注释的代码也就可以了,之后可以自己去查看源码学习其中的每一个函数。

从链接脚本vmlinux.lds出发,可以看出Linux内核入口在stext,会调用safe_svcmode_maskall确保CPU处于SVC模式并关闭所有中断,然后读取处理器ID并调用__lookup_processor_type查看当前系统是否支持此CPU,支持就会获取procinfo信息读取这个处理器的各种参数;调用__vet_atags验证atags或设备树,且STM32MP157是双核A7,会调用__fixup_smp处理多核;调用__create_page_tables创建页表;将__mmap_switched存入r13中,最后调用__enable_mmu使能MMU,这个函数最终调用__turn_mmu_on函数执行在r13中的__mmap_switched函数。

__mmap_switched经过汇编操作寄存器,最终调用start_kernel启动Linux内核,而start_kernel就是我们重点关注的函数,有中文注释,有需求可以对每个函数源码进行解读

start_kernel最终调用了rest_init函数 ,这个函数会调用rcu_scheduler_starting启动RCU锁调度器,然后调用kernel_thread创建kernel_init进程,也就是init进程,其PID为1;调用kernel_thread创建kthreadd内核进程,其PID为2,负责所有内核进程的管理和调度;最后调用cpu_startup_entry进入idle进程,其PID为0,也就是空闲进程,并会调用kernel_thread创建

kernel_init进程函数是在init进程中的,这就是init进程具体的工作 ,会调用kernel_init_freeable完成init进程其他的初始化工作;之后会通过对全局char指针变量ramdisk_execute_command(值为/init),也就是根目录的init程序判断是否可以init,如果有就调用run_init_process运行,如果没有就会看execute_command(通过uboot传递,在bootargs使用"init=xxxx"就可以)来找到可运行的init程序;如果还是没有,就会直接一次查找备用init("/sbin/init","/etc/init","/bin/init"以及"/bin/sh"),如果还是没有就发生错误了。

kernel_init_freeable函数,会调用do_basic_setup用于Linux下设备驱动初始化工作,其中会调用driver_init完成驱动模型子系统初始化;然后会打开设备"/dev/console",调用sys_dup复制两次,保证标准输入、输出、错误都是/dev/console,一般会设置为/dev/ttySTM0,也就是STM32MP1的串口4;调用prepare_namespace挂载根目录。

相关推荐
知识分享小能手6 分钟前
uni-app 入门学习教程,从入门到精通,uni-app组件 —— 知识点详解与实战案例(4)
前端·javascript·学习·微信小程序·小程序·前端框架·uni-app
wahkim14 分钟前
Flutter 学习资源及视频
学习
摇滚侠24 分钟前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 属性优先级 行内写法 变量选择 笔记42
java·spring boot·笔记
摇滚侠28 分钟前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 总结 热部署 常用配置 笔记44
java·spring boot·笔记
云雾J视界38 分钟前
Linux企业级解决方案架构:字节跳动短视频推荐系统全链路实践
linux·云原生·架构·kubernetes·音视频·glusterfs·elk stack
小白要努力sgy1 小时前
待学习--中间件
学习·中间件
rechol1 小时前
汇编与底层编程笔记
汇编·arm开发·笔记
~无忧花开~1 小时前
CSS学习笔记(五):CSS媒体查询入门指南
开发语言·前端·css·学习·媒体
吴鹰飞侠1 小时前
AJAX的学习
前端·学习·ajax
tongsound2 小时前
libmodbus 使用示例
linux·c++