Attribute Inheritance: exec() & fork()

Chapter 28.4 / 28.5 / 28.6 — TLPI
Attribute Inheritance: exec() & fork()
A complete reference — what every process attribute does across fork() and exec()
30+
Attributes
fork()
Inherits
exec()
Resets

Two Key Questions

Every process has many attributes — open files, signal handlers, timers, credentials, memory mappings. When you call fork() or exec(), each attribute is handled in one of three ways:

✅ Inherited/Preserved
Child gets a copy (fork) or attribute survives (exec)
❌ Not Inherited/Reset
Child starts fresh (fork) or attribute resets to default (exec)
↔ Shared Reference
Child and parent share the same underlying resource (fork)

Process Address Space

Attribute exec() fork() Notes
Text segment ❌ Replaced ↔ Shared (CoW) Child shares text; exec() replaces with new program
Stack segment ❌ Replaced ✅ Copied (CoW) fork() inherits parent’s stack content
Data & heap ❌ Replaced ✅ Copied (CoW) brk(), sbrk()
Environment variables See note ✅ Inherited execle()/execve() can replace; other exec() calls preserve
Memory mappings ❌ Not preserved ✅ Mostly mmap(). MADV_DONTFORK mappings not inherited across fork()
Memory locks ❌ No ❌ No mlock(), munlock()

Process Identifiers & Credentials

Attribute exec() fork() Notes
Process ID (PID) ✅ Preserved ❌ New PID for child exec() does not change the PID
Parent PID (PPID) ✅ Preserved ❌ Child’s PPID = parent’s PID exec() does not reparent
Process group ID ✅ Preserved ✅ Inherited setpgid()
Session ID ✅ Preserved ✅ Inherited setsid()
Real UID / GID ✅ Preserved ✅ Inherited setuid(), setgid()
Effective / Saved set IDs See note ✅ Inherited exec() may change eUID if set-user-ID bit is set on the new program
Supplementary group IDs ✅ Preserved ✅ Inherited setgroups(), initgroups()

Files, I/O & Directories

Attribute exec() fork() Notes
Open file descriptors See note ✅ Inherited copy Preserved across exec() unless close-on-exec flag set. Both child and parent share the same underlying open file descriptions.
Close-on-exec flag ✅ Honoured ✅ Inherited FD_CLOEXEC via fcntl(F_SETFD)
File offsets ✅ Preserved ↔ Shared Parent and child share file offset — seeking in one affects the other
Open file status flags ✅ Preserved ↔ Shared O_APPEND, O_NONBLOCK etc. via fcntl(F_SETFL)
Async I/O operations Cancelled ❌ Not inherited aio_read() etc. Outstanding operations cancelled during exec()
Current working directory ✅ Preserved ✅ Inherited chdir()
Root directory ✅ Preserved ✅ Inherited chroot()
File mode creation mask ✅ Preserved ✅ Inherited umask()

Signals

Attribute exec() fork() Notes
Signal dispositions Partially reset ✅ Inherited exec(): caught signals → default. Ignored/default signals unchanged.
Signal mask ✅ Preserved ✅ Inherited sigprocmask()
Pending signal set ✅ Preserved ❌ Not inherited Pending signals for parent are not copied to child
Alternate signal stack ❌ Not preserved ✅ Inherited sigaltstack()

Timers

Attribute exec() fork()
Interval timers (setitimer) ✅ Preserved ❌ Not inherited
alarm() timer ✅ Preserved ❌ Not inherited
POSIX timers (timer_create) ❌ Not preserved ❌ Not inherited

IPC, Locks & Resources

Attribute exec() fork() Notes
SysV shared memory ❌ Detached ✅ Inherited shmat(), shmdt()
POSIX shared memory ❌ No ✅ Inherited shm_open()
POSIX message queues ❌ No ✅ Inherited mq_open(). Child inherits descriptors; notification registrations are NOT inherited.
POSIX named semaphores ❌ No ↔ Shared sem_open(). Child shares references to same semaphores.
SysV semaphore adj. ✅ Preserved ❌ Not inherited semop() undo values
File locks (flock) ✅ Preserved ↔ Ref copied Child inherits reference to same lock
Record locks (fcntl) Mostly ❌ Not inherited Preserved across exec() unless close-on-exec fd
Resource limits ✅ Preserved ✅ Inherited setrlimit()
Process CPU times ✅ Preserved ❌ Not inherited As returned by times()
Capabilities See §39.5 ✅ Inherited capset(). exec() may modify capabilities depending on file capabilities.
Nice value ✅ Preserved ✅ Inherited nice(), setpriority()
Scheduling policy & priority ✅ Preserved ✅ Inherited sched_setscheduler()
CPU affinity ✅ Preserved ✅ Inherited sched_setaffinity()
Exit handlers (atexit) ❌ Not preserved ✅ Inherited atexit(), on_exit()
Controlling terminal ✅ Preserved ✅ Inherited

Example 1 — Verifying Signal Disposition Reset by exec()

