Process Management System Calls in Linux

In the previous parts of this series, we covered system call basics and file I/O. Now it’s time to explore one of the most powerful aspects of Unix-like systems: process management.

Processes are the execution units of programs. Linux provides a set of system calls to create, replace, synchronize and terminate processes. Understanding these is essential for writing multitasking programs, shells, servers and operating system tools.

1. What is a Process?

A process is an instance of a running program. Each process has:

  • A process ID (PID)
  • Its own memory space (stack, heap, text, data)
  • File descriptors
  • Environment variables
  • Scheduling information

Linux uses system calls to control process life cycles: creation, execution, synchronization, and termination.

Creating Processes: fork()

The fork() system call creates a new process by duplicating the calling process.

#include <unistd.h>

pid_t fork(void);
  • On success, returns 0 to the child process.
  • Returns the child’s PID to the parent process.
  • Returns -1 on error.

Both processes continue execution from the point of the fork() call.

Example:

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

int main()
{
    pid_t pid = fork();

    if (pid < 0)
    {
        perror("fork failed");
        return 1;
    } 
    else if (pid == 0)
    {
        printf("Child process (PID = %d)\n", getpid());
    } 
    else
    {
        printf("Parent process (PID = %d, Child PID = %d)\n", getpid(), pid);
    }

    return 0;
}

3. Executing Programs: execve() and Family

fork() creates a new process, but it’s just a copy of the parent. To actually run another program, you use the exec family of system calls.

execve()

#include <unistd.h>

int execve(const char *pathname, char *const argv[], char *const envp[]);
  • pathname: path to executable
  • argv: argument vector (NULL-terminated)
  • envp: environment variables (NULL-terminated)

If successful, execve() does not return, the process image is replaced by the new program.

Example:

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

int main()
{
    char *args[] = { "/bin/ls", "-l", NULL };
    
    execve("/bin/ls", args, NULL);
    perror("execve failed"); // only reached if error occurs
    
    return 1;
}

Other exec Variants

  • execl, execp, execv, etc: convenient wrappers around execve() with slightly different argument styles.
  • Most programs use execvp() because it searches the PATH environment variable.

4. Waiting for Child Processes: wait() and waitpid()

When a child process finishes, it becomes a zombie until the parent collects its exit status with wait() or waitpid().

wait()

#include <sys/wait.h>

pid_t wait(int *wstatus);
  • Blocks until any child process terminates.
  • Returns PID of terminated child.

waitpid()

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • Can wait for a specific child (pid).
  • Supports non-blocking (WNOHANG) and stopped child processes.

Example:

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

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        printf("Child running...\n");
        _exit(42);
    } 
    else
    {
        int status;
        
        wait(&status);
        if (WIFEXITED(status))
        {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

5. Terminating Processes: exit() and _exit()

  • exit(status): cleans up stdio buffers and terminates.
  • _exit(status): lower-level, terminates immediately (used inside child after fork() if exec fails).

The status is returned to the parent through wait() or waitpid().

6. Process Relationships: Orphans and Zombies

  • Zombie process: a terminated child whose exit status has not been collected. It occupies a slot in the process table.
  • Orphan process: a child whose parent has terminated. Linux reassigns orphans to init (PID 1), which adopts them and calls wait().

Managing children properly with wait() is crucial to avoid zombies.

7. Example: A Minimal Program Launcher

This example demonstrates how fork(), execvp(), and waitpid() combine to launch commands.

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

int main()
{
    char *args[] = { "ls", "-l", NULL };

    pid_t pid = fork();
    
    if (pid == 0)
    {
        execvp(args[0], args);
        perror("exec failed");
        _exit(1);
    } 
    else
    {
        int status;
        
        waitpid(pid, &status, 0);
        if (WIFEXITED(status))
        {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

8. TL/DR

  • fork() duplicates a process.
  • execve() replaces the process image with a new program.
  • wait() and waitpid() synchronize parent and child processes.
  • exit() and _exit() terminate processes with a status code.
  • Proper process management prevents zombies and ensures system stability.

Next, in Part 4: Project – Building a Basic Linux Shell, we’ll put all of this together to create a working shell that can run commands, handle redirection and support basic piping.

Leave a Reply

Your email address will not be published. Required fields are marked *