Mixing stdio and System Calls in Linux: Best Practices

 

🔀 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. Use fdopen() to use fprintf(), fgets() on it.
  • Pipes: pipe(), popen() return fds.
  • Special files: open() with special flags like O_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;
}
💡 Rule for fclose() and close(): After calling 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

Q1. What does fileno() return and what can you do with the result?
fileno(fp) returns the integer file descriptor (fd) that the stdio library uses for the stream fp. You can use this fd with any system call that takes an fd — read(), write(), ioctl(), fcntl(), fstat(), fsync(), dup(), etc. For example, fsync(fileno(fp)) flushes both the stdio buffer (implicitly via fflush first) and the kernel buffer to disk.
Q2. Why can’t you just use fopen() on a socket?
fopen() only works for filesystem paths. A socket is created by socket() which returns a file descriptor, not a path. You can’t pass a socket to fopen(). The solution is fdopen(sock_fd, “r”) or fdopen(sock_fd, “w”) to wrap the socket fd in a FILE* stream, enabling the use of fprintf(), fgets(), and other stdio functions on the socket.
Q3. After calling fclose(), can you still use the underlying fd?
No. fclose() closes the underlying file descriptor as part of cleanup. Any subsequent use of that fd is undefined behavior — or worse, it may now refer to a completely different file if the OS reused the fd number. You must not call close(fd) separately after fclose(fp) for the same reason — that’s a double-close bug.
Q4. You mix printf() and write() to stdout. When redirecting to a file, the order is wrong. Why?
When stdout is redirected to a file, it switches to fully-buffered mode (_IOFBF). printf() stores data in the stdio buffer without calling write() until the buffer fills. But write() bypasses the stdio buffer and goes directly to the kernel. So write() output appears in the kernel buffer before the stdio-buffered printf() output. At program exit, the stdio buffer is flushed, and the printf() output appears after the write() output. Fix: call fflush(stdout) before every write() call.
Q5. What does the mode argument in fdopen() control and what happens if it mismatches?
The mode argument (like “r”, “w”, “a”) specifies how the stream will use the fd — read-only, write-only, or append. If the mode is incompatible with how the fd was opened (e.g., fdopen on a read-only fd with mode “w”), fdopen() fails and returns NULL. The mode must be consistent with the fd’s access permissions.
Q6. How do you use both read and write stdio functions on a single socket?
You need two FILE* streams — one for reading and one for writing. Use dup() to get a second fd pointing to the same socket, then fdopen() each separately: FILE *in = fdopen(sockfd, “r”) and FILE *out = fdopen(dup(sockfd), “w”). Each FILE* has its own stdio buffer. Always fflush(out) before reading from in to avoid ordering issues.
Q7. What is the correct sequence to guarantee data is on disk when using stdio?
Three steps: (1) Write via stdio functions like fprintf()/fwrite(). (2) Call fflush(fp) to move data from stdio buffer to kernel buffer cache. (3) Call fsync(fileno(fp)) to move data from kernel buffer to physical disk. Skipping step 2 means fsync works on the kernel buffer but misses data still in stdio’s buffer. Skipping step 3 means data is in kernel RAM but not on disk.

📚 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.

Leave a Reply

Your email address will not be published. Required fields are marked *