The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Process Management

The Foundation of Multitasking in Linux
Understanding how the operating system manages multiple programs and coordinates their execution


📋 Table of Contents


🏗️ Process Fundamentals

What is a Process?

A process is the fundamental unit of execution in Linux—it’s a running instance of a program that has its own memory space, execution context, and system resources. Think of a process as a “container” that holds everything needed to run a program: code, data, stack, heap, file descriptors, and more.

The Process Abstraction:

Process vs. Program: Understanding the Distinction

The relationship between programs and processes is fundamental to understanding how Linux works:

Program (Static):

Process (Dynamic):

Program File (on disk)
       │
       ▼
   Load into Memory
       │
       ▼
   Create Process
       │
       ▼
   Allocate Resources
       │
       ▼
   Begin Execution

Process Memory Layout

Every process has a well-defined memory layout that the kernel manages:

┌─────────────────────────────────────┐
│         Stack                       │ ← Grows downward
│  (local variables, function calls) │   - Function call frames
│                                    │   - Local variables
│                                    │   - Return addresses
├─────────────────────────────────────┤
│         ↑                          │
│         │                          │
│         │                          │
│         │                          │
│         │                          │
├─────────────────────────────────────┤
│         Heap                       │ ← Grows upward
│     (dynamic allocations)          │   - malloc() allocations
│                                    │   - Dynamic data structures
├─────────────────────────────────────┤
│        Global/Static Data          │ ← Fixed size
│      (global variables, etc.)      │   - Global variables
│                                    │   - Static variables
├─────────────────────────────────────┤
│           Code                      │ ← Read-only
│        (program instructions)      │   - Machine instructions
│                                    │   - Constants
└─────────────────────────────────────┘

Memory Management Principles:


🔄 Process Creation and Lifecycle

How Processes Come to Life

Process creation in Linux involves several sophisticated steps that transform a program file into an executing process. This process, known as “forking,” creates a copy of the parent process that can then execute different code or the same code with different data.

The Fork Philosophy

The fork() system call embodies the copy-on-write principle—it creates the illusion of copying an entire process while actually sharing most of the memory until one process modifies it. This optimization is crucial for performance in embedded systems.

Fork Design Principles:

Process Creation Flow

Parent Process
      │
      ▼
   fork() System Call
      │
      ▼
   Kernel Creates Process Descriptor
      │
      ▼
   Allocate New PID
      │
      ▼
   Copy Parent's Memory Descriptors
      │
      ▼
   Set Up Child-Specific Data
      │
      ▼
   Return to Both Processes
      │
      ▼
   Parent: Child PID, Child: 0

Fork Implementation Details:

  1. Process Descriptor: Kernel allocates a new task_struct
  2. Memory Mapping: Child gets copy of parent’s memory descriptors
  3. File Descriptors: Child inherits open files and directories
  4. Signal Handlers: Child inherits signal handling configuration
  5. Working Directory: Child inherits current working directory

Basic Process Creation Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    
    printf("Parent process starting (PID: %d)\n", getpid());
    
    // Create a child process
    pid = fork();
    
    if (pid < 0) {
        // Fork failed
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process
        printf("Child process created (PID: %d, Parent PID: %d)\n", 
               getpid(), getppid());
        
        // Child can execute different code
        printf("Child process executing...\n");
        sleep(2);
        printf("Child process finishing\n");
        exit(0);
    } else {
        // Parent process
        printf("Parent process continuing (Child PID: %d)\n", pid);
        
        // Wait for child to complete
        int status;
        wait(&status);
        printf("Child process completed with status: %d\n", WEXITSTATUS(status));
    }
    
    return 0;
}

Key Concepts in Process Creation:


⏱️ Process Scheduling

Managing CPU Time Among Processes

Process scheduling is the mechanism by which the Linux kernel determines which process should run on the CPU at any given time. The scheduler must balance several competing goals: fairness, responsiveness, throughput, and resource utilization.

Scheduling Philosophy

Linux scheduling follows the fairness principle—all processes should get a reasonable share of CPU time while maintaining system responsiveness. The scheduler adapts to different types of workloads and system requirements.

Scheduling Goals:

Linux Scheduler Architecture

The Linux scheduler operates on multiple levels:

