Linux系统移植篇(十一)Linux 内核启动流程

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

bash 复制代码
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 } 

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 中有如下所示提示内容:

bash 复制代码
/* 
 * 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. 
 ..... 
*/

①、关闭 MMU。 ②、关闭 D-cache。 ③、I-Cache 无所谓。 ④、r0=0。 ⑤、r1=machine nr(也就是机器 ID)。 ⑥、r2=atags 或者设备树(dtb)首地址。

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

bash 复制代码
80 ENTRY(stext) 
...... 
91 @ ensure svc mode and all interrupts masked 
92 safe_svcmode_maskall r9 
93 
94 mrc p15, 0, r9, c0, c0 @ get processor id 
95 bl __lookup_processor_type @ r5=procinfo r9=cpuid 
96 movs r10, r5 @ invalid processor (r5=0)? 
97 THUMB( it eq ) @ force fixup-able long branch encoding 
98 beq __error_p @ yes, error 'p' 
99 
...... 
107 
108 #ifndef CONFIG_XIP_KERNEL 
...... 
113 #else 
114 ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case 
115 #endif 
116 
117 /* 
118 * r1 = machine no, r2 = atags or dtb, 
119 * r8 = phys_offset, r9 = cpuid, r10 = procinfo 
120 */ 
121 bl __vet_atags 
...... 
128 bl __create_page_tables 
129 
130 /* 
131 * The following calls CPU specific code in a position independent 
132 * manner. See arch/arm/mm/proc-*.S for details. r10 = base of 
133 * xxx_proc_info structure selected by __lookup_processor_type 
134 * above. On return, the CPU will be ready for the MMU to be 
135 * turned on, and r0 will hold the CPU control register value.  
136 */ 
137 ldr r13, =__mmap_switched @ address to jump to after 
138 @ mmu has been enabled 
139 adr lr, BSYM(1f) @ return (PIC) address 
140 mov r8, r4 @ set TTBR1 to swapper_pg_dir 
141 ldr r12, [r10, #PROCINFO_INITFUNC] 
142 add r12, r12, r10 
143 ret r12 
144 1: b __enable_mmu 
145 ENDPROC(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 中的定义如下:

bash 复制代码
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 寄存器中。 继续回到示例代码 36.2.1.2 中,第 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 函数。

这里重点说一下内核为什么要关闭MMU,也就是内存映射,后面又要打开?这个过程是为什么?

在内核启动的早期阶段,系统处于物理地址模式下(即 MMU 处于关闭状态),这样可以直接访问物理内存,方便初始化关键数据结构和加载必要的代码。具体原因包括:

初始引导环境简化操作 :上电后,处理器默认运行在关闭 MMU 的状态,此时所有地址都是物理地址,避免了虚拟地址转换的复杂性。这有助于启动代码(bootloader 和内核早期代码)顺利加载并执行。

页表构建:内核在启动过程中需要构建页表,以建立虚拟地址到物理地址的映射。此时必须依赖物理地址操作来确保数据和代码能被正确访问。构建好页表之前,开启 MMU 会导致地址转换错误或不可预知的行为。

切换到虚拟内存管理:一旦内核完成了页表的设置(例如通过调用 __create_page_tables),它就可以启用 MMU,这时所有内存访问都将通过页表进行地址转换。这样做能够提供虚拟内存、内存保护、缓存管理等高级功能,对后续内核和应用程序的稳定性与安全性至关重要。

系统安全与隔离:开启 MMU 后,内核可以实现内核空间和用户空间的隔离,以及各种内存保护机制,这些都是现代操作系统必备的特性。

__mmap_switched 函数定义在文件 arch/arm/kernel/head-common.S 中

bash 复制代码
81 __mmap_switched: 
82 adr r3, __mmap_switched_data 
83 
84 ldmia r3!, {r4, r5, r6, r7} 
85 cmp r4, r5 @ Copy data segment if needed 
86 1: cmpne r5, r6 
87 ldrne fp, [r4], #4 
88 strne fp, [r5], #4 
89 bne 1b 
90 
91 mov fp, #0 @ Clear BSS (and zero fp) 
92 1: cmp r6, r7 
93 strcc fp, [r6],#4 
94 bcc 1b 
95 
96 ARM( ldmia r3, {r4, r5, r6, r7, sp}) 
97 THUMB( ldmia r3, {r4, r5, r6, r7} ) 
98 THUMB( ldr sp, [r3, #16] ) 
99 str r9, [r4] @ Save processor ID 
100 str r1, [r5] @ Save machine type 
101 str r2, [r6] @ Save atags pointer 
102 cmp r7, #0 
103 strne r0, [r7] @ Save control register values 
104 b start_kernel 
105 ENDPROC(__mmap_switched) 

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

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

bash 复制代码
asmlinkage __visible void __init start_kernel(void) 
{ 
 char *command_line; 
 char *after_dashes; 
 
 lockdep_init(); /* lockdep 是死锁检测模块,此函数会初始化
* 两个 hash 表。此函数要求尽可能早的执行! 
 */ 
set_task_stack_end_magic(&init_task);/* 设置任务栈结束魔术数, 
*用于栈溢出检测 
*/ 
 smp_setup_processor_id(); /* 跟 SMP 有关(多核处理器),设置处理器 ID。 
 * 有很多资料说 ARM 架构下此函数为空函数,那是因 
 * 为他们用的老版本 Linux,而那时候 ARM 还没有多 
 * 核处理器。 
*/ 
 debug_objects_early_init(); /* 做一些和 debug 有关的初始化 */ 
 boot_init_stack_canary(); /* 栈溢出检测初始化 */ 
 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 数据 */ 
 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(); 
 idr_init_cache(); /* IDR 初始化,IDR 是 Linux 内核的整数管理机 
 * 制,也就是将一个整数 ID 与一个指针关联起来。 
 */ 
 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(); 
 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(); 
 debug_objects_mem_init(); 
 kmemleak_init(); /* kmemleak 初始化,kmemleak 用于检查内存泄漏 */ 
 setup_per_cpu_pageset(); 
 numa_policy_init(); 
 if (late_time_init) 
 late_time_init(); 
 sched_clock_init(); 
 calibrate_delay(); /* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能 
 * BogoMIPS 设置越大,说明 CPU 性能越好。 
 */ 
 pidmap_init(); /* PID 位图初始化 */ 
 anon_vma_init(); /* 生成 anon_vma slab 缓存 */ 
 acpi_early_init(); 
 ...... 
 thread_info_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(); /* 初始化信号 */ 
 
 page_writeback_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_late_init(); 
 efi_free_boot_services(); 
 } 
 
 ftrace_init(); 
 
 rest_init(); /* rest_init 函数 */ 
} 

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

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

bash 复制代码
383 static noinline void __init_refok rest_init(void) 
384 { 
385 int pid; 
386 
387 rcu_scheduler_starting(); 
388 smpboot_thread_init(); 
389 /* 
390 * We need to spawn init first so that it obtains pid 1, however 
391 * the init task will end up wanting to create kthreads, which, 
392 * if we schedule it before we create kthreadd, will OOPS. 
393 */ 
394 kernel_thread(kernel_init, NULL, CLONE_FS); 
395 numa_default_policy(); 
396 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 
397 rcu_read_lock(); 
398 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 
399 rcu_read_unlock(); 
400 complete(&kthreadd_done); 
401 
402 /* 
403 * The boot idle thread must execute schedule() 
404 * at least once to get things moving: 
405 */ 
406 init_idle_bootup_task(current); 
407 schedule_preempt_disabled(); 
408 /* Call into cpu_idle with preempt disabled */ 
409 cpu_startup_entry(CPUHP_ONLINE); 
410 }

第 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。之所以图 36.2.4.1 中没有显示 PID 为 0 的 idle 进程,那是因为 idle 进程是内核进程。init 进程,kernel_init 就是 init 进程的进程函数。

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

bash 复制代码
928 static int __ref kernel_init(void *unused) 
929 { 
930 int ret; 
931 
932 kernel_init_freeable(); /* init 进程的一些其他初始化工作 */ 
933 /* need to finish all async __init code before freeing the 
memory */ 
934 async_synchronize_full(); /* 等待所有的异步调用执行完成 */ 
935 free_initmem(); /* 释放 init 段内存 */ 
936 mark_rodata_ro(); 
937 system_state = SYSTEM_RUNNING; /* 标记系统正在运行 */ 
938 numa_default_policy(); 
939 
940 flush_delayed_fput(); 
941 
942 if (ramdisk_execute_command) { 
943 ret = run_init_process(ramdisk_execute_command); 
944 if (!ret) 
945 return 0; 
946 pr_err("Failed to execute %s (error %d)\n", 
947 ramdisk_execute_command, ret); 
948 } 
949 
950 /* 
951 * We try each of these until one succeeds. 
952 * 
953 * The Bourne shell can be used instead of init if we are 
954 * trying to recover a really broken machine. 
955 */  
956 if (execute_command) { 
957 ret = run_init_process(execute_command); 
958 if (!ret) 
959 return 0; 
960 panic("Requested init %s failed (error %d).", 
961 execute_command, ret); 
962 } 
963 if (!try_to_run_init_process("/sbin/init") || 
964 !try_to_run_init_process("/etc/init") || 
965 !try_to_run_init_process("/bin/init") || 
966 !try_to_run_init_process("/bin/sh")) 
967 return 0; 
968 
969 panic("No working init found. Try passing init= option to 
kernel. " 
970 "See Linux Documentation/init.txt for guidance."); 
971 } 
 第 932 行,kernel_init_freeable 函数用于完成 init 进程的一些其他初始化工作,稍后再来具
体看一下此函数。 
第 940 行,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 函数,前面说了,kernel_init 会调用此函数来做一些
init 进程初始化工作。kernel_init_freeable 定义在文件 init/main.c 中,缩减后的函数内容如下: 
示例代码 36.2.5.2 kernel_init_freeable 函数 
973 static noinline void __init kernel_init_freeable(void) 
974 { 
975 /* 
976 * Wait until kthreadd is all set-up. 
977 */ 
978 wait_for_completion(&kthreadd_done);/* 等待 kthreadd 进程准备就绪 */ 
...... 
998 
999 smp_init(); /* SMP 初始化 */  
1000 sched_init_smp(); /* 多核(SMP)调度初始化 */ 
1001 
1002 do_basic_setup(); /* 设备初始化都在此函数中完成 */ 
1003 
1004 /* Open the /dev/console on the rootfs, this should never fail */ 
1005 if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 
0) 
1006 pr_err("Warning: unable to open an initial console.\n"); 
1007 
1008 (void) sys_dup(0); 
1009 (void) sys_dup(0); 
1010 /* 
1011 * check if there is an early userspace init. If yes, let it do 
1012 * all the work 
1013 */ 
1014 
1015 if (!ramdisk_execute_command) 
1016 ramdisk_execute_command = "/init"; 
1017 
1018 if (sys_access((const char __user *) ramdisk_execute_command, 
0) != 0) { 
1019 ramdisk_execute_command = NULL; 
1020 prepare_namespace(); 
1021 } 
1022 
1023 /* 
1024 * Ok, we have completed the initial bootup, and 
1025 * we're essentially up and running. Get rid of the 
1026 * initmem segments and start the user-mode stuff.. 
1027 * 
1028 * rootfs is available now, try loading the public keys 
1029 * and default modules 
1030 */ 
1031 
1032 integrity_load_keys(); 
1033 load_default_modules(); 
1034 }

现在总结一下流程吧。

内核入口与解压内核

内核入口代码通常位于各架构目录下的汇编文件(如 x86 的 arch/x86/boot 或 ARM 的 arch/arm/kernel/head.S)。在这一步:

内核首先解压自身(如果内核是压缩的)。

完成最基本的 CPU 和内存环境设置。

设置处理器模式与寄存器

启动时系统一般在关闭 MMU 的物理地址模式下运行,便于直接访问物理内存。早期代码还会根据处理器 ID 找到对应的 procinfo 结构,并做一些必要的寄存器初始化(例如将对应的结构地址保存到特定寄存器中)。

建立页表与启用 MMU

内核在这一阶段构建页表,将物理地址映射到虚拟地址空间。这包括:

调用函数如 __create_page_tables 来创建页表。

通过调用 __enable_mmu(最终由 __turn_mmu_on 实现)来开启 MMU,实现从物理地址模式到虚拟地址模式的切换。

切换后,所有内存访问均通过虚拟地址进行,便于实现内存保护、缓存管理等功能。

启动第一个用户进程:init

内核初始化完成后,启动第一个用户空间进程(通常为 init 进程,PID 为 1),它负责加载和启动后续用户空间的各项服务。

init 进程可能采用 System V init、systemd 或其它初始化系统,进一步启动登录服务、网络服务、图形界面等。

系统进入正常运行状态

随着 init 进程和相关服务的启动,整个系统逐步进入稳定的多任务运行状态,用户可以开始使用系统。

相关推荐
极客代码6 分钟前
Unix 域套接字(本地套接字)
linux·c语言·开发语言·unix·socket·unix域套接字·本地套接字
zxnbmk17 分钟前
ansible速查手册
linux·服务器·ansible
小卓笔记25 分钟前
DNS主从服务器
运维·服务器
Muisti29 分钟前
TCP 通信流程图
服务器·网络·windows
book012137 分钟前
Ansible 自动化运维
运维·自动化·ansible
巷子里的童年ya38 分钟前
Ansible模块
linux·centos·ansible
阿正的梦工坊40 分钟前
Linux 命令:按内存使用大小排序查看 PID 的完全指南
linux·运维·服务器
神秘的土鸡1 小时前
Centos搭建Tomcat服务器:我的实战经验分享(成功版本 详细!)
linux·开发语言·python·tomcat·web
IT小饕餮1 小时前
linux 基础网络配置文件
linux·服务器·网络
szxinmai主板定制专家1 小时前
基于FPGA的3U机箱模拟量高速采样板ADI板卡,应用于轨道交通/电力储能等
arm开发·人工智能·fpga开发·架构