Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

POSIX and System Programming in C

Overview

POSIX (Portable Operating System Interface) is a family of standards specified by the IEEE for maintaining compatibility between operating systems. POSIX defines the system-level API for Unix-like operating systems, and C is the primary language for POSIX programming.

Understanding POSIX is essential for:

  • Systems programming (OS kernels, device drivers, servers)
  • Writing portable Unix/Linux applications
  • Understanding how operating systems work at the API level
  • Many technical interviews (especially for backend/infrastructure roles)

POSIX Standards

StandardYearNameKey Additions
POSIX.11988System InterfaceBasic system calls, process control
POSIX.1b1993Realtime ExtensionsRealtime signals, timers, shared memory
POSIX.1c1995Threads Extensionpthreads
POSIX.1-20012001Single UNIX Spec v3Combined standard
POSIX.1-20082008Single UNIX Spec v4Latest major revision

File I/O

POSIX provides unbuffered I/O through file descriptors — small integers that represent open files:

Opening and Closing Files

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

int main() {
    // Open file for reading
    int fd = open("data.txt", O_RDONLY);
    if (fd == -1) {
        perror("open failed");  // Prints: open failed: No such file or directory
        return 1;
    }
    
    // Open file for writing (create if not exists, truncate if exists)
    int fd_out = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd_out == -1) {
        perror("open output failed");
        close(fd);
        return 1;
    }
    
    // Always close file descriptors
    close(fd);
    close(fd_out);
    
    return 0;
}

File Open Flags

FlagDescription
O_RDONLYRead only
O_WRONLYWrite only
O_RDWRRead and write
O_CREATCreate file if it doesn’t exist
O_TRUNCTruncate file to zero length
O_APPENDAppend to end of file
O_EXCLFail if file exists (with O_CREAT)
O_NONBLOCKNon-blocking mode
O_SYNCSynchronous writes

Reading and Writing

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

#define BUFFER_SIZE 4096

// Read entire file using POSIX I/O
char* read_file(const char *path, size_t *length) {
    int fd = open(path, O_RDONLY);
    if (fd == -1) return NULL;
    
    // Get file size
    off_t size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);
    
    char *buffer = malloc(size + 1);
    if (buffer == NULL) {
        close(fd);
        return NULL;
    }
    
    size_t total_read = 0;
    while (total_read < (size_t)size) {
        ssize_t bytes = read(fd, buffer + total_read, size - total_read);
        if (bytes <= 0) break;  // Error or EOF
        total_read += bytes;
    }
    
    buffer[total_read] = '\0';
    if (length) *length = total_read;
    
    close(fd);
    return buffer;
}

// Copy file using POSIX I/O
int copy_file(const char *src, const char *dst) {
    int fd_in = open(src, O_RDONLY);
    if (fd_in == -1) return -1;
    
    int fd_out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd_out == -1) {
        close(fd_in);
        return -1;
    }
    
    char buffer[BUFFER_SIZE];
    ssize_t bytes;
    
    while ((bytes = read(fd_in, buffer, BUFFER_SIZE)) > 0) {
        ssize_t written = 0;
        while (written < bytes) {
            ssize_t w = write(fd_out, buffer + written, bytes - written);
            if (w <= 0) {
                close(fd_in);
                close(fd_out);
                return -1;
            }
            written += w;
        }
    }
    
    close(fd_in);
    close(fd_out);
    return 0;
}

int main() {
    size_t len;
    char *content = read_file("data.txt", &len);
    if (content) {
        printf("Read %zu bytes: %s\n", len, content);
        free(content);
    }
    
    copy_file("source.txt", "dest.txt");
    return 0;
}

File Descriptors vs FILE*

FeatureFile Descriptors (POSIX)FILE* (stdio)
BufferingUnbufferedBuffered
Functionsopen, read, write, closefopen, fread, fwrite, fclose
PerformanceBetter for large I/OBetter for small, frequent I/O
FlexibilityMore control (flags, modes)Easier to use
Use caseSystem programming, pipes, socketsGeneral file I/O

Process Control