┌─────────────────────────────────────┐
│         User Processes              │ ← User space
├─────────────────────────────────────┤
│         System Call Interface      │ ← Boundary
├─────────────────────────────────────┤
│         Scheduler Core             │ ← Kernel space
│         (CFS - Completely Fair)    │
├─────────────────────────────────────┤
│         CPU Scheduler              │ ← Hardware level
│         (Run queue management)     │
└─────────────────────────────────────┘

Scheduler Components:

Scheduling Policies

Linux supports several scheduling policies, each designed for specific use cases:

SCHED_OTHER (Normal Scheduling):

SCHED_FIFO (Real-time First-In-First-Out):

SCHED_RR (Real-time Round-Robin):

Scheduling Policy Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/resource.h>

int main() {
    int policy;
    struct sched_param param;
    
    // Get current scheduling policy
    policy = sched_getscheduler(0);
    printf("Current scheduling policy: ");
    
    switch (policy) {
        case SCHED_OTHER:
            printf("SCHED_OTHER (normal)\n");
            break;
        case SCHED_FIFO:
            printf("SCHED_FIFO (real-time)\n");
            break;
        case SCHED_RR:
            printf("SCHED_RR (round-robin real-time)\n");
            break;
        default:
            printf("Unknown\n");
    }
    
    // Get current priority
    if (sched_getparam(0, &param) == 0) {
        printf("Current priority: %d\n", param.sched_priority);
    }
    
    // Set real-time scheduling policy (requires root privileges)
    param.sched_priority = 50;
    if (sched_setscheduler(0, SCHED_FIFO, &param) == 0) {
        printf("Successfully set to SCHED_FIFO with priority 50\n");
    } else {
        printf("Failed to set real-time scheduling (may need root privileges)\n");
    }
    
    return 0;
}

Scheduling Priority Management:


📡 Inter-Process Communication

Sharing Data and Coordinating Execution

Inter-process communication (IPC) mechanisms allow processes to exchange data, synchronize their execution, and coordinate access to shared resources. Linux provides several IPC mechanisms, each designed for specific use cases and performance requirements.

IPC Design Philosophy

IPC mechanisms follow the abstraction principle—they provide simple, consistent interfaces that hide the complexity of inter-process communication. The choice of mechanism depends on the specific requirements of the application.

IPC Selection Criteria:

IPC Mechanism Overview

┌─────────────────────────────────────┐
│         User Applications           │
├─────────────────────────────────────┤
│         IPC Mechanisms              │
│  ┌─────────┬─────────┬─────────┐   │
│  │  Pipes  │ Shared  │Message │   │
│  │         │ Memory  │Queues  │   │
│  └─────────┴─────────┴─────────┘   │
├─────────────────────────────────────┤
│         Kernel Support              │
│  (System calls, memory management) │
└─────────────────────────────────────┘

IPC Mechanism Types:

Pipes: Simple Data Flow

Pipes provide the simplest form of IPC, creating a unidirectional communication channel between related processes:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[256];
    
    // Create a pipe
    if (pipe(pipefd) == -1) {
        perror("Pipe creation failed");
        exit(1);
    }
    
    pid = fork();
    
    if (pid < 0) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process - writes to pipe
        close(pipefd[0]); // Close read end
        
        const char *message = "Hello from child process!";
        write(pipefd[1], message, strlen(message) + 1);
        close(pipefd[1]);
        
        printf("Child sent message\n");
        exit(0);
    } else {
        // Parent process - reads from pipe
        close(pipefd[1]); // Close write end
        
        int bytes_read = read(pipefd[0], buffer, sizeof(buffer));
        if (bytes_read > 0) {
            printf("Parent received: %s\n", buffer);
        }
        
        close(pipefd[0]);
        wait(NULL);
    }
    
    return 0;
}

Pipe Characteristics:

Shared Memory: High-Performance Data Sharing

Shared memory provides the fastest IPC mechanism by allowing multiple processes to access the same region of physical memory:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <string.h>

