Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in
  • P project210-USTC
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Commits
  • Issue Boards
Collapse sidebar
  • 叫什么名字好呢队
  • project210-USTC
  • Wiki
  • Home
  • 4.调度

4.调度 · Changes

Page history
Create home/4.调度 authored Aug 14, 2023 by lqzzy's avatar lqzzy
Show whitespace changes
Inline Side-by-side
home/4.调度.md 0 → 100644
View page @ 0fb5d60a
# 调度
任何操作系统都可能碰到进程数多于处理器数的情况,这样就需要考虑如何分享处理器资源。理想的做法是让分享机制对进
程透明。通常我们对进程造成一个自己独占处理器的假象,然后让操作系统的 多路复用机制(multiplex) 将单独的一个物理处
理器模拟为多个虚拟处理器。
## 多路复用
xv6 中 多路复用 的实现如下:当一个进程等待磁盘请求时,xv6 使之进入睡眠状态,然后调度执行另一个进程。另外,当一个
进程耗尽了它在处理器上运行的时间片(100毫秒)后,xv6 使用时钟中断强制它停止运行,这样调度器才能调度运行其他进
程。这样的多路复用机制为进程提供了独占处理器的假象,类似于 xv6 使用内存分配器和页表硬件为进程提供了独占内存的
假象。
实现多路复用有几个难点。首先,应该如何从运行中的一个进程切换到另一个进程?xv6 采用了普通的上下文切换机制;虽
然这里的思想是非常简洁明了的,但是其代码实现是操作系统中最晦涩难懂的一部分。第二,如何让上下文切换透明化?xv6
只是简单地使用时钟中断处理程序来驱动上下文切换。第三,可能出现多个 CPU 同时切换进程的情况,那么我们必须使用
一个带锁的方案来避免竞争。第四,进程退出时必须释放其占用内存与资源,但由于它本身在使用自己的资源(譬如其内核
栈),所以不能由该进程本身释放其占有的所有资源。xv6 希望能够简洁明了地处理这些难点,不过最后其代码实现还是比
较“巧妙”。
## 上下文切换
xv6 在低层次中实现了两种上下文切换:从进程的内核线程切换到当前 CPU 的调度器线程,从调度器线程
到进程的内核线程。xv6 永远不会直接从用户态进程切换到另一个用户态进程;这种切换是通过用户态-内核态切换(系统调
用或中断)、切换到调度器、切换到新进程的内核线程、最后这个陷入返回实现的。本节我们将以内核线程与调度器线程的
切换作为例子来说明。
![](image/2023-08-14-11-15-42.png)
每个 xv6 进程都有自己的内核栈以及寄存器集合。每个 CPU 都有一个单独的调度器线程,这样调度就不会发生在进程的内核线程中,而是在此调度器线程中。线程的切换涉及到了保存旧线程的 CPU 寄存器,恢复新线程之
前保存的寄存器;其中 %esp 和 %eip 的变换意味着 CPU 会切换运行栈与运行代码。
我们来看swtch
```c
# Context switch
#
# void swtch(struct context *old, struct context *new);
#
# Save current registers in old. Load from new.
.globl swtch
swtch:
st.d $ra, $a0, 0
st.d $sp, $a0, 8
st.d $s0, $a0, 16
st.d $s1, $a0, 24
st.d $s2, $a0, 32
st.d $s3, $a0, 40
st.d $s4, $a0, 48
st.d $s5, $a0, 56
st.d $s6, $a0, 64
st.d $s7, $a0, 72
st.d $s8, $a0, 80
st.d $fp, $a0, 88
ld.d $ra, $a1, 0
ld.d $sp, $a1, 8
ld.d $s0, $a1, 16
ld.d $s1, $a1, 24
ld.d $s2, $a1, 32
ld.d $s3, $a1, 40
ld.d $s4, $a1, 48
ld.d $s5, $a1, 56
ld.d $s6, $a1, 64
ld.d $s7, $a1, 72
ld.d $s8, $a1, 80
ld.d $fp, $a1, 88
jirl $zero, $ra, 0
```
可以看到 `swtch` 把当前的寄存器保存到 `old` 中,然后从 `new` 中恢复寄存器,最后跳转到 `new` 中保存的 `ra` 寄存器指向的地址。这样就完成了上下文切换。
还记的什么时候调度器会调用 `swtch` 吗,我们在 `scheduler` 中可以看到,当调度器发现有进程处于 `RUNNABLE` 状态时,就会调用 `swtch` 进行上下文切换。
```c
// 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();
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->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&p->lock);
}
}
}
```
我们现在看到了完整的调度器的调用进程的过程,我们来总结一下这个过程。
**从 `main` 进到 `scheduler` 之后, CPU的上下文是运行 `scheduler` 中的死循环,**
**当调度器发现有进程处于 `RUNNABLE` 状态时,就会调用 `swtch` 进行上下文切换,这样 CPU 的上下文就变成了运行 `RUNNABLE` 进程的内核线程,开始运行这个进程。**
那CPU是如何放弃这个进程的呢?
我们来看 `usertrap`
```c
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
int which_dev = 0;
if((r_csr_prmd() & PRMD_PPLV) == 0)
panic("usertrap: not from user mode");
struct proc *p = myproc();
// save user program counter.
p->trapframe->era = r_csr_era();
if(((r_csr_estat()& CSR_ESTAT_ECODE) >> 16) == 0xb){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->era += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
}
else if((r_csr_estat() >> 16) == PIL || (r_csr_estat() >> 16) == PIS) {
// printf("=== trap ===\n");
uint64 addr = r_csr_badv();
struct VMA* vp = 0;
//to fina the target vma
for( int i=0 ; i<VMA_MAX ; i++ ) {
if( p->vma[i].addr <= addr && addr < p->vma[i].addr + p->vma[i].len && p->vma[i].valid == 1 )
{
vp = &p->vma[i];
break;
}
}
if( vp != 0 )
{
uint64 mem = (uint64)allocpage();
memset( (void*)mem , 0 , PGSIZE );
if( -1 == mappages( p->pagetable , addr, PGSIZE , mem , PTE_U | PTE_V | ( vp->prot << 1 ) ) )
panic("pagefault map error");
vp->mapcnt += PGSIZE; //maintain the mapcnt
ilock( vp->f->ip );
vp->f->ip->fop->read( vp->f->ip , 1 , addr , addr-vp->addr , PGSIZE); //copy a page of the file from the disk
iunlock( vp->f->ip );
}
}
else {
printf("usertrap(): unexpected trapcause %p pid=%d\n", r_csr_estat(), p->pid);
printf(" era=%p badi=%p\n", r_csr_era(), r_csr_badi());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
{
myproc()->user_time++;
yield();
}
usertrapret();
}
```
注意其中的这段代码:
```c
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
{
myproc()->user_time++;
yield();
}
```
当CPU上的进程线程运行了这个时间片后,会触发时间中断,然后进入这个if中,调用函数 `yield`。
我们来看 `yield` 的代码:
```c
// Give up the CPU for one scheduling round.
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);
p->state = RUNNABLE;
sched();
release(&p->lock);
}
```
代码把当前进程的状态设置为 `RUNNABLE` ,也就是可运行态后,调用 `sched` 函数,我们再来看
`sched` 的代码
```c
// Switch to scheduler. Must hold only p->lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->noff, but that would
// break in the few places where a lock is held but
// there's no process.
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get())
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}
```
可以看到, `sched` 在经过一系列检查后,调用 `swtch` 把当前进程的context 保存起来,然后
加载了 CPU的上下文,也就是上面提到过的 `scheduler` 的死循环。
所以我们现在对进程的退出做一个总结:
**当这个进程的时间片用完后,进程通过调用 `yield`,将CPU 的上下文又变成了运行 `scheduler` 的死循环,这样就完成了一次进程的调度。**
这也是我们的OS的调用流程。
\ No newline at end of file
Clone repository
  • home
    • 1.简单的OS宏观理解
    • 2.内存管理
    • 3.中断
    • 4.调度
    • 5.文件系统