Table of Contents
Why “Linux system APIs” Matter
When you move from simple scripts or basic C programs to serious Linux tools, you stop just “using commands” and start interacting with the OS through its system APIs. These APIs are what shells, core utilities, and servers use under the hood.
At a high level:
- System calls (syscalls): the narrow “door” between user space and kernel.
- C library (glibc, musl, etc.): a higher-level, portable wrapper around those syscalls plus extra utilities.
- Linux-specific interfaces: things that exist only on Linux (special syscalls,
ioctls,/proc,/sys, etc.).
This chapter assumes you already know how to write Linux tools in Bash, Python, or C; here we focus on how those tools talk to the system itself.
System Calls vs C Library
Linux exposes functionality primarily via system calls. You almost never invoke syscalls directly by number; you call standard library functions that wrap them.
- Syscall examples:
read,write,openat,clone,ioctl,mmap,statx,futex,epoll_wait. - C library examples:
printf,fopen,strcpy,malloc,popen, which may or may not use syscalls internally.
Conceptually:
- You write:
read(fd, buf, size); - glibc arranges CPU registers and executes
syscall(orint 0x80on old x86). - The kernel validates your request, does the work, and returns a result code.
Key ideas for tool authors:
- Portability vs power:
- Use POSIX functions (e.g.
open,read,write,getpid) for portability. - Use Linux-only APIs (e.g.
epoll,signalfd,inotify) when you need Linux-specific capabilities. - Error handling:
- On error most syscalls return
-1and seterrno. - Use
perror()orstrerror(errno)to inspect errors.
Example C snippet showing syscall-style error handling:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(void) {
ssize_t n = write(1, "hello\n", 6); // fd 1 = stdout
if (n == -1) {
fprintf(stderr, "write failed: %s\n", strerror(errno));
return 1;
}
return 0;
}Basic Unix APIs You’ll Use Everywhere
These are the “bread and butter” APIs for almost every nontrivial Linux tool.
File Descriptors and Basic I/O
Linux resources (files, pipes, sockets, devices) are usually represented as file descriptors (FDs), small integers:
0– stdin1– stdout2– stderr
Core functions:
int open(const char *path, int flags, mode_t mode);ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);off_t lseek(int fd, off_t offset, int whence);int close(int fd);
Example: a tiny cat-like program using only low-level I/O:
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
int fd = 0; // stdin
if (argc > 1) {
fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
}
char buf[4096];
while (1) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n == 0) break; // EOF
if (n < 0) {
if (errno == EINTR) continue;
perror("read");
return 1;
}
ssize_t off = 0;
while (off < n) {
ssize_t m = write(1, buf + off, n - off);
if (m < 0) {
if (errno == EINTR) continue;
perror("write");
return 1;
}
off += m;
}
}
if (fd != 0) close(fd);
return 0;
}Why this matters:
- Shows typical error and partial-write handling.
- Works on files, pipes, sockets, etc. because all are FDs.
Processes and `fork`/`exec`
Linux creates new processes with fork() and runs new programs with execve() (usually via wrappers like execvp()).
Common functions:
pid_t fork(void);int execvp(const char file, char const argv[]);pid_t waitpid(pid_t pid, int *wstatus, int options);
Example: running another program from your tool:
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <errno.h>
int main(void) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// child: exec "ls -l"
char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv);
perror("execvp"); // only reached on error
_exit(127);
}
// parent: wait for child
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("Child exited with code %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
}
return 0;
}This is the foundation for writing:
- Custom shells.
- Supervisors.
- Tools that wrap or monitor other commands.
Signals
Signals are simple async notifications (e.g. SIGINT, SIGTERM).
Useful APIs:
int sigaction(int signum, const struct sigaction act, struct sigaction oldact);sigset_t,sigemptyset,sigaddset,sigprocmaskfor blocking signals.- Linux-specific:
signalfd(read signals as if they were file input).
Trivial signal handler example:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void handle_sigint(int sig) {
(void)sig;
write(1, "Caught SIGINT\n", 14);
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = handle_sigint;
sigaction(SIGINT, &sa, NULL);
while (1) {
pause(); // wait for signals
}
}Linux-Specific System Calls and Interfaces
Here are some APIs that are either Linux-specific or widely used in Linux-centric tools.
`epoll` for Scalable I/O Multiplexing
If your tool needs to handle many sockets or pipes at once, epoll is the modern Linux choice (over select/poll).
Core functions:
int epoll_create1(int flags);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
Minimal usage pattern:
epfd = epoll_create1(0);- For each FD, call
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); - Loop calling
epoll_wait(...)to get ready events.
This is how many high-performance servers and proxies operate.
`inotify` for Filesystem Events
inotify lets your tool react when files or directories change:
int inotify_init1(int flags);int inotify_add_watch(int fd, const char *pathname, uint32_t mask);- Read
struct inotify_eventrecords from the FD.
Example use cases:
- Auto-reloading config on change.
- File synchronizers.
- Log watchers.
`clone`, Namespaces, and `unshare`
Linux containers, some sandboxes, and advanced tools use namespaces and clone to create isolated environments.
Key APIs:
int unshare(int flags);– detach current process from some namespaces.int setns(int fd, int nstype);– join an existing namespace.pid_t clone(int (fn)(void ), void stack, int flags, void arg, ...);– create a process-like entity with fine-grained control.
Flags like:
CLONE_NEWNS– mount namespace.CLONE_NEWUTS– hostname.CLONE_NEWNET– networking.CLONE_NEWPID– PID namespace.
These are powerful but low-level; container frameworks sit on top of them.
`ioctl` and Device-Specific Interfaces
ioctl calls let you interact with devices or special files using custom commands:
int ioctl(int fd, unsigned long request, ...);
The meaning of request and the extra data depends on the device. You see them in:
- Terminal control (e.g.
TIOCGWINSZto get terminal size). - Network configuration (old-style APIs).
- Special hardware drivers.
Example: query terminal size:
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
struct winsize ws;
if (ioctl(1, TIOCGWINSZ, &ws) == -1) {
perror("ioctl");
return 1;
}
printf("rows=%d cols=%d\n", ws.ws_row, ws.ws_col);
return 0;
}Pseudo-filesystems: `/proc` and `/sys`
Linux exposes much of its internal state as virtual files.
`/proc` for Process and Kernel Info
Examples:
/proc/self/stat– info about current process./proc/self/status– human-friendly stats./proc/cpuinfo,/proc/meminfo– hardware and memory info./proc/net/*,/proc/uptime,/proc/loadavg, etc.
You read these with ordinary file APIs: open, read, fopen, fgets.
Example: get process ID from /proc/self (just symbolic link):
#include <stdio.h>
#include <unistd.h>
int main(void) {
FILE *f = fopen("/proc/self/stat", "r");
if (!f) {
perror("fopen");
return 1;
}
int pid;
if (fscanf(f, "%d", &pid) == 1) {
printf("My PID is %d\n", pid);
}
fclose(f);
return 0;
}`/sys` for Devices and Kernel Parameters
/sys is the sysfs filesystem, exposing:
- Devices and buses.
- Block devices, power management.
- Many tunables via “attribute” files.
You typically:
- Read attributes to inspect state.
- Write to attributes (as text) to change some setting, if permitted.
Example: list CPU cores by reading /sys/devices/system/cpu/ using opendir / readdir.
Process Attributes and Resource Limits
Linux offers APIs around scheduling, priorities, CPU sets, and more.
Common ones used by system tools:
int getpriority(int which, id_t who);/setpriority()– niceness.int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);– real-time policies.int setrlimit(int resource, const struct rlimit *rlim);– resource limits (open FDs, memory, CPU time).int prlimit(pid_t pid, int resource, const struct rlimit new_limit, struct rlimit old_limit);– Linux-specific combined get/set.
Example: set a soft limit on open file descriptors:
#include <sys/resource.h>
#include <stdio.h>
int main(void) {
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
perror("getrlimit");
return 1;
}
printf("Old soft limit: %ld\n", (long)rl.rlim_cur);
rl.rlim_cur = 1024;
if (setrlimit(RLIMIT_NOFILE, &rl) == -1) {
perror("setrlimit");
return 1;
}
return 0;
}Memory and Mapping
Most tools rely on normal malloc/free from the C library. But sometimes you want direct control over mappings.
Key APIs:
void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void *addr, size_t length);int mprotect(void *addr, size_t len, int prot);
Common use cases:
- Memory-mapped files for fast I/O.
- Shared memory between processes.
- Custom allocators.
Example: map a file read-only and print its first byte:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s file\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); return 1; }
if (st.st_size == 0) { printf("Empty file\n"); return 0; }
void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); return 1; }
printf("First byte: 0x%02x\n", ((unsigned char *)addr)[0]);
munmap(addr, st.st_size);
close(fd);
return 0;
}Time, Timers, and Clocks
Linux offers POSIX time APIs plus Linux extensions.
Useful APIs for tools:
clock_gettime(clockid_t clk_id, struct timespec *tp);nanosleep(const struct timespec req, struct timespec rem);timerfd_create,timerfd_settime(Linux-specific) – timers as file descriptors.
Example: simple sleep using nanosleep:
#include <time.h>
#include <stdio.h>
int main(void) {
struct timespec ts = { .tv_sec = 1, .tv_nsec = 500000000 }; // 1.5 seconds
if (nanosleep(&ts, NULL) == -1) {
perror("nanosleep");
return 1;
}
printf("Done\n");
return 0;
}Exposing Your Own APIs: Syscalls vs Libraries
Most Linux tool authors do not add new kernel syscalls. Instead, they:
- Wrap existing system APIs in their own library functions.
- Provide a command-line tool plus a library that can be reused by other apps.
Typical layering:
- Raw Linux syscalls (
read,open,epoll,clone, etc.). - Your C (or Rust, etc.) library that provides a clean, stable API.
- One or more CLI tools that call into that library.
Benefits:
- You hide low-level Linux details (e.g.
epoll_ctlflags). - You can later port or adapt to other Unix-like systems by swapping out the low-level layer.
Practical Tips for Using Linux APIs
- Always check return values of system calls and handle errors robustly.
- Be careful with blocking I/O: for servers or responsive UIs, use non-blocking mode and
epoll/poll. - Prefer documented, stable interfaces:
/procand/sysare widely used but can change more often than syscalls; stick to documented files and formats.- Use man pages effectively:
man 2 openfor syscalls (section 2).man 3 printffor library calls.man 7 epoll,man 7 signalfor conceptual overviews.- Leverage higher-level languages when reasonable:
- Many Linux APIs are exposed via Python modules (
os,select,subprocess), Rust crates, etc., which can simplify error handling and memory safety while still using Linux system APIs underneath.
Using Linux system APIs effectively is what turns your programs from “scripts that glue commands together” into first-class Linux tools that behave like the rest of the system.