int main() {
    key_t key = ftok("/tmp", 'A');
    int shmid;
    char *shared_memory;
    
    // Create shared memory segment
    shmid = shmget(key, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("Shared memory creation failed");
        exit(1);
    }
    
    // Attach shared memory to process address space
    shared_memory = shmat(shmid, NULL, 0);
    if (shared_memory == (char *)-1) {
        perror("Shared memory attachment failed");
        exit(1);
    }
    
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process - writes to shared memory
        strcpy(shared_memory, "Hello from child via shared memory!");
        printf("Child wrote to shared memory\n");
        
        // Detach shared memory
        shmdt(shared_memory);
        exit(0);
    } else {
        // Parent process - reads from shared memory
        wait(NULL);
        
        printf("Parent read from shared memory: %s\n", shared_memory);
        
        // Detach shared memory
        shmdt(shared_memory);
        
        // Remove shared memory segment
        shmctl(shmid, IPC_RMID, NULL);
    }
    
    return 0;
}

Shared Memory Characteristics:


🔒 Process Synchronization

Coordinating Access to Shared Resources

Process synchronization mechanisms ensure that multiple processes can coordinate their execution and access shared resources safely. Linux provides several synchronization primitives that can be used across process boundaries.

Synchronization Philosophy

Synchronization follows the safety principle—ensure that shared resources are accessed safely while maintaining system performance and avoiding deadlocks.

Synchronization Goals:

Synchronization Mechanisms

Linux provides several IPC-based synchronization mechanisms:

Semaphores:

File Locks:

Condition Variables:

Semaphore Implementation Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

int main() {
    key_t key = ftok("/tmp", 'C');
    int semid;
    
    // Create semaphore set with one semaphore
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("Semaphore creation failed");
        exit(1);
    }
    
    // Initialize semaphore to 1 (binary semaphore for mutual exclusion)
    union semun {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
    } argument;
    
    argument.val = 1;
    if (semctl(semid, 0, SETVAL, argument) == -1) {
        perror("Semaphore initialization failed");
        exit(1);
    }
    
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process
        struct sembuf operation = {0, -1, 0}; // Wait (decrement)
        
        printf("Child waiting for semaphore...\n");
        if (semop(semid, &operation, 1) == -1) {
            perror("Child: Semaphore wait failed");
            exit(1);
        }
        
        printf("Child acquired semaphore\n");
        sleep(2);
        
        operation.sem_op = 1; // Signal (increment)
        if (semop(semid, &operation, 1) == -1) {
            perror("Child: Semaphore signal failed");
            exit(1);
        }
        
        printf("Child released semaphore\n");
        exit(0);
    } else {
        // Parent process
        struct sembuf operation = {0, -1, 0}; // Wait (decrement)
        
        printf("Parent waiting for semaphore...\n");
        if (semop(semid, &operation, 1) == -1) {
            perror("Parent: Semaphore wait failed");
            exit(1);
        }
        
        printf("Parent acquired semaphore\n");
        sleep(1);
        
        operation.sem_op = 1; // Signal (increment)
        if (semop(semid, &operation, 1) == -1) {
            perror("Parent: Semaphore signal failed");
            exit(1);
        }
        
        printf("Parent released semaphore\n");
        wait(NULL);
        
        // Remove semaphore set
        semctl(semid, 0, IPC_RMID);
    }
    
    return 0;
}

Semaphore Operations:


🔄 Process States and Transitions

Understanding the Process Lifecycle

Processes in Linux can exist in several states, each representing a different phase of their lifecycle. Understanding these states is crucial for effective process management and debugging.

Process State Philosophy

Process states represent the resource availability model—processes move between states based on their resource requirements and system availability. The kernel manages these transitions to optimize system performance.

State Management Goals:

Process State Diagram

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Created   │───▶│  Runnable   │───▶│   Running   │
│   (New)     │    │ (Ready)     │    │ (Active)    │
└─────────────┘    └─────────────┘    └─────────────┘
                           ▲                │
                           │                ▼
                    ┌─────────────┐    ┌─────────────┐
                    │  Sleeping   │◀───│  Blocked    │
                    │ (Waiting)   │    │ (I/O, etc.) │
                    └─────────────┘    └─────────────┘
                           │                │
                           ▼                ▼
                    ┌─────────────┐    ┌─────────────┐
                    │   Stopped   │    │   Zombie    │
                    │(Suspended)  │    │ (Exited)    │
                    └─────────────┘    └─────────────┘

Process States Explained:

State Transition Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void signal_handler(int sig) {
    if (sig == SIGUSR1) {
        printf("Process %d received SIGUSR1\n", getpid());
    }
}

