Controlling Memory Footprint with fork()

Controlling Memory Footprint with fork()
Topic 2 → Subtopic 4  |  Using fork+wait to isolate memory effects
Topic 2
Subtopic 4
Memory
Isolation
3
Code Examples

Keeping the Parent’s Memory Clean

One clever use of fork() is to run a function that might leak memory or fragment the heap, while keeping the parent’s memory completely clean. Because the child runs in its own memory space, any heap damage, leaks, or excessive allocation happens only in the child. When the child exits, all that memory is freed automatically by the kernel.

Keywords:

memory footprint memory leak heap fragmentation fork + wait pattern exit status 8-bit result IPC isolation

💡 The Core Idea

Consider this scenario: you want to call a function func(arg) that:

  • Allocates lots of memory but you don’t have its source (e.g., a third-party library)
  • May leak memory or fragment the heap
  • Returns a result, and after it returns, you want a clean heap

The solution: run func() in a child process.

Without fork() isolation
Parent calls func() directly

func() leaks 10 MB

func() returns result

Parent heap: 10 MB leaked ✗
Heap fragmentation remains
No way to clean up

With fork() isolation
Parent forks child

Child calls func() → leaks 10 MB

Child exits with result

Child’s memory freed by kernel ✓
Parent uses wait() to get result
Parent heap: completely clean ✓

🔨 The footprint.c Pattern (from TLPI)
pid_t child_pid;
int status;

child_pid = fork();
if (child_pid == -1)
    perror("fork"), exit(1);

if (child_pid == 0) {
    /* Child calls the potentially-leaky function */
    /* exit() uses the return value as exit status */
    exit(func(arg));
}

/* Parent waits; gets result via exit status */
if (wait(&status) == -1)
    perror("wait"), exit(1);

/* Decode result from exit status */
int result = WEXITSTATUS(status);  /* 0-255 only! */
Limitation: The exit status is only 8 bits (0–255). For larger return values, use pipes, shared memory, or a file to pass results from child to parent.

💻 Code Example 1: Isolating a Memory-Leaking Function
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* Simulated third-party function with a memory leak */
int leaky_compute(int input)
{
    /* Allocates memory but never frees it (leak!) */
    int i;
    for (i = 0; i < 1000; i++) {
        void *p = malloc(1024);  /* 1 KB each = 1 MB total */
        if (p) {
            ((char*)p)[0] = (char)i;  /* touch it */
        }
        /* Note: no free(p) -- intentional leak */
    }
    return input * input;  /* return square of input */
}

int main(void)
{
    int input = 7;
    int result;
    pid_t child_pid;
    int status;

    printf("[Parent] Calling leaky_compute in a child.\n");
    printf("[Parent] Parent heap is CLEAN before fork.\n");

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

    if (child_pid == 0) {
        /* CHILD: call the leaky function */
        int r = leaky_compute(input);
        printf("[Child] leaky_compute(%d) = %d, exiting.\n",
               input, r);
        /* All leaked memory freed by kernel on exit */
        exit(r);  /* pass result as exit code (0-255) */
    }

    /* PARENT: wait and get result */
    if (wait(&status) == -1) { perror("wait"); exit(1); }

    if (WIFEXITED(status)) {
        result = WEXITSTATUS(status);
        printf("[Parent] Result = %d\n", result);
        printf("[Parent] Parent heap is still CLEAN!\n");
    }

    return 0;
}
Result: The 1 MB of leaked memory exists only in the child’s address space. When the child calls exit(), the kernel reclaims ALL memory including leaked blocks. The parent’s heap is completely unaffected.

💻 Code Example 2: Returning Large Results via a Pipe

Exit status is limited to 8 bits. Use a pipe to return larger data:

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

