Skip to Content
Course content

141: File Descriptors and Low-Level I/O

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern with developers moving from high-level languages to C: they tend to think that FILE * is the primary way the operating system handles files. If you've been using fopen() and fprintf(), it feels like the FILE object is the "handle." This is a misconception. In reality, the kernel doesn't know what a FILE struct is—it only cares about integers.

A FILE pointer is not the actual handle to the file

When you call fopen(), you aren't talking to the OS directly; you're talking to the C Standard Library. The library creates a FILE structure in your process's memory to manage a buffer, and inside that structure, it hides a small integer called a file descriptor (fd). That integer is the actual key to the file in the kernel's table.

I like to prove this to people with a quick snippet. If you use the fileno() function, you can peek inside the wrapper:

#include <stdio.h>

int main() {
    FILE *fp = fopen("debug.log", "w");
    int fd = fileno(fp);
    printf("The library gave me a FILE pointer, but the kernel sees fd: %d\n", fd);
    fclose(fp);
    return 0;
}

Run that, and you'll likely see 3. Why 3? Because the OS always assigns 0 to stdin, 1 to stdout, and 2 to stderr. Your file is the fourth thing the process opened, so it gets index 3. The FILE * is just a fancy concierge; the file descriptor is the actual room key.

Talking directly to the kernel with integers

To do "low-level" I/O, we bypass stdio.h entirely and use system calls defined in <fcntl.h> and <unistd.h>. These functions—open(), read(), write(), and close()—operate exclusively on those integers.

Here is how you'd write a simple status message to a file without the overhead of a buffer:

#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *msg = "System Kernel Heartbeat: OK\n";
    
    // O_WRONLY: write only, O_CREAT: create if missing, 0644: permissions
    int fd = open("heartbeat.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    
    if (fd == -1) {
        // Low-level calls return -1 on failure, not NULL
        return 1;
    }

    write(fd, msg, strlen(msg));
    close(fd);
    return 0;
}

Notice the lack of fprintf. We aren't formatting strings here; we are moving a raw block of bytes from a memory address to a disk location. It's blunt, but it's fast.

Bypassing the buffer for real-time control

You might wonder why I'd ever suggest using write() when fprintf() is so much more convenient. The answer is buffering. stdio.h functions store data in a memory buffer and only "flush" it to the disk when the buffer is full or you call fflush().

In a production environment, if your program crashes, the data sitting in that C-library buffer is gone forever—it never reached the kernel. But when you use write(), you are making a system call. You're telling the kernel, "Take this data right now." If you're writing a database engine or a crash logger, you cannot afford to have your logs trapped in a user-space buffer. You need the raw, unbuffered certainty of a file descriptor.




📋 Practical Task

Build a Raw Binary Log Append Tool

Your task is to create a utility that appends a binary timestamp and a status code to a log file using low-level I/O. This exercise requires you to avoid stdio.h for the actual writing process to ensure no buffering occurs.

Requirements:

  • Use open() with the O_WRONLY | O_APPEND | O_CREAT flags.
  • Define a struct called LogEntry containing a long (for the timestamp) and an int (for the status code).
  • Use write() to save the entire struct as a binary block to a file named system.bin.
  • Ensure you handle the case where open() fails by printing an error to stderr (using the file descriptor 2) and exiting.
  • Close the file descriptor properly before the program terminates.

Hint: Since you are writing a struct, the size to pass to write() should be sizeof(struct LogEntry).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.