Flow of the Context Switch
- Invoke the interrupt (ex: Timer Interrupt (trap.c 109) )
- Invoke the scheduler ( yield (proc.c 390) )
- Start the context switch(swtch (proc.c 387) )
Before Context Switch Interrupt happens
Vector.S(Generate from perl script)
vector 32:
pushl $0 //error code
pushl $32 //vector number of timer interrupt
jmp alltraps
trapasm.S
Ref: some registers references
Segment register saves physical address of the segment offset
# trap frame.globl alltrapsalltraps:
pushl %ds #data segment register
pushl %es #extra segment register
pushl %fs #general segment register
pushl %gs #general segment register
pushal #all local registers
Set up data segments
# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es# Call trap(tf), where tf=%esp by calling convention
pushl %esp
call trap
Then, in trap.c 111, calling yield() to invoke scheduler.
Also, after invoking scheduler, the system starts context switch said before in swtch(struct context **old, struct context *new)
Context Switch starts !
Context switch writes in assembly and links by linker !
// defs.h 129
void swtch(struct context** old, struct context* new);
swtch.S
swtch:
movl 4(%esp) %eax # address of **old !!little endian
movl 8(%esp) %edx # address of *new #save all callee registers
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi
# change esp pointer movl %esp, (%eax) # (%eax) means *old (points to address of old)
movl %edx, %esp # load new callee-save registers
# do not pop the EIP since this is the return address popl %edi
popl %esi
popl %ebx
popl %ebp
ret
Remind: Calling convention on stack
- local variables | EBP(frame pointer) | return address | parameters
Context data structure(proc.h 28)
// reverse order from pushl registers
struct context {
uint edi;
uint esi;
uint ebx;
uint ebp;
uint eip;}
Remind: Context is always on the top of some stack
CPU initialization
main.c
// main.c 39
mpmain(); // finish this processor's set up// Common CPU setup code
static void
mpmain(void)
{
cprintf("cpu%d: starting %d\n", cpuid(), cpuid());
idtinit(); // load idt register
xchg(&(mycpu()->started), 1); // tell startothers() we're up
scheduler(); // start running processes}
What does xchg(volatile uint *addr, uint newval) do?
xchg did a value exchange with the value store in addr, this uses for lock implementation.
Sample from MIT course:
xchg %eax, addr
- free other CPU’s activities to access address of addr
- tmp := *addr
- *addr := %eax
- %eax := tmp
- Unfreeze other CPU’s activities toward address of addr
Back to our topic, focus on function scheduler():
What it does is after initializing all requirements we need for XV6, it launches one proc from current context(cpu->scheduler)
proc.c
// proc.c 326
void
scheduler(void)
{
... // inside loop of the ptable
acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if(p->state != RUNNABLE)
continue;
// switch to chosen process c->proc = p;
switchuvm(p);
p->state = RUNNING;
// do context switch
// c->scheduler points to current context & restore the context of p swtch(&(c->scheduler), p->context );
switchkvm(); c->proc = 0;
}}
Then, we know the context switch to first process to run.
However, how does XV6 prepare context for process?
How context is created?
Well, when first user process created in userinit() or doing the process fork(), both of them invoked allocproc to find an UNUSED proc in ptable
static struct proc* allocproc(void) (proc.c 75)
static struct proc*
allocproc(void)
{
// after finding an UNUSED process
found:
p->state = EMBRYO;
p->pid = nextpid++;
release(&ptable.lock);
// Allocate kernel stack.
if((p->kstack = kalloc()) == 0){
p->state = UNUSED;
return 0;
}
// stack pointer
sp = p->kstack + KSTACKSIZE; //Leave room for trap frame
sp -= sizeof *p->tf;
p->tf = (struct trapframe*)sp;
// Set up new context to start executing at forkret,
// which returns to trapret.
sp -= 4;
*(uint*)sp = (uint)trapret; sp -= sizeof *p->context;
p->context = (struct context*)sp;
memset(p->context, 0, sizeof *p->context);
// that's why it is not required to push eip in swtch.S
p->context->eip = (uint) forkret; return p; }
Dig in more on Context Switch
Discuss about switch from current process to scheduler when time interrupt happens !
void sched(void) (proc.c 371)
swtch(&p->context, mycpu()->scheduler);
# move esp value to what %eax points to
movl %esp, (%eax) # (%eax) means *old (points to address of old)
movl %edx, %esp
Let stack pointer points to the context we want to switch. Here is the address of the cpu->schedule
movl %edx, %esp
Now, we should know the whole context switch flow !
Context Switch Flow
- Interrupt the current process. Then, switch to cpu->scheduler
- Scheduler find a RUNNABLE proc in ptable and context switch from scheduler to another RUNNABLE process
Process -> Scheduler -> Process
The scheduler is the bridge between two processes
The next is how to return back to the user space?
After Context Switch
Destination: Exit back to User-Level
Strategy: Calling Convention ! Use EIP to get the return address
switch.S -> sched(proc.c) -> yield(proc.c) -> trap(trap.c) -> trapret(trapasm.S, but set up in allocproc() ) -> iret (trapasm.S)
Remind: Why should XV6 push ESP onto the stack ?
Ans: As a parameter for void trap(struct trapframe *tf)
Summary
In this post, summarized the flow of the Context Switch in a small system XV6.
It is strait-forward to understand it. Think about every process has its own private space for storing data including registers, stacks. Then, the Context Switch is the mechanism to prepare the resources for CPUs when we want to finish the job from different processes.
Also, the scheduler is the bridge to help processes to do so.