7.2 Code: Context switching

Refer to caption
Figure 7.1: Switching from one user process to another. In this example, xv6 runs with one CPU (and thus one scheduler thread).

Figure 7.1 outlines the steps involved in switching from one user process to another: a trap (system call or interrupt) from user space to the old process’s kernel thread, a context switch to the current CPU’s scheduler thread, a context switch to a new process’s kernel thread, and a trap return to the user-level process. Xv6 has separate threads (saved registers and stacks) in which to execute the scheduler because it is not safe for the scheduler to execute on any process’s kernel stack: some other CPU might wake the process up and run it, and it would be a disaster to use the same stack on two different CPUs. There is a separate scheduler thread for each CPU to cope with situations in which more than one CPU is running a process that wants to give up the CPU. In this section we’ll examine the mechanics of switching between a kernel thread and a scheduler thread.

Switching from one thread to another involves saving the old thread’s CPU registers, and restoring the previously-saved registers of the new thread; the fact that the stack pointer and program counter are saved and restored means that the CPU will switch stacks and switch what code it is executing.

The function swtch saves and restores registers for a kernel thread switch. swtch doesn’t directly know about threads; it just saves and restores sets of RISC-V registers, called contexts. When it is time for a process to give up the CPU, the process’s kernel thread calls swtch to save its own context and restore the scheduler’s context. Each context is contained in a struct context (kernel/proc.h:2), itself contained in a process’s struct proc or a CPU’s struct cpu. swtch takes two arguments: struct context *old and struct context *new. It saves the current registers in old, loads registers from new, and returns.

Let’s follow a process through swtch into the scheduler. We saw in Chapter 4 that one possibility at the end of an interrupt is that usertrap calls yield. yield in turn calls sched, which calls swtch to save the current context in p->context and switch to the scheduler context previously saved in cpu->context (kernel/proc.c:506).

swtch (kernel/swtch.S:3) saves only callee-saved registers; the C compiler generates code in the caller to save caller-saved registers on the stack. swtch knows the offset of each register’s field in struct context. It does not save the program counter. Instead, swtch saves the ra register, which holds the return address from which swtch was called. Now swtch restores registers from the new context, which holds register values saved by a previous swtch. When swtch returns, it returns to the instructions pointed to by the restored ra register, that is, the instruction from which the new thread previously called swtch. In addition, it returns on the new thread’s stack, since that’s where the restored sp points.

In our example, sched called swtch to switch to cpu->context, the per-CPU scheduler context. That context was saved at the point in the past when scheduler called swtch (kernel/proc.c:466) to switch to the process that’s now giving up the CPU. When the swtch we have been tracing returns, it returns not to sched but to scheduler, with the stack pointer in the current CPU’s scheduler stack.