← Back to list

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…

Arshad S · 2026-02-08 07:40 · 1 claps · 5.6 min read
#task-scheduler #embedded-systems #realtime-operating-system
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

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:

  1. Program Counter (PC): Where was I?
  2. Stack Pointer (SP): Where is my data?
  3. General Registers (R0-R12): What values was I calculating?
  4. 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:

  1. The CPU stops whatever it is doing.
  2. The CPU jumps to the Scheduler (the Kernel’s decision maker).
  3. 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.

  1. Save: Copy values from Physical Registers — — →Old Thread’s TCB.
  2. Select: Pick the next TCB from the “Ready Queue.”
  3. Restore: Copy values from New Thread’s TCB — — →Physical Registers.
  4. 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:

  1. **TCB:** To hold the "saved state" of our threads.
  2. **CPU:** To represent the single physical core and its registers.
  3. **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.

  1. 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