Linux File I/O — readv/writev, Non-blocking, Large Files & Temp Files

Linux File I/O — readv/writev, Non-blocking, Large Files & Temp Files

Scatter-gather I/O · O_NONBLOCK · _FILE_OFFSET_BITS · /dev/fd · mkstemp · tmpfile

📦
readv/writev
Scatter-Gather
🚦
O_NONBLOCK
Event-driven I/O
🗃️
Large Files
>2 GB on 32-bit
🗑️
Temp Files
mkstemp/tmpfile

What You Will Learn

The final post in this series covers four practical topics. Scatter-gather I/O with readv/writev lets you read or write multiple separate memory buffers in one atomic call — no temporary buffer needed. Non-blocking I/O with O_NONBLOCK makes system calls return immediately instead of waiting, which is the basis of all event-driven programs. Large file support shows how to handle files bigger than 2 GB on 32-bit systems with one compiler flag. Finally, safe temporary files with mkstemp() and tmpfile() shows how to avoid the security bugs that come from doing it manually.

Topics Covered

readv() scatter input writev() gather output struct iovec preadv() and pwritev() O_NONBLOCK flag EAGAIN error Event loop pattern _FILE_OFFSET_BITS=64 off_t and off64_t /dev/fd virtual directory mkstemp() safe temp file tmpfile() auto-delete

📦 Section 1 — Scatter-Gather I/O: readv() and writev()

Why Do We Need These Calls?

Imagine you want to send a network packet that has three parts: a fixed-size header struct, a body struct, and a variable-length payload array. These three pieces live in different memory locations. To write them to a socket or file, you have three options:

Option 1: Big Temp Buffer Option 2: Three write() Calls Option 3: writev()
malloc(total_size);
memcpy × 3;
write(fd, tmp, total);
free(tmp);
→ Extra allocation + copying
write(fd, &hdr, …)
write(fd, &body, …)
write(fd, payload, …)
→ 3 syscalls
→ NOT atomic!
→ Another writer can interleave
Set up iov[3];
writev(fd, iov, 3);
→ 1 syscall
→ Atomic
→ No malloc needed

The iovec Structure

Both calls take an array of struct iovec. Each element in the array describes one buffer — where it starts in memory and how many bytes it contains:

#include <sys/uio.h>

/* Describes one buffer in the array */
struct iovec {
void *iov_base; /* pointer to buffer start */
size_t iov_len; /* number of bytes in buffer */
};

