Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Linux File I/O Fundamentals: Descriptors, System Calls, and Positional Control

Tech May 12 2

Linux treats all I/O resources uniformly through the file abstraction—regular files, pipes, devices, and sockets are all accessed using the same interface. At the core of this model lies the file descriptor: a small non-negative integer that serves as an index into a per-process table of open files maintained by the kernel.

File Descriptor Basics

Each process starts with three predefined descriptors:

  • 0: Standard input (stdin)
  • 1: Standard output (stdout)
  • 2: Standard error (stderr)

Subsequent calls to open() return the lowest available descriptor starting from 3. The maximum number of concurrently open files per process is constrained by the system limit, viewable via ulimit -n. By default, this value is often 1024, but it can be adjusted.

Descriptors are inherited across fork() and shared across threads unless explicitly closed. Closing a descriptor releases it for reuse by future open() calls.

Core System Calls

The fundamental file I/O operations rely on four system calls:

  • open() — opens or creates a file and returns a descriptor
  • read() — reads data from a descriptor into a buffer
  • write() — writes data from a buffer to a descriptor
  • close() — releases the descriptor and flushes pending writes

Opening Files

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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

The flags argument specifies access mode (O_RDONLY, O_WRONLY, O_RDWR) and optional behaviors (O_CREAT, O_TRUNC, O_APPEND, etc.). When O_CREAT is used, a third mode parameter defines permissions (e.g., 0644).

Reading and Writing

Both read() and write() operate relative to the current file offset, which advances automatically after each call:

ssize_t n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
    // n bytes read successfully
} else if (n == 0) {
    // end-of-file reached
} else {
    // error occurred
}

ssize_t w = write(fd_out, buffer, n);
if (w != n) {
    // partial or failed write
}

A successful read() returns the number of bytes actually transferred, which may be less than requested (e.g., at EOF or due to signal interruption). Similarly, write() may transfer fewer bytes than requested (e.g., disk full), though full writes are typical for regular files.

Controlling File Position

The lseek() system call modifies the current offset without reading or writing:

off_t pos = lseek(fd, 1024, SEEK_SET);  // absolute: byte 1024 from start
pos = lseek(fd, -512, SEEK_CUR);         // relative: move back 512 bytes
pos = lseek(fd, 0, SEEK_END);            // set offset to end of file

It returns the new absolute position from the beginning of the file, or -1 on failure.

Practical Example: Selective Copy with Offset Adjustment

This program copies exactly 1024 bytes starting from byte 500 in the source file to the beginning of the destination file:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    char buf[1024];
    int src_fd = open("src_file", O_RDONLY);
    if (src_fd == -1) {
        perror("cannot open source");
        return 1;
    }

    int dst_fd = open("dest_file", O_WRONLY | O_CREAT | O_EXCL, 0644);
    if (dst_fd == -1) {
        perror("cannot create destination");
        close(src_fd);
        return 1;
    }

    // Skip first 500 bytes in source
    if (lseek(src_fd, 500, SEEK_SET) == -1) {
        perror("seek failed in source");
        goto cleanup;
    }

    ssize_t n = read(src_fd, buf, sizeof(buf));
    if (n <= 0) {
        perror(n == 0 ? "unexpected EOF" : "read error");
        goto cleanup;
    }

    // Reset destination to start
    if (lseek(dst_fd, 0, SEEK_SET) == -1) {
        perror("seek failed in destination");
        goto cleanup;
    }

    if (write(dst_fd, buf, n) != n) {
        perror("write failed");
        goto cleanup;
    }

    printf("Copied %zd bytes successfully\n", n);

cleanup:
    close(dst_fd);
    close(src_fd);
    return 0;
}

Note the use of O_EXCL with O_CREAT to ensure the destination file does not already exist—preventing accidental overwrites. Error handling follows POSIX conventions: check return values, use perror() for diagnostics, and clean up resources before exiting.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.