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).
