The Four Core System Calls

The Four Core System Calls
Topic 1 → Subtopic 2  |  fork() exit() wait() execve()
4
Syscalls
Topic 1
Subtopic 2
3
Code Examples

The Building Blocks of Process Management

Linux process management revolves around four system calls. Think of them as a lifecycle: a process is born (fork), may transform (execve), does its work, and eventually dies (exit). The parent oversees this with wait. Understanding each call in detail is essential for any systems programmer.

Keywords:

fork() exit(status) wait(&status) execve() termination status process image argv envp SIGCHLD

🔁 Process Lifecycle Diagram

Here is how the four system calls fit together in the process lifecycle:

Parent Process Running Program A

fork()

← PARENT
Continues executing
Program A
wait(&status) (optional)
Reads child’s exit status
and continues

CHILD →
Copy of parent
running Program A
execve(B,…) (optional)
Now running Program B
(or still A)
exit(status)

🔔 Kernel sends SIGCHLD to parent when child exits (default: ignored)

① fork() — Birth of a New Process
#include <unistd.h>
pid_t fork(void);
/* Parent gets: child's PID (positive)
   Child gets : 0
   Error      : -1                      */
  • Creates an almost exact duplicate of the calling process
  • Child gets copies of stack, data, heap; text is shared (read-only)
  • Both processes resume from the instruction after fork()
  • Takes no arguments — all state is inherited automatically
  • Child can call getppid() to get parent’s PID

② exit(status) — Terminating Cleanly
#include <stdlib.h>
void exit(int status);

/* status is 0-255. Convention:
   0          = success
   Non-zero   = failure (specific error code)
   EXIT_SUCCESS = 0  (defined in stdlib.h)
   EXIT_FAILURE = 1  (defined in stdlib.h) */

What exit() does, step by step:

Step Action
1 Runs functions registered with atexit()
2 Flushes and closes all open stdio streams
3 Removes temporary files created with tmpfile()
4 Calls the kernel’s _exit() system call
5 Kernel releases all process resources
6 Sends SIGCHLD to parent; process becomes zombie until parent calls wait()

③ wait(&status) — Collecting the Child
#include <sys/wait.h>
pid_t wait(int *status);
/* Returns PID of terminated child, or -1 on error.
   Pass NULL if you don't care about exit status.   */

Decoding the status value with macros:

int status;
pid_t pid = wait(&status);

if (WIFEXITED(status)) {
    /* Child called exit() or returned from main() */
    printf("Exit code: %d\n", WEXITSTATUS(status));

} else if (WIFSIGNALED(status)) {
    /* Child was killed by a signal */
    printf("Killed by signal: %d\n", WTERMSIG(status));

} else if (WIFSTOPPED(status)) {
    /* Child was stopped (e.g., by SIGSTOP) */
    printf("Stopped by signal: %d\n", WSTOPSIG(status));
}
Two purposes of wait(): (1) Blocks parent until a child terminates. (2) Returns the child’s termination status. Without wait(), the child becomes a zombie process.

④ execve() — Running a New Program
#include <unistd.h>
int execve(const char *pathname, char *const argv[],
           char *const envp[]);
/* Returns: -1 on error. On SUCCESS it never returns.
   pathname : full path to the executable
   argv[]   : argument list (argv[0] = program name, NULL terminated)
   envp[]   : environment variables (NULL terminated)             */
  • Completely replaces the current process: code, stack, heap, data
  • The PID remains the same
  • Open file descriptors are preserved (unless close-on-exec is set)
  • If execve() returns, it failed — always check the return
  • Wrapper functions: execl(), execlp(), execle(), execv(), execvp()

