---
title: Buffered I/O (stdio)
url: https://doc.liz6.com/en/systems-programming/05-io-and-file-systems/01-buffered-i-o-stdio
locale: en
area: systems-programming
tags:
- systems-programming
- io-and-file-systems
date: 2026-06-30
modified: 2026-07-16
description: 'Coverage: setvbuf three strategies → fread/fwrite buffering mechanism → user-space buffering vs kernel page cache → line buffering and terminals → custom buffering Applicable to: glibc stdio, musl'
---

# Buffered I/O (stdio)

> Coverage: setvbuf three strategies → fread/fwrite buffering mechanism → user-space buffering vs kernel page cache → line buffering and terminals → custom buffering
> Applicable to: glibc stdio, musl

## Overview

`fread`/`fwrite` do not directly invoke the `read`/`write` system calls—they maintain buffers in user space. This is why `fwrite(buf, 1, 1, fp)` does not trigger a system call (until `fflush` is called or the buffer is full). Understanding this layer is fundamental to troubleshooting issues like "why isn't printf output appearing" or "why is fwrite faster than write."

## setvbuf: Three Buffering Modes

```c
#include <stdio.h>

// _IONBF: Unbuffered → each fwrite triggers a syscall directly
setvbuf(fp, NULL, _IONBF, 0);

// _IOLBF: Line-buffered → flush on '\n' or when buffer is full
setvbuf(fp, NULL, _IOLBF, BUFSIZ);  // BUFSIZ = 8192 (glibc)

// _IOFBF: Fully buffered → flush only when buffer is full
setvbuf(fp, NULL, _IOFBF, BUFSIZ);

// Default strategies:
//   stderr:      Unbuffered (_IONBF)
//   tty (stdout): Line-buffered (_IOLBF)
//   Regular files: Fully buffered (_IOFBF), size = BUFSIZ
```

## FILE Internal Structure (glibc Simplified)

```c
// glibc: libio/fileops.c, libio/genops.c
struct _IO_FILE {
    int     _flags;         // _IO_UNBUFFERED, _IO_LINE_BUF, ...
    char    *_IO_read_ptr;  // Current read pointer
    char    *_IO_read_end;  // End of read buffer (buffer + bytes_read)
    char    *_IO_write_ptr; // Current write pointer
    char    *_IO_write_end; // End of write buffer (buffer + BUFSIZ)
    char    *_IO_buf_base;  // Start of buffer
    char    *_IO_buf_end;   // End of buffer

    int     _fileno;        // Corresponding file descriptor
    // ... locks, wide char, fallback, ...
};

// fwrite(buf, 1, n, fp):
//   1. If n <= (fp->_IO_write_end - fp->_IO_write_ptr):
//      → memcpy to buffer → _IO_write_ptr += n → return n
//      (Very fast! Purely user-space, no syscall)
//   2. Otherwise: flush first (write(fd, buffer, ...)) → then memcpy
//   3. If line-buffered and buf contains '\n': flush up to '\n' position
```

## User-Space Buffering vs Kernel Page Cache

```
fwrite writing 1 byte × 1000 times:
  Fully buffered mode:    1 write syscall (flush buffer) → ~1 kernel call
  Unbuffered mode:        1000 write syscalls → ~1000 kernel calls
  Performance difference: ~100x

Kernel write writing 1 byte × 1000 times to a file:
  Page cache may coalesce (if within the same page) → 1 disk IO
  But each write() incurs syscall overhead (~200ns each)

Typical combination:
  fwrite: User-space batching (reduces number of syscalls)
  page cache: Kernel-level batching (reduces number of disk IOs)
```

## Pitfalls of Line Buffering

```c
// Terminal output (stdout is line-buffered by default):
printf("hello");        // Enters buffer, no output (no '\n')
fprintf(stderr, "!");   // stderr is unbuffered → immediately outputs "!"
printf(" world\n");     // Enters buffer + '\n' → flush → outputs "hello world"

// Output: "!hello world" (not the expected order!)

// Fix:
printf("hello");
fflush(stdout);         // Force flush
fprintf(stderr, "!");
printf(" world\n");

// Or: Disable stdout buffering from the start:
setvbuf(stdout, NULL, _IONBF, 0);
```

## fork and Buffering

```c
// Classic pitfall:
printf("before fork\n");  // Data in buffer but no '\n'? → But '\n' is present → flush
fork();
// OK: flushed before fork

printf("before fork");    // Data in buffer, no '\n'
fork();
// Child process copies the entire FILE structure (including buffer!)
// → Both parent and child will output "before fork" on subsequent flushes
// → Output appears twice!

// Rule: Always fflush(NULL) before fork, or use line buffering to ensure flushing
```

## Custom Buffering Strategies

```c
// Provide your own buffer (instead of glibc's default BUFSIZ allocation):
char mybuf[4096];
setvbuf(fp, mybuf, _IOFBF, sizeof(mybuf));

// Note: The lifetime of mybuf must be ≥ the lifetime of fp
// (glibc does not copy the buffer, it only stores the pointer)
```

## References

- **man**: setvbuf(3), fflush(3), fwrite(3)
- **glibc source**: `libio/fileops.c`, `libio/genops.c`
- **LWN**: "The GNU C Library's stdio implementation"

*Keywords: setvbuf, _IONBF, _IOLBF, _IOFBF, fflush, BUFSIZ, FILE, fork buffer, line buffering*
