6.S081 附加Lab2 CPU进程调度(Scheduler)
文章目录
本实验主要是衍生于 (6.S081-7中断 - Interrupts)的看OS启动源码的过程。
——但是本篇的内容十分浅显(准确来说是错误),我后来发现,scheduler的执行过程,并不是从上往下执行,而是一种奇怪的转交CPU的方式,这一切都因为swtch有趣的实现,它首先保存了进程的上下文,并load了cpu的上下文,并且让当前进程将cpu控制权交给其他线程。具体内容可以看我的博客6.S081 附加Lab3 线程切换——源代码实现(trap,yeild,context,Scheduler)
博客6.S081 附加Lab3 线程切换——源代码实现(trap,yeild,context,Scheduler)的精彩摘要👇
- 线程调度的步骤(—— 能看懂这段话,后面的实验就不用看了,本篇文章就是在讲这个过程。):首先计时器通过触发中断,将CPU控制权交给内核(内核的trap程序,然后trap的处理是调用yield函数),yield函数调用的swtch在保存了即将换出的进程的context(包括栈指针)之后,将CPU控制权,交给了内核的调度程序scheduler,内核调度程序scheduler通过它调用的swtch,将CPU的控制权交给下一个即将调度进来的程序。
本实验的主要内容是,分析xv6操作系统的进程调度(其实是错误理解,正确过程比这个复杂得多,请看6.S081 附加Lab3 线程切换——源代码实现(trap,yeild,context,Scheduler))。
1. OS调用scheduler
下面是OS的main函数,我们可以看到,最后一行,运行了scheduler函数。
volatile static int started = 0;
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(cpuid() == 0){
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(minor(ROOTDEV)); // emulated hard disk
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
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();
}
2. Scheduler的作用
下面是void scheduler(void);
👇,注释部分解释了进程调度的作用:
永远处于循环中(永远不退出)。
-
选择一个进程(接下来去运行它);—— 从代码来看就是从进程队列里面选择一个进程。
-
通过
swtch
函数让这个进程运行;c->proc = p; // cpu当前执行的process = 被选中即将需要执行的process swtch(&c->scheduler, &p->context); // context是OS的上下文,即PC,sp还有通用寄存器
—— 切换c-> proc = p
就顺带切换了页表等内容(同时c -> proc -> context
自然等于p->context
),那么为什么还要写swtch(&c->scheduler, &p->context);
?。——因为这里的swtch(&c->scheduler, &p->context);
就是让cpu的寄存器赋值成p被保存的寄存器(包括pc, sp等,因此一旦被赋值了,将会自动运行该进程的命令。)
- 最终该进程将控制权(通过
swtch
函数)交还给scheduler
。
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns. It loops, doing:
// - choose a process to run.
// - swtch to start running that process.
// - eventually that process transfers control
// via swtch back to the scheduler.
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->scheduler, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
found = 1;
}
release(&p->lock);
}
if(found == 0){
intr_on();
asm volatile("wfi");
}
}
}
其中struct cpu
, struct proc
和struct context
分别如下所示👇。
cpu结构体:
// Per-CPU state.
struct cpu {
struct proc *proc; // The process running on this cpu, or null.
struct context scheduler; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};
extern struct cpu cpus[NCPU];
context:内核上下文结构体
最关键的部分,切换进程,其实就是swtch(&c->scheduler, &p->context);
)—— 这是因为,这里面包含了pc,sp,还有通用寄存器,只要有这些内容,CPU就可以自动执行指令了
// Saved registers for kernel context switches.
struct context {
uint64 ra; // pc
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
进程结构体:
这里的注释很详细,不再多讲。
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Bottom of kernel stack for this process
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // Page table
struct trapframe *tf; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
// added by levi
pagetable_t levi_kernel_pagetable;
};
3. 总结
进程调度Scheduler
由OS的main调用,调用后它永远处于循环中(永远不退出)。它的过程如👇
-
选择一个进程(接下来去运行它);—— 从进程队列里面选择一个进程。
-
通过
swtch
函数让这个进程运行;c->proc = p; // cpu当前执行的process = 被选中即将需要执行的process swtch(&c->scheduler, &p->context); // context是OS的上下文,即PC,sp还有通用寄存器
—— 切换c-> proc = p
就顺带切换了页表等内容;
——swtch(&c->scheduler, &p->context);
就是让cpu的寄存器赋值成p被保存的寄存器(包括pc, sp等,因此一旦被赋值了,将会自动运行该进程的命令。)
- 最终该进程将控制权(通过
swtch
函数)交还给scheduler
。