Threads and Process Control

 

← Threads & Signals Section 33.3 · Threads & Process Control Next: Implementation Models →

Chapter 33 · Section 33.3

Threads and Process Control

What happens when fork(), exec(), or exit() is called in a multithreaded process

1 threadsurvives fork()

All threadsvanish on exec()

All threadsvanish on exit()

pthread_atforkcleanup handlers

Key Terms

fork() in multithreaded exec() in multithreaded exit() in multithreaded pthread_atfork Fork Handlers Mutex state after fork Memory leaks in child prepare / parent / child handlers

Threads and fork()

This is one of the most surprising behaviours in POSIX threading: when a multithreaded process calls fork(), only the calling thread is replicated in the child process. All other threads simply vanish in the child — no cleanup, no destructors, no handlers.

fork() in a Multithreaded Process

PARENT PROCESS
Thread A (main)
Thread B (worker)
Thread C (worker)
Thread B calls fork()

CHILD PROCESS
Thread B only ✓
Thread A — GONE
Thread C — GONE
Only caller survives

The dangerous part: Even though Threads A and C vanish in the child, their effects remain:

  • Global variables are inherited as-is (copied from parent’s memory).
  • Mutexes and condition variables are preserved in whatever state they were in at fork time.
  • If Thread A was in the middle of updating a data structure and had locked a mutex, the child inherits that locked mutex — but Thread A (the owner) no longer exists. The child is now deadlocked.
  • Thread-specific data destructors and cleanup handlers are NOT called for the vanished threads → potential memory leaks in the child.
POSIX recommendation: The safest use of fork() in a multithreaded program is one that is immediately followed by exec(). The exec() replaces the entire process image, clearing all Pthreads objects (mutexes, condition variables, TSD, etc.). Avoid any fork() that is not followed by exec().

Threads and exec()

When any thread calls one of the exec() family functions, the calling program is completely replaced:

  • All threads except the one that called exec() vanish immediately.
  • No thread-specific data destructors are called.
  • No cleanup handlers are executed.
  • All process-private mutexes and condition variables disappear (they were in the old process image).
  • After exec(), the remaining (new) thread’s ID is unspecified — it starts fresh.
This is why fork() + exec() is the safe pattern: fork creates a child with one thread, exec immediately replaces that child’s image. No mutex state is inherited because exec wipes it all.

Threads and exit()

When any thread calls exit() (or the main thread does a return), all threads immediately vanish:

  • No thread-specific data destructors are called.
  • No cleanup handlers are executed.
  • Any threads still running are abruptly terminated.
Function Effect Cleanup handlers?
exit() All threads terminate ❌ No
pthread_exit() Only calling thread terminates ✅ Yes (TSD + cleanup handlers)
exec() All threads + process image replaced ❌ No
_exit() (raw syscall) Immediate process termination ❌ No
NPTL detail: In NPTL, calling _exit() is aliased to call the exit_group() syscall, which terminates all threads in the process. Calling pthread_exit() uses the true _exit() syscall in the kernel, which terminates only the calling thread.

pthread_atfork() — Fork Handlers

For programs that must use fork() without immediately calling exec(), Pthreads provides pthread_atfork() to register cleanup handlers that run around the fork.

#include <pthread.h>

int pthread_atfork(void (*prepare)(void),
                   void (*parent)(void),
                   void (*child)(void));
/* Returns 0 on success, positive error number on error */

/* prepare: called in PARENT, BEFORE fork() creates the child.
 *          Run in REVERSE order of registration.
 *          Purpose: acquire all mutexes → ensure clean state.
 *
 * parent:  called in PARENT, AFTER fork() returns.
 *          Run in FORWARD order of registration.
 *          Purpose: release the mutexes acquired in prepare.
 *
 * child:   called in CHILD, AFTER fork() returns.
 *          Run in FORWARD order of registration.
 *          Purpose: release/reinitialise mutexes for child use.
 */

pthread_atfork() Handler Execution Timeline

PARENT PROCESS
1. prepare() runs ← lock mutexes
2. fork() syscall
3. parent() runs ← unlock mutexes
CHILD PROCESS
(after fork returns in child)
child() runs ← reinit mutexes

The key insight: The prepare handler runs just before fork, so it can lock all mutexes to ensure no other thread is in the middle of a critical section when the fork happens. After fork, both parent and child handlers unlock (or reinitialise) those mutexes.

Library use case: Libraries that internally use threads can use pthread_atfork() to protect their own state around forks called by application code, without the application needing to know about the library’s internal threads.

