CPU从bootloader跳转出来后会开始启动linux kernel,内核启动完成后会将控制权转交给用户空间。

1. RISC-V版本:_start (head.S)

加载内核运行是的个数据段寄存器,重新设置中断描述符表; 开启内核争创运行是的协处理器等资源

加载全局指针

失能浮点运算部件

选择某线程运行boot sequence

清除bss段

保存线程号和DTB信息

setup_vm:

设置内存管理的分页机制;

relocate:

重定位虚拟地址(relocate),包括返回地址、stvec的虚拟地址、kernel页表的satp等,随后切换到kernel的页表首地址

parse_dtb:

解析设备树(”setup.c” : parse_dtb)

1. i386版本:(head_32.S)

i386_start_kernel

2. 跳转到start_kernel(init/main.c: start_kernel)

2.1 关中断单线程阶段

setup_arch():根据体系结构进行初始化
trap_init():异常初始化,为每个CPU配置和建立异常向量表
init_IRQ():初始化设备树描述的中断控制器
time_init():初始化计时系统部分。

2.2 开中断单线程阶段

2.2.1 local_irq_enable()

2.2.2 kmem_cache_init_late()

2.2.3 console_init()

控制台初始化,选择VTconsole(鼠标键盘显示器)、SerialConsole(串口模式)、VGAConsole或者FBConsole模式

2.3 开中断多线程阶段

3. 跳转到rest_init

在调用该函数之前主要进行与操作系统核心层相关的操作,包括进程调度、内存管理和中断系统等主要模块的初始化,而该rest_init函数将创建kernel_init进程。至此0号进程开始创建1号进程。

kernel_thread()

kernel_thread(kernel_init, NULL, CLONE_FS),该函数的第一个参数是函数执政,即此时fork出1号进程来执行kernel_init函数

cpu_startup_entry(CPUHP_ONLINE);

该函数仍由0号进程执行,最后0号进程转化为idle进程。

arch_cpu_idle_prepare

cpu_idle_loop

4. 一号进程kernel_init

该函数主要分为两步,一步是内核线程,接管之前初始化的工作,并开始启动多核进入多核阶段,进而调用外部设备的初始化函数;下一步是装载用户态的init进程,变身为一个用户进程,成为所有其他用户态进程的鼻祖。

4.1 kernel_init_freeable():

4.1.1 准备工作

4.1.2 smp_prepare_cpus()

为启动多核做准备

4.1.3 workqueue_init()

工作队列初始化

4.1.4 do_pre_smp_initcalls()

执行__initcall_start和__initcall0_start之间的.initcall*.init()函数,实际上就是执行所有用early_initcall()定义的initcall函数

实际上linux内核中有9个级别的do_initcalls()函数,该过程只执行优先级最高的函数eraly_initcall(),其余的均在do_basic_setup阶段执行。

4.1.5 smp_init()

多核启动阶段

4.1.6 sched_init_smp()

该函数执行完成后,linux将正式进入多处理器并行状态

4.1.7 do_basic_setup()

4.1.7.1 driver_init()
4.1.7.1.1 devtmpfs_init()

该函数调用kthread_run创建线程devtmpfsd,该线程创建/dev目录下的基本设备节点,如/dev/console、/dev/zero、/dev/null等

4.1.7.1.2 device_init()

创建/sys/devices以及下级节点

4.1.7.1.3 buses_init()
4.1.7.1.4 classes_init()
4.1.7.1.5 firmware_init()
4.1.7.1.6 hypervisor_init()
4.1.7.2 中断处理
4.1.7.3 do_initcalls()

该阶段需要执行剩余的8个级别initcall函数,执行的代码量非常大。 该函数采用__define_initcall宏的形式从外部接收参数并处理
参考链接:调用initcall的机制

4.1.7.3.1 pure_initcall()
4.1.7.3.2 core_initcall()

init_hpet_clocksource():注册HPET的ClockSource

4.1.7.3.3 postcore_initcall()
4.1.7.3.4 arch_initcall()
4.1.7.3.5 subsys_initcall()
4.1.7.3.6 rootfs_initcall()
4.1.7.3.7 device_initcall()/module_init()

该函数将调用__initcall6_start()到__initcall7_start()之间的函数。
载入驱动的函数会调用platform_drc_probe()进行设备树信息提取进而调用probe(dev)指向dev的驱动文件中的_probe函数,例如lowrisc开发的sd控制器的驱动就指向lowrisc_sd_probe(),从而执行该函数体里的驱动代码。

如果linux设备驱动程序采用built-in的方式则使用_define_initcall的方式加载驱动,如果是采用Module的方式,则采用device_initcall的方式加载。
驱动加载顺序由makefile确定,顺序会打印在根目录下的System,map中。
参考链接:linux 驱动module_init

4.1.7.3.8 late_initcall()

4.1.8 打开console控制台设备

4.1.8.1 ksys_open(“/dev/console”)

打开控制台设备,linux中一切皆为文件,/dev/console文件就代表控制台设备

4.1.8.2 ksys_dup(0)

此时kernel_init进程获得了该文件的文件描述符,此时调用两次ksys_dup(0)复制两次,则得到三个文件描述符,这三个文件描述符分别是0、1、2,这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。
此后kernel_init所有的子进程都将继承这3个文件描述符,也就是后面所有的进程一生出来,就默认有标准输入、标准输出、标准错误的文件描述符。

4.1.9 prepare_namespace()

挂载根文件系统,此时若出错,可能是bootloader阶段传递的bootargs设置不对,导致传递了错误的参数给kernel

4.2 numa_default_policy()

将1号进程自己的NUMA内存分配策略改成MPOL_DEFAULT。
这个函数本身用来确定默认的 NUMA 策略。这个策略决定了当分配新的内存页或执行其他与内存相关的操作时,数据应该放置在哪个 NUMA 节点上。

4.3 run_init_process()

装入用户态的init程序,变身为普通进程

4.3.1 getname_kernel()

4.3.2 do_execve()

在根文件系统中寻找init程序,具体路径由bootloader设置的环境变量bootargs提供,一旦init程序被找到,就会启动init进程(该可执行程序的文件名不一定叫init),然后操作系统正运行。
执行init程序后,1号进程由内核态转变成在用户态下运行。

5. 二号进程

kthreadd

专门用来负责为kernel创建其他进程,即运行kthread_create_list全局链表中维护的kthread, 当调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES),创建2号进程管理内核资源

kthread <3号进程>

处理软中断

__do_softirq()

6. init()

init程序需要读取配置文件/etc/inittab,以查看下一步做什么。inittab是一个不可执行的文本文件,它有若干行指令所组成,告诉 init 要进入什么运行级别,以及在哪里可以找到该运行级别的配置文件。

参考文献

  1. 博客园:跟踪Linux启动——从start_kernel到init进程
Logo

更多推荐