Linux programmer's guide: system calls, processes and IPC today

Updated November 2026

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):

BuildSize
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:

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

Inter-process communication on Linux
MechanismGood forNotes
Pipe (pipe(), shell |)Parent and child, one directionHolds 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 directionA pipe with a name in the file system
Unix domain socketLocal client and server, both directionsMost common choice for local services; can pass file descriptors
POSIX shared memory (shm_open + mmap)Large data, fastestObjects live in /dev/shm; you must add your own locking
POSIX message queues, semaphoresSmall messages, countingSee mq_overview(7), sem_overview(7)
System V IPC (msgget, semget, shmget)Old softwareStill supported; inspect with ipcs, remove with ipcrm
D-BusDesktop and system servicesA 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 -f follows child processes, -e trace=file limits output to calls that take a file name, -c prints a summary table, -p PID attaches to a running process, -T shows the time spent in each call and -y prints the path behind each file descriptor. strace -f -e trace=process ./run shows the clone, execve and wait4 calls of the example above; note that glibc's fork() is implemented with clone().
gdb
Breakpoints, stepping and backtraces. Build with -g; see the C++ HOWTO for a session.
perf
CPU profiling with hardware counters: perf stat ./prog counts events, perf record ./prog then perf report shows where time goes, perf top shows it live.
Sanitizers and Valgrind
-fsanitize=address,undefined at build time, or valgrind ./prog without 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.

Sources