Managing Process Lifecycle and Execution Control in Linux

Forking Processes for Distinct Tasks

fork() creates a child process duplicating the parent's address space. While basic usage was covered elsewhere, it enables two primary patterns:

  • A parent duplicates itself so both execute different code paths—for example, the parent waits for network requests while the child handles them.
  • A process replaces its image with another program via exec* functions immediately after forking.

Process Termination Scenarios

A process ends in one of three ways:

  1. Normal completion with correct results.
  2. Normal completion with incorrect results.
  3. Abnormal termination due to faults.

The main function's return value serves as the process exit status. In a shell, echo $? reveals the last foreground process's exit code.

#include <stdio.h>
int main() {
    printf("hello world\n");
    return 20;
}

Running this yields an exit status of 20. Invoking echo $? afterward typically shows 0 because echo itself exits nomrally.

Non-zero codes indicate failure. For instance, ls -e (invalid option) may yield status 2. Exit codes allow parents to diagnose child outcomes. By convention, 0 means success; other values map to specific error conditions.

The strerror() function maps numeric codes to human-readable descriptions. Not all numbers correspond to standard errors—some arise from abnormal terminations like segmentation faults.

Abnormal exits leave meaningless codes; they merely signal that the process did not follow normal control flow.

Common Ways to Exit

  1. Returning from main.
  2. Calling exit().
  3. Calling _exit().

exit() flushes I/O buffers, runs cleanup handlers registered via atexit(), then invokes _exit(). _exit() terminates immediately without buffer flushing.

On termintaion, the OS reclaims the PCB, memory structures, page tables, and associated resources.

Waiting for Child Processes

After fork(), parent and child may finish in any order. To retrieve a child's exit info and avoid zombie states, the parent should invoke wait() or waitpid().

Zombies retain system resources until reaped. Waiting ensures:

  • Parent learns child's outcome.
  • Proper sequencing: child exits before parent.
  • Prevention of resource leaks.

Wait Functions

pid_t wait(int *wstatus);

Returns the PID of the waited-for child, or -1 on error. If wstatus is non-NULL, it receives exit details.

pid_t waitpid(pid_t kid_id, int *wstatus, int opts);

kid_id specifies which child:

  • -1: any child.
  • >0: specific PID.

opts can be 0 (blocking wait) or WNOHANG (non-blocking). Non-blocking mode lets the parent perform other tasks while periodically checking if the child has exited.

wstatus encodes:

  • Normal exit: bits 8–15 hold exit code.
  • Signal-induced exit: bits 0–6 hold terminating signal number.

Helpers:

  • WIFEXITED(wstatus): true if child exited normally.
  • WEXITSTATUS(wstatus): extracts exit code when WIFEXITED is true.

Shells like bash act as parents to commands and use waiting to capture their exit statuses.

Replacing a Process Image

To run a new program in a child, use an exec* family function. These replace the calling process’s code and data with the new program, preserving PID.

#include <unistd.h>
#include <stdio.h>
int main() {
    printf("before replacement\n");
    execl("/bin/ls", "ls", "-l", "-a", NULL);
    printf("this won't print if exec succeeds\n");
    return 1;
}

Upon successful execl, execution continues from the new program's entry point; subsequent lines are skipped. Failure returns -1.

In a fork-exec pattern, the parent and child diverge once replacement occurs due to copy-on-write semantics.

Variants of Exec

  • execl(path, arg0, arg1, ..., NULL): list arguments.
  • execv(path, argv_array): vector (array) of arguments.
  • execlp(file, arg0, ...): searches PATH automatically.
  • execvp(file, argv_array): execv + PATH search.
  • execle(path, arg0, ..., envp): supplies custom environment.
  • execvpe(file, argv_array, envp): vector args + PATH + custom env.

Suffix meanings:

  • l: list (variadic args).
  • v: vector (array of args).
  • p: path search via PATH.
  • e: explicit environment array.

All these are frontends to the underlying execve() syscall.

Successful exec* calls never return; failure returns -1.

Building a Minimal Shell

A simple command-line interpreter can be structured as:

  1. Print prompt.
  2. Read input line.
  3. Parse into command and arguments.
  4. Handle built-in commands in the parent (e.g., cd).
  5. Fork and exec external programs in the child.

Example outline:

while (1) {
    printf("[user@host dir]$ ");
    fflush(stdout);
    // read input into cmd_buf
    // parse cmd_buf into argv[]
    if (is_builtin(cmd_buf)) {
        handle_builtin(cmd_buf);
    } else {
        pid_t cpid = fork();
        if (cpid == 0) {
            execvp(argv[0], argv);
            perror("exec failed");
            _exit(1);
        } else {
            waitpid(cpid, NULL, 0);
        }
    }
}

Built-ins like cd must run in the parent since changing directories affects the current process's environment; otherwise, a child's chdir would vanish when it exits.

Distinguishing built-ins from external commands ensures correct behavior for navigation and environment manipulation.

Tags: Linux process control fork exec wait

Posted on Sun, 17 May 2026 01:42:37 +0000 by jawa