fork — Creating Processes

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

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        // Error
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // Child process
        printf("Child: PID=%d, PPID=%d\n", getpid(), getppid());
        printf("Child: Doing some work...\n");
        sleep(1);
        printf("Child: Done!\n");
        return 42;  // Exit code
    } else {
        // Parent process
        printf("Parent: Created child with PID=%d\n", pid);
        
        int status;
        pid_t child_pid = waitpid(pid, &status, 0);
        
        if (WIFEXITED(status)) {
            printf("Parent: Child exited with code %d\n", WEXITSTATUS(status));
        }
    }
    
    return 0;
}

fork Memory Layout

flowchart TD
    subgraph "Before fork"
        A["Parent Process"]
    end
    
    subgraph "After fork"
        B["Parent Process"] -->|"Child PID"| C["waitpid"]
        D["Child Process"] -->|"Copy of parent"| E["exec or exit"]
    end
    
    A -->|"fork()"| B
    A -->|"fork()"| D
    
    style B fill:#E3F2FD
    style D fill:#E8F5E9

exec — Replacing Process Image

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

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // Child: replace with new program
        printf("Child: About to exec ls\n");
        
        // Various exec forms:
        // execl  — list of args
        // execv  — array of args
        // execle — list of args + environment
        // execve — array of args + environment
        // execlp — list of args, search PATH
        // execvp — array of args, search PATH
        
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);
        
        // exec only returns on error
        perror("exec failed");
        return 1;
    } else {
        wait(NULL);
        printf("Parent: Child finished\n");
    }
    
    return 0;
}

Creating a Simple Shell

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_ARGS 64

int main() {
    char line[1024];
    
    while (1) {
        printf("$ ");
        fflush(stdout);
        
        if (fgets(line, sizeof(line), stdin) == NULL) break;
        
        // Remove newline
        line[strcspn(line, "\n")] = '\0';
        
        if (strcmp(line, "exit") == 0) break;
        
        // Parse arguments
        char *args[MAX_ARGS];
        int argc = 0;
        char *token = strtok(line, " ");
        while (token && argc < MAX_ARGS - 1) {
            args[argc++] = token;
            token = strtok(NULL, " ");
        }
        args[argc] = NULL;
        
        if (argc == 0) continue;
        
        pid_t pid = fork();
        if (pid == 0) {
            execvp(args[0], args);
            perror("command not found");
            exit(1);
        } else if (pid > 0) {
            int status;
            waitpid(pid, &status, 0);
        } else {
            perror("fork failed");
        }
    }
    
    return 0;
}

Signals

Signals are software interrupts sent to a process:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

volatile sig_atomic_t got_signal = 0;

void signal_handler(int signum) {
    // Signal handlers should only use async-signal-safe functions
    got_signal = signum;
    // Write is async-signal-safe
    const char msg[] = "Signal received!\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}

int main() {
    // Register signal handler
    signal(SIGINT, signal_handler);   // Ctrl+C
    signal(SIGTERM, signal_handler);  // kill command
    signal(SIGUSR1, signal_handler);  // User-defined signal
    
    printf("PID: %d\n", getpid());
    printf("Send SIGUSR1: kill -USR1 %d\n", getpid());
    
    while (!got_signal) {
        pause();  // Wait for signal
    }
    
    printf("Received signal %d, exiting\n", got_signal);
    return 0;
}

Common Signals

SignalNumberDefault ActionDescription
SIGHUP1TerminateHangup (terminal closed)
SIGINT2TerminateInterrupt (Ctrl+C)
SIGQUIT3Core dumpQuit (Ctrl+backslash)
SIGKILL9TerminateKill (cannot be caught)
SIGSEGV11Core dumpSegmentation fault
SIGTERM15TerminateTermination request
SIGUSR110TerminateUser-defined signal 1
SIGUSR212TerminateUser-defined signal 2
SIGCHLD17IgnoreChild process state change
SIGSTOP19StopStop process (cannot be caught)
SIGCONT18ContinueContinue stopped process

sigaction — Better Signal Handling

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig, siginfo_t *info, void *context) {
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;  // Use sa_sigaction instead of sa_handler
    sigemptyset(&sa.sa_mask);
    
    sigaction(SIGINT, &sa, NULL);
    
    printf("Press Ctrl+C...\n");
    while (1) pause();
    
    return 0;
}

