From Silicon to Scheduler: A Deep Dive into Cores, Threads, and Context Switching
As embedded engineers working with complex SoCs (System on Chips), we often treat the Operating System as a black box. We write code…

From Silicon to Scheduler: A Deep Dive into Cores, Threads, and Context Switching
As embedded engineers working with complex SoCs (System on Chips), we often treat the Operating System as a black box. We write code, compile it, and it runs. But when performance drops, or when a high-priority interrupt gets delayed, the “black box” view isn’t enough. We need to understand the machinery.
What exactly is a thread? How does a single CPU core run 50 tasks at once? And what is the actual cost of “switching” between them?
This article breaks down the physical and logical architecture of multitasking, culminating in a Python-based Kernel Simulator that lets you watch a context switch happen in real-time.
Part 1: The Hardware Foundation
The Core: The Workshop
The Core is the physical execution unit etched into the silicon. Whether it’s a Cortex-M4 or a high-performance Cortex-A78, a core has:
- Registers: Fast, temporary storage (R0-R15, PC, SP).
- ALU: The calculator (Add, Subtract, XOR).
- Pipeline: The assembly line (Fetch, Decode, Execute).
Crucially, a core can only do one thing at a time. If you have a single-core SoC, you have one pair of hands. You physically cannot pick up two tools at once.
The Thread: The Worker
A Thread is a sequence of instructions managed by the software.
- Hardware Threads (SMT): Some cores have two sets of registers (hands) sharing one ALU (tool). This allows instant switching but doesn’t double the processing power.
- Software Threads: These are virtual workers. The OS allows you to create thousands of them, even on a single core.
The Problem: How do you fit 1,000 threads (workers) into 4 cores (workshops)? The Solution: You cheat. You switch them so fast that it looks simultaneous.
Part 2: The OS Architecture
The Thread Control Block (TCB)
To the hardware, a thread doesn’t exist. It only knows “Current Registers.” The OS creates the concept of a thread using a data structure called the Thread Control Block (or task_struct in Linux).
Think of the TCB as a “Save Game” file. When a thread is paused, the OS saves:
- Program Counter (PC): Where was I?
- Stack Pointer (SP): Where is my data?
- General Registers (R0-R12): What values was I calculating?
- State: Ready, Running, or Blocked?
The Heartbeat: The Hardware Timer
Multitasking relies on a hardware component: the System Timer. The kernel configures this timer to fire an interrupt every X milliseconds (called a Quantum or Time Slice).
When this interrupt fires:
- The CPU stops whatever it is doing.
- The CPU jumps to the Scheduler (the Kernel’s decision maker).
- The Scheduler decides if the current thread has used up its time.
The Context Switch
This is the most expensive operation in an OS. It is pure overhead.
- Save: Copy values from Physical Registers — — →Old Thread’s TCB.
- Select: Pick the next TCB from the “Ready Queue.”
- Restore: Copy values from New Thread’s TCB — — →Physical Registers.
- Resume: The CPU executes the instruction at the new Program Counter.
Part 3: Building the Simulator
We can’t easily see inside a running kernel without JTAG debuggers. So, we will build a Mini-OS in Python to simulate this behavior.
The Logic
We need three classes:
**TCB:** To hold the "saved state" of our threads.**CPU:** To represent the single physical core and its registers.**Kernel:** To manage the timer, the ready queue, and the context switch.
import time
import queue
import random
# --- 1. THE DATA STRUCTURES (Kernel Space) ---
class TCB:
"""The 'Save File' for a thread."""
def __init__(self, tid, name, duration, priority):
self.tid = tid
self.name = name
self.priority = priority # 1 = High, 10 = Low
self.state = "READY"
# Simulating CPU Registers. 'R0' is our working variable.
self.registers = {"R0": 0, "PC": 0}
self.total_work = duration
self.work_done = 0
# This allows the PriorityQueue to sort threads by importance
def __lt__(self, other):
return self.priority < other.priority
class CPU:
"""The Silicon. It only has ONE set of registers."""
def __init__(self):
self.registers = {"R0": 0, "PC": 0}
self.current_thread = None
def load_context(self, tcb):
print(f" [CPU] ⚡ RESTORE: Loading {tcb.name} (Pri:{tcb.priority}) registers...")
# Copy from TCB RAM -> CPU Registers
self.registers = tcb.registers.copy()
def save_context(self, tcb):
print(f" [CPU] 💾 SAVE: Dumping {tcb.name} registers to TCB...")
# Copy from CPU Registers -> TCB RAM
tcb.registers = self.registers.copy()
# --- 2. THE KERNEL (The Manager) ---
class Kernel:
def __init__(self):
self.cpu = CPU()
# A Priority Queue sorts threads automatically (High prio first)
self.ready_queue = queue.PriorityQueue()
self.time_slice = 3 # The Quantum (3 ticks per turn)
def create_thread(self, name, duration, priority):
new_tcb = TCB(random.randint(1000, 9999), name, duration, priority)
self.ready_queue.put(new_tcb)
print(f"[KERNEL] 🐣 Created {name} with Priority {priority}")
def context_switch(self, old_tcb, new_tcb):
print(f"\n[SCHED] 🛑 INTERRUPT! Switching Context...")
# 1. Save the old thread (if it exists and isn't dead)
if old_tcb and old_tcb.state != "TERMINATED":
print(f" [SCHED] Preempting {old_tcb.name}...")
self.cpu.save_context(old_tcb)
old_tcb.state = "READY"
self.ready_queue.put(old_tcb) # Put back in line
# 2. Load the new thread
print(f" [SCHED] Switching to {new_tcb.name}...")
self.cpu.load_context(new_tcb)
self.cpu.current_thread = new_tcb
new_tcb.state = "RUNNING"
def run(self):
print("\n[KERNEL] 🚀 BOOT COMPLETE. STARTING SCHEDULER.\n")
# Main Loop: While there is work to do
while not self.ready_queue.empty() or self.cpu.current_thread:
# If CPU is idle, grab a thread
if self.cpu.current_thread is None:
if not self.ready_queue.empty():
next_thread = self.ready_queue.get()
self.context_switch(None, next_thread)
else:
break # System Idle
current = self.cpu.current_thread
ticks = 0
# --- THE QUANTUM LOOP (Time Slice) ---
while ticks < self.time_slice:
# Check if thread finished naturally
if current.work_done >= current.total_work:
print(f" [CPU] 🎉 {current.name} FINISHED EXECUTION.")
current.state = "TERMINATED"
self.cpu.current_thread = None
break
# EXECUTE INSTRUCTION
current.work_done += 1
self.cpu.registers["R0"] += 1 # The thread modifies the CPU
print(f" [TICK] {current.name} running... | CPU R0 = {self.cpu.registers['R0']}")
time.sleep(0.3)
ticks += 1
# --- END OF QUANTUM ---
# If thread is still running, PREEMPT IT (Force Switch)
if current and current.state == "RUNNING":
print(f" [SCHED] ⏱️ Quantum Expired for {current.name}.")
if not self.ready_queue.empty():
# Get the next highest priority thread
next_thread = self.ready_queue.get()
self.context_switch(current, next_thread)
print("\n[KERNEL] 💤 SYSTEM HALT.")
# --- 3. EXECUTION ---
if __name__ == "__main__":
os = Kernel()
# Notice: Thread A is created first, but has LOW priority (10)
# Thread C is created last, but has HIGH priority (1)
os.create_thread("Thread_A (Video Enc)", duration=6, priority=10)
os.create_thread("Thread_B (Audio Dec)", duration=4, priority=5)
os.create_thread("Thread_C (UI Update)", duration=2, priority=1)
os.run()
Part 4: Analyzing the Simulation
When you run this code, here is the story it tells, step-by-step.
1. The Priority Override
[KERNEL] 🐣 Created Thread_A (Video Enc) with Priority 10
...
[KERNEL] 🐣 Created Thread_C (UI Update) with Priority 1
...
[SCHED] Switching to Thread_C (UI Update)...
Even though Thread A was waiting in line first, the Scheduler (using the Priority Queue) noticed that Thread C is more important (Priority 1). This mimics real-time OS behavior: UI and Audio always beat background processing.
- The Execution (The Quantum)
[TICK] Thread_C (UI Update) running... | CPU R0 = 1
[TICK] Thread_C (UI Update) running... | CPU R0 = 2
[CPU] 🎉 Thread_C (UI Update) FINISHED EXECUTION.
Thread C finishes quickly. It didn’t need a full time slice, so it “Yields” the CPU. The Scheduler immediately looks for the next task.
3. The Context Switch (The Magic)
Now look at the switch between Audio (B) and Video (A).
[TICK] Thread_B (Audio Dec) running... | CPU R0 = 1
...
[SCHED] ⏱️ Quantum Expired for Thread_B.
[CPU] 💾 SAVE: Dumping Thread_B registers to TCB...
Thread B was interrupted! It had counted to R0=3. The CPU saves this 3 into Thread B's memory.
[SCHED] Switching to Thread_A (Video Enc)...
[CPU] ⚡ RESTORE: Loading Thread_A (Pri:10) registers...
[TICK] Thread_A (Video Enc) running... | CPU R0 = 1
The CPU loads Thread A. Notice that R0 reset to 0 (or whatever A's last saved state was). Thread A has no idea that Thread B just used the CPU.
4. The Resume
Later, Thread B will come back:
[CPU] ⚡ RESTORE: Loading Thread_B (Pri:5) registers...
[TICK] Thread_B (Audio Dec) running... | CPU R0 = 4
It picks up exactly at 4. The illusion is complete.
Conclusion
Understanding cores and threads is about understanding state management.
- Cores are the resources.
- Threads are the tasks.
- The OS is the manager that saves the state of one task to load another.
For an embedded engineer, this simulation reveals the cost of your code. Every time you spawn a thread, you consume memory (TCB). Every time your threads fight for the CPU, you burn cycles saving and restoring registers.
Next time you see high “Sys” usage in top, you’ll know exactly what’s happening: Your manager is spending too much time shuffling papers and not enough time letting the workers work.
메타데이터
- post_id
- bee37cb145d2
- slug
- from-silicon-to-scheduler-a-deep-dive-into-cores-threads-and-context-switching-bee37cb145d2
- url
- https://medium.com/@arshad.3e.1/from-silicon-to-scheduler-a-deep-dive-into-cores-threads-and-context-switching-bee37cb145d2
- canonical_url
- https://medium.com/@arshad.3e.1/from-silicon-to-scheduler-a-deep-dive-into-cores-threads-and-context-switching-bee37cb145d2
- author_url
- https://medium.com/@arshad.3e.1
- status
- ok
- fetched_at
- 2026-08-02 12:44:19