File I/O & System Calls#

C provides two layers of file I/O: the standard library (<stdio.h>) for portable, buffered I/O, and POSIX system calls for low-level, unbuffered access. Understanding both is essential for systems programming.


Standard I/O (<stdio.h>)#

Opening and Closing Files#

FILE *fp = fopen("data.txt", "r");
if (!fp) {
    perror("fopen");  // prints: "fopen: No such file or directory"
    return 1;
}

// ... use fp ...

fclose(fp);
ModeMeaning
"r"Read (file must exist)
"w"Write (truncates or creates)
"a"Append (creates if needed)
"r+"Read + write (file must exist)
"w+"Read + write (truncates or creates)
"rb"Read binary
"wb"Write binary

Reading#

// Read character by character
int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);
}

// Read line by line
char line[256];
while (fgets(line, sizeof(line), fp)) {
    printf("%s", line);  // line includes '\n'
}

// Read formatted
int id;
char name[64];
fscanf(fp, "%d %63s", &id, name);

// Read binary
size_t n = fread(buffer, sizeof(int), count, fp);

Writing#

fprintf(fp, "User %d: %s\n", id, name);
fputs("hello\n", fp);
fputc('A', fp);
fwrite(data, sizeof(int), count, fp);

Standard Streams#

StreamFile DescriptorPurpose
stdin0Standard input
stdout1Standard output (line-buffered)
stderr2Standard error (unbuffered)

POSIX I/O (Low-Level)#

Direct system calls — no buffering, no formatting, works with file descriptors (integers):

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

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

char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
if (n == -1) {
    perror("read");
}

write(STDOUT_FILENO, buf, n);
close(fd);
open FlagMeaning
O_RDONLYRead only
O_WRONLYWrite only
O_RDWRRead + write
O_CREATCreate if not exists
O_TRUNCTruncate to zero length
O_APPENDAppend mode
O_NONBLOCKNon-blocking I/O

stdio vs POSIX#

Featurestdio (FILE*)POSIX (int fd)
BufferingAutomatic (user-space)None (kernel only)
Formattingfprintf, fscanfManual
PortabilityC standard (everywhere)POSIX (Unix/Linux/macOS)
PerformanceGood for sequentialBetter for random access
Use caseText files, formatted I/OSockets, pipes, devices

Error Handling: errno#

System calls set errno on failure:

#include <errno.h>
#include <string.h>

int fd = open("missing.txt", O_RDONLY);
if (fd == -1) {
    printf("Error %d: %s\n", errno, strerror(errno));
    // Error 2: No such file or directory
    perror("open");  // shorthand: prints "open: No such file or directory"
}
errno ValueNameMeaning
ENOENT2No such file or directory
EACCES13Permission denied
EEXIST17File exists
EINTR4Interrupted system call
ENOMEM12Out of memory
EAGAIN11Resource temporarily unavailable

Always check return values of system calls. A return of -1 means failure; check errno for the reason.


Signals#

Signals are asynchronous notifications from the OS to a process:

#include <signal.h>

volatile sig_atomic_t running = 1;

void handle_sigint(int sig) {
    (void)sig;
    running = 0;  // set flag — do NOT call printf or malloc here
}

int main(void) {
    signal(SIGINT, handle_sigint);  // or use sigaction for reliability

    while (running) {
        // main loop
    }
    printf("Shutting down gracefully\n");
    return 0;
}
SignalNumberDefaultMeaning
SIGINT2TerminateCtrl+C
SIGTERM15Terminatekill command
SIGKILL9TerminateCannot be caught
SIGSEGV11Core dumpSegmentation fault
SIGPIPE13TerminateWrite to broken pipe
SIGCHLD17IgnoreChild process terminated

Signal Handler Rules#

Signal handlers must be async-signal-safe. You can:

  • Set a volatile sig_atomic_t flag
  • Call _exit(), write(), signal()

You cannot safely call: printf, malloc, free, fopen, or most library functions.


Process Control#

#include <unistd.h>
#include <sys/wait.h>

pid_t pid = fork();  // create child process

if (pid == -1) {
    perror("fork");
} else if (pid == 0) {
    // Child process
    execvp("ls", (char *[]){"ls", "-la", NULL});  // replace with new program
    perror("exec");  // only reached if exec fails
    _exit(1);
} else {
    // Parent process
    int status;
    waitpid(pid, &status, 0);  // wait for child
    if (WIFEXITED(status)) {
        printf("Child exited with %d\n", WEXITSTATUS(status));
    }
}

Key Takeaways#

  • stdio.h provides buffered, portable file I/O. POSIX calls provide low-level, unbuffered access.
  • Always check return values. System calls return -1 on failure; errno tells you why.
  • perror() and strerror(errno) are your friends for error messages.
  • Signal handlers must be minimal — set a flag and return. Do not call printf or malloc.
  • fork() + exec() is the Unix process creation model. The child is a copy of the parent until exec replaces it.
  • Use stdio for text processing and formatted output. Use POSIX I/O for sockets, pipes, and devices.