kernel 将从 entry.S 转入到 main.c 的 C 代码 main()
函数,完成若干项初始化工作,在 userinit() 创建第一个用户进程后进入 mpmain() 函数进行 scheduler() 调度。
下面对 main()
中的所调用的各种初始化函数按调用次序做一个简单的介绍,并且指出它们在哪里继续深入讨论。
物理内存管理使用了链表将空闲页帧进行组织,kinit2() 用于初始化时将所有空闲页帧并构成链表。
cpus[ncpu]
数组和 ismp
多核标 志。apic
中断控制器进行初始化。8259A
中断控制器初始化代码(适用于单核系统)。I/O APIC
中断控制器初始化(适用于多核 SMP
系统)。initlock()
完成进程表 ptable
的互斥锁的初始化(开锁状态)。mpmain()->idtinit()
的时候。initlock()
完成对 ftable
的互斥锁进行初始化(开锁状态),table
记录系统所打开的文件,总数不超过 NFILE。driver 1
。[4MB, 240MB)
地址范围的空闲页帧(4 KB 大小的页帧)构成链表。init
。scheduler()
内核执行流。除了比较简单的初始化函数在下面讨论外。其他复杂的初始化函数将会在各自的子系统分析中加以讨论,例如 kvmalloc()
在内存管理子系统中讨论。
每个处理器都要执行这个初始化,最终各个处理器核 上的段表内容几乎相同:在每 CPU 变量 cpus[].gdt[]
中设置 4 项,分别对应内核代码段 SEG_KCODE
、内核数据段 SEG_KDATA
、用户代码段 SEG_UCODE
、用户数据段 SEG_UDATA
。
各处理器不同的地方是 SEG_KCPU
段,由段寄存器 GS
使用,对应于 “每 CPU” 变量。
当主处理器或 AP 处理器启动到 mpmain()
,说明已经是启动代码的最后阶段,随后将进入 scheduler()
调度器中的无限循环(不再返回)。因此,mpmain()
的第一件事情就是打印 cpuX: starting
的信息,然用 idtinit()
装入中断描述符 IDT 表以响应时钟中断(及其他中断), 接着将 cpu->started
设置为 1,让其他处理器知道本处理器已经完成启动。最后进入到调度器的 scheduler()
中的无限循环中,每隔一个 tick
时钟中断就选取下一个就绪进程来执行。
其他 AP 处理器经历 entryother.S
启动代码后,进入到 mpenter()
。mpenter()
用 switchkvm()
切换到本处理器内存空间;再用 seginit()
设置段表(各处理器段表几乎完全相同),除了 GS 使用的 SEG_KCPU
段用于访问 “每 CPU” 变量;在完成本地中断控制电路 local APIC
的初始化 lapicinit()
,然后就可以进入到 mpmain()
执行最后阶段的代码完成启动并进入到调度器循环中。
startothers()
先将 entryother.S
代码从现在的位置拷贝到 0x7000
物理地址,然后逐个 CPU 启动。每启动一个 CPU 核,需要为它传递独立的堆栈指针、mpenter()
地址和系统共享的页表地址 entrypgdir
。