Chapter 24: Threads
- What a thread is and how it differs from a process
- Why threads are lighter than processes
- User-space vs kernel-space threads
- How threads share the address space of their process
- Thread creation, scheduling, and synchronization
- Our kernel thread implementation
24.1 What is a Thread?
A thread is a single execution flow within a process. Multiple threads within the same process share:
- The same address space (code, data, heap, page tables)
- The same file descriptors
- The same working directory, signals, and other process attributes
Each thread has its own:
- Register state (PC, SP, general-purpose registers)
- Stack (both user and kernel stack)
- Thread ID (TID)
- Scheduling state (can be scheduled independently)
- Errno (per-thread error code)
24.2 Process vs Thread
| Attribute | Process | Thread |
|---|---|---|
| Address space | Private | Shared (with process) |
| Page tables | Own | Shared (same TTBR0) |
| File descriptors | Own | Shared |
| Creation overhead | High (copy page tables, FD table) | Low (just allocate stack + TCB) |
| Context switch time | Higher (TLB flush needed) | Lower (no TLB flush, same ASID) |
| IPC/shared memory | Need explicit mechanism | Implicit (same heap) |
| Isolation | Strong (crash isolated) | Weak (one thread crash kills all) |
24.3 Thread Control Block
A thread is represented by a Thread Control Block (TCB):
/* Thread Control Block */
struct tcb {
/* Identification */
tid_t tid;
struct pcb *process; /* Owning process */
/* Execution state */
struct context context; /* Saved registers */
uint64_t *kernel_stack_top;
uint64_t *kernel_stack_bottom;
/* Scheduling (threads are scheduled individually) */
int priority;
int ticks_remaining;
struct tcb *next_ready;
/* Wait queue (for thread_join) */
struct tcb *waiter; /* Thread waiting for this one */
void *retval; /* Return value from thread_exit */
};
24.4 Thread Creation
Creating a thread is much simpler than creating a process:
/* Create a new thread in the current process */
tid_t sys_thread_create(void (*func)(void *), void *arg) {
struct pcb *proc = get_current_process();
struct tcb *thread = alloc_tcb();
thread->tid = allocate_tid();
thread->process = proc;
/* Allocate kernel stack */
thread->kernel_stack_bottom = kmalloc(KERNEL_STACK_SIZE);
thread->kernel_stack_top = thread->kernel_stack_bottom + KERNEL_STACK_SIZE;
/* Initialize context to run start routine */
thread->context.pc = (uint64_t)thread_entry;
thread->context.sp = (uint64_t)thread->kernel_stack_top - 128; /* Leave room */
/* Pass func and arg to the entry point */
/* (stored at the top of the new stack) */
uint64_t *stack = (uint64_t *)thread->kernel_stack_top;
stack[-1] = (uint64_t)arg;
stack[-2] = (uint64_t)func;
/* Initialize other registers to 0 */
for (int i = 0; i < 18; i++) thread->context.regs[i] = 0;
/* Set priority and scheduling */
thread->priority = proc->priority;
thread->ticks_remaining = TIME_SLICE_TICKS;
/* Add to ready queue */
thread_enqueue(thread);
return thread->tid;
}
/* Thread entry point (runs in kernel mode initially) */
void thread_entry(void) {
/* Get func and arg from stack */
uint64_t *stack = (uint64_t *)(get_sp() + 128);
void (*func)(void *) = (void (*)(void *))stack[-2];
void *arg = (void *)stack[-1];
/* Enable interrupts and switch to user mode (if user-space thread) */
enable_interrupts();
/* Call the thread function */
func(arg);
/* If the function returns, exit the thread */
sys_thread_exit(NULL);
}
24.5 Thread Scheduling
Threads are scheduled independently. The scheduler (Chapter 22) manages a global ready queue of threads, not processes. A process with 4 threads gets 4 entries in the ready queue and therefore 4 times more CPU time than a single-threaded process.
/* Thread scheduler (called from timer interrupt) */
void thread_schedule(void) {
struct tcb *current = get_current_thread();
current->ticks_remaining--;
if (current->ticks_remaining > 0 && !need_resched) return;
current->ticks_remaining = TIME_SLICE_TICKS;
/* Pick next thread */
struct tcb *next = thread_pick_next();
if (!next) return;
/* Save current */
save_context(¤t->context);
/* Switch kernel stack */
set_sp(next->kernel_stack_top);
/* Restore next (if same process, no TLB flush) */
if (current->process != next->process) {
switch_page_table(next->process->page_table);
tlb_flush_asid(next->process->asid);
}
/* If same process: no page table switch, no TLB flush needed */
restore_context(&next->context);
}
24.6 User-Space Threads vs Kernel Threads
Kernel threads (also called kernel-mode threads) run entirely within the kernel. They don't have a user address space. The kernel itself uses threads to perform background tasks: write dirty pages to disk, handle network packets, run periodic cleanup. Kernel threads always run in EL1.
User-space threads execute user code in EL0. They share the user address space of the process. The kernel schedules threads, not processes. A system call like clone() (used by pthreads) creates a new user-space thread with a shared address space but independent stack and registers.
24.7 Thread Synchronization
Because threads share memory, they need synchronization primitives:
- Mutex: protect critical sections (Chapter 26)
- Spinlocks: busy-wait lock for short sections (Chapter 27)
- Semaphores: counting synchronization (Chapter 28)
- Thread join: wait for another thread to exit
- Condition variables: wait for a condition to become true
24.8 Our Implementation
Our kernel supports both kernel threads (for internal kernel tasks) and user-space threads (via the clone system call). Key features:
- TCB: lightweight thread control block (64 bytes)
- Thread scheduler: schedules threads directly (process is just a container)
- No TLB flush on context switch between threads of the same process
- Thread join:
thread_join(thread)blocks until the target thread exits - Thread exit: frees stack and TCB, wakes joining thread
- Idle thread: per-CPU idle thread that runs WFI when nothing is schedulable
24.9 Exercises
Exercise 1: Thread Counter
Write a test: create two threads that each increment a shared counter 1,000,000 times. Run without synchronization and observe the lost updates. Then add a mutex and compare results.
Exercise 2: Kernel Thread
Create a kernel thread that wakes up every 5 seconds, prints "heartbeat", and goes back to sleep. Use a sleep queue and timer for scheduling.
24.10 Summary
Threads are lightweight execution flows that share the address space and resources of a process. Each thread has its own stack, register state, and scheduling entry. Thread creation is cheaper than process creation because page tables and file descriptors are shared. Our kernel schedules threads directly at the scheduler level, not processes, which means multi-threaded processes get proportionally more CPU time. Context switching between threads of the same process is especially fast because no TLB flush is needed.