|$ curl https://forge-ai.dev/api/markdown?path=docs/c/posix
$cat docs/c-—-posix-&-systems-programming.md
updated Recently·50 min read·published

C — POSIX & Systems Programming

CPOSIXUnixSystems ProgrammingAdvancedAdvanced🎯Free Tools
What is POSIX

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.

HeaderPurpose
<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
posix_basics.c
C
1#include <stdio.h>
2#include <unistd.h>
3#include <sys/types.h>
4
5int 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

On macOS, many POSIX functions are available but some Linux-specific extensions (like signalfd or timerfd) are not. Always check the man page and use feature test macros like _POSIX_C_SOURCE for portability.
File Descriptors

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.

file_descriptor.c
C
1#include <fcntl.h>
2#include <unistd.h>
3#include <stdio.h>
4#include <string.h>
5#include <errno.h>
6
7int 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.

file_flags.c
C
1#include <fcntl.h>
2#include <unistd.h>
3#include <stdio.h>
4#include <string.h>
5#include <errno.h>
6
7int 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

Always check return values from POSIX calls. Unlike some higher-level languages, C system calls fail silently if you ignore errors. Use errno consistently and provide meaningful error messages. Consider wrapping common patterns in utility functions.
Processes

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.

fork_example.c
C
1#include <stdio.h>
2#include <unistd.h>
3#include <sys/types.h>
4#include <sys/wait.h>
5
6int 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).

exec_example.c
C
1#include <stdio.h>
2#include <unistd.h>
3#include <sys/wait.h>
4
5int 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

After fork(), only one of parent or child should perform important work, or use proper synchronization. Never assume scheduling order. Also, zombie processes occur when a child exits before the parent calls wait() — always reap children.

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.

zombie_prevention.c
C
1#include <stdio.h>
2#include <unistd.h>
3#include <sys/wait.h>
4#include <signal.h>
5
6volatile sig_atomic_t child_exited = 0;
7
8void handle_child(int sig) {
9 child_exited = 1;
10}
11
12int 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}
Building a Simple Shell

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.

minishell.c
C
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
11int 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.)
23int 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
38int 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

This shell is minimal. A production shell needs signal handling (SIGINT for Ctrl+C), job control (background processes with &), piping (|), I/O redirection (>, <), environment variable expansion ($VAR), and quoting. Each adds significant complexity. Start here and incrementally add features.
Pipes

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.

pipe_example.c
C
1#include <stdio.h>
2#include <unistd.h>
3#include <sys/wait.h>
4
5int 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.

popen_example.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4int 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

Named pipes (FIFOs) work like regular pipes but exist as filesystem entries. They allow unrelated processes to communicate. Create with mkfifo() or the mkfifo command. Open with open() — the open blocks until both reader and writer are connected.
File Operations & Metadata

Beyond basic read/write, POSIX provides file metadata, seeking, locking, and file descriptor manipulation. These operations are essential for building robust file-based applications.

file_metadata.c
C
1#include <stdio.h>
2#include <fcntl.h>
3#include <unistd.h>
4#include <sys/stat.h>
5#include <time.h>
6
7int 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).

file_locking.c
C
1#include <stdio.h>
2#include <fcntl.h>
3#include <unistd.h>
4#include <sys/file.h>
5
6int 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
29int 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.

dup2_example.c
C
1#include <stdio.h>
2#include <fcntl.h>
3#include <unistd.h>
4
5// Redirect stdout to a file, run a command, restore stdout
6void 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
27int main(void) {
28 run_redirected("redirected.txt");
29 return 0;
30}
Environment & Users

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.

environment.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4#include <pwd.h>
5
6extern char **environ;
7
8void print_env(void) {
9 for (char **env = environ; *env; env++) {
10 printf(" %s\n", *env);
11 }
12}
13
14int 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

Never use getenv() for security-sensitive values without verifying the result. Environment variables can be set by any process that shares the environment. For secrets, prefer reading from files with restricted permissions.
Socket Programming

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.

tcp_server.c
C
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
13int 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}
tcp_client.c
C
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
12int 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
54int 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

Always check return values from socket calls and handle partial reads/writes. Network I/O can return fewer bytes than requested. Loop until all bytes are sent or received. For production code, add timeouts with setsockopt and handle SIGPIPE signals.
Complete Example: File Copier

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.

file_copier.c
C
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
11int 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
80int 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}
Process Pool Pattern

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.

process_pool.c
C
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
12void 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
23int 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

In production process pools, handle worker crashes by monitoring SIGCHLD and restarting failed workers. Use more sophisticated work distribution (e.g., work-stealing) for load balancing. Consider using shmem or mmap for shared state when pipes become a bottleneck.
$Blueprint — Engineering Documentation·Section ID: C-POSIX·Revision: 1.0