第一章·主函数
主函数:
\kernel\main.c
void
main()
{
if(cpuid() == 0){ // 主CPU
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode cache
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
userinit(); // first user process
__sync_synchronize(); // GCC 提供的一个内建函数,确保在它之前的所有内存操作(读/写)都在它之后的操作开始之前完成
started = 1;
} else { // 非主CPU
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler(); // 负责选择下一个要运行的进程,并切换到该进程
}
系统加电后,主CPU(xv6最多支持8核心)开始执行(cpuid() == 0),对系统初始化,分别初始化控制台、printf()函数、分页、进程……,然后启动第一个程序,将started更改为1
非主CPU不断查询started变量的值,直到started=1,则开始执行相关初始化操作
__sync_synchronize()
:GCC 提供的一个内建函数,确保在它之前的所有内存操作(读/写)都在它之后的操作开始之前完成
然后,所有的CPU开始并行运行。
主CPU和非主CPU的功能性区别:
- 主 CPU 和 非主 CPU 在功能上是相同的,都是执行相同的操作系统代码和调度任务,但在 系统启动 和 初始化阶段 有不同的职责。
- 主 CPU 负责初始化系统、启动第一个进程,并协调其他 CPU 核心的启动过程。
- 非主 CPU 在主 CPU 完成初始化后,执行自己的初始化工作,并加入进程调度,实现并行执行任务。
这种设计确保了系统初始化时的有序性,同时为多核并行执行提供了支持。
现在假设所有的初始化任务都已经完成了,并且已经创建了第一个用户进程,CPU开始调用 scheduler()
函数, scheduler()
位于 \kernel\proc.c
中,负责选择下一个要运行的进程,并切换到该进程。下面分析下 scheduler()
源码:
\kernel\proc.c
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){ // 一个for无限循环,不断运行查询可调度的进程
intr_on(); // 打开中断,确保设备能够通过中断打断当前的进程
int nproc = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
nproc++;
}
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
}
release(&p->lock);
}
//如果系统中可运行的进程数小于等于 2
//(意味着只有 init 和 sh 进程存在),调度器会调用 wfi 指令(Wait For Interrupt)让 CPU 进入空闲状态,等待中断。
if(nproc <= 2) {
intr_on();
asm volatile("wfi");
}
}
}
proc
数组保存了所有的进程。对于每个进程,首先尝试获取该进程的锁,确保在访问该进程时没有其他线程在修改它。接着检查进程的状态,如果进程不是 UNUSED
(未使用状态),则增加 nproc
计数器。
wfi
是一种让 CPU 进入低功耗空闲状态的指令,直到有外部中断(例如硬件中断)打断 CPU
intr_on()
的主要作用是使当前 CPU 可以响应中断。当中断被启用时,CPU 会中断当前正在执行的指令,跳转到中断处理程序(中断向量)来处理外部事件。
swtch()
负责切换上下文,在切换上下文时会切换PC的值,然后CPU就会从新的PC地址(即新进程的地址)开始运行。
swtch.S
:
.globl swtch
swtch:
sd ra, 0(a0) # pc的值
sd sp, 8(a0) # 栈
...
ld ra, 0(a1)
ld sp, 8(a1)
...
ret
另外,关于锁的使用,主要目的就是实现对进程的保护,具体实现在下文详细说明。
至此,所有的CPU就开始并行的实现对进程的调度,当进程数量较少时,就会执行 wfi
指令,有可执行的指令时,继续执行进程,计算机的工作,就是无休止的执行 for(;;)
代码。
接下来我们按照系统初始化的过程,依次分析相关代码。