💻 Code Example 1: All Four Syscalls Together
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid;
    int status;

    /* Step 1: fork() - create child */
    pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /* CHILD */
        printf("[Child PID=%d] About to exec /bin/date\n", getpid());

        /* Step 2: execve() - run a new program */
        char *argv[] = { "date", NULL };
        char *envp[] = { NULL };
        execve("/bin/date", argv, envp);

        /* Only reached if execve fails */
        perror("execve");
        exit(EXIT_FAILURE);    /* Step 3a: exit() on failure */

    } else {
        /* PARENT */
        printf("[Parent PID=%d] Waiting for child...\n", getpid());

        /* Step 4: wait() - collect child */
        wait(&status);

        if (WIFEXITED(status))
            printf("[Parent] Child exited with code %d\n",
                   WEXITSTATUS(status));
        /* Step 3b: parent exits */
        exit(EXIT_SUCCESS);
    }
}
Output: The parent prints its message, the child runs /bin/date (prints current date/time), then the parent collects the exit status.

💻 Code Example 2: atexit() and exit() Cleanup
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* Cleanup function registered with atexit() */
void cleanup(void)
{
    printf("[PID=%d] cleanup() called by exit()\n", getpid());
}

int main(void)
{
    /* Register cleanup function */
    if (atexit(cleanup) != 0) {
        fprintf(stderr, "atexit failed\n");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Child: exit() WILL call cleanup */
        printf("[CHILD] Calling exit()\n");
        exit(0);
        /* cleanup() is called here automatically */
    } else {
        /* Parent: also calls exit() eventually */
        wait(NULL);
        printf("[PARENT] Calling exit()\n");
        exit(0);
        /* cleanup() is called here too */
    }
}
Note: Both parent and child call cleanup() because both call exit(). This is why after fork(), children should call _exit() (not exit()) to avoid running the parent’s cleanup functions twice.

💻 Code Example 3: exec() Family of Wrappers

Besides execve(), there are several convenient wrappers:

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

/* Helper to fork + exec a command */
void run_command(const char *msg, int use_which)
{
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return; }

    if (pid == 0) {
        printf("[Child] %s\n", msg);
        switch (use_which) {
        case 1:
            /* execl: list arguments directly */
            execl("/bin/echo", "echo", "Hello from execl", NULL);
            break;
        case 2:
            /* execlp: searches PATH, no full path needed */
            execlp("echo", "echo", "Hello from execlp", NULL);
            break;
        case 3: {
            /* execvp: array of args, searches PATH */
            char *args[] = { "echo", "Hello from execvp", NULL };
            execvp("echo", args);
            break;
        }
        }
        perror("exec failed");
        exit(EXIT_FAILURE);
    } else {
        wait(NULL);
    }
}

int main(void)
{
    run_command("using execl",  1);
    run_command("using execlp", 2);
    run_command("using execvp", 3);
    return 0;
}
Function Args style Searches PATH? Custom env?
execve array No Yes (envp)
execl list No No
execlp list Yes No
execle list No Yes (envp)
execv array No No
execvp array Yes No

🅾 Interview Questions
Q1: What is the difference between exit() and _exit()?

exit() is a C library function: it flushes stdio buffers, runs atexit() handlers, then calls _exit(). _exit() is a direct system call: terminates immediately, no flushing. After fork(), the child should use _exit() to avoid double-flushing the parent’s stdio buffers.

Q2: What does WEXITSTATUS(status) return?

It extracts the exit code (0–255) from the status integer returned by wait(). You should only call it after confirming WIFEXITED(status) is true, otherwise the result is undefined.

Q3: Does execve() return on success?

No. On success, execve() completely replaces the process image and never returns. Any code after execve() only executes if execve() failed. Always check the return and call perror() after execve() in production code.

Q4: What is SIGCHLD and when is it sent?

SIGCHLD is sent by the kernel to the parent process when one of its children changes state: exits, is stopped, or is continued. By default it is ignored. Servers often install a SIGCHLD handler that calls wait() inside it to reap children without blocking.

Q5: What is the difference between execve() and execvp()?

execve() requires the full path to the executable and a custom environment array. execvp() searches the PATH environment variable to find the executable (so you can just pass “ls” instead of “/bin/ls”) and inherits the current environment.

Series Navigation
Topic 1 → Subtopic 2 of 3

← Previous Next → Index

Leave a Reply

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