Input, output and files · about 45 min

Streams, buffering and stderr

Why this matters

Unix programs are glued together with pipes and redirection: ./extract < log.txt | sort > report.txt. For your C programs to play well in such pipelines they need to read from standard input, write results to standard output, and send complaints somewhere else, so the complaints do not pollute the data. You also need to understand buffering, because it explains some of the most confusing behavior in C: output that appears in the wrong order, or not at all when a program crashes.

Three streams are open before main starts

When a process starts, it inherits three open file descriptors from whoever launched it (usually the shell). A file descriptor is just a small integer the kernel uses to identify an open file, pipe or terminal for that process:

descriptorC streampurposedefault
0stdinstandard inputthe keyboard (terminal)
1stdoutstandard output: the program's resultsthe terminal
2stderrstandard error: diagnostics, warnings, progressthe terminal

The C library wraps each descriptor in a stream, an object of type FILE that you always handle through a pointer, FILE *. stdin, stdout and stderr are three such pointers, declared in <stdio.h>. printf(...) is shorthand for fprintf(stdout, ...), and getchar() is shorthand for getc(stdin).

   your program                       the kernel                 the outside world

   printf ---> FILE *stdout --buffer--> fd 1 ------------------> terminal, file or pipe
   fprintf(stderr,...) --> FILE *stderr --> fd 2 --------------> terminal (usually)
   scanf  <--- FILE *stdin  <--buffer-- fd 0 <------------------ keyboard, file or pipe

The shell decides where the descriptors point before your program starts:

$ ./prog < in.txt          # fd 0 reads from in.txt
$ ./prog > out.txt         # fd 1 writes to out.txt (truncating it)
$ ./prog >> out.txt        # fd 1 appends to out.txt
$ ./prog 2> err.txt        # fd 2 writes to err.txt
$ ./prog > all.txt 2>&1    # fd 2 goes wherever fd 1 goes
$ ./a | ./b                # a's fd 1 is the write end of a pipe, b's fd 0 the read end

Your program does not know or care. It writes to stdout, and the bytes land in a file, a pipe or a terminal. That is what makes small programs composable.

A filter: copy stdin to stdout

The classic Unix program reads input byte by byte and writes it out. Here is one that also reports what it did, on stderr:

#include <stdio.h>

int main(void)
{
    int c;
    long n = 0;
    while ((c = getchar()) != EOF) {
        putchar(c);
        n++;
    }
    fprintf(stderr, "copied %ld bytes\n", n);
    return 0;
}
$ printf 'hello\nworld\n' > in.txt
$ ./copy < in.txt > out.txt
copied 12 bytes
$ cat out.txt
hello
world
$ ./copy < in.txt 2>/dev/null
hello
world

Points to notice:

Buffering: why output is not written immediately

Every write to a file descriptor is a system call: the program stops, the kernel takes over, copies the bytes, and returns. System calls are expensive compared with ordinary function calls, so calling the kernel once per character would make output very slow. The C library therefore collects output in a buffer, an array inside the FILE object (8192 bytes, BUFSIZ, in glibc), and hands it to the kernel in large chunks.

There are three buffering modes, and the library picks one per stream:

modethe buffer is flushed when...used for
unbufferedimmediately, on every callstderr always
line buffereda newline is written, or the buffer fillsstdout when it is a terminal
fully bufferedthe buffer fillsstdout when it is a file or pipe; files you open

Buffers are also flushed when the program calls exit or returns from main, when you call fflush(stream), when you close the stream with fclose, and (for line-buffered stdout) when the program reads from stdin, so prompts appear before the program waits for input.

The decision between line and full buffering is made at run time by asking "is this descriptor a terminal?". That is why the same program can behave differently with and without a pipe.

Watch it happen

#include <stdio.h>

int main(void)
{
    printf("a");
    fprintf(stderr, "b");
    printf("c\n");
    fprintf(stderr, "d\n");
    return 0;
}

Run in a terminal, stdout is line buffered. Trace each call:

 call                     stdout buffer      appears on screen
 printf("a")              "a"                (nothing)
 fprintf(stderr, "b")     "a"                b
 printf("c\n")            "" (flushed)       bac      <- "ac\n" written after "b"
 fprintf(stderr, "d\n")   ""                 bac
                                             d

Terminal output:

$ ./order
bac
d

Now send both streams through a pipe. stdout becomes fully buffered, so "a" and "c\n" wait until exit flushes them at the very end:

$ ./order 2>&1 | cat
bd
ac

Buffering and crashes