int main() {
    signal(SIGUSR1, signal_handler);
    
    printf("Parent process (PID: %d) starting\n", getpid());
    
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process
        printf("Child process (PID: %d) created\n", getpid());
        
        // Child waits for signal (Sleeping state)
        printf("Child waiting for signal...\n");
        pause();
        
        printf("Child continuing after signal\n");
        exit(0);
    } else {
        // Parent process
        printf("Parent continuing (Child PID: %d)\n", pid);
        
        sleep(1);
        
        // Send signal to child (wakes it from Sleeping state)
        printf("Parent sending SIGUSR1 to child\n");
        kill(pid, SIGUSR1);
        
        // Wait for child to complete
        int status;
        wait(&status);
        printf("Child completed with status: %d\n", WEXITSTATUS(status));
    }
    
    return 0;
}

State Transition Triggers:


🚀 Advanced Process Management

Beyond Basic Process Operations

Advanced process management involves sophisticated techniques for monitoring, controlling, and optimizing process behavior. These techniques are essential for building robust, high-performance embedded systems.

Process Monitoring Philosophy

Process monitoring follows the observability principle—make system behavior visible and understandable so that problems can be identified and resolved quickly.

Monitoring Goals:

Process Information Gathering

Linux provides several mechanisms for gathering process information:

/proc Filesystem:

System Calls:

Library Functions:

Process Control Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/resource.h>

void print_process_info(const char *label) {
    printf("\n=== %s ===\n", label);
    printf("PID: %d\n", getpid());
    printf("Parent PID: %d\n", getppid());
    printf("User ID: %d\n", getuid());
    printf("Group ID: %d\n", getgid());
    
    // Get process priority
    int priority = getpriority(PRIO_PROCESS, 0);
    printf("Priority: %d\n", priority);
    
    // Get resource usage
    struct rusage usage;
    if (getrusage(RUSAGE_SELF, &usage) == 0) {
        printf("User CPU time: %ld.%06ld seconds\n", 
               usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);
        printf("System CPU time: %ld.%06ld seconds\n", 
               usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
        printf("Page faults: %ld\n", usage.ru_majflt);
    }
}

void signal_handler(int sig) {
    printf("Process %d received signal %d\n", getpid(), sig);
    
    if (sig == SIGUSR1) {
        printf("Continuing execution...\n");
    } else if (sig == SIGTERM) {
        printf("Terminating gracefully...\n");
        exit(0);
    }
}

int main() {
    // Set up signal handlers
    signal(SIGUSR1, signal_handler);
    signal(SIGTERM, signal_handler);
    
    print_process_info("Parent Process");
    
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // Child process
        print_process_info("Child Process");
        
        // Set different priority
        setpriority(PRIO_PROCESS, 0, 10);
        printf("Child set priority to 10\n");
        
        // Wait for signals
        while (1) {
            printf("Child waiting for signals...\n");
            sleep(5);
        }
    } else {
        // Parent process
        printf("Parent continuing (Child PID: %d)\n", pid);
        
        sleep(2);
        
        // Send signals to child
        printf("Parent sending SIGUSR1 to child\n");
        kill(pid, SIGUSR1);
        
        sleep(2);
        
        printf("Parent sending SIGTERM to child\n");
        kill(pid, SIGTERM);
        
        // Wait for child to terminate
        int status;
        wait(&status);
        printf("Child terminated with status: %d\n", WEXITSTATUS(status));
    }
    
    return 0;
}

Advanced Process Control Features:


🎯 Conclusion

Process management in Linux provides a sophisticated and flexible system for creating, scheduling, and coordinating multiple processes. The system balances performance, resource efficiency, and system stability while providing powerful IPC mechanisms for process communication and synchronization.

Key Takeaways:

The Path Forward:

As embedded systems become more complex and require more sophisticated multitasking capabilities, the importance of understanding process management will only increase. Linux continues to evolve its process management system, providing new features and optimizations that enable more powerful and efficient embedded applications.

The future of process management lies in the development of more sophisticated scheduling algorithms, better resource management, and more efficient IPC mechanisms. By embracing these developments and applying process management principles systematically, developers can build embedded systems that effectively utilize the operating system’s multitasking capabilities while maintaining system stability and performance.

Remember: Process management is not just about creating processes—it’s about understanding how processes interact, communicate, and compete for resources. The skills you develop here will serve you throughout your embedded systems career, enabling you to build robust, efficient, and maintainable systems.