Skip to content

Context Switching and System Calls

Modern operating systems must efficiently manage multiple processes competing for CPU time. Context switching enables the CPU to switch between different execution contexts, while system calls and interrupts are the mechanisms that trigger these switches. Understanding these fundamental OS concepts is crucial for writing high-performance applications.

System Calls and Context Switching

What are System Calls?

System calls are the interface between user space and kernel space. When a user program needs to access hardware, file systems, or other privileged resources, it must make a system call to request the kernel's services.

The kernel then handles the user's request, interacts with the hardware using device drivers, and returns the result to the user program.

How System Calls Cause Context Switches

Every system call involves a context switch from user modeAn unprivileged mode of the CPU that restricts access to hardware and privileged operations to kernel modeA privileged mode of the CPU that allows access to hardware and privileged operations:

cpp
// User space program making a system call
int main() {
    char buffer[100];

    // This read() call triggers a context switch
    ssize_t bytes_read = read(0, buffer, 100);  // System call

    // This write() call triggers another context switch
    write(1, buffer, bytes_read);  // System call

    return 0;
}

// What happens during a system call (simplified)
void syscall_handler() {
    // 1. Save user process context
    save_user_context();

    // 2. Switch to kernel mode
    switch_to_kernel_mode();

    // 3. Execute kernel code
    handle_system_call();

    // 4. Switch back to user mode
    switch_to_user_mode();

    // 5. Restore user process context
    restore_user_context();
}

System Call Overhead and Performance Impact

System calls have significant performance overhead due to context switching:

Overhead Components:

  1. Mode Switch: User mode → Kernel mode (100-200 CPU cycles)
  2. Parameter Validation: Check user pointers and permissions (50-100 cycles)
  3. Kernel Processing: Actual system call work (varies greatly)
  4. Mode Switch: Kernel mode → User mode (100-200 cycles)
  5. Cache Effects: CPU cache invalidation (100-1000 cycles)

Performance Impact:

  • Typical overhead: 100-1000 CPU cycles per system call
  • High-frequency impact: Can consume 10-50% of CPU time in I/O-bound applications
  • Latency impact: Adds microseconds to every system call

Common System Calls and Their Impact

cpp
// File I/O system calls (high overhead)
int fd = open("file.txt", O_RDONLY);  // ~1000-2000 cycles
read(fd, buffer, 1024);              // ~500-1500 cycles
close(fd);                           // ~200-500 cycles

// Memory management system calls (medium overhead)
void* ptr = malloc(1024);            // ~200-500 cycles
free(ptr);                           // ~100-300 cycles

// Process management system calls (high overhead)
pid_t pid = fork();                  // ~10000-50000 cycles
execvp("program", args);             // ~5000-20000 cycles
wait(&status);                       // ~1000-5000 cycles

// Network system calls (high overhead)
int sock = socket(AF_INET, SOCK_STREAM, 0);  // ~1000-3000 cycles
connect(sock, &addr, sizeof(addr));          // ~10000-100000 cycles
send(sock, data, size, 0);                   // ~500-2000 cycles

1. Batch Operations

cpp
// Bad: Multiple small system calls
for (int i = 0; i < 1000; i++) {
    write(fd, &data[i], 1);  // 1000 system calls
}

// Good: Single large system call
write(fd, data, 1000);  // 1 system call

2. Use Buffered I/O

cpp
// Bad: Direct system calls
int fd = open("file.txt", O_RDONLY);
char buffer[1];
for (int i = 0; i < 1000; i++) {
    read(fd, buffer, 1);  // 1000 system calls
}

// Good: Buffered I/O
FILE* file = fopen("file.txt", "r");
char buffer[1024];
fread(buffer, 1, 1024, file);  // Fewer system calls

3. Memory-Mapped Files

Memory mapping is a technique that allows a process to access a file as if it were a memory region. This is done by creating a memory mapping of the file and then accessing the file through the memory. This is more efficient than reading the file block by blockBy default, files are read in small chunk sizes for eg. 4096 bytes. Each chunk read is a system call..

cpp
// Bad: Multiple read() calls
int fd = open("large_file.dat", O_RDONLY);
char buffer[4096];
for (int i = 0; i < file_size; i += 4096) {
    read(fd, buffer, 4096);  // Many system calls
}

// Good: Memory mapping
int fd = open("large_file.dat", O_RDONLY);
char* data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
// Access data directly - no system calls needed

Context Switching Deep Dive

What is Context Switching?

Context switching is the process of saving the current process's state and loading another process's state. This allows the CPU to switch between multiple processes, giving the illusion of concurrent execution.

Context Switch Process

cpp
Process A Running → Interrupt/Scheduler → Save Context A → Load Context B → Process B Running

What Gets Saved During Context Switch

cpp
// Process context (simplified)
struct task_context {
    // CPU registers
    unsigned long rax, rbx, rcx, rdx;
    unsigned long rsi, rdi, rbp, rsp;
    unsigned long r8, r9, r10, r11, r12, r13, r14, r15;
    unsigned long rip;  // Instruction pointer
    unsigned long rflags;  // Status flags

    // Memory management
    unsigned long cr3;  // Page table base

    // Floating point state
    struct fpu_state fpu;

    // Other process-specific data
    unsigned long fs, gs;  // Segment registers
};

Context Switch Implementation

cpp
// Simplified context switch (x86_64)
void switch_to(struct task_struct *next) {
    struct task_struct *prev = current;

    // 1. Save current process context
    save_context(&prev->context);

    // 2. Update current process pointer
    current = next;

    // 3. Switch memory space (page tables)
    switch_mm(next->mm);

    // 4. Load new process context
    load_context(&next->context);

    // 5. Resume execution of new process
    return_to_user();
}

Context Switch Overhead

Context switching is expensive due to:

  1. CPU register save/restore: ~100-200 CPU cycles
  2. Memory management: Page table switching
  3. Cache invalidation: CPU cache becomes invalid
  4. Scheduler overhead: Finding next process to run

Typical overhead: 1-30 microseconds depending on hardware

Key Concepts Summary

System Calls

  • Purpose: Interface between user and kernel space
  • Overhead: 100-1000 CPU cycles per call
  • Optimization: Batch operations, buffered I/O, memory mapping

Context Switching

  • Purpose: Switch between processes
  • Overhead: 1-30 microseconds
  • Components: Register save/restore, memory switching, cache invalidation

Performance Considerations

  1. Reduce System Calls: Batch operations, use buffered I/O
  2. Optimize Interrupt Handling: Keep handlers short and efficient
  3. Use Polling for High-Frequency Events: Avoid interrupt overhead
  4. CPU Affinity: Keep processes on same CPU to reduce cache misses