Linux programmer's guide: system calls, processes and IPC today
Every Linux program, whatever language it is written in, does its real work through system calls: requests to the kernel to open a file, start a process or send data. The C library wraps those calls in ordinary functions. This guide explains that layer and the core ideas built on it (file descriptors, processes, signals, inter-process communication and threads), with small C programs that compile as shown, and the tools to see what a program is doing.
The original Linux Programmer's Guide (1995) is kept by the Linux Documentation Project at tldp.org. Its chapters on System V IPC, termcap and the PC speaker describe Linux as it was then; this page covers the same ground for today's systems.
System calls and the C library
The syscalls(2) manual calls the system call "the fundamental interface between an application and the Linux kernel". There are several hundred of them. You rarely call them directly: the C library provides a function for each, which usually just puts the arguments in the right registers and switches into the kernel (the Assembly HOWTO shows that step by hand). For a call without a library function, use syscall(2) with the number from <sys/syscall.h>.
When a call fails, the wrapper returns -1 and sets errno. Always check the result and report it with perror() or strerror(errno).
glibc and musl
Debian, Ubuntu, Fedora, RHEL, Arch and openSUSE use the GNU C Library (glibc). Alpine Linux, popular for containers, is built on musl, a smaller C library. Both implement POSIX and the common Linux calls; programs that rely on glibc-only functions or behavior may need changes for musl. musl is good at small static binaries. The small program from the processes section below, built three ways on Debian 13 (GCC 14, glibc 2.41; sizes vary with versions):
| Build | Size |
|---|---|
gcc -O2 run.c (dynamic, glibc) | 16 KB |
gcc -static -O2 run.c (static, glibc) | 795 KB |
musl-gcc -static -O2 run.c (static, musl; package musl-tools) | 45 KB |
Since glibc 2.34, the threads and dynamic-loading functions live in the main libc.so.6, so -lpthread and -ldl are no longer needed (they are still accepted).
Files and file descriptors
An open file, pipe, socket or device is represented in your process by a small integer, the file descriptor. 0, 1 and 2 are standard input, output and error. The basic calls are open() (or openat()), read(), write(), lseek() and close(). Rules that prevent most bugs:
read()andwrite()may transfer fewer bytes than you asked for. Loop until done.- Open with
O_CLOEXECso the descriptor is not leaked into programs you start later. - Check the result of
close()after writing: some errors only show up there. Callfsync()when data must survive a power cut. - The
stdiofunctions (fopen,fprintf) add buffering on top; do not mix them with rawwrite()on the same descriptor.
Processes: fork, exec, wait
A new program starts in two steps. fork() creates a copy of the calling process; it returns the child's process ID in the parent and 0 in the child. Linux makes this cheap with copy-on-write pages: memory is only copied when one side changes it. The child then calls one of the exec functions, which replaces its memory with a new program but keeps the process ID and any open file descriptors not marked close-on-exec. The parent collects the exit status with waitpid().
/* run.c - start a program, wait for it, report how it ended */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) { /* child */
execlp("ls", "ls", "-l", "/etc/hostname", (char *)NULL);
perror("execlp"); /* only reached on failure */
_exit(127);
}
int status; /* parent */
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return EXIT_FAILURE;
}
if (WIFEXITED(status))
printf("child %d exited with status %d\n", (int)pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d killed by signal %d\n", (int)pid, WTERMSIG(status));
return EXIT_SUCCESS;
}
gcc -std=c17 -Wall -Wextra -O2 run.c -o run
./run
A child that has exited but not been waited for stays as a "zombie" entry in the process table, so always wait. In the child after a failed exec, use _exit(), not exit(), so stdio buffers copied from the parent are not flushed twice. posix_spawn() does fork and exec in one call and is often simpler.
Signals
Signals are short notifications: SIGINT from Ctrl+C, SIGTERM from kill or systemd at shutdown, SIGCHLD when a child ends, SIGSEGV on a bad memory access. Each has a default action (terminate, ignore, dump core, stop or continue). SIGKILL and SIGSTOP cannot be caught, blocked or ignored. Install handlers with sigaction(), which behaves the same everywhere, rather than the older signal(). A handler may only call async-signal-safe functions (see signal-safety(7)), so the safe pattern is to set a flag and act on it in the main loop:
/* stop.c - finish cleanly on Ctrl+C or SIGTERM */
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static volatile sig_atomic_t stop_requested = 0;
static void on_signal(int sig)
{
(void)sig;
stop_requested = 1; /* only set a flag: async-signal-safe */
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
while (!stop_requested) {
puts("working...");
sleep(1);
}
puts("cleaning up and exiting");
return 0;
}
The _POSIX_C_SOURCE line is needed with -std=c17; without it, strict C mode hides struct sigaction. After exec, signals you were catching go back to their default action. Event-loop programs can receive signals as readable data with signalfd() instead.
Talking between processes
| Mechanism | Good for | Notes |
|---|---|---|
Pipe (pipe(), shell |) | Parent and child, one direction | Holds 65,536 bytes by default; writes up to 4,096 bytes (PIPE_BUF) are atomic; writing with no reader raises SIGPIPE |
FIFO (mkfifo) | Unrelated processes, one direction | A pipe with a name in the file system |
| Unix domain socket | Local client and server, both directions | Most common choice for local services; can pass file descriptors |
POSIX shared memory (shm_open + mmap) | Large data, fastest | Objects live in /dev/shm; you must add your own locking |
| POSIX message queues, semaphores | Small messages, counting | See mq_overview(7), sem_overview(7) |
System V IPC (msgget, semget, shmget) | Old software | Still supported; inspect with ipcs, remove with ipcrm |
| D-Bus | Desktop and system services | A message bus built on Unix sockets |
Threads
Threads share one address space, so they communicate through memory but must protect shared data with mutexes or atomics. Use POSIX threads (pthread_create, pthread_mutex_lock) or C11 <threads.h>; in C++ use std::thread. Compile with -pthread for portability. Find data races with ThreadSanitizer (-fsanitize=thread). For ways to spread work over cores, see the Parallel Processing HOWTO.
Seeing what a program does
strace- Shows every system call.
strace -ffollows child processes,-e trace=filelimits output to calls that take a file name,-cprints a summary table,-p PIDattaches to a running process,-Tshows the time spent in each call and-yprints the path behind each file descriptor.strace -f -e trace=process ./runshows theclone,execveandwait4calls of the example above; note that glibc'sfork()is implemented withclone(). gdb- Breakpoints, stepping and backtraces. Build with
-g; see the C++ HOWTO for a session. perf- CPU profiling with hardware counters:
perf stat ./progcounts events,perf record ./progthenperf reportshows where time goes,perf topshows it live. - Sanitizers and Valgrind
-fsanitize=address,undefinedat build time, orvalgrind ./progwithout rebuilding, catch memory errors and leaks.
Reading the manual
The Linux man-pages project documents every system call (section 2), library function (section 3) and overview topic (section 7): man 2 open, man 3 printf, man 7 signal. Install them with sudo apt install manpages-dev on Debian and Ubuntu, or sudo dnf install man-pages on Fedora. The same pages are online at man7.org.