← Runtimes
Runtime walkthrough 7 questions · 6 languages

I/O, buffering, and external state

Printed text appears at once in most programs. This lesson shows where text can wait before it appears.

printf keeps text in memory inside the program, and sends the text on later: when you call fflush, at a newline in some modes, when that memory is full, or when the program ends normally. Where the output goes decides the mode: usually each line at once on a screen, and in large blocks to a file or to another program.

Before you begin

Why this matters

C programs surprise people with output more than most: a prompt that never appears, a debug line missing after a crash, error messages printed before results that came first. All of them come from text that printf keeps in memory, and the rules for when it is sent. Once you know those rules, you can predict the output and fix it.

What this helps you reason about

  • Predict whether printf output is visible yet, given where the output goes.
  • Explain why errors and results appear in a different order when both are saved to one file.
  • Spot output that a crash or an abrupt exit will lose, and send it out in time.

When you call printf in a terminal, you usually see each line at once, and that model is correct for a terminal. In C, the answer depends on where output goes: the same text may appear at once, a moment later, or never. This lesson follows text from printf through the C library’s buffer to the operating system, and shows why the stepper and a terminal can print the same program differently.

Already know this? The short version
  • printf writes into stdout’s FILE buffer; with glibc, stdout is line-buffered on a terminal and fully buffered to a pipe or file, and stderr is unbuffered (the standard requires only that stdout is fully buffered if and only if it can be determined not to refer to an interactive device, and that stderr is not fully buffered).
  • fflush, a newline in line-buffered mode, a full buffer, and exit (including return from main) flush; setvbuf chooses the mode, and must be called before any other operation on the stream.
  • _exit, _Exit, abort (in glibc 2.27 and later), and crashes skip the flush, so buffered text is lost.
  • When stdout and stderr share one file, stderr lines can come before earlier stdout lines; fflush(stdout) before writing to stderr, and remember that fflush is not fsync.
  • The stepper sends C’s stdout to a file, so it is fully buffered there; other lessons’ C experiments call setvbuf(stdout, NULL, _IOLBF, 0).

Go to the final check· Go to the practice tickets

Words in this lesson (8)
operating system
The software, such as Linux, macOS, or Windows, that runs programs and controls the screen, files, and other devices. Every write a program makes is a request to it.
stream
A destination that receives bytes in order. A program writes to streams; it does not draw on the screen itself.
standard output and standard error
The two output streams every program starts with. Standard output is for normal results; standard error is for messages about problems.
terminal and pipe
A terminal is the window where you type commands. A pipe is a connection that carries one program’s output into another program’s input.
buffer
An area of memory where written text waits inside the program before it is sent on to the operating system.
flush
To send the text waiting in a buffer on to the operating system now. A flush is not a disk write.
line buffering and full buffering
A line-buffered stream sends its text at each newline. A fully buffered stream sends its text only when the buffer is full or is flushed.
_exit
A POSIX function that ends the process at once, without flushing stdio buffers. _Exit (C99) also ends the process at once; whether it flushes is left to the implementation, and glibc does not.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / A write call

What does a print call promise?

Here is a useful model: printf writes to the screen immediately. For most small programs, it predicts exactly what you see.

Underneath, a program does not draw on the screen itself. It asks the operating system to write bytes to a stream, and the operating system delivers them.

Every program starts with two output streams. Standard output is for the program’s normal results; printf writes there. Standard error is for messages about problems; fprintf(stderr, ...) writes there.

The program does not choose where these streams go. Whoever starts the program chooses. A stream may go to a terminal, to a file, or through a pipe into another program.

The program below asks for line buffering in its first line. The next sections explain why the stepper needs that line.

main.c
setvbuf(stdout, NULL, _IOLBF, 0);
printf("loading\n");
int total = 0;
for (int i = 1; i <= 3; i++) {
    total += i;
}
printf("ready %d\n", total);

A write passes text to a stream. The program does not choose where that stream goes.

Precisely: FILE streams and file descriptors

In C, stdout is a FILE *: a C library object that holds a buffer. Underneath it is a file descriptor, number 1, which the operating system uses to name standard output. The POSIX function write(1, ...) writes to the descriptor directly, past the FILE buffer.

Experiment

Watch when each write reaches the output panel

The stepper sends a C program’s standard output to a file, and C fully buffers standard output that is not a terminal. setvbuf(stdout, NULL, _IOLBF, 0) asks for line buffering instead, the mode C uses on a terminal: each newline sends the line on.

Why it matters In C, “printf writes immediately” is true only on a terminal and only for whole lines. Where output goes decides when it appears.

Predict at which stop loading first appears. Then delete the setvbuf line and predict again.

#include <stdio.h>

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0);
    printf("loading\n");
    int total = 0;
    for (int i = 1; i <= 3; i++) {
        total += i;
    }
    printf("ready %d\n", total);
    return 0;
}
What to look for

