This is Day 20 of the Homebrew OS Advent Calendar 2019. It’s part of my xv6 explanation series, focusing on scheduling because it’s self-contained for this event. For why xv6, how the series works, or how to run it, please read the introduction.
This chapter covers:
How context switching works (this post)
How the scheduler switches processes (this post)
How sleep/wakeup switches processes (next post)
Scheduling sequence diagram
Here’s the rough flow from a timer interrupt to the scheduler picking the next process. Over time the “new” process becomes the “old” one. The point where the new process resumes is right after it previously called swtch() when it was the old process.
L14-18: Save the current process’s context (callee-saved registers).
eip is saved by the call instruction.
L21: Store the current stack pointer (the saved context) into old.
L22: Set the stack pointer to new.
L24-29: Restore the new process’s context.
ret restores eip.
Code: Scheduling
trap()
On timer interrupt, yield() is called. trap.c
36void37trap(structtrapframe*tf)~~~103// Force process to give up CPU on clock tick.
104// If interrupts were on while locks held, would need to check nlock.
105if(myproc()&&myproc()->state==RUNNING&&106tf->trapno==T_IRQ0+IRQ_TIMER)107yield();
yield()
proc.c
384// Give up the CPU for one scheduling round.
385void386yield(void)387{388acquire(&ptable.lock);//DOC: yieldlock
389myproc()->state=RUNNABLE;390sched();391release(&ptable.lock);392}
L388: Acquire ptable lock. L389: Mark the current process RUNNABLE instead of RUNNING. L390: Call sched(). L391: Release ptable lock (execution resumes here after switching back).
L371: Ensure the lock is held so other CPUs can’t change process state. L380: Context switch to the scheduler.
About the ptable lock:
yield() acquires/releases it.
sched() assumes the caller already holds/releases it; unusual but necessary.
During swtch() invariants aren’t maintained; without the lock, another CPU could see process A as RUNNABLE, choose it, while A’s kernel stack is mid-switch.
scheduler()
Now the main scheduling loop: find the next process and switch.
proc.c
322void323scheduler(void)324{325structproc*p;326structcpu*c=mycpu();327c->proc=0;328329for(;;){330// Enable interrupts on this processor.
331sti();332333// Loop over process table looking for process to run.
334acquire(&ptable.lock);335for(p=ptable.proc;p<&ptable.proc[NPROC];p++){336if(p->state!=RUNNABLE)337continue;338339// Switch to chosen process. It is the process's job
340// to release ptable.lock and then reacquire it
341// before jumping back to us.
342c->proc=p;343switchuvm(p);344p->state=RUNNING;345346swtch(&(c->scheduler),p->context);347switchkvm();348349// Process is done running for now.
350// It should have changed its p->state before coming back.
351c->proc=0;352}353release(&ptable.lock);354355}356}
L329: Infinite loop. L335-337: Search for a RUNNABLE process p. L343: Set p’s TSS (Task State Segment) in TR so interrupts use p’s kernel stack. L342: Set c->proc so myproc() returns the running process.
L346: Context switch to p. L347: Switch address spaces (switchkvm). L351: When back in the scheduler, clear c->proc.
If no runnable process is found: L334/353: Lock ptable only while searching.
If the scheduler idled while holding the lock, other CPUs couldn’t context-switch or run syscalls needing ptable.
Processes couldn’t be marked RUNNABLE.
Prevents duplicate pids and duplicated process table entries.
L331: Enable interrupts.
Shell etc. are often blocked on I/O; if interrupts were disabled while idling, I/O waits would never finish.
We traced xv6 from context switch to scheduling. Debugging with gdb while reading the code makes it more fun! Tomorrow’s Advent post is SugarHigh_bin on “Concurrent symbolic fuzzing with memory model awareness.”
If you have comments or corrections, please contact me on Twitter or open an Issue.