/* signal_exec_demo.c — Show that caught signals revert to default after exec() */
/* child_prog.c — a small program to exec into */
/* Compile: gcc -o child_prog child_prog.c  */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
    /* After exec(), SIGUSR1 disposition is back to default (terminate) */
    struct sigaction sa;
    sigaction(SIGUSR1, NULL, &sa);
    if (sa.sa_handler == SIG_DFL)
        printf("[child_prog] SIGUSR1 = SIG_DFL (default) after exec()\n");
    else if (sa.sa_handler == SIG_IGN)
        printf("[child_prog] SIGUSR1 = SIG_IGN (ignored) after exec()\n");
    else
        printf("[child_prog] SIGUSR1 has a custom handler after exec()\n");
    return 0;
}
/* signal_exec_demo.c — Main program */
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

static void my_handler(int sig) { (void)sig; }

int main(void)
{
    /* Install a custom handler for SIGUSR1 */
    struct sigaction sa = { .sa_handler = my_handler };
    sigaction(SIGUSR1, &sa, NULL);
    printf("[parent] SIGUSR1 has custom handler\n");

    /* Ignore SIGUSR2 */
    signal(SIGUSR2, SIG_IGN);
    printf("[parent] SIGUSR2 is ignored\n");

    pid_t pid = fork();
    if (pid == 0) {
        /* exec() replaces caught handlers with SIG_DFL.
           Ignored signals (SIG_IGN) stay ignored after exec(). */
        char *args[] = { "./child_prog", NULL };
        execv("./child_prog", args);
        _exit(1);
    }
    waitpid(pid, NULL, 0);
    return 0;
}
/*
 * Output:
 * [parent] SIGUSR1 has custom handler
 * [parent] SIGUSR2 is ignored
 * [child_prog] SIGUSR1 = SIG_DFL (default) after exec()
 *
 * SIGUSR2 would still be SIG_IGN in child_prog — ignored
 * signals survive exec(). Only CAUGHT signals revert to default.
 */

Example 2 — File Offset Sharing After fork()

/* fork_offset.c — Parent and child share file offset after fork() */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    /* Create a test file */
    int fd = open("/tmp/fork_test.txt", O_RDWR | O_CREAT | O_TRUNC, 0600);
    write(fd, "ABCDEFGHIJ", 10);
    lseek(fd, 0, SEEK_SET);   /* Rewind to start */

    pid_t pid = fork();
    if (pid == 0) {
        /* Child reads 3 bytes — advances SHARED offset by 3 */
        char buf[4] = {0};
        read(fd, buf, 3);
        printf("[Child]  read '%s' (offset now at 3)\n", buf);
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    /* Parent continues reading — offset is at 3 (child moved it!) */
    char buf[8] = {0};
    read(fd, buf, 7);
    printf("[Parent] read '%s' (expected: DEFGHIJ)\n", buf);

    close(fd);
    unlink("/tmp/fork_test.txt");
    return 0;
}
/*
 * Output:
 * [Child]  read 'ABC' (offset now at 3)
 * [Parent] read 'DEFGHIJ' (expected: DEFGHIJ)
 *
 * Parent and child share the same open file description including
 * the file offset — child advancing it affects parent.
 */

Example 3 — Close-on-Exec Flag

/* cloexec_demo.c — FD_CLOEXEC closes fd automatically on exec() */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    /* Open fd without close-on-exec */
    int fd_normal = open("/dev/null", O_RDONLY);

    /* Open fd with close-on-exec */
    int fd_cloexec = open("/dev/null", O_RDONLY | O_CLOEXEC);

    /* Or set it after the fact: */
    /* fcntl(fd_cloexec, F_SETFD, FD_CLOEXEC); */

    printf("Normal fd: %d, CLOEXEC fd: %d\n", fd_normal, fd_cloexec);

    pid_t pid = fork();
    if (pid == 0) {
        /* exec a shell that checks if the fds are open */
        char cmd[128];
        snprintf(cmd, sizeof(cmd),
                 "ls -l /proc/%d/fd 2>&1 | grep -E '%d|%d'",
                 getpid(), fd_normal, fd_cloexec);

        char *args[] = { "/bin/sh", "-c", cmd, NULL };
        execv("/bin/sh", args);
        _exit(1);
    }
    waitpid(pid, NULL, 0);
    /*
     * Output will show fd_normal is open in child after exec(),
     * but fd_cloexec will NOT appear (it was closed by exec()).
     */
    close(fd_normal);
    close(fd_cloexec);
    return 0;
}

28.5 — Chapter Summary

  • Process accounting: kernel writes an acct record for each terminated process when enabled via acct(). Uses comp_t compressed time format. Version 3 adds PID, PPID, and 32-bit UIDs.
  • clone(): Linux-specific system call that creates a new process. Differs from fork() by allowing precise control over resource sharing via flags, and child starts at a supplied function with its own stack.
  • fork(), vfork(), clone() all call do_fork() internally — they differ only in their flags.
  • CLONE_VM, CLONE_FILES, CLONE_SIGHAND, CLONE_THREAD: the core flags for POSIX thread creation (NPTL).
  • Namespace flags (CLONE_NEWPID, CLONE_NEWNET, etc.) are the foundation of Linux containers.
  • waitpid() extensions: __WCLONE for non-SIGCHLD children, __WALL for all children, __WNOTHREAD to limit scope to own children.
  • Speed: clone() ≈ vfork() >> fork() for large processes. With exec(), differences shrink because exec() itself is the bottleneck.
  • fork() attribute inheritance: child inherits most attributes (fds, cwd, signals, credentials) but not timers, pending signals, or resource usage.
  • exec() attribute effects: PID preserved; caught signal handlers reset to default; fds preserved unless FD_CLOEXEC; POSIX timers and shared memory detached.

