C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
141: File Descriptors and Low-Level I/O
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 theO_WRONLY | O_APPEND | O_CREATflags. - Define a
structcalledLogEntrycontaining along(for the timestamp) and anint(for the status code). - Use
write()to save the entire struct as a binary block to a file namedsystem.bin. - Ensure you handle the case where
open()fails by printing an error tostderr(using the file descriptor2) 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).
There are no comments for now.