Loading learning content...
Every meaningful interaction between an application and the operating system ultimately flows through system calls. When your program opens a file, creates a process, allocates memory, or communicates over a network, it invokes POSIX system calls—the fundamental building blocks of portable systems programming.
POSIX defines hundreds of functions, but a core set of perhaps 50-100 calls handles the vast majority of systems programming tasks. Mastering these calls transforms you from someone who uses frameworks blindly to someone who understands what those frameworks actually do.
By the end of this page, you will understand the major categories of POSIX system calls, their precise semantics and error handling patterns, how they compose to build higher-level functionality, and the practical idioms that professional systems programmers use daily.
Process control is the foundation of Unix systems programming. Everything in Unix revolves around processes—the fundamental unit of execution and resource ownership. POSIX provides a minimal but complete set of primitives for process lifecycle management.
The Core Process Calls
Four calls form the heart of Unix process management: fork(), exec(), wait(), and _exit(). Together, they enable arbitrarily complex process hierarchies and command execution patterns.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <sys/wait.h>#include <errno.h>#include <string.h> /** * fork() - Create a new process by duplicating the calling process. * * Returns: * - To parent: PID of child (positive integer) * - To child: 0 * - On failure: -1 (only to parent, child not created) * * Key semantics: * - Child inherits copies of parent's memory, file descriptors, etc. * - File descriptors share underlying file descriptions (seek position) * - Child gets its own unique PID and PPID * - Child's resource utilizations reset to zero */void demonstrate_fork(void) { printf("Parent PID: %d\n", getpid()); pid_t pid = fork(); if (pid < 0) { /* fork() failed - handle error */ fprintf(stderr, "fork failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } if (pid == 0) { /* Child process */ printf("Child: My PID is %d, parent is %d\n", getpid(), getppid()); /* Child does its work here */ _exit(EXIT_SUCCESS); /* Use _exit(), not exit() in child */ } /* Parent process continues here */ printf("Parent: Created child with PID %d\n", pid); /* Wait for child to complete */ int status; pid_t waited = waitpid(pid, &status, 0); if (waited == -1) { perror("waitpid"); exit(EXIT_FAILURE); } 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)); }}The exec() Family
The exec() family replaces the current process image with a new program. There are seven variants that differ in how arguments and environment are passed:
| Function | Arguments | Path Search | Environment |
|---|---|---|---|
execl() | List (variadic) | No (full path) | Inherited |
execlp() | List (variadic) | Yes (PATH) | Inherited |
execle() | List (variadic) | No (full path) | Explicit |
execv() | Vector (array) | No (full path) | Inherited |
execvp() | Vector (array) | Yes (PATH) | Inherited |
execvpe() | Vector (array) | Yes (PATH) | Explicit |
execve() | Vector (array) | No (full path) | Explicit |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /** * The fork-exec pattern: the Unix way to run external programs. * * This pattern is fundamental: * 1. Fork to create a child process * 2. In child: exec to replace with desired program * 3. In parent: wait for child completion */int run_command(const char *program, char *const argv[]) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { /* Child: execute the program */ execvp(program, argv); /* If we get here, exec failed */ perror("execvp"); _exit(127); /* Convention: 127 = command not found */ } /* Parent: wait for child */ int status; if (waitpid(pid, &status, 0) == -1) { perror("waitpid"); return -1; } if (WIFEXITED(status)) { return WEXITSTATUS(status); } return -1; /* Child terminated abnormally */} /* Usage example */void example_run_ls(void) { char *argv[] = {"ls", "-la", "/tmp", NULL}; int result = run_command("ls", argv); printf("ls exited with status: %d\n", result);}After fork(), the child process should call _exit() rather than exit() when terminating without exec(). The regular exit() function runs atexit handlers and flushes stdio buffers, which can cause corruption if the parent also flushes the same buffers. The _exit() function terminates immediately without cleanup.
Additional Process Control Functions
getpid() / getppid(): Get process ID / parent process IDgetuid() / getgid(): Get user ID / group IDsetsid(): Create a new session (for daemons)setpgid(): Set process group (for job control)kill(): Send a signal to a process or process grouppause(): Wait until a signal is deliveredIn Unix, 'everything is a file.' This philosophy means that file operations form the universal interface for interacting with diverse resources—regular files, directories, devices, pipes, sockets, and more. POSIX file operations center on file descriptors: small non-negative integers that reference open file descriptions.
The Core File I/O Calls
Five calls form the basis of all file operations: open(), close(), read(), write(), and lseek().
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/stat.h>#include <errno.h> /** * open() - Open or optionally create a file. * * Parameters: * path: pathname of file to open * oflag: O_RDONLY, O_WRONLY, O_RDWR (exactly one required) * plus optional flags: O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_NONBLOCK * mode: permissions if O_CREAT (modified by umask) * * Returns: file descriptor on success, -1 on error * * Critical semantics: * - Returns lowest available file descriptor * - Atomically creates file if O_CREAT | O_EXCL * - O_APPEND makes all writes atomic at end of file */int open_file_examples(void) { int fd; /* Open existing file for reading */ fd = open("/etc/passwd", O_RDONLY); if (fd == -1) { perror("open /etc/passwd"); return -1; } close(fd); /* Create new file (fail if exists) - secure pattern */ fd = open("/tmp/newfile.txt", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); /* 0600 - owner r/w only */ if (fd == -1) { if (errno == EEXIST) { printf("File already exists\n"); } else { perror("open new file"); } return -1; } close(fd); /* Open for append - atomic writes at end */ fd = open("/tmp/logfile.txt", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP); /* 0640 */ if (fd == -1) { perror("open logfile"); return -1; } return fd; /* Return for further operations */}Read and Write Operations
The read() and write() calls transfer data between file descriptors and user-space buffers. Their apparent simplicity hides important subtleties:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <errno.h> /** * CRITICAL: read() and write() may return fewer bytes than requested. * * This is NOT an error condition. It happens when: * - Reading: less data available (end of file, pipe, socket) * - Writing: resource limits, non-blocking I/O, signals * * Correct code MUST handle partial reads/writes. */ /* Robust read: keep trying until we get all requested bytes */ssize_t read_all(int fd, void *buf, size_t count) { char *ptr = buf; size_t remaining = count; while (remaining > 0) { ssize_t n = read(fd, ptr, remaining); if (n < 0) { if (errno == EINTR) { continue; /* Interrupted by signal, retry */ } return -1; /* Actual error */ } if (n == 0) { break; /* End of file */ } ptr += n; remaining -= n; } return count - remaining; /* Total bytes read */} /* Robust write: keep trying until all bytes written */ssize_t write_all(int fd, const void *buf, size_t count) { const char *ptr = buf; size_t remaining = count; while (remaining > 0) { ssize_t n = write(fd, ptr, remaining); if (n < 0) { if (errno == EINTR) { continue; /* Interrupted by signal, retry */ } return -1; /* Actual error */ } /* write() returning 0 is unusual but not impossible */ ptr += n; remaining -= n; } return count; /* All bytes written */} /* File copy example using robust I/O */int copy_file(const char *src, const char *dst) { int src_fd = open(src, O_RDONLY); if (src_fd == -1) { perror("open source"); return -1; } int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (dst_fd == -1) { perror("open destination"); close(src_fd); return -1; } char buffer[4096]; ssize_t bytes_read; while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) { if (write_all(dst_fd, buffer, bytes_read) < 0) { perror("write"); close(src_fd); close(dst_fd); return -1; } } if (bytes_read < 0) { perror("read"); close(src_fd); close(dst_fd); return -1; } close(src_fd); close(dst_fd); return 0;}The most common bug in systems programming is assuming read() or write() transfers the full requested amount. Network sockets, pipes, and terminals routinely return partial transfers. Production code must always use loops to ensure complete transfers.
File Positioning and Other Operations
lseek(): Reposition file read/write offsetdup() / dup2(): Duplicate file descriptors (essential for redirection)fcntl(): Manipulate file descriptor propertiesftruncate(): Truncate or extend a file to a specified lengthfsync() / fdatasync(): Synchronize file data to storagepread() / pwrite(): Read/write at offset without changing position (atomic)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <unistd.h>#include <fcntl.h> /** * lseek() - Reposition read/write file offset. * * Parameters: * fd: file descriptor * offset: offset to seek to (can be negative) * whence: SEEK_SET (from start), SEEK_CUR (from current), SEEK_END (from end) * * Returns: resulting offset from start of file, or -1 on error */void lseek_examples(int fd) { off_t pos; /* Go to start of file */ pos = lseek(fd, 0, SEEK_SET); /* Go to end of file */ pos = lseek(fd, 0, SEEK_END); printf("File size: %lld bytes\n", (long long)pos); /* Move forward 100 bytes from current position */ pos = lseek(fd, 100, SEEK_CUR); /* Move backward 50 bytes from current position */ pos = lseek(fd, -50, SEEK_CUR); /* Create a sparse file (hole) - data between positions is zero */ lseek(fd, 1000000, SEEK_SET); write(fd, "x", 1); /* File appears 1MB but uses minimal disk space */} /** * dup2() - Duplicate file descriptor to specific number. * * This is how shells implement I/O redirection: * cmd > file becomes dup2(file_fd, STDOUT_FILENO) */void redirect_stdout_to_file(const char *filename) { int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return; } /* Make stdout refer to the file */ if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2"); close(fd); return; } close(fd); /* Original fd no longer needed */ /* Now printf goes to the file */ printf("This goes to %s\n", filename);}Directories in Unix are special files that contain mappings from names to inode numbers. POSIX provides a complete set of calls for navigating, modifying, and querying the filesystem hierarchy.
Core Directory Operations
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <string.h>#include <dirent.h>#include <sys/stat.h>#include <unistd.h>#include <errno.h> /** * Directory traversal with opendir/readdir/closedir * * These functions provide portable directory reading. * The struct dirent contains at minimum: * - d_name: null-terminated filename * - d_ino: inode number (may not be meaningful on all systems) */void list_directory(const char *path) { DIR *dir = opendir(path); if (dir == NULL) { perror("opendir"); return; } struct dirent *entry; /* readdir returns NULL on end-of-directory OR error */ /* Distinguish by checking errno */ errno = 0; while ((entry = readdir(dir)) != NULL) { /* Skip . and .. */ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } printf("%s\n", entry->d_name); } if (errno != 0) { perror("readdir"); } closedir(dir);} /** * Get file metadata with stat/lstat/fstat * * struct stat contains: * st_mode: file type and permissions * st_ino: inode number * st_dev: device ID * st_nlink: number of hard links * st_uid: owner user ID * st_gid: owner group ID * st_size: file size in bytes * st_atime: last access time * st_mtime: last modification time * st_ctime: last status change time */void print_file_info(const char *path) { struct stat sb; /* lstat doesn't follow symlinks; stat does */ if (lstat(path, &sb) == -1) { perror("lstat"); return; } printf("%s:\n", path); printf(" Size: %lld bytes\n", (long long)sb.st_size); printf(" Inode: %llu\n", (unsigned long long)sb.st_ino); printf(" Links: %lu\n", (unsigned long)sb.st_nlink); printf(" Permissions: %o\n", sb.st_mode & 07777); /* Check file type */ printf(" Type: "); if (S_ISREG(sb.st_mode)) printf("regular file\n"); else if (S_ISDIR(sb.st_mode)) printf("directory\n"); else if (S_ISLNK(sb.st_mode)) printf("symbolic link\n"); else if (S_ISFIFO(sb.st_mode)) printf("FIFO/pipe\n"); else if (S_ISSOCK(sb.st_mode)) printf("socket\n"); else if (S_ISCHR(sb.st_mode)) printf("character device\n"); else if (S_ISBLK(sb.st_mode)) printf("block device\n");}Filesystem Modification Operations
| Function | Purpose | Key Notes |
|---|---|---|
mkdir() | Create directory | Parent must exist; permissions modified by umask |
rmdir() | Remove empty directory | Directory must be empty |
link() | Create hard link | Same filesystem only; increases link count |
unlink() | Remove directory entry | File deleted when link count reaches 0 |
symlink() | Create symbolic link | Can cross filesystems; target need not exist |
readlink() | Read symbolic link target | Returns path, not null-terminated unless you add it |
rename() | Rename/move file | Atomic within same filesystem |
chmod() / fchmod() | Change permissions | Only owner or root can change |
chown() / fchown() | Change ownership | Usually only root can change owner |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <sys/stat.h>#include <unistd.h> /** * Secure file creation pattern using link/unlink. * * This pattern ensures atomic replacement: * 1. Create new file with temporary name * 2. Write complete content * 3. Sync to disk * 4. Rename temp file to target (atomic) * * If crash occurs, either old version or new version exists, * never a partial file. */int atomic_file_update(const char *target, const char *content) { char tempfile[256]; snprintf(tempfile, sizeof(tempfile), "%s.tmp.%d", target, getpid()); /* Create temporary file */ int fd = open(tempfile, O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) { perror("open tempfile"); return -1; } /* Write content */ size_t len = strlen(content); if (write(fd, content, len) != (ssize_t)len) { perror("write"); close(fd); unlink(tempfile); return -1; } /* Sync to disk before rename */ if (fsync(fd) == -1) { perror("fsync"); close(fd); unlink(tempfile); return -1; } close(fd); /* Atomic rename */ if (rename(tempfile, target) == -1) { perror("rename"); unlink(tempfile); return -1; } return 0;}Always use write-to-temp-then-rename for updating important files. The rename() system call is atomic on POSIX systems (within the same filesystem). This prevents data corruption if your program crashes or is killed during the write.
Signals are software interrupts that provide a mechanism for handling asynchronous events. POSIX defines a robust signal handling framework that supersedes the unreliable signals of early Unix.
Standard Signals
POSIX requires support for numerous signals. The most important ones include:
| Signal | Default Action | Description | Common Use |
|---|---|---|---|
SIGINT | Terminate | Interrupt from keyboard | Ctrl+C handling |
SIGTERM | Terminate | Termination signal | Graceful shutdown requests |
SIGKILL | Terminate | Kill (cannot be caught) | Force kill unresponsive processes |
SIGSTOP | Stop | Stop process (cannot be caught) | Job control |
SIGCONT | Continue | Continue if stopped | Resume stopped process |
SIGHUP | Terminate | Hangup | Daemon config reload |
SIGCHLD | Ignore | Child stopped or terminated | Reap zombie processes |
SIGPIPE | Terminate | Broken pipe | Writing to closed pipe/socket |
SIGSEGV | Core dump | Invalid memory reference | Debugging crashes |
SIGALRM | Terminate | Timer signal | Timeouts |
SIGUSR1 | SIGUSR2 | Terminate | User-defined signals |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <string.h>#include <signal.h>#include <unistd.h>#include <errno.h> /* Volatile sig_atomic_t for signal-handler communication */volatile sig_atomic_t shutdown_requested = 0; /** * Signal handler function. * * CRITICAL: Signal handlers must be async-signal-safe. * Most library functions are NOT safe to call from handlers. * * Safe operations: * - Setting volatile sig_atomic_t variables * - Calling async-signal-safe functions (write, _exit, etc.) * * UNSAFE (do NOT call from signal handlers): * - printf, malloc, free, most standard library functions */void signal_handler(int signum) { /* Set flag - main loop will check and handle */ shutdown_requested = 1; /* If you must log, use write() not printf() */ const char msg[] = "Shutdown signal received\n"; write(STDERR_FILENO, msg, sizeof(msg) - 1);} /** * sigaction() - The POSIX way to install signal handlers. * * Always use sigaction(), never signal(). * signal() has undefined behavior for many details. */void setup_signal_handlers(void) { struct sigaction sa; /* Clear the structure */ memset(&sa, 0, sizeof(sa)); /* Set handler function */ sa.sa_handler = signal_handler; /* Block other signals during handler execution */ sigfillset(&sa.sa_mask); /* Flags: SA_RESTART makes interrupted syscalls retry */ sa.sa_flags = SA_RESTART; /* Install handler for SIGINT (Ctrl+C) */ if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction SIGINT"); exit(EXIT_FAILURE); } /* Install handler for SIGTERM */ if (sigaction(SIGTERM, &sa, NULL) == -1) { perror("sigaction SIGTERM"); exit(EXIT_FAILURE); }} /** * Main loop checking for shutdown. */int main(void) { setup_signal_handlers(); printf("Running... Press Ctrl+C to stop\n"); while (!shutdown_requested) { /* Do work */ sleep(1); printf("."); fflush(stdout); } printf("\nShutting down gracefully...\n"); /* Cleanup code here */ return 0;}Calling async-signal-unsafe functions from a signal handler causes undefined behavior—deadlocks, data corruption, crashes. The set of safe functions is small: write(), _exit(), signal(), sigaction(), and few others. When in doubt, set a flag and handle in the main program.
Signal Blocking and Masks
POSIX provides fine-grained control over which signals can be delivered:
sigprocmask(): Examine or change the signal masksigpending(): Get pending blocked signalssigsuspend(): Atomically set mask and wait for signalsigwait(): Wait for signal from a set (synchronous handling)POSIX defines multiple IPC mechanisms with different characteristics. Understanding when to use each is essential for systems design.
Pipes: Anonymous Channels for Related Processes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <string.h> /** * pipe() - Create an anonymous pipe. * * Creates a unidirectional data channel: * pipefd[0]: read end * pipefd[1]: write end * * Data written to pipefd[1] can be read from pipefd[0]. * Pipe has limited buffer size (typically 64KB on Linux). */void pipe_example(void) { int pipefd[2]; char buffer[256]; if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { /* Child: read from pipe */ close(pipefd[1]); /* Close unused write end */ ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; printf("Child received: %s\n", buffer); } close(pipefd[0]); _exit(EXIT_SUCCESS); } else { /* Parent: write to pipe */ close(pipefd[0]); /* Close unused read end */ const char *msg = "Hello from parent!"; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); /* Signals EOF to reader */ wait(NULL); }} /** * Implementing shell pipelines: cmd1 | cmd2 * * This is how "ls -l | grep foo" works. */void shell_pipeline_example(void) { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } /* First child: runs "ls -l", stdout to pipe */ pid_t pid1 = fork(); if (pid1 == 0) { close(pipefd[0]); /* Close read end */ dup2(pipefd[1], STDOUT_FILENO); /* stdout -> pipe write */ close(pipefd[1]); execlp("ls", "ls", "-l", NULL); perror("execlp ls"); _exit(127); } /* Second child: runs "grep foo", stdin from pipe */ pid_t pid2 = fork(); if (pid2 == 0) { close(pipefd[1]); /* Close write end */ dup2(pipefd[0], STDIN_FILENO); /* stdin <- pipe read */ close(pipefd[0]); execlp("grep", "grep", "txt", NULL); perror("execlp grep"); _exit(127); } /* Parent: close pipe and wait */ close(pipefd[0]); close(pipefd[1]); waitpid(pid1, NULL, 0); waitpid(pid2, NULL, 0);}POSIX Shared Memory
For high-performance IPC, POSIX shared memory allows processes to share memory regions:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <sys/mman.h>#include <sys/stat.h>#include <unistd.h>#include <string.h> #define SHM_NAME "/my_shared_mem"#define SHM_SIZE 4096 /** * POSIX shared memory with shm_open/mmap. * * Steps: * 1. shm_open() - create/open shared memory object * 2. ftruncate() - set size * 3. mmap() - map into address space * 4. Use the memory * 5. munmap() - unmap * 6. shm_unlink() - remove shared memory object */ /* Writer process */void shared_memory_writer(void) { /* Create shared memory object */ int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666); if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); } /* Set size */ if (ftruncate(fd, SHM_SIZE) == -1) { perror("ftruncate"); exit(EXIT_FAILURE); } /* Map into address space */ void *addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } close(fd); /* fd no longer needed after mmap */ /* Write data */ const char *message = "Hello from shared memory!"; memcpy(addr, message, strlen(message) + 1); printf("Writer: wrote message\n"); /* In real code, use semaphores for synchronization */ sleep(5); /* Keep memory available */ /* Cleanup */ munmap(addr, SHM_SIZE); shm_unlink(SHM_NAME);} /* Reader process */void shared_memory_reader(void) { /* Open existing shared memory */ int fd = shm_open(SHM_NAME, O_RDONLY, 0666); if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); } /* Map read-only */ void *addr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } close(fd); /* Read data */ printf("Reader: %s\n", (char *)addr); munmap(addr, SHM_SIZE);}mkfifo() creates named pipes accessible by path; enables IPC between unrelated processesmq_open(), mq_send(), mq_receive() provide typed, prioritized message passingsem_open(), sem_wait(), sem_post() for named semaphores; sem_init() for unnamedmmap() on regular files enables file I/O via memory operations and sharing between processesPOSIX threads (pthreads), defined in POSIX.1c, provide portable multi-threading capabilities. Understanding pthreads is essential for concurrent programming on Unix-like systems.
Thread Creation and Management
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <pthread.h>#include <unistd.h> /** * Thread function signature: void *(*start_routine)(void *) * * Takes void* argument, returns void* result. * Return value or pthread_exit() argument passed to pthread_join(). */void *thread_function(void *arg) { int thread_num = *(int *)arg; printf("Thread %d: starting\n", thread_num); sleep(1); /* Simulate work */ printf("Thread %d: finishing\n", thread_num); /* Return value accessible via pthread_join */ int *result = malloc(sizeof(int)); *result = thread_num * 10; return result;} int main(void) { pthread_t threads[4]; int thread_args[4]; /* Create threads */ for (int i = 0; i < 4; i++) { thread_args[i] = i; int rc = pthread_create(&threads[i], NULL, thread_function, &thread_args[i]); if (rc != 0) { fprintf(stderr, "pthread_create failed: %d\n", rc); exit(EXIT_FAILURE); } } /* Wait for all threads and collect results */ for (int i = 0; i < 4; i++) { void *result; int rc = pthread_join(threads[i], &result); if (rc != 0) { fprintf(stderr, "pthread_join failed: %d\n", rc); } else { printf("Thread %d returned: %d\n", i, *(int *)result); free(result); /* Clean up thread-allocated memory */ } } printf("All threads completed\n"); return 0;}Thread Synchronization
POSIX provides several synchronization primitives:
| Primitive | Functions | Use Case |
|---|---|---|
| Mutex | pthread_mutex_init/lock/unlock/destroy | Mutual exclusion; protect shared data |
| Condition Variable | pthread_cond_init/wait/signal/broadcast | Wait for condition; producer-consumer |
| Read-Write Lock | pthread_rwlock_init/rdlock/wrlock/unlock | Multiple readers OR single writer |
| Barrier | pthread_barrier_init/wait | Synchronize threads at a point |
| Spinlock | pthread_spin_init/lock/unlock | Busy-wait mutex; very short critical sections |
1234567891011121314151617181920212223242526272829303132
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <pthread.h> /* Shared data protected by mutex */int counter = 0;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void *increment_counter(void *arg) { for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&mutex); counter++; /* Protected access */ pthread_mutex_unlock(&mutex); } return NULL;} int main(void) { pthread_t t1, t2; pthread_create(&t1, NULL, increment_counter, NULL); pthread_create(&t2, NULL, increment_counter, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Final counter value: %d\n", counter); /* Should be 200000; without mutex would be less due to races */ pthread_mutex_destroy(&mutex); return 0;}POSIX threads require linking with the pthread library. Always compile with gcc -pthread or add -lpthread to your linker flags. Without this, pthreads functions may appear to work but behave incorrectly.
POSIX defines multiple clock types and time representation formats for different use cases—from simple human-readable times to high-resolution monotonic clocks for benchmarking.
Time Representations
| Type | Header | Description | Resolution |
|---|---|---|---|
time_t | <time.h> | Seconds since Unix epoch (1970-01-01) | 1 second |
struct timeval | <sys/time.h> | Seconds + microseconds | 1 microsecond |
struct timespec | <time.h> | Seconds + nanoseconds | 1 nanosecond |
struct tm | <time.h> | Broken-down time (year, month, day, etc.) | 1 second |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <time.h>#include <sys/time.h> /** * time() - Simple seconds since epoch */void simple_time_example(void) { time_t now = time(NULL); printf("Seconds since epoch: %ld\n", (long)now); /* Convert to local time */ struct tm *local = localtime(&now); printf("Local time: %04d-%02d-%02d %02d:%02d:%02d\n", local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec); /* Format as string */ char buffer[64]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %Z", local); printf("Formatted: %s\n", buffer);} /** * clock_gettime() - High-resolution time with multiple clocks * * Clock types: * CLOCK_REALTIME - Wall clock (can be adjusted) * CLOCK_MONOTONIC - Steady clock (for measuring intervals) * CLOCK_PROCESS_CPUTIME_ID - CPU time for process * CLOCK_THREAD_CPUTIME_ID - CPU time for thread */void high_resolution_time_example(void) { struct timespec ts; /* Get real time (wall clock) */ clock_gettime(CLOCK_REALTIME, &ts); printf("Real time: %ld.%09ld seconds\n", (long)ts.tv_sec, ts.tv_nsec); /* Measure elapsed time with monotonic clock */ struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); /* Work to measure */ volatile int sum = 0; for (int i = 0; i < 1000000; i++) sum += i; clock_gettime(CLOCK_MONOTONIC, &end); /* Calculate elapsed time */ long elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000L + (end.tv_nsec - start.tv_nsec); printf("Elapsed: %ld ns (%.3f ms)\n", elapsed_ns, elapsed_ns / 1000000.0);} /** * nanosleep() - High-resolution sleep */void precise_sleep(long milliseconds) { struct timespec req, rem; req.tv_sec = milliseconds / 1000; req.tv_nsec = (milliseconds % 1000) * 1000000; /* nanosleep may be interrupted by signals */ while (nanosleep(&req, &rem) == -1) { req = rem; /* Sleep remaining time */ }}Always use CLOCK_MONOTONIC (not CLOCK_REALTIME) for measuring time intervals. CLOCK_REALTIME can jump backward or forward due to NTP adjustments or manual clock changes. CLOCK_MONOTONIC never goes backward.
We have examined the core POSIX system calls that form the foundation of portable systems programming. Let's consolidate the essential knowledge:
fork(), exec(), wait(), and _exit() form the Unix process model. The fork-exec pattern is fundamental.open(), read(), write(), close(), and lseek() handle all file I/O. Always handle partial reads/writes.opendir()/readdir() for traversal; stat() for metadata; rename() for atomic updates.sigaction() (not signal()). Keep handlers async-signal-safe. Set flags, handle in main loop.clock_gettime() with CLOCK_MONOTONIC for measurements; CLOCK_REALTIME for wall-clock time.What's Next
The next page explores the portability benefits that POSIX provides—how conforming to POSIX enables software to run across diverse operating systems, and the practical strategies for writing maximally portable code.
You now understand the essential POSIX system calls for process management, file I/O, signals, IPC, threading, and time operations. These primitives form the building blocks for all systems-level programming on POSIX-compliant platforms.