What You Will Learn
This part explores one of the most surprising behaviours in Linux multi-process programming: after a fork(), if both parent and child call exit(), output from printf() can appear twice — and write() output appears only once and in a different order. Understanding why this happens and how to prevent it requires understanding where stdio buffers live in memory.
Consider this simple program (Listing 25-2 from the textbook):
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
if (fork() == -1)
errExit("fork");
exit(EXIT_SUCCESS); /* both parent and child call exit() */
}
| Run mode | Command | Actual output |
|---|---|---|
| To terminal | ./prog |
Hello worldCiao (expected, normal) |
| To file | ./prog > a.txt |
CiaoHello worldHello world ← appears TWICE! |
Two strange things happen when redirected to a file:
printf()output appears twicewrite()output appears beforeprintf()output
The key is understanding where stdio buffers live and how buffering mode changes when output is redirected.
Two levels of buffering in Linux I/O:
| Buffer Level | Where it lives | Affected by fork()? | Which functions use it |
|---|---|---|---|
| Userspace stdio buffer | Process’s own memory (heap) | YES — duplicated by fork() | printf(), fprintf(), fwrite(), fputs() … |
| Kernel buffer cache | Kernel memory (not in process) | NO — shared, never duplicated | write(), read() (system calls) |
How buffering mode changes based on destination:
| stdout destination | Buffering mode | When is buffer flushed? | printf with \n after fork()? |
|---|---|---|---|
| Terminal (TTY) | Line-buffered | On each newline ‘\n’ | Already written to kernel before fork() → appears once |
| File / pipe / redirect | Block-buffered | Only when buffer is full or exit()/fflush() called | Still in buffer at fork() time → duplicated → appears TWICE |
write() was already in the kernel buffer before fork() — it appears only once and before the printf output (because it was in the kernel first).There are several ways to prevent this problem:
| Solution | How to apply | Best for |
|---|---|---|
| ① fflush() before fork() | Call fflush(NULL) or fflush(stdout) immediately before fork() to ensure all stdio buffers are empty |
Most common, simple fix |
| ② Disable buffering | setvbuf(stdout, NULL, _IONBF, 0) or setbuf(stdout, NULL) |
When you want every printf to go directly to kernel (may impact performance) |
| ③ Child uses _exit() | Child calls _exit() instead of exit() — skips stdio flush in child |
General best practice for child processes (also avoids double atexit handlers) |
| ④ Only one process calls exit() | Design the program so only parent calls exit(); child calls _exit() |
Recommended general architecture for fork()-based programs |
exit(). All other processes should terminate via _exit(). This ensures exit handlers and stdio flushing happen exactly once.write() Output Appears Before printf() OutputWhen redirected to a file, “Ciao” from write() appears before “Hello world” from printf(), even though printf() was called first in the source code. Here is why:
| Function | Buffer path | When reaches file | Appears in output |
|---|---|---|---|
printf("Hello world\n") |
→ userspace stdio buffer → (waits for flush) → kernel buffer → file | Only when exit() flushes the stdio buffer |
Second (after Ciao) |
write(..., "Ciao\n", 5) |
→ directly to kernel buffer → file | Immediately — no userspace buffering | First |
printf, fprintf, fwrite) and system call I/O (write) on the same file descriptor without being aware of buffering interactions. When you must mix them, flush the stdio buffer first with fflush().Here is a concise summary of all topics covered across the full Chapter 25 series:
- Abnormal termination happens when a signal with default “terminate” action is delivered to the process.
- Normal termination uses
_exit()(system call, immediate) orexit()(C library: calls atexit handlers, flushes stdio, then calls_exit()). - Only the bottom 8 bits (0–255) of the exit status reach the parent via
WEXITSTATUS(). - By convention, 0 = success, non-zero = error. Use
EXIT_SUCCESSandEXIT_FAILUREconstants. - At termination (normal or abnormal), the kernel: closes FDs, releases file locks, detaches SysV shm, applies semadj, sends SIGHUP if controlling process, closes POSIX semaphores/mqueues, sends SIGHUP+SIGCONT to orphaned stopped groups, removes memory locks, unmaps mmap regions.
- Exit handlers registered with
atexit()(portable) oron_exit()(glibc only) are called in reverse registration order byexit(). Not called by_exit()or on signal. fork()duplicates the userspace stdio buffer. If both parent and child callexit(), both flush — causing duplicate output. Fix: call_exit()in the child, or callfflush()beforefork().write()goes directly to the kernel — its output is never duplicated byfork(), and it is not affected by stdio buffering mode.
If a child process makes the call
exit(-1), what exit status (as returned by WEXITSTATUS()) will be seen by the parent?Answer:
The parent will see 255.
Reasoning step by step:
exit(-1)passes-1(anint) as the status argument to_exit().- -1 in 32-bit two’s complement binary is:
0xFFFFFFFF - Only the bottom 8 bits are made available to the parent:
0xFF 0xFF= 255 (as an unsigned 8-bit value)WEXITSTATUS(status)extracts those 8 bits and returns 255.
/* Verify Exercise 25-1 answer */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int wstatus;
pid = fork();
if (pid == 0) {
exit(-1); /* child exits with -1 */
}
waitpid(pid, &wstatus, 0);
if (WIFEXITED(wstatus))
printf("WEXITSTATUS = %d\n", WEXITSTATUS(wstatus));
/* Output: WEXITSTATUS = 255 */
return 0;
}
Coding Examples
Run this program redirected to a file and you will see the double printf output. This is the exact Listing 25-2 scenario from the textbook.
/* fork_stdio_buf.c (Listing 25-2 from TLPI)
* Compile : gcc -Wall -o fork_stdio_buf fork_stdio_buf.c
*
* To terminal: ./fork_stdio_buf
* Output: Hello world
* Ciao
*
* To file: ./fork_stdio_buf > a.txt && cat a.txt
* Output: Ciao
* Hello world
* Hello world ← printf appears TWICE (buffer was duplicated by fork)
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
/*
* printf() goes to userspace stdio buffer.
* When stdout is a terminal: buffer flushed immediately (line-buffered).
* When stdout is a file: stays in buffer (block-buffered).
*/
printf("Hello world\n");
/*
* write() goes DIRECTLY to kernel buffer — no userspace buffering.
* Not duplicated by fork(). Always appears first in the file.
*/
write(STDOUT_FILENO, "Ciao\n", 5);
/*
* fork() creates a child that inherits a COPY of the stdio buffer.
* If printf() output is still in the buffer at this point,
* BOTH parent and child have a copy of it.
*/
if (fork() == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
/*
* Both parent AND child reach here and call exit().
* exit() flushes stdio buffers in both processes.
* → printf output is written to the file TWICE.
*/
exit(EXIT_SUCCESS);
}
./fork_stdio_buf > a.txt && cat a.txtYou will see “Hello world” appear twice. “Ciao” appears first because write() bypassed the userspace buffer.
The simplest fix: call fflush(NULL) before fork() to drain all userspace stdio buffers. After this, fork() copies nothing in the stdio buffer.
/* fix1_fflush_before_fork.c
* Compile : gcc -Wall -o fix1 fix1_fflush_before_fork.c
* Run : ./fix1 > a.txt && cat a.txt
* Output (correct): Ciao
* Hello world ← appears ONCE
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
/*
* FIX: flush ALL stdio streams before fork().
* fflush(NULL) flushes every open stdio stream.
* After this, the stdio buffer is empty — fork() copies nothing.
*/
fflush(NULL);
if (fork() == -1) { perror("fork"); exit(EXIT_FAILURE); }
/* Now both can safely call exit() — nothing left to double-flush */
exit(EXIT_SUCCESS);
}
The architecturally correct fix: child calls _exit(), which skips stdio flushing and atexit handlers. Only the parent flushes.
/* fix2_child_uses__exit.c
* Compile : gcc -Wall -o fix2 fix2_child_uses__exit.c
* Run : ./fix2 > a.txt && cat a.txt
* Output (correct): Ciao
* Hello world ← appears ONCE
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/*
* CHILD: use _exit() — does NOT flush stdio buffers.
* The child's copy of the stdio buffer is discarded.
* Only the parent will flush "Hello world" to the file.
*/
_exit(EXIT_SUCCESS);
}
/* PARENT: wait for child, then exit() normally */
wait(NULL);
exit(EXIT_SUCCESS); /* flushes stdio once — correct */
}
exit(). Child processes call _exit(). This applies to both the stdio buffer duplication problem and the atexit double-call problem.Interview Questions & Answers
Answer: stdio buffers are maintained in userspace memory (the process heap). When fork() is called, the child receives a complete copy of the parent’s address space — including the stdio buffer. If printf() output was still in the buffer at fork time (because stdout is block-buffered when redirected to a file), both parent and child have a copy. When both call exit(), both flush their copies, producing duplicate output.
Answer: write() is a system call that transfers data directly to the kernel buffer cache. The kernel buffer is shared kernel memory — it is not part of the process’s address space and is not duplicated by fork(). Only userspace memory (stdio buffers, heap, stack, global variables) is duplicated.
Answer: No. When stdout is connected to a terminal, it is line-buffered. This means data is flushed immediately when a newline character is written. So printf("Hello world\n") flushes immediately to the kernel before fork() is ever called — the stdio buffer is empty at fork time, nothing is duplicated. The problem only occurs when stdout is block-buffered (which happens when redirected to a file or pipe).
Answer: (1) Call fflush(NULL) or fflush(stdout) immediately before fork() to empty all stdio buffers. (2) Disable buffering on the stdio stream with setvbuf(stdout, NULL, _IONBF, 0) or setbuf(stdout, NULL). (3) In the child process, call _exit() instead of exit() — _exit() does not flush stdio buffers. Option 3 is also the best architectural practice for fork-based programs.
Answer: write() transfers data directly to the kernel buffer cache — it is immediately available in the file. printf() data, when stdout is block-buffered (file redirect), stays in the userspace stdio buffer and only reaches the file when the buffer is flushed by exit() or fflush(). So even though printf() was called first in the source code, its data appears in the file after write() output, because it was buffered longer.
Answer: 255. -1 as a 32-bit signed integer is 0xFFFFFFFF. Only the bottom 8 bits (0xFF = 255) are passed to the parent via the kernel’s exit status mechanism. WEXITSTATUS() extracts those 8 bits and returns 255, not -1. This is why you should never pass negative values to exit() — they produce confusing and non-portable exit status values.
Answer: In a program that creates child processes, typically only one process (almost always the parent) should call exit(). All child processes should call _exit(). This guarantees that: (1) atexit handlers are called exactly once, (2) stdio buffers are flushed exactly once (no duplicate output), (3) cleanup actions registered in the parent do not accidentally run in child processes that may be in an inconsistent state.
🎉 Chapter 25 Series Complete!
You have covered all sections: _exit() vs exit(), kernel cleanup details, atexit() exit handlers, on_exit() exit handlers, and the fork/stdio buffer interaction. Ready for Chapter 26 — Process Monitoring (wait & waitpid).
