Linux Process Lifecycle: Creation, Termination, Waiting, and Replacement

Process Creation with fork()

The fork() system call is the primary mechanism for creating new processes in Linux. Defined in <unistd.h>, it creates an exact duplicate of the calling process.

Return Values and Process Branching

Upon successful execution, fork() returns twice: once in the parent process with the child's Process ID (PID), and once in the child process with a value of 0. A return value of -1 indicates failure.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t process_id = fork();
    
    if (process_id < 0) {
        perror("Fork failed");
        return 1;
    } else if (process_id == 0) {
        printf("Child process - PID: %d, Parent PID: %d\n", 
               getpid(), getppid());
    } else {
        printf("Parent process - PID: %d, Child PID: %d\n", 
               getpid(), process_id);
    }
    return 0;
}

Internal Mechanics of fork()

When a process invokes fork(), the kernel performs several operations:

  1. Allocates a new Process Control Block (PCB) and memory structures for the child.
  2. Copies parent process attributes to the child, utilizing Copy-On-Write (COW) optimization.
  3. Inserts the child into the scheduler's run queue.
  4. Returns control to both processes for scheduling.

Copy-On-Write Optimization

Modern Linux kernels implement COW to enhance memory efficiency. Initially, parent and child share physical memory pages. When either process attempts to modify a shared page, the kernel allocates a new physical page, copies the data, and updates the page table mappings. This defers memory allocation until modification is absolutely necessary.

Why Different Return Values?

A parent can have multiple children, requiring unique identifiers to track each one. Conversely, each child has exactly one parent, eliminating the need for special identification. Thus, the parent receives the child's PID for management, while the child receives 0 to distinguish itself.

Process Termination

Normal Termination Scenarios

Processes can terminate normally through:

  • Returning from the main() function.
  • Calling exit() or _exit().

The exit code (0 for success, non-zero for errors) can be retrieved using echo $? in the shell.

exit() vs _exit()

The exit() function performs cleanup before termination:

  • Invokes functions registered with atexit().
  • Flushes standard I/O buffers.
  • Calls _exit() internally.

In contrast, _exit() terminates immediately without cleanup:

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

int main() {
    printf("Testing buffer behavior");
    // exit() would print the message; _exit() may not
    _exit(0);
}

Abnormal Termination

Processes terminate abnormally when receiving signals such as SIGSEGV (segmentation fault) or SIGKILL. Exit codes hold no meaning in abnormal termination contexts.

Process Waiting

The Necessity of Waiting

When a child process terminates, it becomes a zombie process, retaining its PCB for the parent to collect. Failing to wait creates resource leaks. Zombie processes resist termination signals because they are already dead.

wait() and waitpid() System Calls

The wait() function blocks until any child terminates:

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

int main() {
    pid_t child_pid = fork();
    
    if (child_pid == 0) {
        printf("Child executing...\n");
        sleep(2);
        exit(5);
    } else {
        int exit_status;
        pid_t terminated = wait(&exit_status);
        
        if (terminated > 0) {
            if (WIFEXITED(exit_status)) {
                printf("Child exited with code: %d\n", 
                       WEXITSTATUS(exit_status));
            } else if (WIFSIGNALED(exit_status)) {
                printf("Child killed by signal: %d\n", 
                       WTERMSIG(exit_status));
            }
        }
    }
    return 0;
}

Decoding the Status Integer

The status parameter encodes termination information in its lower 16 bits:

  • Bits 0-6: Termination signal (non-zero indicates abnormal termination).
  • Bit 7: Core dump flag.
  • Bits 8-15: Exit code (valid only for normal termination).

Macros simplify status interpretation:

  • WIFEXITED(status): True if the child exited normally.
  • WEXITSTATUS(status): Extracts the exit code.
  • WIFSIGNALED(status): True if the child was killed by a signal.
  • WTERMSIG(status): Extracts the signal number.

Blocking vs Non-Blocking Waits

The waitpid() function offers more control:

pid_t waitpid(pid_t pid, int *status, int options);

The options parameter determines behavior:

  • 0: Blocking wait (suspends parent until child exits).
  • WNOHANG: Non-blocking (returns immediately; 0 if child still running).

Non-blocking polling example:

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

int main() {
    pid_t child_id = fork();
    
    if (child_id == 0) {
        sleep(5);
        exit(10);
    }
    
    int status;
    pid_t result;
    
    while (1) {
        result = waitpid(child_id, &status, WNOHANG);
        
        if (result == 0) {
            printf("Child still running...\n");
            sleep(1);
        } else if (result == child_id) {
            if (WIFEXITED(status)) {
                printf("Child exit code: %d\n", WEXITSTATUS(status));
            }
            break;
        } else {
            perror("waitpid error");
            break;
        }
    }
    return 0;
}

Process Replacement with exec Family

Concept of Process Replacement

The exec family replaces the current process image with a new program. The process ID remains unchanged; only the code, data, heap, and stack are replaced.

exec Function Variants

#include <unistd.h>

int execl(const char *pathname, const char *arg, ... /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

Naming Conventions

  • l (list): Arguments passed as individual parameters.
  • v (vector): Arguments passed as an array.
  • p (path): Searches for the executable in PATH.
  • e (environment): Accepts custom environment variables.

Practical Examples

Using execl with full path:

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

int main() {
    printf("Before exec...\n");
    execl("/bin/ls", "ls", "-la", NULL);
    // This line only executes if exec fails
    perror("exec failed");
    return 1;
}

Using execvp with PATH search:

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

int main() {
    char *args[] = {"grep", "pattern", "file.txt", NULL};
    execvp("grep", args);
    perror("exec failed");
    return 1;
}

Custom Environment Variables

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

int main() {
    char *env_vars[] = {"USER=custom_user", "PATH=/bin", NULL};
    char *args[] = {"env", NULL};
    
    execve("/usr/bin/env", args, env_vars);
    perror("exec failed");
    return 1;
}

Implementing a Simple Shell

A basic shell implementation combines fork, exec, and wait:

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

#define MAX_INPUT 256
#define MAX_ARGS 64

void parse_input(char *input, char **arguments) {
    int index = 0;
    char *token = strtok(input, " \t\n");
    
    while (token != NULL && index < MAX_ARGS - 1) {
        arguments[index++] = token;
        token = strtok(NULL, " \t\n");
    }
    arguments[index] = NULL;
}

int main() {
    char user_input[MAX_INPUT];
    char *cmd_args[MAX_ARGS];
    
    while (1) {
        printf("myshell> ");
        fflush(stdout);
        
        if (fgets(user_input, MAX_INPUT, stdin) == NULL) {
            break;
        }
        
        if (strlen(user_input) <= 1) continue;
        
        parse_input(user_input, cmd_args);
        
        if (strcmp(cmd_args[0], "exit") == 0) {
            break;
        }
        
        pid_t child_proc = fork();
        
        if (child_proc == 0) {
            execvp(cmd_args[0], cmd_args);
            perror("Command not found");
            exit(127);
        } else if (child_proc > 0) {
            int exit_info;
            waitpid(child_proc, &exit_info, 0);
            
            if (WIFEXITED(exit_info) && WEXITSTATUS(exit_info) != 0) {
                fprintf(stderr, "Command exited with status %d\n", 
                        WEXITSTATUS(exit_info));
            }
        }
    }
    
    return 0;
}

This shell reads commands, creates child processes for execution, and waits for completion. The child process is replaced by the command using execvp, while the parent waits to collect the exit status.

Tags: Linux Process Management System Programming fork exec

Posted on Sat, 05 Sep 2026 16:31:50 +0000 by gigantorTRON