Code Example 1 — fork() in a Multithreaded Process (Danger Demo)

This example demonstrates the dangerous scenario: a worker thread holds a mutex when fork() is called from another thread. The child process inherits the locked mutex with no thread to unlock it.

/*
 * ep_fork_mutex_danger.c — Demonstrate mutex deadlock risk after fork()
 * Compile: gcc -o ep_fork_mutex_danger ep_fork_mutex_danger.c -lpthread
 *
 * WARNING: This program intentionally creates a situation that can deadlock.
 * It is an educational demonstration of what NOT to do.
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static volatile int data = 0;

/* Worker thread — holds the mutex for 3 seconds */
void *worker(void *arg)
{
    printf("[Worker] Acquiring mutex and sleeping (simulating long critical section)\n");
    pthread_mutex_lock(&lock);
    data = 42;       /* updating shared data */
    sleep(3);        /* simulate work — mutex held during this time */
    data = 100;
    printf("[Worker] Releasing mutex, data = %d\n", data);
    pthread_mutex_unlock(&lock);
    return NULL;
}

int main(void)
{
    pthread_t tid;
    pid_t child_pid;

    pthread_create(&tid, NULL, worker, NULL);

    /* Give worker time to acquire the mutex */
    sleep(1);

    printf("[Main] Calling fork() while worker holds the mutex...\n");
    child_pid = fork();

    if (child_pid == 0) {
        /* CHILD PROCESS — only this thread runs here.
         * The worker thread VANISHED. But the mutex is still LOCKED.
         * If we try to lock it, we deadlock. */
        printf("[Child PID %d] Trying to lock the mutex...\n", getpid());

        /* Use trylock to avoid permanently hanging (for demo purposes) */
        int ret = pthread_mutex_trylock(&lock);
        if (ret == 0) {
            printf("[Child] Mutex was unlocked (lucky!), data = %d\n", data);
            pthread_mutex_unlock(&lock);
        } else {
            printf("[Child] MUTEX IS LOCKED and no one can unlock it! "
                   "Deadlock if we call lock().\n");
            printf("[Child] data = %d (may be inconsistent)\n", data);
        }
        /* Safe pattern: exec() immediately after fork() */
        /* execlp("echo", "echo", "child exec done", NULL); */
        exit(0);
    } else {
        /* PARENT */
        waitpid(child_pid, NULL, 0);
        pthread_join(tid, NULL);
        printf("[Main] Done. This demonstrates the fork() + mutex danger.\n");
    }
    return 0;
}
Key takeaway: In the child, the worker thread that owns the mutex is gone. pthread_mutex_lock() in the child will block forever. This is why “fork + exec” is the safe pattern, and why pthread_atfork() handlers exist for the cases where you can’t exec.

Code Example 2 — pthread_atfork() Safe Fork Pattern

This example correctly uses pthread_atfork() to acquire all mutexes before fork and reinitialise them cleanly in the child.

/*
 * ep_atfork_safe.c — Safe fork() using pthread_atfork() handlers
 * Compile: gcc -o ep_atfork_safe ep_atfork_safe.c -lpthread
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

static pthread_mutex_t data_lock = PTHREAD_MUTEX_INITIALIZER;
static int shared_data = 0;

/* ---- Fork handler: runs in PARENT, BEFORE fork() ---- */
static void prepare_handler(void)
{
    printf("[prepare] Acquiring all mutexes before fork...\n");
    pthread_mutex_lock(&data_lock);
    /* In a real program: lock ALL mutexes to ensure consistent state */
}

/* ---- Fork handler: runs in PARENT, AFTER fork() returns ---- */
static void parent_handler(void)
{
    printf("[parent] Releasing mutexes in parent after fork...\n");
    pthread_mutex_unlock(&data_lock);
}

/* ---- Fork handler: runs in CHILD, AFTER fork() returns ---- */
static void child_handler(void)
{
    printf("[child] Reinitialising mutexes in child process...\n");
    /* REINITIALISE — do NOT just unlock(), because the mutex was
     * locked by a thread in the parent that no longer exists in child.
     * pthread_mutex_init() creates a fresh, unlocked mutex. */
    pthread_mutex_init(&data_lock, NULL);
}

void *worker(void *arg)
{
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&data_lock);
        shared_data++;
        printf("[Worker] Updated shared_data = %d\n", shared_data);
        pthread_mutex_unlock(&data_lock);
        usleep(100000);  /* 100ms */
    }
    return NULL;
}

