The "Holy Bible" for embedded engineers
The Foundation of Multitasking in Linux
Understanding how the operating system manages multiple programs and coordinates their execution
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:
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
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 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() 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:
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:
task_struct#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:
fork() returns different values to parent and childProcess 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.
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:
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:
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):
#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, ¶m) == 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, ¶m) == 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 (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 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:
┌─────────────────────────────────────┐
│ User Applications │
├─────────────────────────────────────┤
│ IPC Mechanisms │
│ ┌─────────┬─────────┬─────────┐ │
│ │ Pipes │ Shared │Message │ │
│ │ │ Memory │Queues │ │
│ └─────────┴─────────┴─────────┘ │
├─────────────────────────────────────┤
│ Kernel Support │
│ (System calls, memory management) │
└─────────────────────────────────────┘
IPC Mechanism Types:
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 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 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 follows the safety principle—ensure that shared resources are accessed safely while maintaining system performance and avoiding deadlocks.
Synchronization Goals:
Linux provides several IPC-based synchronization mechanisms:
Semaphores:
File Locks:
flock(), fcntl() with F_SETLKCondition Variables:
#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:
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 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:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Created │───▶│ Runnable │───▶│ Running │
│ (New) │ │ (Ready) │ │ (Active) │
└─────────────┘ └─────────────┘ └─────────────┘
▲ │
│ ▼
┌─────────────┐ ┌─────────────┐
│ Sleeping │◀───│ Blocked │
│ (Waiting) │ │ (I/O, etc.) │
└─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Stopped │ │ Zombie │
│(Suspended) │ │ (Exited) │
└─────────────┘ └─────────────┘
Process States Explained:
#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 involves sophisticated techniques for monitoring, controlling, and optimizing process behavior. These techniques are essential for building robust, high-performance embedded systems.
Process monitoring follows the observability principle—make system behavior visible and understandable so that problems can be identified and resolved quickly.
Monitoring Goals:
Linux provides several mechanisms for gathering process information:
/proc Filesystem:
/proc/<pid>/ directoriesSystem Calls:
getpid(): Get current process IDgetppid(): Get parent process IDgetuid(): Get user IDgetgid(): Get group IDLibrary Functions:
ps command: Process status informationtop command: Real-time process monitoringstrace command: System call tracing#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:
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.