If the process dies abnormally (a segmentation fault, or abort()), exit never runs, and whatever is still in the buffer is lost:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("starting\n");
    abort();
}
$ ./crash
starting
Aborted (core dumped)
$ ./crash > out.txt
Aborted (core dumped)
$ cat out.txt
$

In the terminal you see "starting"; redirected to a file, nothing. This is a classic debugging trap: you add printf calls to find where a program crashes, redirect the output to a file, and the last messages before the crash vanish, pointing you at the wrong place. Two fixes:

You can also change a stream's mode with setvbuf, for example setvbuf(stdout, NULL, _IONBF, 0); to make stdout unbuffered, but that makes heavy output slow.

Input is buffered too

stdin is buffered in the other direction: the library asks the kernel for a big chunk, then getchar and scanf hand it out piece by piece. On a terminal, the kernel itself also holds your typing until you press Enter (so you can use Backspace), which is why a program calling getchar() does not react to a single key press.

Common mistakes

Recap

Going further: read man 3 setvbuf and man 3 stdin. Then try the order program with stdbuf -o0 ./order | cat, which uses a trick to change stdout's buffering from outside the program.

Exercises

Exercise 1 · quiz

Where error messages go

A program is run as ./report > summary.txt. It hits a problem and wants the user to see a message on the terminal, without the message ending up inside summary.txt. Where should it write the message?

Answer
  • stderr, with fprintf(stderr, ...)

> redirects only file descriptor 1, stdout. stderr (descriptor 2) still goes to the terminal, which is exactly why it exists: diagnostics stay visible while the real output is captured or piped into another program. To capture stderr too you would write 2> errors.txt or 2>&1.

Exercise 2 · write code

Data to stdout, warnings to stderr

Read integers from standard input until end of input (use scanf("%d", &n) == 1 as the loop condition). Add up the numbers that are zero or positive. For each negative number, print a warning to stderr in exactly this form:

warning: ignoring -5

When input ends, print the result to stdout in exactly this form, and exit with status 0:

sum: 12

For the input 4 -5 8 -1, stdout must contain only sum: 12 and stderr must contain the two warning lines. The check looks at both streams separately.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int n;
    int sum = 0;
    while (scanf("%d", &n) == 1) {
        /* warn about negatives on stderr, add the rest */
        sum += n;
    }
    printf("sum: %d\n", sum);
    return 0;
}

Example

input

1 2 3

expected output

sum: 6

Example

input

4 -5 8 -1

expected output

sum: 12
Hint 1

fprintf is printf with an extra first argument: the stream to write to.

Hint 2

fprintf(stderr, "warning: ignoring %d\n", n); inside an if (n < 0); otherwise add n to sum.

Reference solution
#include <stdio.h>

int main(void)
{
    int n;
    int sum = 0;
    while (scanf("%d", &n) == 1) {
        if (n < 0) {
            fprintf(stderr, "warning: ignoring %d\n", n);
        } else {
            sum += n;
        }
    }
    printf("sum: %d\n", sum);
    return 0;
}

Exercise 3 · quiz

Predict the interleaving

This program is run as ./prog 2>&1 | cat, so both streams end up in the same pipe, and the terminal shows whatever arrives in the order it arrives.

#include <stdio.h>

int main(void)
{
    printf("a");
    fprintf(stderr, "b");
    printf("c\n");
    fprintf(stderr, "d\n");
    return 0;
}

What appears?

Answer
  • bd on one line, then ac

stderr is unbuffered, so "b" and "d\n" are written the moment fprintf is called. stdout is going into a pipe, not a terminal, so it is fully buffered: "a" and "c\n" sit in the buffer until the program exits, when exit() flushes them. Result: "bd" first, then "ac". If you run the same program directly in a terminal, stdout is line buffered instead, and you would see "bac" then "d". Same program, different output order, depending only on where stdout points.

Exercise 4 · write code

Do not lose the last words

The starter prints a progress message and then crashes on purpose with abort() (which kills the process immediately, without the normal cleanup that exit does). When stdout is a pipe or a file, as it is in the checker, the message never appears: it was still in the buffer.

Fix the program so that checkpoint 1 reached is guaranteed to be written before the crash, without removing the crash. The expected stdout is exactly:

checkpoint 1 reached

(The program will still die from abort(); the exit status is not checked.)

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("checkpoint 1 reached\n");
    /* ... imagine a bug here ... */
    abort();
}

Example

expected output

checkpoint 1 reached
Hint 1

The \n does not flush stdout when it is not a terminal.

Hint 2

fflush(stdout); pushes the buffer's contents to the operating system right now.

Reference solution
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("checkpoint 1 reached\n");
    fflush(stdout);
    /* ... imagine a bug here ... */
    abort();
}