stdio Library Buffering in Linux: Complete Developer Guide
📦 stdio Library Buffering
printf() and fwrite() add their own buffer on top of the kernel. Here’s how to control it.
In Part 1, we saw the kernel buffer cache. But there’s another buffer that comes before that — inside the C standard library itself. When you call printf() or fwrite(), the C library doesn’t call write() every time. It stores data in its own buffer and calls write() only when the buffer fills up. This is stdio buffering — and understanding it prevents hard-to-find bugs.
1. The Three Buffering Modes
The C standard library supports three modes of buffering for streams (FILE * objects):
_IONBF — No Buffering
Every single character you write triggers a write() system call immediately. No data is held back.
Default for: stderr
Use when: You need every byte delivered immediately (error logs, debug output)
_IOLBF — Line Buffering
Data accumulates until a newline ('\n') is written (or buffer fills), then write() is called.
Default for: stdout when connected to a terminal
Use when: Interactive terminal output, line-oriented protocols
_IOFBF — Full Buffering
Data accumulates in a fixed buffer. write() is called only when the buffer is full (or stream is flushed/closed).
Default for: stdout/stdin when redirected to a file
Use when: Disk files — maximum efficiency
Visual: Where stdio Buffer Sits
printf(“hello”); fwrite(buf, 1, n, fp); fputs(str, fp);
Controlled by: setvbuf() · setbuf() · fflush()
Default size: 8192 bytes (BUFSIZ)
Controlled by: fsync() · fdatasync() · O_SYNC
2. setvbuf() — Set Buffering Mode
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
/* Returns: 0 on success, nonzero on error */
Parameters:
stream— the FILE* to configure (e.g., stdout, or a file you opened with fopen())buf— pointer to your own buffer (or NULL to let the library allocate one)mode— one of_IONBF,_IOLBF,_IOFBFsize— size of the buffer (ignored if buf is NULL in glibc)
⚠️ Rule: You must call setvbuf() before any I/O operations on the stream. Call it right after opening the file.
Code Example: Using setvbuf()
#include <stdio.h>
#include <stdlib.h>
int main(void) {
/* --- Example 1: Disable buffering on stdout --- */
/* Useful when you want every printf() to appear immediately */
if (setvbuf(stdout, NULL, _IONBF, 0) != 0) {
perror("setvbuf stdout unbuffered");
return 1;
}
printf("This appears immediately (no buffering)\n");
/* --- Example 2: Use your own buffer for a file --- */
FILE *fp = fopen("data.txt", "w");
if (!fp) { perror("fopen"); return 1; }
#define MY_BUF_SIZE 8192
/* IMPORTANT: buf must be static or heap — NOT a local stack variable
because it must survive until the stream is closed */
static char my_buf[MY_BUF_SIZE];
/* Set full buffering with our custom buffer */
if (setvbuf(fp, my_buf, _IOFBF, MY_BUF_SIZE) != 0) {
perror("setvbuf file");
fclose(fp);
return 1;
}
/* These writes go to my_buf first — no system call yet */
fputs("Line 1\n", fp);
fputs("Line 2\n", fp);
/* ... more writes ... */
fclose(fp); /* Flushes my_buf to kernel buffer and disk */
/* --- Example 3: Line buffering for a log file --- */
FILE *log = fopen("app.log", "a");
if (!log) { perror("fopen log"); return 1; }
/* Each fprintf ending with '\n' triggers a write() */
setvbuf(log, NULL, _IOLBF, 0);
fprintf(log, "App started: timestamp=%ld\n", (long)0);
fprintf(log, "Processing...\n"); /* Each line written immediately */
fclose(log);
return 0;
}
static or malloc() instead.3. setbuf() and setbuffer() — Simpler Wrappers
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
/* buf = NULL → _IONBF (no buffering) */ /* buf = pointer → _IOFBF with BUFSIZ (8192) bytes */
/* BSD extension — not in SUSv3, but available on most UNIX */ #define _BSD_SOURCE
void setbuffer(FILE *stream, char *buf, size_t size);
Both are simplified wrappers around setvbuf(). The call setbuf(fp, buf) is equivalent to:
setvbuf(fp, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ);
/* BUFSIZ is defined in stdio.h — typically 8192 in glibc */
Code Example: setbuf() and setbuffer()
#include <stdio.h>
#define _BSD_SOURCE
int main(void) {
FILE *fp = fopen("output.txt", "w");
if (!fp) { perror("fopen"); return 1; }
/* Method 1: setbuf(fp, NULL) — disable buffering */
setbuf(fp, NULL);
fputs("This goes to kernel immediately\n", fp);
fclose(fp);
/* Method 2: setbuf(fp, buf) — full buffering with BUFSIZ */
static char buf[BUFSIZ];
fp = fopen("output2.txt", "w");
if (!fp) { perror("fopen"); return 1; }
setbuf(fp, buf); /* Equivalent: setvbuf(fp, buf, _IOFBF, BUFSIZ) */
fputs("Buffered output\n", fp);
fclose(fp);
/* Method 3: setbuffer — full buffering with custom size */
static char small_buf[256];
fp = fopen("output3.txt", "w");
if (!fp) { perror("fopen"); return 1; }
setbuffer(fp, small_buf, sizeof(small_buf));
fputs("Custom buffer size\n", fp);
fclose(fp);
return 0;
}
4. fflush() — Force Flush at Any Time
#include <stdio.h>
int fflush(FILE *stream);
/* Returns: 0 on success, EOF on error */ /* stream = NULL → flushes ALL open stdio streams */
fflush(fp) forces all data in the stdio buffer to be passed to the kernel via write(). It does not guarantee data is on disk — it only moves data from stdio buffer to kernel buffer cache. To guarantee disk write, follow with fsync(fileno(fp)).
Automatic flush happens when:
- The buffer fills up
- A newline is written (line-buffered mode)
- The stream is closed with
fclose() - The program exits normally (via
exit()) - In glibc: when reading from
stdin(implicitly flushesstdout)
Code Example: fflush() Use Cases
#include <stdio.h>
#include <unistd.h> /* for sleep() */
#include <string.h>
/* Use case 1: Progress bar / interactive prompts */
void show_progress(void) {
for (int i = 0; i <= 100; i += 10) {
printf("\rProgress: [");
for (int j = 0; j < i/10; j++) printf("=");
for (int j = i/10; j < 10; j++) printf(" ");
printf("] %d%%", i);
/* Without fflush, nothing appears on terminal until '\n' or buffer fills
because stdout is line-buffered when connected to terminal.
When redirected to a file/pipe, stdout becomes FULLY buffered! */
fflush(stdout);
sleep(1); /* Simulate work */
}
printf("\nDone!\n");
}
/* Use case 2: Flushing before a prompt */
void ask_user(void) {
printf("Enter your name: "); /* No newline at the end! */
fflush(stdout); /* Must flush or prompt may not appear */
char name[64];
fgets(name, sizeof(name), stdin);
printf("Hello, %s", name);
}
/* Use case 3: Flush all streams before fork() */
#include <unistd.h>
void safe_fork(void) {
/* If you fork() without flushing, child inherits the stdio buffer
with unflushed data. Both parent and child will eventually write
that data — causing duplicate output! */
fflush(NULL); /* Flush ALL open streams */
pid_t pid = fork();
if (pid == 0) {
/* child: do child work */
_exit(0); /* Use _exit() in child — not exit() — to avoid flushing again */
}
/* parent continues */
}
/* Use case 4: Guaranteed disk write */
#include <fcntl.h> /* for fileno() */
void guaranteed_disk_write(FILE *fp, const char *data) {
fputs(data, fp);
fflush(fp); /* Moves stdio buf → kernel buf */
fsync(fileno(fp)); /* Moves kernel buf → disk */
/* Now data is guaranteed on disk */
}
int main(void) {
show_progress();
ask_user();
return 0;
}
fork(), the buffer is copied to the child. Both parent and child will flush it on exit, causing duplicate output. Always call fflush(NULL) before fork().5. Practical Scenarios and Gotchas
Scenario: stdout Behavior Changes When Redirected
/* save as demo.c */
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Line 1\n");
write(STDOUT_FILENO, "write() output\n", 15);
printf("Line 2\n");
return 0;
}
/* Compile: gcc -o demo demo.c
Running interactively (stdout = terminal, LINE-buffered):
$ ./demo
Line 1 ← printf flushes on '\n'
write() output ← goes directly to fd 1
Line 2
Running with redirection (stdout = file, FULLY-buffered):
$ ./demo > output.txt && cat output.txt
write() output ← write() bypasses stdio buffer, goes to kernel directly
Line 1 ← printf output flushed at exit, AFTER write()
Line 2
The order changes! This surprises many developers.
Fix: add fflush(stdout) before the write() call.
*/
Code Example: Correctly Mixing printf and write
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(void) {
/* Always flush before mixing write() and printf() on same fd */
printf("This comes from printf — part 1. ");
fflush(stdout); /* ← Critical! */
write(STDOUT_FILENO, "This from write(). ", 19);
printf("Back to printf.\n");
fflush(stdout); /* Flush again before next write() */
write(STDOUT_FILENO, "Final write.\n", 13);
return 0;
}
/* Output will now always be in the correct order */
Code Example: Embedded System — Unbuffered Serial Log
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
/* In embedded systems, you often want logs to appear immediately.
This logger disables stdio buffering so every log line is written
to the kernel buffer right away. */
static FILE *log_fp = NULL;
void logger_init(const char *filename) {
if (filename) {
log_fp = fopen(filename, "a");
} else {
log_fp = stderr; /* stderr is already unbuffered by default */
}
if (!log_fp) { perror("log init"); return; }
/* Disable buffering — each log_write() will immediately call write() */
setvbuf(log_fp, NULL, _IONBF, 0);
}
void log_write(const char *level, const char *fmt, ...) {
if (!log_fp) return;
time_t now = time(NULL);
struct tm *t = localtime(&now);
fprintf(log_fp, "[%02d:%02d:%02d] [%s] ",
t->tm_hour, t->tm_min, t->tm_sec, level);
va_list args;
va_start(args, fmt);
vfprintf(log_fp, fmt, args);
va_end(args);
fputc('\n', log_fp);
/* No fflush needed — _IONBF means each write is immediate */
}
int main(void) {
logger_init("/tmp/app.log");
log_write("INFO", "System initialized");
log_write("DEBUG", "Sensor value = %d", 42);
log_write("ERROR", "BLE connection dropped, retrying...");
/* Even if program crashes, all logs are safely in kernel buffer */
return 0;
}
🎯 Interview Questions – stdio Buffering
✅ Summary of Part 2
- stdio has its own buffer before calling the kernel’s write(). This is Layer 1.
- Three modes: _IONBF (no buffer), _IOLBF (line), _IOFBF (full).
- stderr = unbuffered. stdout to terminal = line-buffered. stdout to file = fully buffered.
- Use
setvbuf()to change mode. Must call before any I/O on that stream. fflush(fp)forces stdio → kernel. Usefflush(NULL)to flush everything.- Always
fflush(NULL)beforefork(). Use_exit()in the child. fflush()alone does NOT put data on disk — usefsync()for that.