ssize_t readv (int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

/* iovcnt = number of iovec entries (max IOV_MAX = 1024 on Linux) */

writev(): Gather Output — Three Separate Buffers → One Contiguous Block in File
iov[0]
base = &hdr
len = 8
+ iov[1]
base = &body
len = 12
+ iov[2]
base = payload
len = 20
FILE
[hdr:8][body:12][payload:20]
40 bytes, atomic

Example 1 — writev() to Write a Protocol Packet

/*
 * writev_packet.c  —  Write header + body + payload in one atomic syscall
 * No temporary buffer. No three separate write() calls.
 * Build: gcc writev_packet.c -o writev_packet
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <stdint.h>

/* Fixed-size packet header */
struct pkt_hdr {
    uint16_t type;       /* message type     */
    uint16_t flags;      /* option bits      */
    uint32_t body_len;   /* body length      */
} __attribute__((packed));

/* Fixed-size packet body */
struct pkt_body {
    uint32_t sender_id;
    uint32_t sequence;
} __attribute__((packed));

int main(void)
{
    struct pkt_hdr  hdr  = { .type = 1, .flags = 0, .body_len = 0 };
    struct pkt_body body = { .sender_id = 42, .sequence = 100 };

    const char *payload = "sensor_value=27.3";
    hdr.body_len = strlen(payload);

    /* Set up iovec array — one entry per memory region */
    struct iovec iov[3];
    iov[0].iov_base = &hdr;
    iov[0].iov_len  = sizeof(hdr);          /* 8 bytes  */

    iov[1].iov_base = &body;
    iov[1].iov_len  = sizeof(body);         /* 8 bytes  */

    iov[2].iov_base = (void *)payload;
    iov[2].iov_len  = strlen(payload);      /* variable */

    size_t total = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;

    int fd = open("packet.bin", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    /*
     * writev() writes all 3 buffers as one contiguous, atomic block.
     * Equivalent to: memcpy all → single write() but WITHOUT the copy.
     */
    ssize_t written = writev(fd, iov, 3);
    if (written == -1) { perror("writev"); return 1; }

    printf("writev wrote %zd bytes (header=%zu + body=%zu + payload=%zu)\n",
           written, iov[0].iov_len, iov[1].iov_len, iov[2].iov_len);

    /* Always check for partial writes (important on sockets/pipes) */
    if ((size_t)written != total) {
        fprintf(stderr, "Partial write: only %zd of %zu bytes sent\n",
                written, total);
    }

    close(fd);
    return 0;
}

Example 2 — readv() to Scatter a File into Multiple Structs

/*
 * readv_packet.c  —  Read a file and scatter it into separate structs
 * Works on the file created by writev_packet.c
 * Build: gcc readv_packet.c -o readv_packet
 * Run after: ./writev_packet
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <stdint.h>

struct pkt_hdr  { uint16_t type; uint16_t flags; uint32_t body_len; } __attribute__((packed));
struct pkt_body { uint32_t sender_id; uint32_t sequence; }            __attribute__((packed));

int main(void)
{
    struct pkt_hdr  hdr;
    struct pkt_body body;
    char payload[128] = {0};

    /* Set up iovec to receive data into the three regions */
    struct iovec iov[3];
    iov[0].iov_base = &hdr;
    iov[0].iov_len  = sizeof(hdr);

    iov[1].iov_base = &body;
    iov[1].iov_len  = sizeof(body);

    iov[2].iov_base = payload;
    iov[2].iov_len  = sizeof(payload) - 1;

    int fd = open("packet.bin", O_RDONLY);
    if (fd == -1) { perror("open"); return 1; }

    /*
     * readv() reads bytes from the file and SCATTERS them
     * directly into our three structs in order.
     * No temporary buffer, one syscall.
     */
    ssize_t nread = readv(fd, iov, 3);
    if (nread == -1) { perror("readv"); return 1; }

    payload[hdr.body_len] = '\0';  /* null-terminate payload */

    printf("readv read %zd bytes total\n\n", nread);
    printf("Header : type=%u  flags=%u  body_len=%u\n",
           hdr.type, hdr.flags, hdr.body_len);
    printf("Body   : sender_id=%u  sequence=%u\n",
           body.sender_id, body.sequence);
    printf("Payload: %s\n", payload);

    close(fd);
    return 0;
}

Example 3 — Read a File’s Contents into Multiple Buffers at Once

/*
 * readv_multi.c  —  Fill three separate arrays from a single readv call
 * Shows the "scatter" aspect: one read disperses data to multiple destinations.
 * Build: gcc readv_multi.c -o readv_multi
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <string.h>

int main(void)
{
    /* Create a file with 30 bytes of known data */
    int wfd = open("multi_test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    write(wfd, "AAAAAABBBBBBBBBCCCCCCCCCCCCCCC", 30);
    close(wfd);

    /* Three separate destination buffers */
    char buf_a[7]  = {0};  /* will get bytes 0-5   = "AAAAAA" */
    char buf_b[10] = {0};  /* will get bytes 6-14  = "BBBBBBBBB" */
    char buf_c[14] = {0};  /* will get bytes 15-29 = "CCCCCCCCCCCCC" */

    struct iovec iov[3];
    iov[0].iov_base = buf_a; iov[0].iov_len = 6;
    iov[1].iov_base = buf_b; iov[1].iov_len = 9;
    iov[2].iov_base = buf_c; iov[2].iov_len = 13;

    int fd = open("multi_test.txt", O_RDONLY);
    ssize_t n = readv(fd, iov, 3);

    printf("readv read %zd bytes total\n", n);
    printf("buf_a (6 bytes) : '%s'\n", buf_a);
    printf("buf_b (9 bytes) : '%s'\n", buf_b);
    printf("buf_c (13 bytes): '%s'\n", buf_c);
    printf("\nData was scattered directly into three separate arrays.\n");
    printf("No intermediate buffer. One syscall.\n");

    close(fd);
    return 0;
}
📌 preadv() / pwritev(): Linux 2.6.30+ adds versions that combine scatter-gather with a specified offset (like pread/pwrite). Use these when you need both features — multi-buffer I/O at a fixed offset — in a thread-safe way. Add #define _GNU_SOURCE and use preadv(fd, iov, iovcnt, offset).

🚦 Section 2 — Non-blocking I/O with O_NONBLOCK

What Does “Blocking” Mean?

By default when your process calls read() on a pipe or socket with no data, the kernel puts your process to sleep. It does not run at all. It waits, consuming zero CPU, until data arrives. This is called blocking.

For a simple program that does one thing at a time, this is perfectly fine. But imagine a program monitoring 10 sensors connected as pipes. If sensor 1 is slow, blocking on it freezes reading from all other sensors too. Non-blocking I/O solves this by making the call return immediately.

Blocking read() — Default Non-blocking read() — O_NONBLOCK
Data available → returns bytes
No data → process sleeps ∞
EOF → returns 0
Error → returns -1
Cannot do anything while waiting
Data available → returns bytes
No data → returns -1, errno=EAGAIN
EOF → returns 0
Error → returns -1
Free to poll other fds, do other work
Where O_NONBLOCK Works: Pipes, FIFOs, sockets, character devices (terminals, serial ports). For regular disk files O_NONBLOCK is mostly ignored because the kernel buffer cache makes disk reads appear instantaneous.

Example 1 — Reading a Pipe in Non-blocking Mode

/*
 * nonblock_pipe.c  —  Non-blocking read from a pipe
 * Without O_NONBLOCK: read() would block forever on an empty pipe.
 * With O_NONBLOCK: read() returns -1 with errno=EAGAIN immediately.
 * Build: gcc nonblock_pipe.c -o nonblock_pipe
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

/* Helper: add O_NONBLOCK to any fd using fcntl */
void make_nonblocking(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main(void)
{
    int pipefd[2];
    pipe(pipefd);   /* pipefd[0] = read, pipefd[1] = write */

    make_nonblocking(pipefd[0]);   /* make read end non-blocking */

    char buf[64] = {0};
    ssize_t n;

    printf("=== Attempt 1: pipe is empty ===\n");
    n = read(pipefd[0], buf, sizeof(buf));
    if (n == -1 && errno == EAGAIN) {
        printf("read() returned EAGAIN — not blocked!\n");
        printf("Process is free to do other work here.\n");
    }

    printf("\n(doing other work for 100ms...)\n");
    usleep(100000);

    printf("\n=== Attempt 2: write data, then read ===\n");
    write(pipefd[1], "Temperature:72.5", 16);

    n = read(pipefd[0], buf, sizeof(buf));
    if (n > 0) {
        buf[n] = '\0';
        printf("read() got %zd bytes: '%s'\n", n, buf);
    }

    printf("\n=== Attempt 3: pipe empty again ===\n");
    n = read(pipefd[0], buf, sizeof(buf));
    if (n == -1 && errno == EAGAIN) {
        printf("EAGAIN again — expected.\n");
    }

    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

Example 2 — Simple Event Loop Monitoring Two Pipes

The event loop is the design pattern used by every high-performance server and embedded firmware. Instead of blocking on one source, the loop polls all sources and handles whichever ones have data:

/*
 * event_loop.c  —  Poll two pipes without blocking on either
 * This is the fundamental pattern behind nginx, Redis, Node.js.
 * Build: gcc event_loop.c -o event_loop
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define NUM_SOURCES 2

int set_nonblocking(int fd) {
    int f = fcntl(fd, F_GETFL);
    return fcntl(fd, F_SETFL, f | O_NONBLOCK);
}

int main(void)
{
    int pipes[NUM_SOURCES][2];

    /* Create two independent data sources */
    for (int i = 0; i < NUM_SOURCES; i++) {
        pipe(pipes[i]);
        set_nonblocking(pipes[i][0]);   /* make read-ends non-blocking */
    }

    /* Simulate data arriving at different times */
    write(pipes[0][1], "Source0:data_A", 14);
    write(pipes[1][1], "Source1:data_X", 14);
    close(pipes[0][1]);   /* signal EOF on source 0 */
    close(pipes[1][1]);

    printf("Event loop started — polling %d sources\n\n", NUM_SOURCES);

    int active = NUM_SOURCES;
    int iteration = 0;

    while (active > 0) {
        iteration++;
        printf("-- Iteration %d --\n", iteration);

        for (int i = 0; i < NUM_SOURCES; i++) {
            if (pipes[i][0] == -1) continue;  /* already closed */

            char buf[64] = {0};

            /*
             * Non-blocking read: returns immediately with EAGAIN
             * if no data. Never blocks the whole loop.
             */
            ssize_t n = read(pipes[i][0], buf, sizeof(buf) - 1);

            if (n > 0) {
                printf("  Source %d: got '%s'\n", i, buf);
            } else if (n == 0) {
                printf("  Source %d: EOF — removing\n", i);
                close(pipes[i][0]);
                pipes[i][0] = -1;
                active--;
            } else if (errno == EAGAIN) {
                printf("  Source %d: no data right now\n", i);
            }
        }

        if (active > 0) usleep(20000);  /* 20ms poll interval */
    }

    printf("\nAll sources done. Total iterations: %d\n", iteration);
    return 0;
}
💡 EAGAIN vs EWOULDBLOCK: On Linux these two error codes are identical — the kernel defines them as the same integer. POSIX says both exist for historical reasons. Best practice: check for either: if (errno == EAGAIN || errno == EWOULDBLOCK)

🗃️ Section 3 — Large File Support — Files Bigger Than 2 GB

The Problem

On 32-bit systems, the file offset type off_t is a signed 32-bit integer. The maximum positive value of a signed 32-bit int is 2,147,483,647 — exactly 2 GB. Any file larger than 2 GB cannot be seeked to or read correctly on a 32-bit system without the LFS extensions.

This was fine in the 1990s. Today a single video file, database file, or disk image routinely exceeds this. The solution is to compile your program with _FILE_OFFSET_BITS=64, which makes off_t 64-bit automatically and remaps all file functions to their 64-bit equivalents.

Item Default 32-bit With _FILE_OFFSET_BITS=64 Max size
off_t 32-bit signed 64-bit signed 2 GB → 8 EB
open() 32-bit version → remapped to open64()
lseek() 32-bit version → remapped to lseek64()
stat() 32-bit version → remapped to stat64()

How to Enable Large File Support

Two equivalent ways:

/* Method 1: at the top of your C source file */
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>

/* Method 2: compile-time flag (preferred — no code change) */
$ gcc -D_FILE_OFFSET_BITS=64 myprogram.c -o myprogram

Example 1 — Creating and Seeking in a Large File (Sparse)

/*
 * largefile.c  —  Seek to 3 GB and write data on a 32-bit capable program
 * On 32-bit without LFS: lseek to 3GB would silently fail or overflow.
 * With -D_FILE_OFFSET_BITS=64: works correctly.
 *
 * Note: This creates a SPARSE file. The kernel does NOT allocate 3 GB of
 * actual disk space. Only the blocks you actually write are stored.
 * 'ls -lh' shows 3 GB, but 'du -sh' shows just a few kilobytes.
 *
 * Build: gcc -D_FILE_OFFSET_BITS=64 largefile.c -o largefile
 */
#define _FILE_OFFSET_BITS 64

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

int main(void)
{
    int fd = open("large_test.bin", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    /*
     * With _FILE_OFFSET_BITS=64, off_t is 64-bit.
     * This value fits in off_t and lseek handles it correctly.
     * Without LFS, this would overflow and seek to a wrong position.
     */
    off_t three_gb = (off_t)3 * 1024 * 1024 * 1024;

    printf("Seeking to 3 GB offset (%lld bytes)...\n",
           (long long)three_gb);

    if (lseek(fd, three_gb, SEEK_SET) == -1) {
        perror("lseek: system may not support large files here");
        return 1;
    }

    if (write(fd, "CHECKPOINT_3GB", 14) != 14) {
        perror("write");
        return 1;
    }
    printf("Wrote marker at 3 GB offset.\n");

    /* Read it back using pread — safe for large offsets too */
    char buf[15] = {0};
    pread(fd, buf, 14, three_gb);
    printf("Read back: '%s'\n", buf);

    /* Report file size */
    off_t size = lseek(fd, 0, SEEK_END);
    printf("File apparent size: %lld bytes (%.2f GB)\n",
           (long long)size,
           (double)size / (1024.0 * 1024 * 1024));
    printf("(Actual disk use is tiny — it is a sparse file)\n");

    close(fd);
    unlink("large_test.bin");   /* clean up */
    return 0;
}
/* Build: gcc -D_FILE_OFFSET_BITS=64 largefile.c -o largefile */

Example 2 — Printing off_t Correctly

When LFS is enabled, off_t may be wider than long on 32-bit systems. Using %ld to print it can silently truncate the value. Always cast to long long and use %lld:

/*
 * off_t_print.c  —  Correct way to print off_t values with LFS enabled
 * Build: gcc -D_FILE_OFFSET_BITS=64 off_t_print.c -o off_t_print
 */
#define _FILE_OFFSET_BITS 64
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int fd = open("size_test.bin", O_RDWR | O_CREAT | O_TRUNC, 0644);

    off_t big_offset = (off_t)5 * 1024 * 1024 * 1024;  /* 5 GB */
    lseek(fd, big_offset, SEEK_SET);
    write(fd, "X", 1);

    off_t current = lseek(fd, 0, SEEK_CUR);

    /* WRONG on 32-bit with LFS — off_t may be bigger than long */
    /* printf("Offset: %ld\n", current);  ← may print garbage! */

    /* CORRECT — always cast off_t to long long */
    printf("Offset : %lld bytes\n",  (long long)current);
    printf("Offset : %.2f GB\n",
           (double)current / (1024.0 * 1024 * 1024));

    close(fd);
    unlink("size_test.bin");
    return 0;
}

📁 Section 4 — The /dev/fd Virtual Directory

What is /dev/fd?

Linux provides a special virtual directory at /dev/fd (which is a symlink to /proc/self/fd). For every file descriptor your process currently has open, this directory contains a virtual file named by that fd number. Opening /dev/fd/N is equivalent to calling dup(N) — you get a new fd pointing to the same OFD.

Path Alias Equivalent to Common use
/dev/fd/0 /dev/stdin dup(0) Pass stdin as a filename to tools that need a path
/dev/fd/1 /dev/stdout dup(1) Write to stdout via a filename argument
/dev/fd/2 /dev/stderr dup(2) Write errors to stderr via a filename argument
/dev/fd/N dup(N) Any open fd accessible as a filename
/*
 * devfd_demo.c  —  Explore /dev/fd: list open fds and access one via path
 * Build: gcc devfd_demo.c -o devfd_demo
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>

int main(void)
{
    /* Open a few files */
    int fd1 = open("/etc/hostname", O_RDONLY);
    int fd2 = open("/tmp/devfd_test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd2, "hello from fd2\n", 15);

    printf("Opened: fd1=%d (/etc/hostname)  fd2=%d (/tmp/devfd_test.txt)\n\n",
           fd1, fd2);

    /* List everything in /proc/self/fd to see our open fds */
    printf("Contents of /proc/self/fd:\n");
    DIR *dir = opendir("/proc/self/fd");
    struct dirent *e;
    char link[256], path[64];
    while ((e = readdir(dir)) != NULL) {
        if (e->d_name[0] == '.') continue;
        snprintf(path, sizeof(path), "/proc/self/fd/%s", e->d_name);
        ssize_t n = readlink(path, link, sizeof(link) - 1);
        if (n > 0) { link[n]='\0'; printf("  fd %s → %s\n", e->d_name, link); }
    }
    closedir(dir);

    /* Access fd2's file via /dev/fd path — equivalent to dup(fd2) */
    char devfd_path[32];
    snprintf(devfd_path, sizeof(devfd_path), "/dev/fd/%d", fd2);

    int fd_via_path = open(devfd_path, O_RDONLY);
    printf("\nOpened %s → fd %d\n", devfd_path, fd_via_path);

    char buf[32] = {0};
    lseek(fd_via_path, 0, SEEK_SET);
    read(fd_via_path, buf, 14);
    printf("Read via /dev/fd path: '%s'\n", buf);

    close(fd1); close(fd2); close(fd_via_path);
    unlink("/tmp/devfd_test.txt");
    return 0;
}
/* Shell equivalents:
   sort data.txt | diff /dev/stdin saved.txt   (compare live vs saved)
   some_tool --output=/dev/stdout               (force stdout as filename)
*/

🗑️ Section 5 — Temporary Files: mkstemp() and tmpfile()

Why Manual Temp File Creation is Dangerous

The naive approach to creating a temporary file is: generate a unique-looking name like /tmp/app_12345.tmp, then open it. This is a serious security vulnerability.

Between generating the name and opening the file, a malicious process can create a file (or symlink) with the same name first. When your program opens it, you end up writing to whatever the attacker’s symlink points to — which could be /etc/passwd. This is a classic TOCTOU attack (Time Of Check, Time Of Use).

The safe functions mkstemp() and tmpfile() atomically generate a unique name AND open the file in a single operation, eliminating this window entirely.

Function Returns Auto-deleted? Filename visible? Best for
mkstemp() int fd No — call unlink() yourself Yes — in template When you need to pass the filename to another program or library
tmpfile() FILE * Yes — on fclose() No — hidden Pure scratch space within your process

Example 1 — mkstemp() in Detail

/*
 * mkstemp_demo.c  —  Safe temporary file creation with mkstemp()
 * mkstemp() atomically generates a unique name AND opens the file.
 * Build: gcc mkstemp_demo.c -o mkstemp_demo
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

int main(void)
{
    /*
     * Template: the last 6 characters MUST be "XXXXXX".
     * mkstemp() replaces them with a unique random string.
     * The buffer must be writable (not a const string literal!).
     */
    char template[] = "/tmp/ep_tutorial_XXXXXX";

    printf("Template before: '%s'\n", template);

    /*
     * mkstemp() atomically:
     *   1. Finds a unique filename by replacing XXXXXX
     *   2. Creates the file
     *   3. Opens it with O_RDWR | O_CREAT | O_EXCL
     *   4. Returns the file descriptor
     * No window for another process to interfere.
     */
    int fd = mkstemp(template);
    if (fd == -1) { perror("mkstemp"); return 1; }

    printf("Template after : '%s'\n", template);
    printf("fd = %d\n\n", fd);

    /* mkstemp always creates the file with mode 0600 (owner-only rw) */
    struct stat st;
    fstat(fd, &st);
    printf("Permissions: %04o  (only owner can read/write)\n\n",
           (unsigned)(st.st_mode & 07777));

    /*
     * Best practice: unlink() the file immediately after opening.
     * The directory entry is removed so:
     *   - 'ls /tmp' no longer shows it
     *   - It is automatically deleted when fd is closed or process exits
     * We can still use fd for I/O — the file content stays until close().
     */
    unlink(template);
    printf("File unlinked: no longer visible in directory.\n");
    printf("But still accessible via fd %d for I/O.\n\n", fd);

    /* Use the file normally */
    const char *content = "Intermediate computation results: [1, 4, 9, 16, 25]";
    write(fd, content, strlen(content));

    /* Seek back and read */
    lseek(fd, 0, SEEK_SET);
    char buf[128] = {0};
    read(fd, buf, sizeof(buf) - 1);
    printf("Temp file content: %s\n\n", buf);

    /* Closing fd deletes the file from disk completely */
    close(fd);
    printf("fd closed. File is now gone from disk.\n");
    return 0;
}

Example 2 — tmpfile() — Simplest Scratch Space

/*
 * tmpfile_demo.c  —  Auto-deleted temp file with stdio interface
 * tmpfile() creates an anonymous temp file that deletes itself on fclose().
 * You never see the filename. Use it when you just need scratch space.
 * Build: gcc tmpfile_demo.c -o tmpfile_demo
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    /*
     * tmpfile() atomically:
     *   1. Creates a uniquely named temp file
     *   2. Opens it as a FILE* stream (read+write)
     *   3. Immediately unlinks it (removes from directory)
     * The file lives as long as the FILE* is open.
     * When you call fclose(), the file is gone completely.
     */
    FILE *tmp = tmpfile();
    if (!tmp) { perror("tmpfile"); return 1; }

    printf("Temp file created (anonymous, no visible path)\n\n");

    /* Use stdio functions — fprintf, fscanf, fgets work normally */
    fprintf(tmp, "Measurement 1: voltage = 3.3V\n");
    fprintf(tmp, "Measurement 2: current = 0.15A\n");
    fprintf(tmp, "Measurement 3: power   = 0.495W\n");

    /* Rewind and read back */
    rewind(tmp);
    char line[80];
    printf("Contents of temp file:\n");
    while (fgets(line, sizeof(line), tmp)) {
        printf("  %s", line);
    }

    printf("\nClosing — file will be deleted automatically.\n");
    fclose(tmp);
    /* The file is now gone. No manual unlink() needed. */

    return 0;
}

