System Programming in C
Learn fork/exec, signals, pipes, mmap, and socket basics for Unix/Linux systems programming in C.
fork and exec
fork and exec together are the Unix way to start new programs. fork creates an identical copy of the current process — same code, same memory, same file descriptors. exec replaces the current process image with a new program, keeping the same PID. Every shell command you run uses this pattern.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main(void) {
printf("Parent PID: %d\n", getpid());
pid_t pid = fork(); /* creates a copy of this process */
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
/* Child process — pid == 0 in the child */
printf("Child PID: %d, parent: %d\n", getpid(), getppid());
/* exec replaces the child's memory with a new program */
char *args[] = {"/bin/ls", "-la", "/tmp", NULL};
execv("/bin/ls", args);
/* Only reached if execv fails — the new program never loaded */
perror("execv");
exit(1);
} else {
/* Parent process — pid is the child's PID */
int status;
waitpid(pid, &status, 0); /* block until child exits */
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
}
}
return 0;
}
Running a Command and Capturing Output
popen combines fork, exec, and pipe into a single call, giving you a FILE * connected to a command’s stdout. It is the simplest way to capture the output of a shell command from C.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Run a shell command and return its stdout as a heap string (caller must free) */
char *run_command(const char *cmd) {
FILE *fp = popen(cmd, "r");
if (!fp) return NULL;
size_t capacity = 256, len = 0;
char *buf = malloc(capacity);
if (!buf) { pclose(fp); return NULL; }
int c;
while ((c = fgetc(fp)) != EOF) {
if (len + 1 >= capacity) {
capacity *= 2;
char *tmp = realloc(buf, capacity);
if (!tmp) { free(buf); pclose(fp); return NULL; }
buf = tmp;
}
buf[len++] = (char)c;
}
buf[len] = '\0';
pclose(fp);
return buf;
}
int main(void) {
char *output = run_command("uname -a");
if (output) {
printf("System: %s", output);
free(output);
}
return 0;
}
Signals
Signals are asynchronous notifications delivered to a process by the kernel or another process. They are the mechanism behind Ctrl+C, graceful shutdown requests, and child process notifications. Signal handlers run asynchronously — they interrupt normal execution at any point — so they must only call async-signal-safe functions.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdatomic.h>
/* volatile sig_atomic_t is the only type guaranteed safe to modify in a signal handler */
static volatile sig_atomic_t running = 1;
static volatile sig_atomic_t reload_config = 0;
void handle_sigint(int sig) {
(void)sig;
running = 0; /* tell main loop to exit cleanly */
}
void handle_sighup(int sig) {
(void)sig;
reload_config = 1; /* request config reload — main loop handles it */
}
int main(void) {
/* Use sigaction (not signal) for reliable, portable signal handling */
struct sigaction sa_int = {0};
sa_int.sa_handler = handle_sigint;
sigemptyset(&sa_int.sa_mask);
sa_int.sa_flags = SA_RESTART; /* restart interrupted system calls */
sigaction(SIGINT, &sa_int, NULL);
struct sigaction sa_hup = {0};
sa_hup.sa_handler = handle_sighup;
sigemptyset(&sa_hup.sa_mask);
sigaction(SIGHUP, &sa_hup, NULL);
/* Ignore SIGPIPE — common for network servers: prevents crash on broken connection */
signal(SIGPIPE, SIG_IGN);
printf("Running (PID %d). Press Ctrl+C to stop.\n", getpid());
while (running) {
if (reload_config) {
reload_config = 0;
printf("Reloading configuration...\n");
}
sleep(1);
printf("tick\n");
}
printf("Shutting down cleanly.\n");
return 0;
}
Pipes
A pipe is a unidirectional byte stream connecting two processes. The kernel provides a read end and a write end. Data written to the write end can be read from the read end — this is the | operator in shell scripts, implemented as a system call in C.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(void) {
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: reads from the pipe */
close(pipefd[1]); /* close write end — child only reads */
char buf[256];
ssize_t n;
while ((n = read(pipefd[0], buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
printf("Child received: %s", buf);
}
close(pipefd[0]);
_exit(0);
} else {
/* Parent: writes to the pipe */
close(pipefd[0]); /* close read end — parent only writes */
const char *messages[] = {"Hello from parent\n", "Line 2\n", "Done\n"};
for (int i = 0; i < 3; i++) {
write(pipefd[1], messages[i], strlen(messages[i]));
}
close(pipefd[1]); /* closing write end sends EOF to child */
waitpid(pid, NULL, 0);
}
return 0;
}
Memory-Mapped Files (mmap)
mmap maps a file or anonymous memory directly into the process’s virtual address space. For large files, it can be faster than read/write because the kernel manages the actual I/O lazily — only loading pages that are actually accessed. It also enables zero-copy sharing between processes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
/* Read a file using mmap — efficient for large files, no buffer management */
void process_with_mmap(const char *filename) {
int fd = open(filename, O_RDONLY);
if (fd == -1) { perror("open"); return; }
struct stat st;
fstat(fd, &st);
size_t size = (size_t)st.st_size;
/* Map the file into the address space — reads trigger page faults, not read() calls */
void *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd); /* can close fd after mmap — the mapping persists */
if (data == MAP_FAILED) { perror("mmap"); return; }
/* Access the file as if it were an array of bytes in memory */
const char *text = (const char *)data;
int lines = 0;
for (size_t i = 0; i < size; i++) {
if (text[i] == '\n') lines++;
}
printf("File has %zu bytes and %d lines\n", size, lines);
munmap(data, size); /* unmap when done */
}
/* Anonymous mmap — allocate large memory blocks without malloc fragmentation */
void anon_mmap_example(void) {
size_t size = 64 * 1024 * 1024; /* 64 MB */
void *buf = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) { perror("mmap"); return; }
memset(buf, 0, size);
printf("Allocated %zu MB with mmap\n", size / (1024 * 1024));
munmap(buf, size);
}
int main(void) {
/* Create test file */
FILE *fp = fopen("/tmp/test_mmap.txt", "w");
fprintf(fp, "line 1\nline 2\nline 3\n");
fclose(fp);
process_with_mmap("/tmp/test_mmap.txt");
anon_mmap_example();
return 0;
}
TCP Socket Basics
Sockets are the Unix API for network communication. A TCP socket provides a reliable, ordered byte stream between two endpoints. The server binds to a port and listens; the client connects. Both sides then read and write as if working with a file.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* Simple TCP echo server — sends back everything it receives */
void run_server(int port) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { perror("socket"); return; }
/* SO_REUSEADDR lets the server restart immediately after a crash */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; /* listen on all interfaces */
addr.sin_port = htons(port); /* htons converts to network byte order */
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); close(server_fd); return;
}
listen(server_fd, 5); /* backlog of 5 pending connections */
printf("Server listening on port %d\n", port);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) { perror("accept"); close(server_fd); return; }
printf("Client connected: %s\n", inet_ntoa(client_addr.sin_addr));
char buf[1024];
ssize_t n;
while ((n = recv(client_fd, buf, sizeof(buf) - 1, 0)) > 0) {
buf[n] = '\0';
printf("Received: %s", buf);
send(client_fd, buf, n, 0); /* echo back */
}
close(client_fd);
close(server_fd);
}
int main(void) {
/* Uncomment to run the server — test with: nc localhost 8080 */
/* run_server(8080); */
printf("Socket example — uncomment run_server() to test\n");
return 0;
}
Compile with no extra flags on Linux: gcc -Wall -std=c11 -o server server.c
On older systems you may need -lsocket -lnsl.