🔀 Mixing stdio and System Calls
fileno(), fdopen(), and how to safely combine both worlds.
Sometimes you need to use both the stdio library (printf, fgets) and raw system calls (read, write, ioctl) on the same file. For example: you open a socket file descriptor with socket(), but want to use fprintf() on it. Or you have a FILE* but need its underlying descriptor for fcntl(). Linux provides two functions for exactly this: fileno() and fdopen().
1. fileno() — Get fd from FILE*
#include <stdio.h>
int fileno(FILE *stream);
/* Returns: file descriptor on success, -1 on error */
fileno(fp) takes a stdio FILE* and returns the underlying integer file descriptor that the stdio library uses internally. You can then use this fd with any system call that accepts a file descriptor.
Common uses:
- Call
fsync(fileno(fp))to flush to disk after writing via stdio - Call
fstat(fileno(fp), &st)to get file metadata - Call
fcntl(fileno(fp), F_SETFL, ...)to change file flags - Call
dup(fileno(fp))to duplicate a stdio stream’s descriptor
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void) {
FILE *fp = fopen("data.txt", "w");
if (!fp) { perror("fopen"); return 1; }
/* Write via stdio */
fprintf(fp, "Hello from stdio!\n");
/* --- Use 1: Get fd and call fsync() for guaranteed disk write --- */
int fd = fileno(fp);
printf("FILE* fp uses file descriptor: %d\n", fd);
fflush(fp); /* STEP 1: flush stdio buffer → kernel buffer */
fsync(fd); /* STEP 2: kernel buffer → disk */
printf("Data is now guaranteed on disk\n");
/* --- Use 2: Get file info via fstat() --- */
struct stat st;
if (fstat(fd, &st) == 0) {
printf("File size: %lld bytes\n", (long long)st.st_size);
printf("File inode: %lu\n", (unsigned long)st.st_ino);
}
/* --- Use 3: Check if it's a regular file or terminal --- */
if (isatty(fileno(stdout))) {
printf("stdout is a terminal (line-buffered mode)\n");
} else {
printf("stdout is redirected (fully-buffered mode)\n");
}
fclose(fp); /* This also closes the underlying fd */
return 0;
}
/* Important: fileno(stdin) = 0, fileno(stdout) = 1, fileno(stderr) = 2 */
2. fdopen() — Create FILE* from fd
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
/* Returns: FILE* on success, NULL on error */ /* mode: “r” read, “w” write, “a” append, “r+” read+write, etc. */
fdopen(fd, mode) creates a new FILE* stream that wraps an existing file descriptor. This is the reverse of fileno().
This is especially important for file types that you can only obtain as a file descriptor — not via fopen():
- Sockets:
socket()returns an fd. Usefdopen()to usefprintf(),fgets()on it. - Pipes:
pipe(),popen()return fds. - Special files:
open()with special flags likeO_TMPFILE.
The mode must be compatible with the access mode of fd. You cannot open an fd that was opened read-only as “w”.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
/* Example 1: Wrap an open() fd with stdio */
int main_example1(void) {
/* Open a file with special flags (e.g., O_SYNC) using open() */
int fd = open("/tmp/sync_output.txt",
O_WRONLY | O_CREAT | O_TRUNC | O_SYNC,
0644);
if (fd == -1) { perror("open"); return -1; }
/* Now wrap it with a FILE* so we can use fprintf() */
FILE *fp = fdopen(fd, "w");
if (!fp) { perror("fdopen"); close(fd); return -1; }
/* Use stdio functions — every buffer flush will trigger a sync write */
fprintf(fp, "Line 1: %d\n", 100);
fprintf(fp, "Line 2: %s\n", "hello");
/* fclose() closes the FILE* AND the underlying fd */
/* Do NOT call close(fd) separately after fclose(fp) — double close bug! */
fclose(fp);
return 0;
}
/* Example 2: Use stdio on a socket (network programming) */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int use_stdio_on_socket(void) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) { perror("socket"); return -1; }
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
};
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect"); close(sock); return -1;
}
/* Wrap the socket fd for reading */
FILE *in_fp = fdopen(sock, "r");
/* Duplicate fd for writing (we need two FILEs for one socket in "r"/"w" modes) */
FILE *out_fp = fdopen(dup(sock), "w");
if (!in_fp || !out_fp) { perror("fdopen"); close(sock); return -1; }
/* Now use convenient stdio functions on the socket */
fprintf(out_fp, "GET / HTTP/1.0\r\n\r\n");
fflush(out_fp); /* Must flush! stdio is buffered. */
char line[1024];
while (fgets(line, sizeof(line), in_fp) != NULL) {
printf("%s", line);
}
fclose(in_fp);
fclose(out_fp);
return 0;
}
/* Example 3: fdopen on a pipe */
#include <stdio.h>
#include <unistd.h>
int stdio_on_pipe(void) {
int pipefd[2]; /* pipefd[0] = read end, pipefd[1] = write end */
if (pipe(pipefd) == -1) { perror("pipe"); return -1; }
/* Wrap write end as a stdio stream */
FILE *write_end = fdopen(pipefd[1], "w");
if (!write_end) { perror("fdopen write"); return -1; }
/* Wrap read end as a stdio stream */
FILE *read_end = fdopen(pipefd[0], "r");
if (!read_end) { perror("fdopen read"); return -1; }
/* Write via stdio */
fprintf(write_end, "Hello through pipe!\n");
fclose(write_end); /* This closes pipefd[1], signaling EOF to reader */
/* Read via stdio */
char buf[256];
while (fgets(buf, sizeof(buf), read_end) != NULL) {
printf("Received: %s", buf);
}
fclose(read_end);
return 0;
}
int main(void) {
main_example1();
stdio_on_pipe();
return 0;
}
3. The Output Ordering Bug — and How to Fix It
Mixing printf() and write() on the same file descriptor can produce output in the wrong order, because printf() goes through the stdio buffer while write() bypasses it and goes directly to the kernel.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
/* BROKEN: Output order is unpredictable when stdout is redirected */
void broken_example(void) {
printf("From printf: first\n");
write(STDOUT_FILENO, "From write: second\n", 19);
printf("From printf: third\n");
/* When redirected: output.txt may contain:
From write: second
From printf: first
From printf: third <-- write() appears FIRST because it bypassed stdio buffer */
}
/* FIXED: Flush stdio buffer before every write() */
void fixed_example(void) {
printf("From printf: first\n");
fflush(stdout); /* ← Force stdio buffer → kernel before write() */
write(STDOUT_FILENO, "From write: second\n", 19);
fflush(stdout); /* ← Flush any pending stdio data */
printf("From printf: third\n");
/* Now order is always correct */
}
/* BETTER ALTERNATIVE: Use only one approach */
void cleaner_approach(void) {
/* Option A: Use only write() for everything */
const char *msg1 = "First line\n";
const char *msg2 = "Second line\n";
write(STDOUT_FILENO, msg1, strlen(msg1));
write(STDOUT_FILENO, msg2, strlen(msg2));
/* Option B: Use only stdio (printf/fwrite) for everything */
printf("First line\n");
printf("Second line\n");
/* Mixing is the source of problems — avoid unless necessary */
}
/* WHEN YOU MUST MIX: Use a wrapper that always flushes */
ssize_t safe_write(int fd, const void *buf, size_t len) {
/* If fd matches stdout's fd, flush stdio first */
if (fd == fileno(stdout)) fflush(stdout);
if (fd == fileno(stderr)) fflush(stderr);
return write(fd, buf, len);
}
int main(void) {
printf("--- Broken example (redirect to file to see problem) ---\n");
broken_example();
printf("\n--- Fixed example ---\n");
fixed_example();
return 0;
}
/* Test: ./a.out > /tmp/out.txt && cat /tmp/out.txt */
4. Real-World Pattern: Logger with fd Flexibility
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
/* A logger that accepts either a FILE* or an fd — demonstrates both directions */
typedef struct {
FILE *stream; /* stdio stream (may be NULL) */
int raw_fd; /* underlying file descriptor */
} Logger;
/* Initialize from a file path (using fopen → fileno) */
Logger logger_from_file(const char *path) {
Logger L = {0};
L.stream = fopen(path, "a");
if (L.stream) {
L.raw_fd = fileno(L.stream); /* Get the fd for fsync use */
setvbuf(L.stream, NULL, _IOLBF, 0); /* Line-buffered */
}
return L;
}
/* Initialize from an existing fd (using fdopen) */
Logger logger_from_fd(int fd) {
Logger L = {0};
L.raw_fd = fd;
L.stream = fdopen(fd, "a"); /* Wrap fd as stdio stream */
if (L.stream)
setvbuf(L.stream, NULL, _IOLBF, 0);
return L;
}
void logger_write(Logger *L, const char *level, const char *fmt, ...) {
if (!L->stream) return;
time_t now = time(NULL);
char ts[20];
struct tm t;
localtime_r(&now, &t);
strftime(ts, sizeof(ts), "%H:%M:%S", &t);
fprintf(L->stream, "[%s] [%-5s] ", ts, level);
va_list ap;
va_start(ap, fmt);
vfprintf(L->stream, fmt, ap);
va_end(ap);
fprintf(L->stream, "\n");
/* Line buffering means the '\n' triggers fflush automatically */
}
/* Flush all the way to disk — uses fileno to get fd for fsync */
void logger_sync(Logger *L) {
if (L->stream) fflush(L->stream);
if (L->raw_fd >= 0) fsync(L->raw_fd);
}
void logger_close(Logger *L) {
logger_sync(L);
if (L->stream) fclose(L->stream); /* This closes raw_fd too */
L->stream = NULL;
L->raw_fd = -1;
}
int main(void) {
/* Method 1: Open by path */
Logger L1 = logger_from_file("/tmp/app.log");
logger_write(&L1, "INFO", "Application started");
logger_write(&L1, "DEBUG", "BLE init: addr=%s", "AA:BB:CC:DD:EE:FF");
logger_write(&L1, "ERROR", "Connection failed, err=%d", -5);
logger_sync(&L1); /* Ensure on disk */
logger_close(&L1);
/* Method 2: Use an existing fd (e.g., syslog socket, pipe, etc.) */
int fd = open("/tmp/raw_log.txt", O_WRONLY|O_CREAT|O_APPEND, 0644);
Logger L2 = logger_from_fd(fd);
logger_write(&L2, "INFO", "Logger from raw fd = %d", fd);
logger_close(&L2); /* Also closes fd */
return 0;
}
fclose(fp), do NOT also call close(fileno(fp)). fclose() already closes the underlying fd. Calling close() again is a double-close bug — it could close a different file that was assigned the same fd number.🎯 Interview Questions – Mixing stdio and System Calls
📚 Complete Chapter Summary — File I/O Buffering
🗄️ Kernel Buffer Cache
write() → kernel cache (fast). Disk write is async. Use 4 KB+ buffers. pdflush flushes every 30s. read-ahead for sequential access.
📦 stdio Buffering
_IONBF / _IOLBF / _IOFBF. Use setvbuf() to change mode. fflush() to force flush. Flush before fork(). stderr is unbuffered by default.
💾 Kernel Sync
fsync() = data + all metadata. fdatasync() = data + essential metadata (faster). O_SYNC = auto-sync on each write. Batch writes + occasional fsync = best pattern.
🚀 posix_fadvise
SEQUENTIAL doubles read-ahead. RANDOM disables it. WILLNEED prefetches now. DONTNEED frees cache. Hint only — never affects correctness.
⚡ O_DIRECT
Bypasses page cache. Slower for most apps. Needs aligned buffer, offset, length (multiples of 512+ bytes). Use posix_memalign(). Only for DB engines.
🔀 Mixing stdio + syscalls
fileno(fp) → fd. fdopen(fd, mode) → FILE*. Always fflush() before write() on same fd. fclose() closes fd too — don’t double-close.