28.6 — Exercise

Exercise 28-1

Write a program to measure and compare the speed of fork() and vfork() on your system. Each child process should exit immediately, and the parent should wait for each child before creating the next. Use the shell built-in time command to measure total execution time. Compare your results with Table 28-3.

/* exercise_28_1.c — Benchmark fork() vs vfork() with immediate child exit */
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <unistd.h>

#define DEFAULT_ITER 100000

int main(int argc, char *argv[])
{
    int iterations = (argc > 1) ? atoi(argv[1]) : DEFAULT_ITER;
    struct timeval t1, t2;

    printf("Benchmarking %d iterations...\n\n", iterations);

    /* --- fork() --- */
    gettimeofday(&t1, NULL);
    for (int i = 0; i < iterations; i++) {
        pid_t pid = fork();
        if (pid == -1) { perror("fork"); exit(1); }
        if (pid == 0)  _exit(0);
        if (waitpid(pid, NULL, 0) == -1) { perror("waitpid"); exit(1); }
    }
    gettimeofday(&t2, NULL);

    double fork_sec = (t2.tv_sec - t1.tv_sec)
                    + (t2.tv_usec - t1.tv_usec) / 1e6;

    /* --- vfork() --- */
    gettimeofday(&t1, NULL);
    for (int i = 0; i < iterations; i++) {
        pid_t pid = vfork();
        if (pid == -1) { perror("vfork"); exit(1); }
        if (pid == 0)  _exit(0);    /* Must use _exit, NOT exit() */
        if (waitpid(pid, NULL, 0) == -1) { perror("waitpid"); exit(1); }
    }
    gettimeofday(&t2, NULL);

    double vfork_sec = (t2.tv_sec - t1.tv_sec)
                     + (t2.tv_usec - t1.tv_usec) / 1e6;

    printf("fork():  %6.2f s  (%8.0f/s)\n",
           fork_sec,  iterations / fork_sec);
    printf("vfork(): %6.2f s  (%8.0f/s)\n",
           vfork_sec, iterations / vfork_sec);
    printf("Speedup: %.1fx\n", fork_sec / vfork_sec);

    return 0;
}
/* Compile: gcc -O2 -o exercise_28_1 exercise_28_1.c
   Run:     time ./exercise_28_1 100000
   Try:     time ./exercise_28_1 100000  (with small malloc before loop)
            to see how process size affects fork() but not vfork()  */

Interview Questions

Q1. Does a child process inherit its parent’s alarm() timer after fork()?
No. Interval timers set by alarm() or setitimer() are not inherited by the child after fork(). The child starts with no pending timers. However, if the parent performs exec(), the alarm timer is preserved across the exec() call — the same process keeps its timer running through an exec().
Q2. What happens to signal handlers after exec()?
Signals with custom handlers (set via signal() or sigaction()) revert to their default disposition after exec(). This is because the handler function’s address no longer exists in the new program’s address space. Signals that were set to SIG_IGN (ignore) remain ignored after exec(). Signals set to SIG_DFL remain at default. The signal mask is preserved across exec().
Q3. Parent writes to a file, then forks. Child seeks the file to offset 0 and reads. What does the parent read next?
The parent and child share the same open file description (including the file offset). The child’s lseek(fd, 0, SEEK_SET) moves the shared offset to 0. When the child reads, it advances the offset to the number of bytes read. The parent’s next read will continue from wherever the child left the offset, not from the parent’s previous position. This is the sharing behavior of fork() on open file descriptions.
Q4. What is FD_CLOEXEC and why is it important?
FD_CLOEXEC (set via fcntl(fd, F_SETFD, FD_CLOEXEC) or O_CLOEXEC at open) causes the file descriptor to be automatically closed when the process executes a new program via exec(). Without it, file descriptors leak into child programs — a security risk (child inherits access to parent’s files) and a resource waste. In modern code, O_CLOEXEC is set by default on most file-opening operations that will be followed by exec().
Q5. Does atexit() handler registration survive exec()?
No. atexit() handlers do not survive exec(). The handler functions are part of the old program’s address space, which is replaced by exec(). The new program starts with a clean atexit() registration list. However, atexit() handlers ARE inherited by fork() — the child has the same handlers registered, and they will run when the child calls exit().
Q6. A process has a POSIX named semaphore open. What happens after fork()? After exec()?
After fork(): the child inherits a reference to the same named semaphore. Both parent and child now share that semaphore and can signal/wait on it. After exec(): named semaphores are NOT inherited — the new program does not have the semaphore open unless it opens it explicitly by name. This is different from file descriptors, which do survive exec() (unless FD_CLOEXEC is set).

Leave a Reply

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