Step past the first printf and look at the output panel: loading is there before the loop. After you delete setvbuf, the panel stays empty at every stop and both lines appear only when the run has ended.

Expected output

loading
ready 6

02 / Buffers

Where does written text wait?

Asking the operating system to write takes time, much more than copying bytes in memory. So the C library keeps a buffer inside every FILE stream: an area of memory where written text waits until there is enough to send at once. printf adds text to stdout’s buffer. The text reaches the operating system later, when the buffer is flushed.

So the model from section 01, that a print writes to the screen immediately, needs one addition: written text can still be inside the program. Anything that does not go through the buffer can arrive first, such as write: a system call, which is a direct request to the operating system.

main.c
/* into stdout's buffer */
printf("1 queued\n");
/* straight to the operating system */
write(STDOUT_FILENO, "2 direct\n", 9);
fflush(stdout);

A buffer holds written text inside the program. Until it is sent on, nothing outside the program can see it.

Coming from Java: Java’s System.out flushes at every println; C’s stdout to a pipe or file holds text until fflush, exit, or a full buffer.

Precisely: what the stepper can see

The stepper runs C under GDB with standard output sent to a file, and reads that file at each stop. It cannot look inside the C library, so text in stdout’s buffer stays invisible until it is flushed. The C standard does not fix the buffer’s size or exactly when a full buffer is sent. This is also why the C experiments in other lessons start with setvbuf(stdout, NULL, _IOLBF, 0): it makes their output appear line by line in the stepper, as in a terminal.

Experiment

Make a direct write arrive before a buffered one

printf adds text to stdout’s buffer, which the stepper’s file output makes a full buffer. write is a system call: it passes bytes to the operating system at once, past the buffer. fflush sends the buffer on, and a normal exit flushes it again.

Why it matters On a terminal this program prints 1, 2, 3; redirected to a file or pipe it prints 2, 1, 3. Standard error behaves like the direct path here, which is why error messages can appear before output printed earlier.

Predict the order of the three lines in the stepper. Then add setvbuf(stdout, NULL, _IOLBF, 0); as the first line of main and predict again.

#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("1 queued\n");
    write(STDOUT_FILENO, "2 direct\n", 9);
    fflush(stdout);
    printf("3 queued\n");
    return 0;
}
What to look for

Watch the output panel at each stop. 2 direct appears at the stop after write. 1 queued appears only at the stop after fflush. 3 queued appears only when the run has ended. After the edit, each line appears at the stop after its own call.

Expected output

2 direct
1 queued
3 queued

03 / Flushing

What moves text out of a buffer?

To flush a buffer is to send the text waiting in it on to the operating system now. Buffers are flushed in four common ways. The program asks for it: an explicit flush. A newline is written: line buffering. The buffer is full and needs room. And some buffers, in some languages, are flushed when the program exits normally. Which of them apply depends on the language and on the buffer.

The C standard names three buffering modes. An unbuffered stream sends text as soon as possible. A line-buffered stream sends it at each newline. A fully buffered stream sends it when the buffer is full. In every mode, fflush(stream) sends the buffer now, and exit flushes every stream when the program ends normally, including by returning from main. setvbuf chooses the mode, but only before any other operation on that stream.

main.c
printf("Name: ");
fflush(stdout); /* a prompt has no newline */

Flushing moves text from a buffer to the operating system. A partial line needs fflush in every mode except unbuffered.

Precisely: a flush is not a disk write

fflush hands bytes to the operating system. For a file, the system may keep them in memory before writing them to the disk; POSIX fsync asks it to finish. The standard also intends line-buffered output to be sent when the program asks for input, which is why a prompt often appears on a terminal without fflush.

04 / Terminals and pipes

Why can the same program print differently in a terminal?

Many buffers change their behavior depending on where output goes. A person at a terminal wants to see each line as soon as it is printed. A program reading from a pipe, or a file being written, does better with fewer, larger writes. So a common choice is line buffering for a terminal and full buffering for a pipe or a file. A fully buffered stream is flushed only when its buffer fills up or when the program asks.

C makes exactly this choice. The standard says stdout is fully buffered if and only if it can be determined not to refer to an interactive device, and that stderr is not fully buffered. With the GNU C library, stdout is line-buffered on a terminal and fully buffered to a pipe or file, and stderr is unbuffered. The stepper sends stdout to a file, so it is fully buffered there: in the buffers experiment, 2 direct arrives before 1 queued. In a terminal, the same program prints 1, 2, 3.

main.c
#include <stdio.h>
#include <unistd.h>

int main(void) {
    if (isatty(STDOUT_FILENO)) {
        printf("terminal: line buffered\n");
    } else {
        printf("pipe or file: fully buffered\n");
    }
    return 0;
}

With common C libraries such as glibc, the same printf call is line-buffered in a terminal and fully buffered in a pipe or file. Know where your output goes.

Coming from Python: Python makes the same choice for sys.stdout: line-buffered on a terminal, block-buffered to a pipe or file.