Pipes

Pipes enable inter-process communication:

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

int main() {
    int pipefd[2];  // pipefd[0] = read end, pipefd[1] = write end
    
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // Child: writer
        close(pipefd[0]);  // Close read end
        
        const char *msg = "Hello from child!";
        write(pipefd[1], msg, strlen(msg) + 1);
        
        close(pipefd[1]);
        return 0;
    } else {
        // Parent: reader
        close(pipefd[1]);  // Close write end
        
        char buffer[256];
        ssize_t bytes = read(pipefd[0], buffer, sizeof(buffer));
        
        printf("Parent received: %s (%zd bytes)\n", buffer, bytes);
        
        close(pipefd[0]);
        wait(NULL);
    }
    
    return 0;
}

Pipe Diagram

flowchart LR
    subgraph "Child Process"
        A["Write end fd 1"]
    end
    subgraph "Pipe Kernel Buffer"
        B["Unidirectional data flow"]
    end
    subgraph "Parent Process"
        C["Read end fd 0"]
    end
    
    A -->|"write()"| B
    B -->|"read()"| C

Environment Variables

#include <stdio.h>
#include <stdlib.h>

int main() {
    // Get environment variable
    char *path = getenv("PATH");
    if (path) {
        printf("PATH = %s\n", path);
    }
    
    // Set environment variable
    setenv("MY_VAR", "hello", 1);  // 1 = overwrite if exists
    printf("MY_VAR = %s\n", getenv("MY_VAR"));
    
    // Unset environment variable
    unsetenv("MY_VAR");
    
    // Alternative: putenv (less safe — takes ownership of string)
    // putenv("MY_VAR=hello");
    
    return 0;
}

Error Handling

POSIX functions typically return -1 on error and set errno:

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

int main() {
    int fd = open("/nonexistent", O_RDONLY);
    
    if (fd == -1) {
        // Method 1: perror — prints human-readable error
        perror("open");
        // Output: open: No such file or directory
        
        // Method 2: strerror — get error string
        printf("Error %d: %s\n", errno, strerror(errno));
        // Output: Error 2: No such file or directory
        
        // Method 3: Check specific error
        if (errno == ENOENT) {
            printf("File not found\n");
        } else if (errno == EACCES) {
            printf("Permission denied\n");
        }
    }
    
    return 0;
}

Common Errno Values

ErrorNumberDescription
ENOENT2No such file or directory
EACCES13Permission denied
EEXIST17File exists
ENOMEM12Out of memory
EINVAL22Invalid argument
EMFILE24Too many open files
EAGAIN11Resource temporarily unavailable
EINTR4Interrupted system call

Common Mistakes

MistakeConsequenceFix
Not checking fork() returnRunning code in wrong processAlways check pid < 0, == 0, > 0
Not checking open() returnUsing invalid fdCheck for -1
Forgetting to close fdsResource leakAlways close() when done
Using printf in signal handlerUndefined behavior (not async-signal-safe)Use write()
Ignoring EINTR from read()/write()Premature terminationRetry on EINTR
Not handling partial write()Incomplete dataLoop until all bytes written
Using perror without checking errnoMisleading error messagesOnly call after error

Interview Questions

  1. What is the difference between fork() and exec()?

    • fork() creates a new process (copy of parent). exec() replaces the current process image with a new program.
  2. What are file descriptors?

    • Small non-negative integers that represent open files. 0=stdin, 1=stdout, 2=stderr.
  3. How do pipes work in Unix?

    • Unidirectional communication channel. pipe() creates two fds: read end and write end.
  4. What signals cannot be caught?

    • SIGKILL (9) and SIGSTOP (19) cannot be caught, blocked, or ignored.
  5. What is the difference between wait() and waitpid()?

    • wait() waits for any child. waitpid() waits for a specific child (or any child with pid=-1).