Processes, Threads, and Context Switching
LESSON
Processes, Threads, and Context Switching
By the end of this lesson, you will be able to...
Separate the resources owned by a process from the execution state owned by a thread.
Trace one timer-driven switch from a running task to another task and back to U-mode.
Identify which state must be saved, shared, restored, or released during scheduling and exit.
Idea in one sentence: A process owns the environment for work, a thread owns one path of execution through that environment, and a context switch preserves one path so another can run.
calc is computing in U-mode when the timer interrupts it. Another program, shell, is ready to run. The kernel wants to give shell the CPU now, then later resume calc at exactly the instruction where it stopped. It must do this without confusing their registers, stacks, address spaces, files, or cleanup responsibilities.
The phrase “switch from process calc to process shell” hides several different operations. The timer causes a trap. Trap entry saves user-visible state. The scheduler changes task state. A low-level switch saves kernel execution registers and changes stacks. The new task eventually returns through its own trap frame. If the address space differs, the memory-translation context changes too. Each layer owns a different kind of state.
This lesson makes the layers visible. The next lesson asks which runnable task should be chosen and when it should block or wake. First we need the simpler guarantee: a task that stops can later continue as itself.
Core Insight
A process is a resource container. A thread is an execution context. A minimal teaching kernel may initially place one thread in each process, which makes the words feel interchangeable, but they answer different questions.
| Question | Process answer | Thread answer |
|---|---|---|
| What memory view does this work use? | address space and page tables | uses its process's address space |
| Which open resources belong to it? | descriptor table, credentials, accounting, child state | may temporarily use them |
| Where does execution resume? | not enough information by itself | saved registers, stack, program counter, state |
| What can be shared? | can contain several threads | threads in one process share process resources by design |
Plain meaning:
A process is a workshop with tools, storage, and an address. A thread is one worker's current place in the instructions and their own notebook. Two workers in the same workshop share the tools but not the same notebook page.
In this scenario:
calc owns one user address space, its descriptors, and a kernel stack for its current thread. shell owns different process resources and a different thread context. The CPU can run only one of their threads at a time on this core.
Technical name:
The saved register and stack state used to stop and later resume a thread is its context. Moving the CPU from one saved context to another is a context switch.
Four Kinds of State, Not One Giant Blob
The most useful way to reason about a switch is to classify state before saving anything.
| State | Example | Usually owned by | What happens on a switch? |
|---|---|---|---|
| user trap state | user registers, user PC, prior privilege state | current thread | saved on trap; restored when returning to that thread's U-mode code |
| kernel thread context | kernel stack pointer, return address, callee-saved registers | current thread | saved by low-level swtch-style code; restored to resume kernel execution |
| process resources | page table, descriptors, credentials, heap-owned process objects | process | retained; address-space context may change when switching processes |
| CPU scheduler state | scheduler stack and saved registers | this CPU | restored while the CPU selects another runnable thread |
The trap frame from lesson 003 is not the entire context switch. It records state at a U-mode-to-kernel transition. A thread can also yield while running kernel code, sleep waiting for an event, or be resumed inside the kernel before it ever returns to U-mode. That requires a saved kernel stack pointer and kernel registers as well.
Conversely, a raw kernel context is not enough to resume user code. When the new thread eventually executes the trap return, its user trap frame supplies the U-mode registers, program counter, and prior privilege state. The two saved records cooperate.
This is why a process control block often contains references to both kinds of state: process-wide resources and one or more thread records. A simple kernel may combine them in one struct proc; a larger kernel usually separates task, thread, memory, and file objects more visibly. The ownership rules from lesson 005 still apply whichever layout is chosen.
The Naive Switch Loses the Place to Return To
Imagine a naive scheduler that records only this:
current = calc
next = shell
current = next
return to user mode
It has no answer to basic questions. Where is calc's kernel stack? Which instruction in the kernel should execute when calc is selected again? Which user registers were interrupted by the timer? Is calc still running, runnable, sleeping, or exiting? Does the CPU now translate user addresses through shell's page table?
The failure is not merely “some registers are wrong.” A switch can resume on the wrong kernel stack, return through the wrong trap frame, leave two CPUs believing they run the same thread, or free a process while its thread still uses its kernel stack.
The real mechanism uses explicit state transitions and two context records:
timer trap
-> save interrupted user state in A's trap frame
-> mark A runnable or blocked under scheduler rules
-> save A's kernel context and switch to scheduler context
-> select B, mark B running
-> restore B's kernel context
-> return through B's trap frame when B is ready for U-mode
The policy for choosing B is deliberately left open here. Round robin, priority, fairness, and wakeup rules come next. This lesson is about preserving the state correctly regardless of which runnable thread wins.
Worked Trace: Timer Interrupt from calc to shell
Assume one CPU core and two one-thread processes. Both are already created and have their own user address spaces. calc is running; shell is runnable.
Input state:
| Task | Process resources | Thread state | Current execution |
|---|---|---|---|
calc |
address space AS_calc, descriptor table |
RUNNING |
U-mode, computing at user PC 0x8120 |
shell |
address space AS_shell, descriptor table |
RUNNABLE |
saved kernel context from earlier yield |
1. The timer creates a trap record for calc
The timer interrupt arrives asynchronously. Hardware enters the supervisor trap path described in lesson 003. Entry code saves calc's user registers into its trap frame, including the user program counter and privilege-return state. The kernel can now safely use its own stack and registers.
Intermediate state: calc has not yet changed process or thread identity. It is still the current task, but it is executing kernel timer-handling code rather than its user instruction stream.
2. The kernel changes the thread state, not the process's meaning
The timer handler decides this is a preemption point. Under the scheduler's synchronization rules, it changes calc from RUNNING to RUNNABLE and places it on a runnable structure. It must do this while preserving an invariant such as: one runnable thread appears at most once in the queue, and no thread is simultaneously marked RUNNING on two CPUs.
calc's address space, descriptors, and process-owned objects still exist. It did not exit; it merely stopped using the CPU. A context switch changes which thread runs, not who owns a file object or whether a process's pages should be freed.
3. swtch saves calc's kernel continuation
The kernel now needs to leave its own call chain. Low-level switch code saves the kernel registers that the calling convention requires it to preserve—typically a return address, stack pointer, and callee-saved registers—into calc's kernel context. It then restores the scheduler context for this CPU.
Changing the saved stack pointer makes this more than a function call. When calc is restored, execution returns on its kernel stack after this switch.
4. The scheduler selects shell and restores its kernel context
The scheduler sees shell as RUNNABLE, changes its state to RUNNING, and records it as the current task on the CPU. It establishes the process context needed for shell; because shell has a different address space, this includes selecting AS_shell's translation root. On RISC-V, satp carries the address-space root and ASID together. The kernel must obey the architecture's translation-cache rules, including SFENCE.VMA when page-table updates or ASID reuse require it.
The scheduler calls the low-level switch in the opposite direction. It saves its own kernel context and restores shell's saved context. Control continues on shell's kernel stack where shell previously yielded, blocked, or first started.
5. shell returns to its own user state
If shell is ready to leave the kernel, return code uses shell's trap frame, not calc's. It restores shell's user registers and executes the privileged return instruction. The CPU resumes shell in U-mode at its saved user PC.
Output state:
| Task | Thread state | Saved context now means |
|---|---|---|
calc |
RUNNABLE |
user trap frame at 0x8120; kernel continuation after its switch |
shell |
RUNNING |
actively executing its own U-mode code |
Later the scheduler may choose calc. Restoring its kernel context resumes after swtch; return code then restores the trap frame and the user instruction stream continues at 0x8120. The timer interrupt did not cause the user instruction to run twice or disappear.
Naive failure contrast: Saving only calc's user registers would not preserve the kernel call chain that leads back from yield or timer handling. Saving only the kernel context would not recover calc's U-mode registers and user PC. The two contexts are layers, not duplicates.
Check: calc calls a blocking read and is marked SLEEPING rather than RUNNABLE. Should the scheduler later restore its context just because it still has a valid saved context?
Think first, then reveal.
Answer: No. A valid context says how to resume; thread state says whether it may run now. The scheduler must wait for the event or timeout that changes calc to RUNNABLE. Restoring a sleeping thread would violate the blocking contract.
Creation, Sharing, and Exit
Process creation assembles a resource container and at least one initial thread. A minimal path allocates a process record, kernel stack, trap frame, page table, and initial context. The first kernel context often starts at a small setup function rather than “returning” from a previous switch. That setup function eventually prepares the initial U-mode return.
Creating an additional thread inside an existing process is different. The new thread normally receives:
- its own kernel stack and saved execution context;
- its own user stack and initial program counter;
- its own scheduling state and thread-local bookkeeping;
- shared access to the process's address space, descriptors, and many process resources.
That sharing is powerful and dangerous. It makes communication through ordinary memory cheap, but it means one thread can change data another thread can see. Thread creation therefore connects directly to lesson 009's synchronization rules. It also makes ownership explicit: the process or thread group must not release shared page tables or descriptors until every thread that can use them has exited or transferred its claim.
Exit reverses creation through the ownership ledger. A thread first stops being runnable and releases its thread-local resources, including its kernel stack only after no CPU can execute on it. When the last thread leaves a process, process-wide teardown can close descriptor claims, release the address space and page-table frames, and free the process object. A “zombie” or exit record may retain a small result until a parent collects it; that is another named lifetime, not a forgotten process.
Costs and Boundaries
Context switches make responsive multitasking possible, but they are not free. Saving registers, changing kernel stacks, selecting a task, and possibly changing address-space translation state cost instructions. Switching between unrelated processes can also disturb TLB and cache locality. Giving every thread a kernel stack consumes memory even when it is sleeping.
The trade-off is between immediate responsiveness and locality. Short time slices let a waiting interactive task run sooner, but frequent switches spend more time moving state and reduce cache warmth. Long slices reduce overhead for CPU-bound work, but delay another ready task. Lesson 007 turns that trade-off into explicit scheduler policy.
This mechanism does not itself ensure fairness, prevent races in shared process memory, or safely wake a blocked thread. It provides a reliable state container and a safe execution handoff. The visible signal when that handoff fails is often a return to the wrong address, a corrupted stack, a task stuck forever in RUNNING, or a fault caused by using an address space that belongs to another process.
Common Confusions
Confusion: A process and a thread are always the same thing
Why it is tempting:
Many small kernels begin with one thread per process.
Better model:
A process owns the resource environment; a thread owns a path through it. One process can host several threads that share the environment but have separate registers, stacks, and runnable states.
Confusion: Every trap causes a context switch
Why it is tempting:
Traps move execution from user code into the kernel, which resembles a switch.
Better model:
A trap enters kernel code for the same thread. The kernel may return immediately to that thread. A context switch occurs only when the kernel saves one thread's execution context and restores another's.
Confusion: A context switch always changes address spaces
Why it is tempting:
Switching processes often changes page tables.
Better model:
Switching between threads in the same process normally keeps the address space. Switching between processes may change it. Context and address-space changes are related but distinct operations.
Practice: Complete the State Ledger
Two threads, A and B, belong to one process and share address space AS_P. A is running in U-mode. B is blocked waiting for a pipe. A timer interrupts A and the scheduler runs another process C instead.
Fill this ledger before checking the rubric:
| Item | A after timer | B | C when selected |
|---|---|---|---|
| thread state | ? | ? | ? |
| user trap frame | ? | ? | ? |
| kernel context | ? | ? | ? |
| address space | ? | ? | ? |
A strong answer states:
AbecomesRUNNABLEwith a new trap frame and saved kernel continuation;BremainsSLEEPINGuntil its pipe event;CbecomesRUNNING.AandBretainAS_P; selectingCchanges toC's process address space if it differs.Bis not made runnable merely becauseAwas preempted. Its event controls its state.
Check: A kernel switches to a new thread's kernel stack but returns to user mode using the old thread's trap frame. What class of failure should you predict?
Think first, then reveal.
Answer: It may restore the old thread's user registers, PC, and privilege-return state while the kernel believes the new thread is current. That is an isolation and correctness failure: the two layers of context must belong to the same selected thread.
Resources
- [BOOK] xv6 RISC-V textbook — Focus: process records, scheduler contexts,
swtch, and trap frames. - [COURSE] MIT 6.1810 multithreading lab — Focus: saving and restoring a thread context on its own stack.
- [REFERENCE] RISC-V Supervisor-Level ISA — Focus:
satp, ASIDs, and translation-cache synchronization during address-space changes.
Key Takeaways
- Processes own resource environments; threads own independently schedulable execution contexts within those environments.
- A trap frame preserves user-visible state, while a kernel context preserves the kernel continuation and stack; both are needed for a safe return to user code.
- Scheduler state transitions decide whether a saved context may run.
RUNNABLEandSLEEPINGare as important as register values. - Context switching can change address spaces, but it need not when threads share a process.
- Creation and exit must follow the same ownership rules as other kernel objects: do not release process-wide resources until no thread can still use them.