Precisely: forcing a mode

setvbuf(stdout, NULL, _IOLBF, 0) as the first operation on stdout makes it line-buffered wherever it goes, which is what the first experiment does; _IONBF makes it unbuffered. On Linux, stdbuf -oL can do the same for a program you cannot change, if it relies on the C library’s defaults. macOS follows the same defaults: its setbuf manual page says stdout is line-buffered when it refers to a terminal and fully buffered otherwise.

05 / Two streams

Why do errors and results come out of order?

Standard output and standard error are separate streams, so they can go to different places. Whoever starts a program can send its standard output to a file while standard error still appears in the terminal. That is why results go to standard output and problems to standard error: a program that reads the results never sees the warnings.

The two streams can also go to the same file. Then the order of lines in that file depends on buffering. If standard output is buffered and standard error is not, a warning can appear in the file before results that the program wrote earlier.

In C this happens every day: redirected stdout is fully buffered and stderr is unbuffered. When both go to one file, error messages can appear before results printed earlier. Flush stdout before writing to stderr when order matters.

main.c
for (int i = 0; i < count; i++) {
    if (valid(values[i])) {
        printf("%d\n", convert(values[i]));
    } else {
        fflush(stdout); /* earlier results first */
        fprintf(stderr, "warning: skipped %s\n", values[i]);
    }
}

Results go to standard output, problems to standard error. Flush stdout before writing to stderr when both may share a file.

Precisely: redirecting in a terminal, and the output panel

In a terminal, tool > results.txt runs a program named tool, and > sends its standard output to the file results.txt. tool > run.log 2>&1 sends both streams to run.log. Read 2>&1 one piece at a time: 2 is the number of standard error, > sends it somewhere, and &1 means stream 1, standard output, not a file named 1. So 2>&1 means “send standard error where standard output goes”. The stepper writes standard error to a separate file and does not show it. The buffers experiment uses write(STDOUT_FILENO, ...) as the unbuffered path; stderr behaves the same way.

06 / Lost output

What happens to waiting text when a program stops early?

Text in a buffer is still inside the program. If the program stops before the buffer is flushed, that text is gone. This happens when a program crashes, when the operating system stops it, and when it ends through a call that skips the normal cleanup.

In C, exit flushes every open stream, and returning from main calls exit. _exit (POSIX) and _Exit (C99) end the process without that cleanup. A crash such as a segmentation fault skips it too, and so does abort on current systems, so text still in a buffer is lost. That is how a program can crash after printing a debug line that never appears: the line was still in the buffer.

main.c
printf("saved\n");
fflush(stdout);
printf("lost\n"); /* still in the buffer */
_exit(0);

Text you printed is safe only after it has been flushed. Before a program may stop abruptly, flush what must survive.

Precisely: _Exit and debugging

The C standard leaves it to the implementation whether _Exit and abort flush streams. The GNU C library flushes in neither: abort stopped flushing in glibc 2.27. To keep debug output through a crash, write it to stderr, or call setvbuf(stdout, NULL, _IONBF, 0) at the start of main.

All of this describes hosted C, running under an operating system. On a microcontroller, firmware usually never returns from main, so exit’s flush never happens. Its stdio often comes from a small library such as newlib, retargeted to send output to a serial port, and how that library buffers depends on its configuration.

Experiment

Exit while text is still in a buffer

Returning from main calls exit, which flushes every open stream. _exit ends the process at once, without touching stdio buffers, so lost is thrown away with the process. A crash such as a segmentation fault loses buffered text the same way.

Why it matters Text you printed is not safe until it has been flushed. Before a program may stop abruptly, fflush what must survive.

Predict whether lost is printed. Then replace _exit(0) with return 0; and predict again.

#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("saved\n");
    fflush(stdout);
    printf("lost\n");
    _exit(0);
}
What to look for

Watch the output panel. saved appears at the stop after the flush. lost never appears, not even after the run has ended.

Expected output

saved

07 / Try it

Can you predict what reaches the output?

This program’s output goes to a pipe. Predict the output before revealing the answer, and decide first how stdout is buffered.

main.c
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("one\n");
    fflush(stdout);
    printf("two\n");
    write(STDOUT_FILENO, "three\n", 6);
    _exit(0);
}

What does this print through a pipe, and what would change in a terminal?

Reveal the reasoning

one, then three. fflush sends one. two waits in stdout’s buffer, which is fully buffered because output goes to a pipe. write sends three straight to the operating system. _exit then ends the process without flushing, so two is lost. In a terminal, stdout is line-buffered, so two is sent at its newline and the output is one, two, three.

Practice tickets

  1. IO-101 · Show progress while photos copy → Free
  2. IO-102 · Keep the move journal after a crash → Sign in · Pro
  3. IO-103 · Separate results from warnings → Sign in · Pro

Keep exploring

Stack and heap →

Primary references