Example 3 — mkstemp() + fdopen() When You Need stdio AND the Filename

/*
 * mkstemp_stdio.c  —  Combine mkstemp() with fdopen() for stdio on temp file
 * Use when you need BOTH the filename (to pass to another library)
 * AND the fprintf/fgets stdio interface.
 * Build: gcc mkstemp_stdio.c -o mkstemp_stdio
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    char template[] = "/tmp/ep_combined_XXXXXX";

    int fd = mkstemp(template);
    if (fd == -1) { perror("mkstemp"); return 1; }

    printf("Temp file: %s\n\n", template);

    /*
     * fdopen() wraps the raw fd in a stdio FILE* stream.
     * Now we can use fprintf/fgets on the mkstemp fd.
     * "w+" means write+read on an existing fd.
     */
    FILE *fp = fdopen(fd, "w+");
    if (!fp) { perror("fdopen"); close(fd); return 1; }

    /* Use stdio to write */
    fprintf(fp, "Config: baudrate=115200\n");
    fprintf(fp, "Config: parity=none\n");
    fprintf(fp, "Config: stopbits=1\n");
    fflush(fp);

    /* Can also pass 'template' (the filename) to a library that needs a path */
    printf("Filename '%s' can be passed to any library or tool.\n\n", template);

    /* Read back using fgets */
    rewind(fp);
    char line[80];
    printf("Contents:\n");
    while (fgets(line, sizeof(line), fp)) {
        printf("  %s", line);
    }

    fclose(fp);     /* this also closes the underlying fd */
    unlink(template);   /* manually clean up since we did NOT unlink earlier */
    printf("\nDone. Temp file removed.\n");
    return 0;
}
🚫 Never use these — they are broken by design: tmpnam(), tempnam(), mktemp(). All three generate a name without opening the file, leaving a race window for an attacker. They are deprecated in POSIX. Some compilers now show a warning when you use them. Always use mkstemp() or tmpfile().

📚 Complete Series Summary — Linux Advanced File I/O
Post System Calls / Flags What You Learned
Post 1 O_CREAT|O_EXCL, O_APPEND Race conditions in file creation and appending; how kernel atomicity eliminates them
Post 2 fcntl(F_GETFL/F_SETFL/F_GETFD) Runtime flag control; fd/OFD/inode three-layer model; why dup’d fds share offsets
Post 3 dup, dup2, dup3, pread, pwrite How shell redirection works internally; thread-safe I/O without lseek
Post 4 readv, writev, O_NONBLOCK, _FILE_OFFSET_BITS, /dev/fd, mkstemp, tmpfile Scatter-gather I/O; event loop pattern; large files; safe temp files

🎓 Next in the Linux Systems Series

Coming up: Process Management — fork(), exec(), wait(), and exit() from the ground up.

← Post 3: dup() and pread() Linux Course Index →

Leave a Reply

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