C — POSIX & Systems Programming
POSIX (Portable Operating System Interface) is a family of standards maintained by IEEE that defines a consistent API for Unix-like operating systems. C was designed for Unix, and POSIX is the bridge that lets you write portable C code that works across Linux, macOS, BSD, and other Unix systems. Understanding POSIX is essential for systems programming in C.
POSIX defines interfaces for process management, file I/O, signals, threads, networking, and more. While not every function is available on every system, the core POSIX API is remarkably consistent. The key headers you will use frequently are listed below.
| Header | Purpose |
|---|---|
| <unistd.h> | Basic system calls: read, write, fork, exec, close, lseek, getpid |
| <fcntl.h> | File control: open, fcntl, file flags and locking |
| <sys/types.h> | System data types: pid_t, ssize_t, off_t, mode_t |
| <sys/wait.h> | Process waiting: wait, waitpid, WEXITSTATUS macros |
| <sys/stat.h> | File status: stat, fstat, S_ISREG, permissions |
| <sys/socket.h> | Socket API: socket, bind, listen, accept, connect |
| <signal.h> | Signal handling: signal, sigaction, kill, raise |
| <errno.h> | Error codes: errno, perror, strerror |
| 1 | #include <stdio.h> |
| 2 | #include <unistd.h> |
| 3 | #include <sys/types.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | printf("Process ID: %d\n", getpid()); |
| 7 | printf("Parent Process ID: %d\n", getppid()); |
| 8 | printf("User ID: %d\n", getuid()); |
| 9 | printf("Group ID: %d\n", getgid()); |
| 10 | printf("Host name: "); |
| 11 | fflush(stdout); |
| 12 | char hostname[256]; |
| 13 | if (gethostname(hostname, sizeof(hostname)) == 0) { |
| 14 | printf("%s\n", hostname); |
| 15 | } else { |
| 16 | perror("gethostname"); |
| 17 | } |
| 18 | return 0; |
| 19 | } |
note
File descriptors are integer handles to open files, devices, pipes, and sockets. Every process has a file descriptor table. The first three entries are reserved: 0 is stdin, 1 is stdout, 2 is stderr. Unlike stdio (which buffers data), POSIX file I/O is unbuffered — data goes directly to the kernel.
| 1 | #include <fcntl.h> |
| 2 | #include <unistd.h> |
| 3 | #include <stdio.h> |
| 4 | #include <string.h> |
| 5 | #include <errno.h> |
| 6 | |
| 7 | int main(void) { |
| 8 | // Open a file for writing (create if missing, truncate if exists) |
| 9 | int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| 10 | if (fd == -1) { |
| 11 | perror("open"); |
| 12 | return 1; |
| 13 | } |
| 14 | |
| 15 | const char *msg = "Hello, file descriptors!\n"; |
| 16 | ssize_t bytes_written = write(fd, msg, strlen(msg)); |
| 17 | if (bytes_written == -1) { |
| 18 | perror("write"); |
| 19 | close(fd); |
| 20 | return 1; |
| 21 | } |
| 22 | |
| 23 | printf("Wrote %zd bytes\n", bytes_written); |
| 24 | close(fd); |
| 25 | |
| 26 | // Now read it back |
| 27 | fd = open("output.txt", O_RDONLY); |
| 28 | if (fd == -1) { |
| 29 | perror("open for read"); |
| 30 | return 1; |
| 31 | } |
| 32 | |
| 33 | char buf[256]; |
| 34 | ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1); |
| 35 | if (bytes_read == -1) { |
| 36 | perror("read"); |
| 37 | close(fd); |
| 38 | return 1; |
| 39 | } |
| 40 | buf[bytes_read] = '\0'; |
| 41 | printf("Read %zd bytes: %s", bytes_read, buf); |
| 42 | |
| 43 | close(fd); |
| 44 | return 0; |
| 45 | } |
The open() flags control how the file is accessed. Common flags include O_RDONLY (read only), O_WRONLY (write only), O_RDWR (read write), O_CREAT (create if missing), O_TRUNC (truncate to zero), and O_APPEND (append mode). Permissions use octal notation: 0644 means owner read/write, group read, others read.
Error handling in POSIX uses errno. When a system call fails, it typically returns -1 and sets errno to indicate the specific error. Use perror() for a human-readable message, or strerror(errno) for programmatic access.
| 1 | #include <fcntl.h> |
| 2 | #include <unistd.h> |
| 3 | #include <stdio.h> |
| 4 | #include <string.h> |
| 5 | #include <errno.h> |
| 6 | |
| 7 | int main(void) { |
| 8 | // Demonstrate different open flags |
| 9 | int fd; |
| 10 | |
| 11 | // O_APPEND: writes always go to end of file |
| 12 | fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); |
| 13 | write(fd, "line 1\n", 7); |
| 14 | write(fd, "line 2\n", 7); |
| 15 | close(fd); |
| 16 | |
| 17 | // O_EXCL: fail if file already exists (atomic creation) |
| 18 | fd = open("unique.txt", O_WRONLY | O_CREAT | O_EXCL, 0644); |
| 19 | if (fd == -1) { |
| 20 | if (errno == EEXIST) { |
| 21 | printf("File already exists!\n"); |
| 22 | } else { |
| 23 | perror("open with O_EXCL"); |
| 24 | } |
| 25 | } else { |
| 26 | printf("Created new file\n"); |
| 27 | close(fd); |
| 28 | } |
| 29 | |
| 30 | // Error handling patterns |
| 31 | fd = open("/nonexistent/file", O_RDONLY); |
| 32 | if (fd == -1) { |
| 33 | fprintf(stderr, "Failed to open: %s (errno=%d)\n", |
| 34 | strerror(errno), errno); |
| 35 | } |
| 36 | |
| 37 | return 0; |
| 38 | } |
best practice
fork()creates a new process by duplicating the calling process. The child process gets a copy of the parent's memory, file descriptors, and state. fork() returns twice: once in the parent (with the child's PID) and once in the child (with 0). This is one of the most powerful and elegant APIs in Unix.
| 1 | #include <stdio.h> |
| 2 | #include <unistd.h> |
| 3 | #include <sys/types.h> |
| 4 | #include <sys/wait.h> |
| 5 | |
| 6 | int main(void) { |
| 7 | printf("Parent process (PID=%d) starting\n", getpid()); |
| 8 | |
| 9 | pid_t child_pid = fork(); |
| 10 | |
| 11 | if (child_pid == -1) { |
| 12 | perror("fork"); |
| 13 | return 1; |
| 14 | } |
| 15 | |
| 16 | if (child_pid == 0) { |
| 17 | // Child process |
| 18 | printf(" Child (PID=%d) says hello\n", getpid()); |
| 19 | printf(" Child parent PID=%d\n", getppid()); |
| 20 | return 42; // Exit code |
| 21 | } else { |
| 22 | // Parent process |
| 23 | printf("Parent knows child PID=%d\n", child_pid); |
| 24 | |
| 25 | int status; |
| 26 | pid_t waited = waitpid(child_pid, &status, 0); |
| 27 | |
| 28 | if (waited == -1) { |
| 29 | perror("waitpid"); |
| 30 | return 1; |
| 31 | } |
| 32 | |
| 33 | if (WIFEXITED(status)) { |
| 34 | printf("Child exited with code %d\n", WEXITSTATUS(status)); |
| 35 | } else if (WIFSIGNALED(status)) { |
| 36 | printf("Child killed by signal %d\n", WTERMSIG(status)); |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | return 0; |
| 41 | } |
The exec family replaces the current process image with a new program. After exec, the process continues running the new program with the same PID. There are several variants: execl (argument list), execv (argument vector), execvp (PATH search), execve (with environment).
| 1 | #include <stdio.h> |
| 2 | #include <unistd.h> |
| 3 | #include <sys/wait.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | pid_t pid = fork(); |
| 7 | |
| 8 | if (pid == -1) { |
| 9 | perror("fork"); |
| 10 | return 1; |
| 11 | } |
| 12 | |
| 13 | if (pid == 0) { |
| 14 | // Child: replace with ls command |
| 15 | execlp("ls", "ls", "-la", "/tmp", NULL); |
| 16 | |
| 17 | // exec only returns on error |
| 18 | perror("execlp"); |
| 19 | _exit(1); |
| 20 | } |
| 21 | |
| 22 | // Parent waits |
| 23 | int status; |
| 24 | waitpid(pid, &status, 0); |
| 25 | |
| 26 | if (WIFEXITED(status)) { |
| 27 | printf("ls exited with code %d\n", WEXITSTATUS(status)); |
| 28 | } |
| 29 | |
| 30 | return 0; |
| 31 | } |
warning
A zombie process is one that has exited but whose entry still exists in the process table because the parent hasn't called wait(). If the parent never waits, the zombie persists until the parent exits (init inherits it). Use WNOHANG to check for exited children without blocking.
| 1 | #include <stdio.h> |
| 2 | #include <unistd.h> |
| 3 | #include <sys/wait.h> |
| 4 | #include <signal.h> |
| 5 | |
| 6 | volatile sig_atomic_t child_exited = 0; |
| 7 | |
| 8 | void handle_child(int sig) { |
| 9 | child_exited = 1; |
| 10 | } |
| 11 | |
| 12 | int main(void) { |
| 13 | // Set up SIGCHLD handler |
| 14 | struct sigaction sa; |
| 15 | sa.sa_handler = handle_child; |
| 16 | sigemptyset(&sa.sa_mask); |
| 17 | sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; |
| 18 | sigaction(SIGCHLD, &sa, NULL); |
| 19 | |
| 20 | // Spawn multiple children |
| 21 | for (int i = 0; i < 3; i++) { |
| 22 | pid_t pid = fork(); |
| 23 | if (pid == 0) { |
| 24 | printf("Child %d (PID=%d) running\n", i, getpid()); |
| 25 | sleep(1 + i); |
| 26 | _exit(i); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // Non-blocking wait loop |
| 31 | while (1) { |
| 32 | int status; |
| 33 | pid_t pid = waitpid(-1, &status, WNOHANG); |
| 34 | |
| 35 | if (pid > 0) { |
| 36 | printf("Reaped child PID=%d, exit=%d\n", |
| 37 | pid, WEXITSTATUS(status)); |
| 38 | } else if (pid == 0) { |
| 39 | // No children have exited yet |
| 40 | usleep(100000); // 100ms |
| 41 | } else { |
| 42 | break; // No more children |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | printf("All children reaped\n"); |
| 47 | return 0; |
| 48 | } |
A shell is the classic POSIX programming exercise. It combines process creation (fork), program execution (exec), and process management (wait) into a cohesive application. Here is a minimal shell that reads commands, forks, executes them, and reports the exit status.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | #include <unistd.h> |
| 5 | #include <sys/wait.h> |
| 6 | |
| 7 | #define MAX_CMD_LEN 1024 |
| 8 | #define MAX_ARGS 64 |
| 9 | |
| 10 | // Tokenize input line into argument vector |
| 11 | int parse_line(char *line, char **args) { |
| 12 | int argc = 0; |
| 13 | char *token = strtok(line, " \t\n"); |
| 14 | while (token && argc < MAX_ARGS - 1) { |
| 15 | args[argc++] = token; |
| 16 | token = strtok(NULL, " \t\n"); |
| 17 | } |
| 18 | args[argc] = NULL; |
| 19 | return argc; |
| 20 | } |
| 21 | |
| 22 | // Execute built-in commands (cd, exit, etc.) |
| 23 | int builtin(char **args, int argc) { |
| 24 | if (strcmp(args[0], "exit") == 0) { |
| 25 | exit(argc > 1 ? atoi(args[1]) : 0); |
| 26 | } |
| 27 | if (strcmp(args[0], "cd") == 0) { |
| 28 | const char *dir = argc > 1 ? args[1] : getenv("HOME"); |
| 29 | if (chdir(dir) == -1) { |
| 30 | perror("cd"); |
| 31 | return 1; |
| 32 | } |
| 33 | return 0; |
| 34 | } |
| 35 | return -1; // Not a builtin |
| 36 | } |
| 37 | |
| 38 | int main(void) { |
| 39 | char line[MAX_CMD_LEN]; |
| 40 | char *args[MAX_ARGS]; |
| 41 | |
| 42 | while (1) { |
| 43 | printf("$ "); |
| 44 | fflush(stdout); |
| 45 | |
| 46 | if (!fgets(line, sizeof(line), stdin)) { |
| 47 | printf("\n"); |
| 48 | break; |
| 49 | } |
| 50 | |
| 51 | int argc = parse_line(line, args); |
| 52 | if (argc == 0) continue; |
| 53 | |
| 54 | // Check builtins first |
| 55 | if (builtin(args, argc) >= 0) continue; |
| 56 | |
| 57 | // Fork and exec |
| 58 | pid_t pid = fork(); |
| 59 | if (pid == -1) { |
| 60 | perror("fork"); |
| 61 | continue; |
| 62 | } |
| 63 | |
| 64 | if (pid == 0) { |
| 65 | // Child: execute the command |
| 66 | execvp(args[0], args); |
| 67 | fprintf(stderr, "%s: command not found\n", args[0]); |
| 68 | _exit(127); |
| 69 | } |
| 70 | |
| 71 | // Parent: wait for child |
| 72 | int status; |
| 73 | waitpid(pid, &status, 0); |
| 74 | |
| 75 | if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { |
| 76 | fprintf(stderr, "Exit code: %d\n", WEXITSTATUS(status)); |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | return 0; |
| 81 | } |
pro tip
Pipes provide unidirectional communication between processes. Data written to the write end of a pipe is read from the read end. Pipes are the backbone of Unix command pipelines — when you run cat file | grep pattern | wc, each pipe connects the stdout of one process to the stdin of the next.
| 1 | #include <stdio.h> |
| 2 | #include <unistd.h> |
| 3 | #include <sys/wait.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | int pipefd[2]; // pipefd[0]=read, pipefd[1]=write |
| 7 | |
| 8 | if (pipe(pipefd) == -1) { |
| 9 | perror("pipe"); |
| 10 | return 1; |
| 11 | } |
| 12 | |
| 13 | pid_t pid = fork(); |
| 14 | |
| 15 | if (pid == -1) { |
| 16 | perror("fork"); |
| 17 | return 1; |
| 18 | } |
| 19 | |
| 20 | if (pid == 0) { |
| 21 | // Child: writer |
| 22 | close(pipefd[0]); // Close unused read end |
| 23 | const char *msg = "Data from child process!\n"; |
| 24 | write(pipefd[1], msg, __builtin_strlen(msg)); |
| 25 | close(pipefd[1]); // Signal EOF |
| 26 | _exit(0); |
| 27 | } |
| 28 | |
| 29 | // Parent: reader |
| 30 | close(pipefd[1]); // Close unused write end |
| 31 | |
| 32 | char buf[256]; |
| 33 | ssize_t n; |
| 34 | while ((n = read(pipefd[0], buf, sizeof(buf) - 1)) > 0) { |
| 35 | buf[n] = '\0'; |
| 36 | printf("Parent received: %s", buf); |
| 37 | } |
| 38 | |
| 39 | close(pipefd[0]); |
| 40 | waitpid(pid, NULL, 0); |
| 41 | |
| 42 | return 0; |
| 43 | } |
For simpler cases, popen() opens a pipe to or from a command. It handles the fork/exec/pipe setup automatically and returns a FILE* stream you can read from or write to.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | // Read output of a command |
| 6 | FILE *fp = popen("ls -la /tmp", "r"); |
| 7 | if (!fp) { |
| 8 | perror("popen"); |
| 9 | return 1; |
| 10 | } |
| 11 | |
| 12 | char line[256]; |
| 13 | int count = 0; |
| 14 | while (fgets(line, sizeof(line), fp)) { |
| 15 | count++; |
| 16 | } |
| 17 | |
| 18 | int status = pclose(fp); |
| 19 | printf("ls produced %d lines, exit status: %d\n", |
| 20 | count, WEXITSTATUS(status)); |
| 21 | |
| 22 | // Write to a command's stdin |
| 23 | fp = popen("sort", "w"); |
| 24 | if (!fp) { |
| 25 | perror("popen for write"); |
| 26 | return 1; |
| 27 | } |
| 28 | |
| 29 | fprintf(fp, "banana\n"); |
| 30 | fprintf(fp, "apple\n"); |
| 31 | fprintf(fp, "cherry\n"); |
| 32 | pclose(fp); |
| 33 | |
| 34 | return 0; |
| 35 | } |
note
Beyond basic read/write, POSIX provides file metadata, seeking, locking, and file descriptor manipulation. These operations are essential for building robust file-based applications.
| 1 | #include <stdio.h> |
| 2 | #include <fcntl.h> |
| 3 | #include <unistd.h> |
| 4 | #include <sys/stat.h> |
| 5 | #include <time.h> |
| 6 | |
| 7 | int main(void) { |
| 8 | struct stat st; |
| 9 | |
| 10 | if (stat("file_descriptor.c", &st) == -1) { |
| 11 | perror("stat"); |
| 12 | return 1; |
| 13 | } |
| 14 | |
| 15 | printf("File size: %lld bytes\n", (long long)st.st_size); |
| 16 | printf("Permissions: %o\n", st.st_mode & 0777); |
| 17 | printf("Owner UID: %d\n", st.st_uid); |
| 18 | printf("Last modified: %s", ctime(&st.st_mtime)); |
| 19 | printf("Is regular: %s\n", S_ISREG(st.st_mode) ? "yes" : "no"); |
| 20 | printf("Is directory: %s\n", S_ISDIR(st.st_mode) ? "yes" : "no"); |
| 21 | |
| 22 | // lseek: move the file position |
| 23 | int fd = open("file_descriptor.c", O_RDONLY); |
| 24 | if (fd == -1) { |
| 25 | perror("open"); |
| 26 | return 1; |
| 27 | } |
| 28 | |
| 29 | // Read first 10 bytes |
| 30 | char buf[11]; |
| 31 | read(fd, buf, 10); |
| 32 | buf[10] = '\0'; |
| 33 | printf("First 10 bytes: %s\n", buf); |
| 34 | |
| 35 | // Seek to beginning and read again |
| 36 | lseek(fd, 0, SEEK_SET); |
| 37 | read(fd, buf, 10); |
| 38 | printf("After seek: %s\n", buf); |
| 39 | |
| 40 | // Seek to end to find file size |
| 41 | off_t size = lseek(fd, 0, SEEK_END); |
| 42 | printf("File size via lseek: %lld\n", (long long)size); |
| 43 | |
| 44 | close(fd); |
| 45 | return 0; |
| 46 | } |
File locking prevents concurrent access from corrupting data. flock() provides advisory locking (cooperative — processes must voluntarily check). Lock types are LOCK_SH (shared/read), LOCK_EX (exclusive/write), LOCK_UN (unlock), and LOCK_NB (non-blocking).
| 1 | #include <stdio.h> |
| 2 | #include <fcntl.h> |
| 3 | #include <unistd.h> |
| 4 | #include <sys/file.h> |
| 5 | |
| 6 | int append_to_log(const char *logpath, const char *message) { |
| 7 | int fd = open(logpath, O_WRONLY | O_CREAT | O_APPEND, 0644); |
| 8 | if (fd == -1) { |
| 9 | perror("open log"); |
| 10 | return -1; |
| 11 | } |
| 12 | |
| 13 | // Acquire exclusive lock (blocks until available) |
| 14 | if (flock(fd, LOCK_EX) == -1) { |
| 15 | perror("flock"); |
| 16 | close(fd); |
| 17 | return -1; |
| 18 | } |
| 19 | |
| 20 | // Write is now safe — we hold the lock |
| 21 | dprintf(fd, "[%ld] %s\n", (long)time(NULL), message); |
| 22 | |
| 23 | // Release lock and close |
| 24 | flock(fd, LOCK_UN); |
| 25 | close(fd); |
| 26 | return 0; |
| 27 | } |
| 28 | |
| 29 | int main(void) { |
| 30 | // Simulate concurrent writes from multiple processes |
| 31 | for (int i = 0; i < 5; i++) { |
| 32 | pid_t pid = fork(); |
| 33 | if (pid == 0) { |
| 34 | char msg[64]; |
| 35 | snprintf(msg, sizeof(msg), "Log entry from PID %d", getpid()); |
| 36 | append_to_log("app.log", msg); |
| 37 | _exit(0); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | for (int i = 0; i < 5; i++) { |
| 42 | wait(NULL); |
| 43 | } |
| 44 | |
| 45 | printf("All writes complete. Check app.log\n"); |
| 46 | return 0; |
| 47 | } |
dup2() duplicates a file descriptor onto a specific number. This is how I/O redirection works — to redirect stdout to a file, you duplicate the file descriptor onto fd 1. This is fundamental to building shells and pipeline-based programs.
| 1 | #include <stdio.h> |
| 2 | #include <fcntl.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | // Redirect stdout to a file, run a command, restore stdout |
| 6 | void run_redirected(const char *output_file) { |
| 7 | // Save original stdout |
| 8 | int saved_stdout = dup(STDOUT_FILENO); |
| 9 | |
| 10 | // Open file and redirect stdout to it |
| 11 | int fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| 12 | dup2(fd, STDOUT_FILENO); |
| 13 | close(fd); |
| 14 | |
| 15 | // This output goes to the file |
| 16 | printf("This goes to the file\n"); |
| 17 | fflush(stdout); |
| 18 | |
| 19 | // Restore stdout |
| 20 | dup2(saved_stdout, STDOUT_FILENO); |
| 21 | close(saved_stdout); |
| 22 | |
| 23 | // This output goes to terminal again |
| 24 | printf("This goes to the terminal\n"); |
| 25 | } |
| 26 | |
| 27 | int main(void) { |
| 28 | run_redirected("redirected.txt"); |
| 29 | return 0; |
| 30 | } |
The environment is a set of key-value string pairs inherited by child processes. Access them through the global environ variable or use getenv() and setenv() for convenience. User and group IDs determine permissions and access control.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <unistd.h> |
| 4 | #include <pwd.h> |
| 5 | |
| 6 | extern char **environ; |
| 7 | |
| 8 | void print_env(void) { |
| 9 | for (char **env = environ; *env; env++) { |
| 10 | printf(" %s\n", *env); |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | int main(void) { |
| 15 | // Environment variable access |
| 16 | const char *home = getenv("HOME"); |
| 17 | const char *path = getenv("PATH"); |
| 18 | const char *shell = getenv("SHELL"); |
| 19 | |
| 20 | printf("HOME: %s\n", home ? home : "(not set)"); |
| 21 | printf("PATH: %s\n", path ? path : "(not set)"); |
| 22 | printf("SHELL: %s\n", shell ? shell : "(not set)"); |
| 23 | |
| 24 | // Set an environment variable |
| 25 | setenv("MY_VAR", "hello_world", 1); |
| 26 | printf("MY_VAR: %s\n", getenv("MY_VAR")); |
| 27 | |
| 28 | // User and group information |
| 29 | printf("UID: %d, GID: %d\n", getuid(), getgid()); |
| 30 | printf("EUID: %d, EGID: %d\n", geteuid(), getegid()); |
| 31 | |
| 32 | struct passwd *pw = getpwuid(getuid()); |
| 33 | if (pw) { |
| 34 | printf("User: %s (uid=%d, home=%s, shell=%s)\n", |
| 35 | pw->pw_name, pw->pw_uid, pw->pw_dir, pw->pw_shell); |
| 36 | } |
| 37 | |
| 38 | return 0; |
| 39 | } |
warning
Sockets provide network communication between processes, potentially across different machines. The BSD socket API (standardized in POSIX) is the foundation of all network programming. TCP provides reliable, ordered delivery. UDP is connectionless and faster but unreliable.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | #include <unistd.h> |
| 5 | #include <arpa/inet.h> |
| 6 | #include <sys/socket.h> |
| 7 | |
| 8 | #define PORT 8080 |
| 9 | #define BACKLOG 10 |
| 10 | #define BUF_SIZE 1024 |
| 11 | |
| 12 | // TCP Server |
| 13 | int run_server(void) { |
| 14 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); |
| 15 | if (server_fd == -1) { |
| 16 | perror("socket"); |
| 17 | return 1; |
| 18 | } |
| 19 | |
| 20 | int opt = 1; |
| 21 | setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
| 22 | |
| 23 | struct sockaddr_in addr = { |
| 24 | .sin_family = AF_INET, |
| 25 | .sin_addr.s_addr = INADDR_ANY, |
| 26 | .sin_port = htons(PORT), |
| 27 | }; |
| 28 | |
| 29 | if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { |
| 30 | perror("bind"); |
| 31 | close(server_fd); |
| 32 | return 1; |
| 33 | } |
| 34 | |
| 35 | if (listen(server_fd, BACKLOG) == -1) { |
| 36 | perror("listen"); |
| 37 | close(server_fd); |
| 38 | return 1; |
| 39 | } |
| 40 | |
| 41 | printf("Server listening on port %d\n", PORT); |
| 42 | |
| 43 | while (1) { |
| 44 | struct sockaddr_in client_addr; |
| 45 | socklen_t client_len = sizeof(client_addr); |
| 46 | |
| 47 | int client_fd = accept(server_fd, |
| 48 | (struct sockaddr *)&client_addr, |
| 49 | &client_len); |
| 50 | if (client_fd == -1) { |
| 51 | perror("accept"); |
| 52 | continue; |
| 53 | } |
| 54 | |
| 55 | char client_ip[INET_ADDRSTRLEN]; |
| 56 | inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip)); |
| 57 | printf("Client connected: %s\n", client_ip); |
| 58 | |
| 59 | // Echo loop |
| 60 | char buf[BUF_SIZE]; |
| 61 | ssize_t n; |
| 62 | while ((n = read(client_fd, buf, sizeof(buf))) > 0) { |
| 63 | write(client_fd, buf, n); // Echo back |
| 64 | } |
| 65 | |
| 66 | printf("Client disconnected\n"); |
| 67 | close(client_fd); |
| 68 | } |
| 69 | |
| 70 | close(server_fd); |
| 71 | return 0; |
| 72 | } |
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | #include <unistd.h> |
| 5 | #include <arpa/inet.h> |
| 6 | #include <sys/socket.h> |
| 7 | |
| 8 | #define PORT 8080 |
| 9 | #define BUF_SIZE 1024 |
| 10 | |
| 11 | // TCP Client |
| 12 | int run_client(const char *server_ip) { |
| 13 | int sock_fd = socket(AF_INET, SOCK_STREAM, 0); |
| 14 | if (sock_fd == -1) { |
| 15 | perror("socket"); |
| 16 | return 1; |
| 17 | } |
| 18 | |
| 19 | struct sockaddr_in server_addr = { |
| 20 | .sin_family = AF_INET, |
| 21 | .sin_port = htons(PORT), |
| 22 | }; |
| 23 | |
| 24 | if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) == -1) { |
| 25 | fprintf(stderr, "Invalid address: %s\n", server_ip); |
| 26 | close(sock_fd); |
| 27 | return 1; |
| 28 | } |
| 29 | |
| 30 | if (connect(sock_fd, (struct sockaddr *)&server_addr, |
| 31 | sizeof(server_addr)) == -1) { |
| 32 | perror("connect"); |
| 33 | close(sock_fd); |
| 34 | return 1; |
| 35 | } |
| 36 | |
| 37 | printf("Connected to %s:%d\n", server_ip, PORT); |
| 38 | |
| 39 | // Send data and receive echo |
| 40 | const char *msg = "Hello from client!\n"; |
| 41 | write(sock_fd, msg, strlen(msg)); |
| 42 | |
| 43 | char buf[BUF_SIZE]; |
| 44 | ssize_t n = read(sock_fd, buf, sizeof(buf) - 1); |
| 45 | if (n > 0) { |
| 46 | buf[n] = '\0'; |
| 47 | printf("Server echoed: %s", buf); |
| 48 | } |
| 49 | |
| 50 | close(sock_fd); |
| 51 | return 0; |
| 52 | } |
| 53 | |
| 54 | int main(int argc, char *argv[]) { |
| 55 | if (argc < 2) { |
| 56 | fprintf(stderr, "Usage: %s <server-ip>\n", argv[0]); |
| 57 | return 1; |
| 58 | } |
| 59 | return run_client(argv[1]); |
| 60 | } |
info
This example combines many POSIX concepts into a practical utility: a file copier using low-level I/O with progress reporting, error handling, and file permissions preservation. It demonstrates how to build robust tools with the POSIX API.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <fcntl.h> |
| 4 | #include <unistd.h> |
| 5 | #include <sys/stat.h> |
| 6 | #include <errno.h> |
| 7 | #include <string.h> |
| 8 | |
| 9 | #define COPY_BUF_SIZE 65536 // 64KB buffer |
| 10 | |
| 11 | int copy_file(const char *src_path, const char *dst_path) { |
| 12 | // Get source file metadata |
| 13 | struct stat src_stat; |
| 14 | if (stat(src_path, &src_stat) == -1) { |
| 15 | fprintf(stderr, "Cannot stat '%s': %s\n", src_path, strerror(errno)); |
| 16 | return -1; |
| 17 | } |
| 18 | |
| 19 | if (!S_ISREG(src_stat.st_mode)) { |
| 20 | fprintf(stderr, "'%s' is not a regular file\n", src_path); |
| 21 | return -1; |
| 22 | } |
| 23 | |
| 24 | // Open source |
| 25 | int src_fd = open(src_path, O_RDONLY); |
| 26 | if (src_fd == -1) { |
| 27 | fprintf(stderr, "Cannot open '%s': %s\n", src_path, strerror(errno)); |
| 28 | return -1; |
| 29 | } |
| 30 | |
| 31 | // Create destination with same permissions |
| 32 | int dst_fd = open(dst_path, O_WRONLY | O_CREAT | O_TRUNC, |
| 33 | src_stat.st_mode & 0777); |
| 34 | if (dst_fd == -1) { |
| 35 | fprintf(stderr, "Cannot create '%s': %s\n", dst_path, strerror(errno)); |
| 36 | close(src_fd); |
| 37 | return -1; |
| 38 | } |
| 39 | |
| 40 | // Copy loop |
| 41 | char buf[COPY_BUF_SIZE]; |
| 42 | off_t total = 0; |
| 43 | ssize_t bytes_read; |
| 44 | |
| 45 | while ((bytes_read = read(src_fd, buf, COPY_BUF_SIZE)) > 0) { |
| 46 | ssize_t written = 0; |
| 47 | while (written < bytes_read) { |
| 48 | ssize_t n = write(dst_fd, buf + written, bytes_read - written); |
| 49 | if (n == -1) { |
| 50 | if (errno == EINTR) continue; |
| 51 | fprintf(stderr, "Write error: %s\n", strerror(errno)); |
| 52 | close(src_fd); |
| 53 | close(dst_fd); |
| 54 | return -1; |
| 55 | } |
| 56 | written += n; |
| 57 | } |
| 58 | total += bytes_read; |
| 59 | |
| 60 | // Progress reporting |
| 61 | if (src_stat.st_size > 0) { |
| 62 | int pct = (int)(total * 100 / src_stat.st_size); |
| 63 | printf("\rProgress: %d%% (%lld/%lld bytes)", |
| 64 | pct, (long long)total, (long long)src_stat.st_size); |
| 65 | fflush(stdout); |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | if (bytes_read == -1) { |
| 70 | fprintf(stderr, "Read error: %s\n", strerror(errno)); |
| 71 | } |
| 72 | |
| 73 | printf("\nCopied %lld bytes\n", (long long)total); |
| 74 | |
| 75 | close(src_fd); |
| 76 | close(dst_fd); |
| 77 | return 0; |
| 78 | } |
| 79 | |
| 80 | int main(int argc, char *argv[]) { |
| 81 | if (argc != 3) { |
| 82 | fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]); |
| 83 | return 1; |
| 84 | } |
| 85 | |
| 86 | return copy_file(argv[1], argv[2]) == 0 ? 0 : 1; |
| 87 | } |
A process pool pre-forks a fixed number of worker processes that wait for work. This avoids the overhead of forking for each task. Workers communicate with the parent through pipes or shared memory. This pattern is used in web servers, task queues, and parallel processing frameworks.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <unistd.h> |
| 4 | #include <sys/wait.h> |
| 5 | #include <signal.h> |
| 6 | #include <string.h> |
| 7 | |
| 8 | #define NUM_WORKERS 4 |
| 9 | #define MAX_TASKS 20 |
| 10 | |
| 11 | // Worker reads tasks from pipe, processes, writes results |
| 12 | void worker_loop(int read_fd, int write_fd) { |
| 13 | int task_id; |
| 14 | while (read(read_fd, &task_id, sizeof(task_id)) == sizeof(task_id)) { |
| 15 | // Simulate work |
| 16 | int result = task_id * task_id; |
| 17 | printf("Worker PID=%d: task %d -> result %d\n", |
| 18 | getpid(), task_id, result); |
| 19 | write(write_fd, &result, sizeof(result)); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | int main(void) { |
| 24 | int task_pipe[2]; // parent -> worker |
| 25 | int result_pipe[2]; // worker -> parent |
| 26 | |
| 27 | pipe(task_pipe); |
| 28 | pipe(result_pipe); |
| 29 | |
| 30 | // Fork worker processes |
| 31 | pid_t workers[NUM_WORKERS]; |
| 32 | for (int i = 0; i < NUM_WORKERS; i++) { |
| 33 | pid_t pid = fork(); |
| 34 | if (pid == 0) { |
| 35 | // Child: close unused pipe ends |
| 36 | close(task_pipe[1]); |
| 37 | close(result_pipe[0]); |
| 38 | worker_loop(task_pipe[0], result_pipe[1]); |
| 39 | close(task_pipe[0]); |
| 40 | close(result_pipe[1]); |
| 41 | _exit(0); |
| 42 | } |
| 43 | workers[i] = pid; |
| 44 | } |
| 45 | |
| 46 | // Parent: close worker-side pipe ends |
| 47 | close(task_pipe[0]); |
| 48 | close(result_pipe[1]); |
| 49 | |
| 50 | // Send tasks |
| 51 | for (int i = 0; i < MAX_TASKS; i++) { |
| 52 | write(task_pipe[1], &i, sizeof(i)); |
| 53 | } |
| 54 | close(task_pipe[1]); // Signal no more tasks |
| 55 | |
| 56 | // Collect results |
| 57 | for (int i = 0; i < MAX_TASKS; i++) { |
| 58 | int result; |
| 59 | read(result_pipe[0], &result, sizeof(result)); |
| 60 | printf("Result: %d\n", result); |
| 61 | } |
| 62 | close(result_pipe[0]); |
| 63 | |
| 64 | // Wait for all workers |
| 65 | for (int i = 0; i < NUM_WORKERS; i++) { |
| 66 | waitpid(workers[i], NULL, 0); |
| 67 | } |
| 68 | |
| 69 | printf("All workers finished\n"); |
| 70 | return 0; |
| 71 | } |
best practice