/* Function that does heavy work and returns a large string */
void heavy_work(char *result_buf, size_t buflen)
{
    /* Simulate intensive work with large memory usage */
    char *tmp = malloc(4 * 1024 * 1024);  /* 4 MB temp buffer */
    if (tmp) {
        memset(tmp, 'X', 4 * 1024 * 1024);
        /* ... do work ... */
        free(tmp);  /* or maybe it leaks */
    }
    /* Return a complex result string */
    snprintf(result_buf, buflen, "Result: processed %d items, "
             "checksum=0xDEADBEEF", 42);
}

int main(void)
{
    int pipefd[2];
    char result[256];
    pid_t pid;

    /* Create pipe BEFORE fork */
    if (pipe(pipefd) == -1) { perror("pipe"); exit(1); }

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

    if (pid == 0) {
        /* CHILD: close read end, use write end */
        close(pipefd[0]);

        char buf[256];
        heavy_work(buf, sizeof(buf));  /* may leak memory */

        /* Write result to pipe */
        write(pipefd[1], buf, strlen(buf) + 1);
        close(pipefd[1]);
        _exit(0);
    }

    /* PARENT: close write end, read result */
    close(pipefd[1]);
    ssize_t n = read(pipefd[0], result, sizeof(result) - 1);
    if (n > 0) result[n] = '\0';
    close(pipefd[0]);

    wait(NULL);

    printf("[Parent] Child returned: %s\n", result);
    printf("[Parent] Memory is clean.\n");
    return 0;
}

💻 Code Example 3: Backtracking Algorithm with Clean Parent State
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* Simulate a game tree search that allocates memory */
int tree_search(int depth, int branch_factor)
{
    if (depth == 0) return 1;  /* leaf node: found */

    /* Allocate a "node" (won't be freed = leak) */
    int *node = malloc(1024);
    if (node) node[0] = depth;

    int result = 0;
    for (int b = 0; b < branch_factor; b++) {
        result += tree_search(depth - 1, branch_factor);
    }
    return result;
}

/* Run tree_search in a child; parent stays clean */
int isolated_tree_search(int depth, int branch_factor)
{
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == -1) return -1;

    if (pid == 0) {
        close(pipefd[0]);
        int r = tree_search(depth, branch_factor);
        write(pipefd[1], &r, sizeof(r));
        close(pipefd[1]);
        _exit(0);
    }

    close(pipefd[1]);
    int result;
    read(pipefd[0], &result, sizeof(result));
    close(pipefd[0]);
    wait(NULL);
    return result;
}

int main(void)
{
    printf("Tree search result (depth=3, branch=2): %d\n",
           isolated_tree_search(3, 2));
    printf("Tree search result (depth=4, branch=2): %d\n",
           isolated_tree_search(4, 2));
    printf("Parent heap remains clean throughout.\n");
    return 0;
}

🅾 Interview Questions
Q1: How can you call a function that leaks memory without affecting the caller?

Fork a child process, run the function in the child, and use exit()/wait() to return the result. All memory leaked by the function (in the child’s heap) is automatically freed by the kernel when the child exits. The parent’s heap is untouched.

Q2: What are the limitations of returning values via exit status?

The exit status is only 8 bits wide (0–255). Values larger than 255 are truncated. For larger or more complex return values, use pipes, shared memory (mmap/shm), a temporary file, or a socket to pass data from child to parent.

Q3: Why does the kernel free ALL memory when a child exits, even leaked memory?

The kernel tracks all memory pages mapped to a process. When a process terminates, the kernel reclaims all pages in its virtual address space regardless of whether the process ever called free(). Memory leaks only matter during a process’s lifetime. On exit, the OS always cleans up completely.

Q4: Name two real-world use cases for the fork-to-isolate-memory pattern.

(1) Browser tab isolation: each tab runs in its own process so a memory leak in one tab doesn’t affect others or the browser itself. (2) Test runners: each test case runs in a forked child so memory state from one test can’t affect the next, and a crash/leak is fully isolated.

Series Navigation
Topic 2 → Subtopic 4 of 4  |  Next: Topic 3 → File Sharing

← Previous Next: File Descriptors After fork() → Index

Leave a Reply

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