System calls are the gateway between your program and the operating system kernel. Think of them as the reception desk at a secure government building: your program (a visitor) cannot walk into the vault and grab files directly. Instead, you fill out a request form (set up registers), ring a bell (execute the syscall instruction), and a trusted employee (the kernel) fetches what you need and hands it back through the window.Every printf, every file open, every network packet your C program sends eventually passes through this gateway. Understanding system calls is the difference between knowing C syntax and understanding how programs actually interact with hardware.
In practice, you almost never call system calls directly. Instead, you call libc wrapper functions that set up the arguments, execute the syscall instruction, check for errors, and set errno. This is like having an assistant who fills out the government forms for you.
#include <unistd.h>#include <fcntl.h>int main(void) { // These are libc wrapper functions, not raw syscalls. // open() calls the kernel's sys_open, which allocates a file descriptor, // checks permissions, and returns an integer handle to your program. int fd = open("file.txt", O_RDONLY); char buffer[1024]; // read() asks the kernel to copy bytes from the file into your buffer. // The kernel does the actual disk I/O (or returns cached data from the page cache). ssize_t bytes = read(fd, buffer, sizeof(buffer)); // write() to STDOUT_FILENO (fd 1) asks the kernel to send bytes to the terminal. write(STDOUT_FILENO, buffer, bytes); // close() tells the kernel we are done with this file descriptor. // The kernel frees the fd slot and flushes any pending writes. close(fd); return 0;}
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <unistd.h>#include <fcntl.h>int main(void) { int fd = open("/nonexistent", O_RDONLY); if (fd == -1) { // errno is set by the system call fprintf(stderr, "Error code: %d\n", errno); fprintf(stderr, "Error message: %s\n", strerror(errno)); perror("open"); // Prints: "open: No such file or directory" // Common errno values switch (errno) { case ENOENT: printf("File not found\n"); break; case EACCES: printf("Permission denied\n"); break; case EEXIST: printf("File exists\n"); break; case EINTR: printf("Interrupted\n"); break; case EINVAL: printf("Invalid argument\n"); break; case ENOMEM: printf("Out of memory\n"); break; case ENOSPC: printf("No space left\n"); break; } return 1; } close(fd); return 0;}// Robust error-handling wrapper// Why EINTR handling matters: if a signal arrives while your program is blocked// in a system call, the kernel interrupts the call and returns -1 with errno=EINTR.// This is not an error -- it just means "try again." Without this retry loop,// your program would falsely report errors whenever it receives a signal (which// happens more often than you think: SIGCHLD when a child process exits,// SIGALRM from timers, etc.).int safe_open(const char *path, int flags) { int fd; do { fd = open(path, flags); } while (fd == -1 && errno == EINTR); // Retry on signal interruption return fd;}// Robust read (handles partial reads and interrupts)// A common beginner mistake: assuming read() always returns the full count.// In reality, read() can return fewer bytes than requested for many reasons:// - Signal interrupted the call// - Reading from a pipe or socket (data arrives in chunks)// - Reading near end of file// - Kernel decided to return a partial buffer// Production code MUST loop until it gets all requested bytes or hits EOF/error.ssize_t safe_read(int fd, void *buf, size_t count) { ssize_t total = 0; char *ptr = buf; while (count > 0) { ssize_t n = read(fd, ptr, count); if (n == -1) { if (errno == EINTR) continue; // Signal interrupted us, just retry return -1; // Actual error (EBADF, EIO, etc.) } if (n == 0) break; // EOF -- no more data available total += n; ptr += n; count -= n; } return total;}
File descriptors are the kernel’s universal handle for “anything you can read from or write to.” Files, pipes, sockets, terminals, even special devices like /dev/null — they all look the same to your program: just an integer you pass to read() and write(). This uniform interface is one of Unix’s most powerful design decisions, and it is why shell pipes like cat file.txt | grep pattern | wc -l just work.
#include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <sys/stat.h>int main(void) { // Standard file descriptors -- every process starts with these three open: // 0 = stdin (where input comes from, usually the keyboard) // 1 = stdout (where output goes, usually the terminal) // 2 = stderr (where error messages go, also the terminal but separate) // Open returns the lowest available fd number. // Since 0, 1, 2 are already taken, the first open() usually returns 3. int fd = open("file.txt", O_RDWR | O_CREAT, 0644); printf("Opened fd: %d\n", fd); // Usually 3 // Duplicate file descriptor int fd2 = dup(fd); // fd2 points to same file // Duplicate to specific number int fd3 = dup2(fd, 10); // fd 10 now points to same file // Get/set file descriptor flags int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_NONBLOCK); // Get file info struct stat st; fstat(fd, &st); printf("Size: %ld bytes\n", st.st_size); printf("Mode: %o\n", st.st_mode); printf("Is regular file: %d\n", S_ISREG(st.st_mode)); printf("Is directory: %d\n", S_ISDIR(st.st_mode)); close(fd); close(fd2); close(fd3); return 0;}