kernel_entry→start_kernel()
vmlinux原始内核启动:内核的初始启动入口是位于arch/loongarch/kernel/head.S中的kernel_entry → start_kernel()
vmlinuxz压缩版内核启动:在解压前真正的执行入口是arch/loongarch/boot/compressed/head.S中的start →kernel_entry→start_kernel()
- 执行decompress_kernel()进行自解压,解压内容释放到内存里面形成一个原始内核。
第一入口:kernel_entry
1 | SYM_CODE_START(kernel_entry) |
- 通过一个循环来清零.bss 段中的全局数据;
- 将 a0~a3 寄存器中的值保存到 fw_arg0~fw_arg3四个内存变量,这四个变量包含 BIOS 或者引导程序传递给内核的参数;
- 配置DMWIN0和DMWIN1映射窗口地址;
- 打开PG=1;
- 进行CPU类型相关的初始化;
- 使用init_thread_union的地址来初始化GP寄存器,GP是全局指针;
- 初始化SP寄存器,SP是堆栈指针;
- 最后的 b start_kernel 是跳转到第二入口处继续执行,第二入口即 start_kernel()函数;
处理器0的Status寄存器
- IE:全局中断使能位,为 1 表示开中断,为 0 表示关中断;
- EXL:异常级别指示,为 1 表示 CPU 处于异常模式,异常模式表示发生了除复位、NMI和 Cache 错误以外的某种异常。
- ERL:错误级别指示,为 1 表示 CPU 处于错误模式,错误模式表示发生了复位、NMI或者 Cache 错误之类的某种异常。
- KSU:特权模式位:为 0 表示 CPU 处于核心态(内核态),为 1 表示 CPU 处于管理态,为 2 表示 CPU 处于用户态,为 3 表示未定义。核心态权限最高,可以执行任意指令(特权指令和非特权令),可以访问任意地址空间(核心空间、管理空间和用户空间);管理态权限居中,不能执行特权指令,能访问管理地址空间和用户地址空间;用户态权限最低,不能执行特权指令,只能访问用户地址空间。另外,当 EXL 或者 ERL 置位时,不管 KSU 如何取值,CPU 自动处于核心态。
- UX:为 1 表示启用 64 位用户地址空间段;
- SX:为 1 表示启用 64 位管理地址空间段;
- KX:为 1 表示启用 64 位核心地址空间段;
- IM7~IM0:中断掩码位,MIPS 在 CPU 层面一共有 8 个中断源,分别有 8 个掩码位与之对应,为 1 的位表示允许该中断触发,为 0 的位表示禁止该中断触发;
- NMI:为 1 表示发生了 NMI(不可屏蔽中断);
- SR:为 1 表示发生了软件复位;BEV:控制异常向量的入口,为 1 表示使用启动时异常向量入口,为 0 表示使用运行时异常向量入口;
- PX:为 1 表示在用户态使能 64 位操作数指令(如 daddu、dsubu 等);
- FR:浮点协处理器模式切换,为 1 表示有 32 个双精度浮点寄存器可用,为 0 表示只有16 个双精度浮点寄存器可用;
- CU3~CU0:标识四个协处理器是否可用,协处理器 0(CP0)是系统控制协处理器,在所有 MIPS 处理器上总是可用的;协处理器 1(CP1)通常是浮点协处理器(FPU),在所有龙芯处理器上总是可用的;协处理器 2(CP2)在龙芯 3 号上总是可用的,表示多媒体指令协处理器。
龙芯 3 号总是使用 64 位内核,所以 setup_c0_status_pri 实际上就是设置当前模式为内核态模(KSU),启用内核的 64 位地址段访问能力(KX),启用系统控制协处理器(CU0),启用多媒体指令协处理器(CU2),清除异常状态并禁止中断(清零 EXL、ERL、IE)。BEV等位保持 BIOS 设置的原值(内核尚未建立运行时异常向量)。
init_thread_union相关
在 Linux 中,进程和线程都是运行的程序实体,进程有独立的地址空间,若干个线程共享同一个地址空间;也就是说,线程是一种特殊的进程。Linux 中线程的容器并不是进程,而是线程组。例如:一个运行中的多线程程序是一个线程组,里面包含多个线程;一个运行中的单线程程序也是一个线程组,里面包含一个线程。单线程程序的那个唯一线程,就是一般意义上的进程。
内核本身也可以视为一个特殊的进程,它可以派生出很多共享地址空间的内核线程,因此这个拥有许多线程的内核又可以视为一个特殊的线程组。
1 | union thread_union { |
每一个进程(包括普通进程和内核线程)用一个进程描述符 task_struct 表示;每一个进程都有一个体系结构相关的线程信息描述符,即 thread_info;每一个进程都有一个内核态栈,用于处理异常、中断或者系统调用。Linux 内核为每个进程分配一个大小为 THREAD_SIZE的内存区(大小通常就是一个页面),把 thread_info 和内核栈放在一起,即 thread_union。
thread_union 地址从低处开始往上是 thread_info,从高处开始往下是内核栈,task_struct 中的stack 指针指向 thread_union。这里的 init_task 就是 Linux 中 0 号进程的 task_struct,0 号进程一开始就是内核自身,在完成启动初始化以后,变身为 Idle 进程(空闲进程)。
第二入口:start_kernel()
1 | asmlinkage __visible void __init __no_sanitize_address start_kernel(void) |
虽然这棵树很庞大,但我们大致可以将整个 start_kernel()的过程分为三个大的阶段:关中断单线程阶段(从 start_kernel()头部开始直到 local_irq_enable()结束);开中断单线程阶段(从local_irq_enable()开始直到 rest_init()前夕);开中断多线程阶段(rest_init()的整个过程)。
第一阶段:关中断单线程阶段
启动初期的初始化过程必须关中断进行(中断处理的基础设施尚未准备好),所以start_kernel()开始执行不久之后就通过 local_irq_disable()来关闭中断。
boot_cpu_init(),这个函数是设置启动 CPU(通常是 0 号 CPU)的存在性状态。
1 | void __init boot_cpu_init(void) |
一个逻辑 CPU 有四种存在性状态:possible,表示物理上有可能存在;present,表示物理上确实存在;online,表示已经在线;active,表示已经在线并且处于活动状态。possible 和 present 的区别跟 CPU 物理热插拔有关,如果物理上移除一个 CPU,present 数目就会减少一个。present 和online 的区别是CPU 逻辑热插拔,在不改变硬件的情况下,可以对 /sys/devices/system/cpu/cpuN/online 写 0来关闭一个 CPU,写 1 则重新打开。online 和 active 非常相似,前者表示这个 CPU 可以调度任务了,后者表示可以往这个 CPU 迁移任务了。两者的区别在于,在通过逻辑热插拔关闭一个 CPU 的过程中,被关闭的 CPU 首先必须退出 active 状态,然后才能退出 online 状态。
整个 boot_cpu_init()的功能,就是将启动核(在龙芯上面就是 0 号核)的状态设置成 possible 的,present 的,online 的并且是 active 的。
然后是一个重要函数 setup_arch(),这是根据体系结构进行相关的初始化,LOONGARCH的setup_arch()定义在 arch/loongarch/kernel/setup.c 中。
接下来的 trap_init()异常初始化,这个函数都是体系结构相关的并且非常重要。
1 | void __init setup_arch(char **cmdline_p) |
setup_command_line(),建立内核命令行参数。内核命令行参数可以写在启动配置文件(boot.cfg 或 grub.cfg)中,由 BIOS 或者启动器(BootLoader,如 Grub)传递给内核;或缺省参数。
setup_nr_cpu_ids(),它获取 cpu_possible_mask 中的最大 CPU 编号(所有 possible 状态的逻辑 CPU 的最大编号),并将其赋值给全局变量 nr_cpu_ids。
setup_per_cpu_areas(),建立每 CPU 变量区,每 CPU 变量用 DEFINE_PER_CPU(type, name)语句定义,在功能上等价于用 type name[NR_CPUS]定义一个数组。
smp_prepare_boot_cpu()是一个体系结构相关的函数,在 LOONGARCH 上主要是把 0 号逻辑 CPU设成 possible 的和 online 的,该函数在功能上和 boot_cpu_init()有所重复。
接下来的 trap_init()异常初始化,这个函数都是体系结构相关的并且非常重要。
随后的 mm_init()是内存管理初始化。体系结构相关的内存管理部分已经在 setup_arch()里面完成(其中会将 BIOS 传递的固件内存分布图转换成 BootMem 内存分布图),这里主要是调用mem_init()建立内存分布图(将 BootMem 内存分布图转换为伙伴系统的内存分布图,对其中的每个可用的页帧调用set_page_count()将其引用计数设为0),调用kmem_cache_init()完成 SLAB 内存对象管理器的初始化,以及调用 vmalloc_init()完成非连续内存区管理器的初始化。
sched_init(),调度器初始化,完成以后主核就可以进行任务调度了。sched_init()会通过for_each_possible_cpu()迭代器在初始化每个 CPU 的运行队列(运行队列 rq 用于进程组织和调度),其中包括将 CPU 负载水平(即 rq->cpu_load[]数组,记录了最近 5 个时钟节拍内的CPU 平均负载水平)初值设为 0。接下来,sched_init()里面还有几个比较重要的步骤就是对init_task 的操作:init_task 的大部分成员字段已经通过 INIT_TASK()在定义的时候就初始化好了,这里只需调用 set_load_weight()设置 init_task 的负荷权重(负荷权重跟基于优先级的进程调度有关,详见第 6 章),将其调度类设为 fair_sched_class(公平调度类,使用 CFS 调度策略对其进行调度),再调用 init_idle()将内核自己进程化(准备工作完成后,调度类会被重新设置为 idle_sched_class,使用专门的 IDLE 调度策略)。从现在开始内核也是一个“进程”了,即零号进程。
1 | /* |
rcu_init(),RCU 是一种内核同步原语,全称 Read-Copy-Update(读-复制-更新),和自旋锁、信号量、读写锁等同步原语有类似的 API,但 RCU 本身并不是锁。
early_irq_init(),初始化中断描述符。中断描述符就是 irq_desc[NR_IRQS]数组,包含了每个中断号(IRQ)的芯片数据 irq_data 和中断处理程序 irqaction 等各种信息。本函数只是设置缺省信息,比如芯片数据都设成 no_irq_chip ,中断处理程序都设成handle_bad_irq()。真正有意义的信息由后面体系结构相关的 init_IRQ()函数完成。
init_timers(),基本定时器初始化;hrtimers_init(),高分辨率定时器初始化。
softirq_init(),软中断初始化。软中断和硬中断的概念来自于早期内核中的“上半部”和“下半部”。上半部是中断处理里面非常紧急、必须立即完成的那部分工作;下半部是不那么紧急,可以延迟完成的那部分工作。软中断在概念上基本上就是继承自下半部。当前内核中定义了 11 种软中断(优先级从高到低):
1 | enum |
SCHED_SOFTIRQ 在之前的 sched_init()里面进行初始化,RCU_SOFTIRQ 在之前的rcu_init()里面进行初始化,TIMER_SOFTIRQ 和 HRTIMER_SOFTIRQ 在之前的 init_timers(和 hrtimers_init()中进行初始化,本函数主要完成 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 的初始化,其他类型的软中断分布在各自的子系统里面完成。
timekeeping_init(),timekeeping 的意思是系统时间维护。该函数的作用主要是初始化各种时间相关的变量,如 jiffies,xtime 等等。Jiffies 记录了系统启动以来所经历的节拍数,而xtime 记录的时间可以精确到纳秒。随后的 time_init()是一个体系结构相关的函数,会进一步初始化计时系统。
PerfEvents 和 OProfile 是 Linux 内核中的两种性能剖析工具,perf_event_init()和profileinit()分别完成其初始化。
中断有关的初始化都已经完成,现在可以开中断了。开中断的函数是 local_irq_enable(),对于 LOONGARCH 来讲就是设置协处理器 0 中 Status 寄存器的 IE 位。
第二阶段:开中断单线程阶段
第二阶段中断已经打开,所以虽然现在内核还是以单线程的方式执行,但是一旦产生断就会切换控制流。因此,这一阶段除了按顺序执行代码流程以外,还可能以交错方式执行中断处理的代码。
console_init(),控制台初始化。
numa_policy_init(),NUMA 内存分配策略初始化。
calibrate_delay(),用于计算 loops_per_jiffy 的值。loops_per_jiffy 的含义是每个时钟节拍对应的空循环数,这个值用于以后实现各种 delay()类的忙等函数。
fork_init(),Linux 用 fork()系统调用来创建新进程。本函数的作用是初始化 fork()所用到的一些数据结构,如创建名为”task_struct”的 SLAB 内存对象缓存,将最大线程数设置为MAXTHREADS,等等。
signals_init(),跟信号相关的数据结构初始化。信号之于进程,好比中断之于内核,用于打断当前的执行流程,去完成一些更重要的工作。
cgroup_init(),CGroup 全称 Contol Group,即控制组,是内核一种控制资源分配的机制。本函数完成控制组相关数据结构的初始化,并且创建相应的 sysfs 和 procfs 节点。
现在,所有调度有关的子系统已经全部初始化完成,接下来可以创建新的内核线程,以并发的方式继续内核启动了。因为显卡尚未初始化,所以第二阶段显示器上依然没有输出信息。
第三阶段:开中断多线程阶段
rest_init(),顾名思义,第三阶段就是余下的初始化工作任务。函数 rest_init()的主要工作是通过 kernel_thread()创建了 1 号进程 kernel_init 和 2 号进程 kthreadd(实际上是两个内核线程)。1 号进程的执行体函数是 kernel_init(),它完成接下来的大部分初始化工作。2 号进程则是除 0、1、2 号进程以外其他所有内核线程的祖先(如果 1号进程在运行过程中需要创建新的内核线程,会委托 2 号进程来创建)。
1 号进程和 2 号进程创建以后,内核自己的初始化工作就基本完成了。但是别忘了,内核自己是 0 号进程,因此它也有必须持续进行的“工作”。内核初始化的最后一步是执行 cpu_startup_entry(),而后者的主要工作是调用cpu_idle_loop()。从名字可以看出,0 号进程现在成了空闲进程(即 IDLE 进程),它的工作就是“休息”(如果别的进程有事要做,就调度别的进程,反之意味着系统空闲,回到零号进程)。顺着调用链追踪下去,可以发现 0 号进程的核心过程是循环执行 arch_cpu_idle(),而具体到 LOONGARCH 处理器,则是 cpu_wait()。cpu_wait()可以有多种实现,一般就是执行 WAIT指令进入节能状态。
1 号进程与 2 号进程会派生很多新的内核线程来完成各种内核功能。在 SMP 系统上,1号进程会打开所有辅核,让后面的内核启动真正并行起来。包括显卡驱动在内的各种设备驱动都在 1 号进程里面完成,因此第三阶段除了起始点以外的的大部分时间是有显示信息输出的。
1 | noinline void __ref rest_init(void) |