Loading learning content...
Before sophisticated synchronization primitives like semaphores, mutexes, and modern atomic operations existed, operating system designers faced a fundamental challenge: how do you ensure that a sequence of instructions executes without interruption?
This question lies at the heart of concurrent programming. When multiple processes or threads compete for shared resources, the operating system's ability to context switch at any moment creates the possibility of race conditions, data corruption, and system instability. The most direct solution—disabling the very mechanism that enables context switches—emerged as one of the earliest approaches to achieving mutual exclusion.
Disabling interrupts represents the brute-force approach to synchronization: if you prevent the CPU from responding to any external events, you guarantee that your critical section will execute atomically. No timer interrupt means no preemption. No I/O interrupt means no device-triggered scheduling. The processor becomes a single-minded execution engine, focused exclusively on completing your code.
By the end of this page, you will understand the interrupt disable mechanism at the hardware level, how it achieves mutual exclusion, the precise CPU instructions involved, and why this technique became a foundational building block for more sophisticated synchronization methods. You'll also gain insight into the historical context that made this approach so significant in early operating system development.
To understand the interrupt disable approach, we must first deeply comprehend what interrupts are and why they exist. An interrupt is a signal to the processor emitted by hardware or software indicating an event that needs immediate attention.
The Hardware Interrupt Mechanism:
Modern processors are designed around an asynchronous event model. While the CPU executes instructions sequentially, external devices—keyboards, network cards, disk controllers, timers—operate independently and at their own pace. When these devices need CPU attention, they assert an electrical signal on a dedicated interrupt line connected to the processor.
The CPU, upon completing its current instruction, checks the interrupt line. If an interrupt is pending and interrupts are enabled, the processor:
This mechanism enables preemptive multitasking. The timer interrupt, typically firing 100-1000 times per second on modern systems, allows the operating system to regain control at regular intervals and decide which process should run next.
| Interrupt Type | Source | Purpose | Typical Frequency |
|---|---|---|---|
| Timer Interrupt | Hardware Timer (PIT, APIC Timer) | Preemptive scheduling, timekeeping | 100-1000 Hz (10ms-1ms intervals) |
| Keyboard Interrupt | Keyboard Controller | Key press/release notification | Event-driven (user typing) |
| Disk I/O Interrupt | Disk Controller | I/O completion notification | Per I/O operation completion |
| Network Interrupt | NIC (Network Interface Card) | Packet arrival/transmission complete | Thousands per second under load |
| Page Fault | Memory Management Unit | Virtual memory page miss | Varies by workload |
| System Call (Software) | INT/SYSCALL instruction | User-to-kernel transition | Per system call invocation |
In the context of synchronization, the timer interrupt is the most critical. It's the mechanism that allows the OS scheduler to preempt running processes. Without timer interrupts, a CPU-bound process could monopolize the processor indefinitely. When discussing 'disabling interrupts' for synchronization, we're primarily concerned with blocking this timer-driven preemption.
The Interrupt Flag: Hardware-Level Control
Processors provide a status register (also called flags register or processor status word) containing individual bits that control processor behavior. Among these is the interrupt enable flag (IF on x86 architectures, or equivalent on other processors).
This single bit is the key to the interrupt disable approach. By clearing this bit, we tell the processor: "Do not respond to any maskable hardware interrupts until I say otherwise."
The interrupt disable approach to mutual exclusion is elegantly simple in concept:
This guarantees that while a process is in its critical section, no context switch can occur. The timer interrupt that would trigger the scheduler is ignored. The process runs to completion within the critical section before any other process can execute.
The Hardware Instructions:
Different processor architectures provide different instructions for interrupt control. Here's how the major architectures handle it:
12345678910111213141516171819202122232425262728293031
; x86 Architecture - Interrupt Control Instructions; ================================================ ; CLI - Clear Interrupt Flag; Sets IF = 0, disabling maskable interruptscli ; Single instruction, ~1-2 cycles ; STI - Set Interrupt Flag ; Sets IF = 1, enabling maskable interruptssti ; Single instruction, ~1-2 cycles ; Practical usage pattern in kernel code:enter_critical_section: pushf ; Save current FLAGS register (including IF) cli ; Disable interrupts ; ... critical section code ... popf ; Restore original FLAGS (including IF state) ; Alternative pattern using explicit state tracking:save_and_disable: pushf ; Push FLAGS onto stack pop eax ; Save to EAX register cli ; Disable interrupts ; EAX now holds the original interrupt state restore_interrupts: push eax ; Push saved FLAGS popf ; Restore FLAGS register ; Note: PUSHF/POPF saves/restores ALL flags, ensuring; we restore the exact interrupt state that existed before12345678910111213141516171819202122232425262728293031323334353637
@ ARM Architecture - Interrupt Control@ ===================================== @ ARM uses the CPSR (Current Program Status Register)@ Bit 7 (I) - IRQ disable bit@ Bit 6 (F) - FIQ disable bit @ Disable IRQ interrupts (ARMv7 and earlier)cpsid i @ Clear IRQ enable bit in CPSR @ Enable IRQ interrupts cpsie i @ Set IRQ enable bit in CPSR @ Disable both IRQ and FIQcpsid if @ Clear both interrupt bits @ Legacy method using MRS/MSR (works on all ARM):disable_interrupts: mrs r0, cpsr @ Read CPSR into r0 orr r0, r0, #0x80 @ Set bit 7 (I flag) msr cpsr_c, r0 @ Write back to CPSR control byte enable_interrupts: mrs r0, cpsr @ Read CPSR into r0 bic r0, r0, #0x80 @ Clear bit 7 (I flag) msr cpsr_c, r0 @ Write back to CPSR control byte @ Save/restore pattern for nested critical sections:save_and_disable: mrs r0, cpsr @ Save original CPSR cpsid i @ Disable interrupts @ r0 now holds the original interrupt state bx lr @ Return with state in r0 restore_interrupts: msr cpsr_c, r0 @ Restore from saved state bx lrCorrect Usage Pattern: Save and Restore
A critical subtlety in the interrupt disable approach is proper handling of nested calls. Consider the following problematic scenario:
function_a():
disable_interrupts() // Interrupts now OFF
critical_work_a()
call function_b() // function_b also needs protection
disable_interrupts() // Already off, but that's fine
critical_work_b()
enable_interrupts() // PROBLEM: Re-enables interrupts!
// Interrupts are now ON, but function_a hasn't finished!
more_critical_work_a() // This is no longer protected!
enable_interrupts()
The correct pattern is to save the current interrupt state before disabling and restore that saved state when done. This handles nesting correctly:
function_a():
saved_state_a = save_and_disable() // Save: OFF or ON
critical_work_a()
call function_b()
saved_state_b = save_and_disable() // Save: OFF (it's currently off)
critical_work_b()
restore(saved_state_b) // Restore: OFF (stays off)
more_critical_work_a() // Still protected!
restore(saved_state_a) // Restore original state
One of the most common bugs in kernel code is unconditionally enabling interrupts at the end of a critical section. Always save the interrupt state before disabling, and restore that saved state afterward. This ensures proper behavior in nested scenarios and when called from contexts where interrupts may already be disabled.
To understand why disabling interrupts provides mutual exclusion, we must trace through the execution precisely:
The Preemption Chain:
Breaking the Chain:
When we execute the cli instruction (or equivalent), we set IF = 0. Now:
The critical section runs to completion with absolute certainty. This is the strongest possible guarantee—the mechanism operates at the hardware level, not through software conventions or cooperation between threads.
Formal Proof of Mutual Exclusion:
We can formally prove that the interrupt disable approach satisfies the mutual exclusion property on a uniprocessor system:
Theorem: If a process P disables interrupts before entering its critical section and enables them after exiting, no other process can execute while P is in its critical section.
Proof:
Key Insight: This proof relies critically on the uniprocessor assumption. On a multiprocessor system, process Q might be running simultaneously on a different CPU, which has its own interrupt flag. Disabling interrupts on CPU₀ says nothing about CPU₁'s behavior.
The interrupt disable approach is elegant because it provides an absolute guarantee. There's no race condition in the mechanism itself—cli is a single atomic instruction that cannot be interrupted. Compare this to software-only solutions like Peterson's Algorithm, which require careful reasoning about memory ordering and interleaving.
Modern operating systems provide abstractions over the raw interrupt control instructions, making them safer and more convenient to use. Let's examine how major operating systems expose this functionality.
Linux Kernel Implementation:
The Linux kernel provides a family of functions for interrupt control, each suited to different use cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
/* Linux Kernel Interrupt Control Functions *//* Located in include/linux/irqflags.h */ #include <linux/irqflags.h> /* Basic disable/enable - DON'T use these directly */local_irq_disable(); /* cli equivalent */local_irq_enable(); /* sti equivalent */ /* Safe pattern: save and restore (RECOMMENDED) */unsigned long flags; local_irq_save(flags); /* Save IF state, then disable *//* ... critical section ... */local_irq_restore(flags); /* Restore saved IF state */ /* Example: Protecting kernel data structure access */void update_shared_counter(struct counter *cnt, int delta) { unsigned long flags; local_irq_save(flags); /* * This section is now atomic with respect to: * - Timer interrupts (no preemption) * - Other hardware interrupts * - Softirqs that might access cnt */ cnt->value += delta; cnt->update_count++; local_irq_restore(flags);} /* When you also need to disable preemption explicitly *//* (useful when interrupts might already be disabled) */preempt_disable();/* ... critical section ... */preempt_enable(); /* Combined: disable interrupts + disable preemption */unsigned long flags;local_irq_save(flags);preempt_disable(); /* Belt and suspenders *//* ... critical section ... */preempt_enable();local_irq_restore(flags); /* * SPINLOCK INTEGRATION: * spin_lock_irq() and spin_lock_irqsave() combine * spinlock acquisition with interrupt disabling */spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* Method 1: Disable interrupts unconditionally */spin_lock_irq(&my_lock);/* ... critical section ... */spin_unlock_irq(&my_lock); /* Method 2: Save/restore interrupt state (preferred) */unsigned long flags;spin_lock_irqsave(&my_lock, flags);/* ... critical section ... */spin_unlock_irqrestore(&my_lock, flags);Windows Kernel Implementation:
The Windows kernel uses a concept called IRQL (Interrupt Request Level) to manage interrupt priorities and critical sections:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
/* Windows Kernel IRQL Management *//* Located in wdm.h, ntddk.h */ #include <ntddk.h> /* * Windows IRQL Levels (x64): * PASSIVE_LEVEL (0) - Normal thread execution * APC_LEVEL (1) - Asynchronous procedure calls * DISPATCH_LEVEL (2) - DPCs, no paging, no page faults * DIRQL (3-26) - Device interrupt levels * HIGH_LEVEL (31) - All interrupts disabled */ KIRQL oldIrql; /* Raise to DISPATCH_LEVEL - disables preemption */KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);/* * At DISPATCH_LEVEL: * - Cannot page fault * - Cannot call wait functions * - Cannot access pageable memory * - Thread scheduling disabled on this CPU */// ... critical section code ...KeLowerIrql(oldIrql); /* Raise to HIGH_LEVEL - disables ALL interrupts */KeRaiseIrql(HIGH_LEVEL, &oldIrql);/* * At HIGH_LEVEL: * - NO interrupts will be serviced * - Use only for very brief periods * - Clock, profiling, everything is blocked */// ... very short critical section ...KeLowerIrql(oldIrql); /* Spinlock with interrupt management */KSPIN_LOCK mySpinLock;KeInitializeSpinLock(&mySpinLock); /* Acquire spinlock, raising IRQL to DISPATCH_LEVEL */KeAcquireSpinLock(&mySpinLock, &oldIrql);// ... critical section ...KeReleaseSpinLock(&mySpinLock, oldIrql); /* For interrupt contexts, use different API */KLOCK_QUEUE_HANDLE lockHandle;KeAcquireInStackQueuedSpinLock(&mySpinLock, &lockHandle);// ... critical section ...KeReleaseInStackQueuedSpinLock(&lockHandle);Windows' IRQL system is more sophisticated than a simple enable/disable flag. It creates a hierarchy of interrupt levels, allowing selective blocking of lower-priority interrupts while still servicing higher-priority ones. This provides finer-grained control, though it also introduces more complexity.
FreeBSD Implementation:
FreeBSD uses a similar model with critical_enter() and critical_exit() for lightweight critical sections:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
/* FreeBSD Kernel Critical Sections *//* Located in sys/systm.h */ #include <sys/systm.h> /* * critical_enter() disables preemption by incrementing * a per-thread nesting counter. It does NOT disable * hardware interrupts entirely. */critical_enter();/* * In this critical section: * - This thread cannot be preempted * - Interrupts CAN still fire and be handled * - But the handler won't cause a context switch */update_data_structure();critical_exit(); /* For hardware interrupt disabling, use intr API */register_t saved_intr; saved_intr = intr_disable(); /* Disable HW interrupts *//* * Now in "hard" critical section: * - No interrupts at all * - No preemption * - Keep it SHORT */manipulate_interrupt_controller();intr_restore(saved_intr); /* Restore interrupt state */ /* Spinlock integration */struct mtx my_mutex;mtx_init(&my_mutex, "example", NULL, MTX_SPIN); mtx_lock_spin(&my_mutex);/* * Spinlock acquired, preemption disabled * On UP system, may disable interrupts */// ... critical section ...mtx_unlock_spin(&my_mutex);The interrupt disable approach has deep historical roots in the evolution of operating systems. Understanding this history illuminates why this technique remains relevant despite its limitations.
The Pre-Interrupt Era (1940s-1950s):
The earliest computers were strictly sequential machines. Programs ran from start to completion without interruption. The concept of multiple programs "sharing" the processor simply didn't exist. The CPU was a scarce, expensive resource operated in batch mode.
The Interrupt Revolution (Late 1950s-1960s):
The introduction of hardware interrupts transformed computing:
With interrupts came the first synchronization problems. Engineers quickly discovered that if the operating system could interrupt a program at any point, data structures accessed by both user code and interrupt handlers could become corrupted.
The CLI/STI Solution:
The natural solution was to give privileged code the ability to temporarily disable interrupts. This was simple, fast, and absolutely effective on the single-processor machines of the era.
| Era | Problem | Solution | Key Insight |
|---|---|---|---|
| Late 1950s | I/O completion during computation | Interrupt flag disable | Atomicity through hardware control |
| 1960s | Time-sharing context switches | Timer interrupt masking | Scheduler-level mutual exclusion |
| 1970s | Complex device drivers | Priority-based interrupt levels | Hierarchical interrupt management |
| 1980s | Multiprocessor systems | Spinlocks + interrupt disable | Local CPU protection only |
| 1990s-2000s | SMP scalability | Per-CPU data + RCU | Minimize global synchronization |
| 2010s+ | Many-core systems | Lock-free algorithms | Avoid disabling interrupts entirely |
Significance in OS Design:
The interrupt disable mechanism became the primitive building block upon which other synchronization mechanisms were built:
Semaphores: Early implementations of wait() and signal() disabled interrupts to atomically decrement/increment the semaphore counter and manipulate wait queues
Spinlocks: On multiprocessor systems, spinlocks combine atomic test-and-set operations with interrupt disabling to ensure the lock holder isn't preempted
Scheduler Locks: The operating system's scheduler itself runs with interrupts disabled during critical operations like context switching and run queue manipulation
Interrupt Handlers: Interrupt handlers often run with their own interrupt level disabled to prevent recursive invocation
Legacy in Modern Systems:
Even in contemporary operating systems, interrupt disabling remains essential in specific contexts:
While we rarely use raw interrupt disabling in application-level code today, it remains the foundation upon which all other synchronization primitives rest. Every mutex lock, every semaphore wait, every atomic operation ultimately relies on hardware mechanisms that include interrupt control. Understanding this foundation illuminates the entire synchronization stack.
We've explored the interrupt disable approach to mutual exclusion in depth. Let's consolidate the essential concepts:
What's Next:
Now that we understand the fundamental mechanism, we'll examine a critical limitation: the interrupt disable approach only works on single-processor systems. In the next page, we'll explore why this is the case and what challenges arise in multiprocessor environments.
You now understand the interrupt disable approach at both the hardware and operating system levels. You've seen the processor instructions involved, the correct save/restore patterns, and the historical context that made this approach foundational to OS synchronization. Next, we'll examine why this elegant solution fails in multiprocessor environments.