int main(void)
{
    pthread_t tid;
    pid_t child_pid;

    /* Register fork handlers BEFORE creating threads */
    pthread_atfork(prepare_handler, parent_handler, child_handler);

    pthread_create(&tid, NULL, worker, NULL);
    sleep(1);  /* let worker run a bit */

    printf("[Main] Calling fork()...\n");
    child_pid = fork();

    if (child_pid == 0) {
        /* CHILD — mutex is cleanly reinitialised, safe to use */
        printf("[Child PID %d] Mutex is clean. Accessing shared_data = %d\n",
               getpid(), shared_data);
        pthread_mutex_lock(&data_lock);
        shared_data = 999;
        printf("[Child] Set shared_data = %d (child's own copy)\n",
               shared_data);
        pthread_mutex_unlock(&data_lock);
        exit(0);
    } else {
        waitpid(child_pid, NULL, 0);
        pthread_join(tid, NULL);
        printf("[Main] Parent shared_data = %d (unchanged by child)\n",
               shared_data);
    }
    return 0;
}
Remember: In the child handler, always use pthread_mutex_init() to reinitialise mutexes — do NOT call pthread_mutex_unlock(). The child doesn’t own the mutex (the parent thread that locked it in prepare is gone from the child), so unlocking it would be undefined behaviour.

Interview Questions

Q1. When a multithreaded process calls fork(), how many threads exist in the child?
Exactly one — only the thread that called fork() is replicated in the child. All other threads in the parent vanish in the child without running any cleanup handlers or thread-specific data destructors. The child gets a copy of the parent’s entire memory (global variables, heap, Pthreads objects) but only one thread to use it.
Q2. Why can a fork() in a multithreaded program cause a deadlock in the child?
If another thread held a mutex at the moment of fork, the child inherits the mutex in a locked state. But the thread that owns the mutex doesn’t exist in the child. If the surviving thread in the child calls pthread_mutex_lock() on that mutex, it will block forever — classic deadlock. The solution is either fork() + exec() (which wipes out all mutex state) or use pthread_atfork() to lock all mutexes in the prepare handler and reinitialise them in the child handler.
Q3. What are the three handlers in pthread_atfork() and when does each run?
prepare: Runs in the parent process just before fork() creates the child. Multiple prepare handlers run in reverse registration order. Purpose: acquire all mutexes to ensure a consistent state is captured by the child.

parent: Runs in the parent process just after fork() returns. Runs in forward registration order. Purpose: release the mutexes acquired in prepare.

child: Runs in the child process just after fork() returns. Runs in forward registration order. Purpose: reinitialise (not just unlock) mutexes for safe use by the child.

Q4. What is the difference between exit() and pthread_exit() in a multithreaded program?
exit() terminates the entire process — all threads vanish immediately with no cleanup. pthread_exit() terminates only the calling thread, runs that thread’s cleanup handlers and thread-specific data destructors, and allows other threads to continue running. If the main thread calls pthread_exit(), the process does not terminate — it continues until all other threads exit or until exit() is called.
Q5. When fork() is called from a thread other than the main thread under LinuxThreads (legacy), what PID does the child get?
This is one of the LinuxThreads nonconformances: if exec() is called from any thread other than the main thread, the resulting process has the PID of the calling thread, not the main thread’s PID. Under SUSv3, after exec(), the process ID should be the same as the main thread’s PID. NPTL corrects this — all threads share the same PID (the main thread’s PID) due to the CLONE_THREAD flag.
Q6. Why is fork() + exec() considered the safe multithreaded fork pattern?
After fork(), the child has one thread and inherits potentially locked mutexes, inconsistent data structures, and memory leaks from missing destructors. But if exec() is called immediately, the entire process image is replaced — the new program starts fresh with no inherited mutex state, no Pthreads objects, and no thread-specific data. The exec effectively wipes out all the problematic inherited state before anything can go wrong.

Section Summary

  • fork(): Only the calling thread survives in the child. Mutex state is inherited — danger of deadlock.
  • exec(): All threads vanish; process image replaced. Clears all Pthreads state.
  • exit(): All threads vanish immediately, no cleanup.
  • pthread_exit(): Only the calling thread exits, with proper cleanup.
  • pthread_atfork(): Register prepare/parent/child handlers to safely use fork() without exec().
  • Safe rule: fork() should always be followed by exec() in multithreaded programs.

Keep Learning — It’s Free

EmbeddedPathashala — free embedded systems education for students.

Visit EmbeddedPathashala

Leave a Reply

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