Loading learning content...
Modern computing is fundamentally cooperative. A web request may touch a reverse proxy, application server, database, cache, and logging service—each a separate process, each needing to exchange data with others. A graphics application may use one process for the UI, another for rendering, and another for network updates. The shell orchestrates pipelines of processes through which data flows.
Communication system calls form the fifth and final category in our taxonomy. Where process control creates and destroys processes, communication enables them to work together. Where file management provides persistent storage, communication provides ephemeral channels for real-time data exchange. Where device management lets programs talk to hardware, communication lets programs talk to each other.
This category encompasses diverse mechanisms: pipes for streaming data between related processes, sockets for network and local communication, shared memory for zero-copy data sharing, message queues for structured message passing, and signals for asynchronous notifications. Each has distinct semantics suited to particular patterns of cooperation.
By the end of this page, you will understand how pipes enable unidirectional streaming between processes, how sockets provide both local and network communication, how shared memory achieves zero-copy data sharing, how POSIX message queues provide structured messaging, and how signals deliver asynchronous notifications. You'll grasp when to use each mechanism and how they compose to build complex multi-process systems.
Pipes are the oldest and simplest inter-process communication mechanism in UNIX. They provide a unidirectional channel through which data flows from one process to another—like a physical pipe carrying water.
Anonymous Pipes — pipe()
int pipe(int pipefd[2]);
Creates a pipe and returns two file descriptors:
Data written to pipefd[1] becomes available for reading from pipefd[0]. Anonymous pipes only work between related processes (parent-child or siblings through a common ancestor).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <sys/wait.h> // Basic parent-child communication via pipevoid basic_pipe_example() { int pipefd[2]; pid_t pid; // Create the pipe BEFORE forking if (pipe(pipefd) == -1) { perror("pipe"); return; } pid = fork(); if (pid == -1) { perror("fork"); return; } if (pid == 0) { // Child: will READ from pipe close(pipefd[1]); // Close unused write end char buffer[256]; 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(0); } else { // Parent: will WRITE to pipe close(pipefd[0]); // Close unused read end const char *message = "Hello from parent!"; write(pipefd[1], message, strlen(message)); close(pipefd[1]); // Close triggers EOF on reader wait(NULL); // Collect child }} // Implementing shell-style piping: cmd1 | cmd2void shell_pipe_example() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return; } // First child: runs "ls -la" pid_t pid1 = fork(); if (pid1 == 0) { // Connect stdout to pipe write end dup2(pipefd[1], STDOUT_FILENO); close(pipefd[0]); // Close read end close(pipefd[1]); // Close original write end execlp("ls", "ls", "-la", NULL); perror("exec ls"); exit(1); } // Second child: runs "grep '.c'" pid_t pid2 = fork(); if (pid2 == 0) { // Connect stdin to pipe read end dup2(pipefd[0], STDIN_FILENO); close(pipefd[0]); // Close original read end close(pipefd[1]); // Close write end execlp("grep", "grep", ".c", NULL); perror("exec grep"); exit(1); } // Parent: close both ends and wait close(pipefd[0]); close(pipefd[1]); wait(NULL); wait(NULL);} // Bidirectional communication with two pipesvoid bidirectional_pipes() { int parent_to_child[2]; int child_to_parent[2]; pipe(parent_to_child); pipe(child_to_parent); pid_t pid = fork(); if (pid == 0) { // Child close(parent_to_child[1]); // Close write to parent_to_child close(child_to_parent[0]); // Close read from child_to_parent // Read request from parent char request[100]; ssize_t n = read(parent_to_child[0], request, sizeof(request) - 1); request[n] = '\0'; // Process and send response char response[100]; snprintf(response, sizeof(response), "Processed: %s", request); write(child_to_parent[1], response, strlen(response)); close(parent_to_child[0]); close(child_to_parent[1]); exit(0); } else { // Parent close(parent_to_child[0]); // Close read from parent_to_child close(child_to_parent[1]); // Close write to child_to_parent // Send request const char *request = "Hello"; write(parent_to_child[1], request, strlen(request)); close(parent_to_child[1]); // Signal no more data // Read response char response[100]; ssize_t n = read(child_to_parent[0], response, sizeof(response) - 1); response[n] = '\0'; printf("Parent got: %s\n", response); close(child_to_parent[0]); wait(NULL); }}Pipe Capacity and Blocking
Pipes have limited capacity (typically 64KB on Linux). When the pipe is full, write() blocks until the reader consumes data. When empty, read() blocks until data arrives. This provides natural flow control but can cause deadlocks if not handled carefully.
Named Pipes (FIFOs)
Unlike anonymous pipes, FIFOs (First In, First Out) have names in the filesystem and can connect unrelated processes:
int mkfifo(const char *pathname, mode_t mode);
Creates a special file. Processes open it with open()—one for reading, one for writing. They then use standard read()/write() calls.
| Aspect | Anonymous Pipe | Named Pipe (FIFO) |
|---|---|---|
| Creation | pipe() system call | mkfifo() or mknod() |
| Naming | No name, just fd pair | Pathname in filesystem |
| Relationship | Related processes only | Any processes |
| Persistence | Exists while open | File exists until deleted |
| Use case | Shell pipelines, fork-based | Client-server, daemons |
Writing to a pipe with no readers generates SIGPIPE, which terminates the process by default. This is why 'yes | head' terminates cleanly—yes receives SIGPIPE when head exits. Long-running processes should either handle SIGPIPE or use write() with MSG_NOSIGNAL (for sockets) or ignore SIGPIPE and check for EPIPE errors.
Sockets provide the most general-purpose communication mechanism, supporting both local (Unix domain) and network (Internet) communication with various transport protocols.
Socket API Overview
The socket API follows a create-bind-listen-accept (server) or create-connect (client) pattern:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <sys/un.h>#include <netinet/in.h>#include <arpa/inet.h> // ========================================// Unix Domain Socket (local IPC)// ========================================#define SOCKET_PATH "/tmp/example.sock" void unix_domain_server() { int server_fd, client_fd; struct sockaddr_un addr; // Create socket server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket"); return; } // Remove any existing socket file unlink(SOCKET_PATH); // Bind to path memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("bind"); close(server_fd); return; } // Listen for connections if (listen(server_fd, 5) == -1) { perror("listen"); close(server_fd); return; } printf("Server listening on %s\n", SOCKET_PATH); // Accept and handle a connection client_fd = accept(server_fd, NULL, NULL); if (client_fd == -1) { perror("accept"); } else { char buf[256]; ssize_t n = read(client_fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("Received: %s\n", buf); const char *response = "Hello from server"; write(client_fd, response, strlen(response)); } close(client_fd); } close(server_fd); unlink(SOCKET_PATH);} void unix_domain_client() { int sock; struct sockaddr_un addr; sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { perror("socket"); return; } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("connect"); close(sock); return; } const char *message = "Hello from client"; write(sock, message, strlen(message)); char response[256]; ssize_t n = read(sock, response, sizeof(response) - 1); if (n > 0) { response[n] = '\0'; printf("Server says: %s\n", response); } close(sock);} // ========================================// TCP Internet Socket// ========================================void tcp_server(int port) { int server_fd, client_fd; struct sockaddr_in addr; // Create IPv4 TCP socket server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket"); return; } // Allow address reuse (avoid "address already in use") int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // Bind to all interfaces on specified port memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port); // Host to network byte order if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("bind"); close(server_fd); return; } if (listen(server_fd, 10) == -1) { perror("listen"); close(server_fd); return; } printf("TCP server listening on port %d\n", port); // Accept connections in a loop while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd == -1) { perror("accept"); continue; } // Get client info char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip)); printf("Client connected: %s:%d\n", client_ip, ntohs(client_addr.sin_port)); // Echo server: read and send back char buf[1024]; ssize_t n; while ((n = recv(client_fd, buf, sizeof(buf), 0)) > 0) { send(client_fd, buf, n, 0); } printf("Client disconnected\n"); close(client_fd); } close(server_fd);} void tcp_client(const char *host, int port) { int sock; struct sockaddr_in addr; sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) { perror("socket"); return; } memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) { perror("inet_pton"); close(sock); return; } if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("connect"); close(sock); return; } printf("Connected to %s:%d\n", host, port); // Send and receive const char *msg = "Hello, server!"; send(sock, msg, strlen(msg), 0); char response[1024]; ssize_t n = recv(sock, response, sizeof(response) - 1, 0); if (n > 0) { response[n] = '\0'; printf("Echo: %s\n", response); } close(sock);}| Domain | Type | Protocol | Use Case |
|---|---|---|---|
| AF_UNIX | SOCK_STREAM | 0 | Local reliable byte stream |
| AF_UNIX | SOCK_DGRAM | 0 | Local unreliable datagrams |
| AF_INET | SOCK_STREAM | IPPROTO_TCP | TCP over IPv4 |
| AF_INET | SOCK_DGRAM | IPPROTO_UDP | UDP over IPv4 |
| AF_INET6 | SOCK_STREAM | IPPROTO_TCP | TCP over IPv6 |
| AF_NETLINK | SOCK_RAW | various | Kernel communication |
Unix domain sockets (AF_UNIX) are significantly faster than TCP loopback (AF_INET on 127.0.0.1) for local IPC—they skip the network stack. Applications like Docker, MySQL, PostgreSQL, and systemd use Unix sockets extensively. They also support passing file descriptors between processes (sendmsg/recvmsg with SCM_RIGHTS), enabling credential passing and privilege separation patterns.
When processes need to share large amounts of data with minimal overhead, shared memory is the ultimate solution. Rather than copying data through kernel buffers (as with pipes and sockets), processes map the same physical memory into their address spaces and access it directly.
POSIX Shared Memory API
Modern systems use POSIX shared memory, created in the /dev/shm filesystem:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/mman.h>#include <sys/stat.h>#include <sys/wait.h>#include <semaphore.h> #define SHM_NAME "/example_shm"#define SHM_SIZE 4096 // Shared data structuretypedef struct { int counter; char message[256]; sem_t mutex; // For synchronization} SharedData; void posix_shm_writer() { // Create shared memory object int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666); if (fd == -1) { perror("shm_open"); return; } // Set size if (ftruncate(fd, sizeof(SharedData)) == -1) { perror("ftruncate"); close(fd); return; } // Map into address space SharedData *data = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { perror("mmap"); close(fd); return; } close(fd); // fd no longer needed after mmap // Initialize semaphore (process-shared) sem_init(&data->mutex, 1, 1); // 1 = process-shared, initial value 1 // Write data for (int i = 0; i < 10; i++) { sem_wait(&data->mutex); data->counter = i; snprintf(data->message, sizeof(data->message), "Message #%d from writer", i); printf("Writer: counter=%d\n", data->counter); sem_post(&data->mutex); usleep(100000); // 100ms } munmap(data, sizeof(SharedData));} void posix_shm_reader() { // Open existing shared memory int fd = shm_open(SHM_NAME, O_RDONLY, 0); if (fd == -1) { perror("shm_open (reader)"); return; } // Map read-only SharedData *data = mmap(NULL, sizeof(SharedData), PROT_READ, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { perror("mmap (reader)"); close(fd); return; } close(fd); // Read data for (int i = 0; i < 10; i++) { // Note: reader can't use sem_wait on read-only mapping // In practice, use a separate semaphore or atomic operations printf("Reader: counter=%d, message=%s\n", data->counter, data->message); usleep(100000); } munmap(data, sizeof(SharedData));} // Producer-consumer with shared memory ring buffer#define RING_SIZE 16 typedef struct { int buffer[RING_SIZE]; int head; // Next write position int tail; // Next read position sem_t empty; // Counts empty slots sem_t full; // Counts full slots sem_t mutex; // Mutual exclusion} RingBuffer; void ring_buffer_init(RingBuffer *rb) { rb->head = rb->tail = 0; sem_init(&rb->empty, 1, RING_SIZE); // All slots empty sem_init(&rb->full, 1, 0); // No slots full sem_init(&rb->mutex, 1, 1); // Unlocked} void ring_buffer_produce(RingBuffer *rb, int item) { sem_wait(&rb->empty); // Wait for empty slot sem_wait(&rb->mutex); // Lock rb->buffer[rb->head] = item; rb->head = (rb->head + 1) % RING_SIZE; sem_post(&rb->mutex); // Unlock sem_post(&rb->full); // Signal full slot available} int ring_buffer_consume(RingBuffer *rb) { sem_wait(&rb->full); // Wait for full slot sem_wait(&rb->mutex); // Lock int item = rb->buffer[rb->tail]; rb->tail = (rb->tail + 1) % RING_SIZE; sem_post(&rb->mutex); // Unlock sem_post(&rb->empty); // Signal empty slot available return item;} // Anonymous shared memory (between parent-child only)void anonymous_shared_memory() { // MAP_ANONYMOUS + MAP_SHARED = shared memory without file int *shared = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) { perror("mmap"); return; } *shared = 0; pid_t pid = fork(); if (pid == 0) { // Child increments for (int i = 0; i < 10000; i++) { (*shared)++; // WARNING: Not atomic! } exit(0); } else { // Parent increments for (int i = 0; i < 10000; i++) { (*shared)++; // WARNING: Not atomic! } wait(NULL); // Without synchronization, result is unpredictable! printf("Shared counter (race): %d (expected 20000)\n", *shared); } munmap(shared, sizeof(int));}Shared memory provides no built-in synchronization. Multiple processes reading/writing the same data without locks produces race conditions and corruption. Use semaphores (process-shared), mutex (pthread_mutexattr_setpshared), or atomic operations (stdatomic.h / GCC atomics) for correct concurrent access.
Shared Memory vs Other IPC
| Metric | Pipes/Sockets | Shared Memory |
|---|---|---|
| Copy overhead | Kernel copy | Zero copy |
| Latency | Microseconds | Nanoseconds |
| Synchronization | Built-in (blocking) | Manual required |
| Data structure | Byte stream | Arbitrary |
| Security | Kernel-mediated | Direct access |
Shared memory is optimal for large data structures and high-frequency updates but requires careful synchronization and lacks the natural flow control of streams.
Message queues provide structured message-oriented communication. Unlike byte-stream pipes, messages are discrete units with boundaries preserved. Unlike shared memory, the kernel manages the queue, providing natural synchronization.
POSIX Message Queues
The modern POSIX API (mq_*) is preferred over System V message queues:
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);
int mq_close(mqd_t mqdes);
int mq_unlink(const char *name);
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <mqueue.h>#include <fcntl.h>#include <errno.h>#include <sys/stat.h> #define QUEUE_NAME "/example_mq"#define MAX_MSG_SIZE 256#define MAX_MSG_COUNT 10 void message_queue_sender() { // Create message queue with specified attributes struct mq_attr attr = { .mq_flags = 0, .mq_maxmsg = MAX_MSG_COUNT, .mq_msgsize = MAX_MSG_SIZE, .mq_curmsgs = 0 }; mqd_t mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0666, &attr); if (mq == (mqd_t)-1) { perror("mq_open"); return; } // Send messages with different priorities const char *messages[] = { "Low priority message", "Normal priority message", "High priority message", "Urgent message!" }; unsigned priorities[] = {1, 5, 10, 31}; // 0-31, higher = more important for (int i = 0; i < 4; i++) { if (mq_send(mq, messages[i], strlen(messages[i]) + 1, priorities[i]) == -1) { perror("mq_send"); } else { printf("Sent (priority %u): %s\n", priorities[i], messages[i]); } } mq_close(mq);} void message_queue_receiver() { mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY); if (mq == (mqd_t)-1) { perror("mq_open (receiver)"); return; } // Get queue attributes to determine message size struct mq_attr attr; mq_getattr(mq, &attr); char *buffer = malloc(attr.mq_msgsize); // Receive messages (returns highest priority first!) printf("\nReceiving messages (highest priority first):\n"); for (int i = 0; i < 4; i++) { unsigned priority; ssize_t len = mq_receive(mq, buffer, attr.mq_msgsize, &priority); if (len >= 0) { printf("Received (priority %u): %s\n", priority, buffer); } else { perror("mq_receive"); break; } } free(buffer); mq_close(mq); mq_unlink(QUEUE_NAME); // Remove the queue} // Non-blocking message queue operationsvoid nonblocking_mq() { struct mq_attr attr = { .mq_flags = 0, .mq_maxmsg = 5, .mq_msgsize = 64, }; mqd_t mq = mq_open("/nonblock_mq", O_CREAT | O_RDWR | O_NONBLOCK, 0666, &attr); if (mq == (mqd_t)-1) { perror("mq_open"); return; } // Fill the queue const char *msg = "Test message"; int count = 0; while (mq_send(mq, msg, strlen(msg) + 1, 0) != -1) { count++; } printf("Filled queue with %d messages\n", count); // Attempt to send to full queue (non-blocking returns immediately) if (mq_send(mq, msg, strlen(msg) + 1, 0) == -1 && errno == EAGAIN) { printf("Queue full - would block\n"); } // Drain and attempt to receive from empty queue char buf[64]; while (mq_receive(mq, buf, sizeof(buf), NULL) != -1); if (mq_receive(mq, buf, sizeof(buf), NULL) == -1 && errno == EAGAIN) { printf("Queue empty - would block\n"); } mq_close(mq); mq_unlink("/nonblock_mq");} // Async notification when message arrivesvoid async_mq_notification() { struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = 64, }; mqd_t mq = mq_open("/async_mq", O_CREAT | O_RDWR, 0666, &attr); // Set up notification (via signal or thread) struct sigevent sev = { .sigev_notify = SIGEV_SIGNAL, .sigev_signo = SIGUSR1, }; // Or: .sigev_notify = SIGEV_THREAD for thread-based notification mq_notify(mq, &sev); // Will receive SIGUSR1 when message arrives // Now wait for signal... // After handling, must call mq_notify again for next notification mq_close(mq); mq_unlink("/async_mq");}| Feature | Description |
|---|---|
| Message boundaries | Messages are discrete units, not byte streams |
| Priority ordering | Higher priority messages delivered first |
| Persistence | Messages persist in queue until received |
| Blocking semantics | Send blocks if full, receive blocks if empty |
| Notification | Can request signal/thread when message arrives |
| Inspection | Can query queue depth, message count |
POSIX message queues appear as virtual filesystem objects under /dev/mqueue/ (if mounted). You can use standard commands like ls /dev/mqueue/ and cat /dev/mqueue/myqueue to inspect queue state. This aids debugging but also has security implications—ensure appropriate permissions.
Signals are software interrupts—asynchronous notifications delivered to a process. Unlike other IPC mechanisms that transfer data, signals primarily convey events and can interrupt normal program flow.
Signal Fundamentals
Each signal has:
| Signal | Number | Default Action | Common Source |
|---|---|---|---|
| SIGHUP | 1 | Terminate | Terminal hangup, daemon reload |
| SIGINT | 2 | Terminate | Ctrl+C, interrupt request |
| SIGQUIT | 3 | Core dump | Ctrl+\, quit with dump |
| SIGKILL | 9 | Terminate | Unconditional termination |
| SIGSEGV | 11 | Core dump | Segmentation fault |
| SIGPIPE | 13 | Terminate | Broken pipe |
| SIGTERM | 15 | Terminate | Polite termination request |
| SIGCHLD | 17 | Ignore | Child terminated/stopped |
| SIGUSR1 | 10 | Terminate | User-defined |
| SIGUSR2 | 12 | Terminate | User-defined |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <signal.h>#include <unistd.h>#include <sys/wait.h> // ========================================// Modern signal handling with sigaction()// ========================================volatile sig_atomic_t got_sigint = 0; void sigint_handler(int sig, siginfo_t *info, void *context) { // In signal handlers, only async-signal-safe functions are allowed! // write() is safe; printf() is NOT. const char *msg = "\nCaught SIGINT!\n"; write(STDOUT_FILENO, msg, strlen(msg)); got_sigint = 1;} void setup_signal_handler() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sigint_handler; // Use 3-arg handler sa.sa_flags = SA_SIGINFO | SA_RESTART; // Extended info, restart syscalls sigemptyset(&sa.sa_mask); // Don't block extra signals during handler if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); exit(1); }} // ========================================// Sending signals// ========================================void send_signal_examples() { pid_t child = fork(); if (child == 0) { // Child: just sleep printf("Child %d sleeping...\n", getpid()); sleep(60); printf("Child woke up normally\n"); exit(0); } sleep(1); // Let child start // Send SIGTERM to child printf("Parent sending SIGTERM to child %d\n", child); kill(child, SIGTERM); int status; waitpid(child, &status, 0); if (WIFSIGNALED(status)) { printf("Child killed by signal %d\n", WTERMSIG(status)); }} // ========================================// Blocking signals during critical sections// ========================================void critical_section_example() { sigset_t block_set, old_set; // Create set containing SIGINT and SIGTERM sigemptyset(&block_set); sigaddset(&block_set, SIGINT); sigaddset(&block_set, SIGTERM); // Block signals sigprocmask(SIG_BLOCK, &block_set, &old_set); printf("Signals blocked - performing critical operation\n"); // Critical code here - signals are queued, not delivered sleep(3); printf("Critical operation complete\n"); // Check if signal was pending sigset_t pending; sigpending(&pending); if (sigismember(&pending, SIGINT)) { printf("SIGINT was received while blocked\n"); } // Restore old signal mask - pending signals now delivered sigprocmask(SIG_SETMASK, &old_set, NULL);} // ========================================// Waiting for specific signals// ========================================void wait_for_signal() { sigset_t wait_set; sigemptyset(&wait_set); sigaddset(&wait_set, SIGUSR1); // Block SIGUSR1 so it doesn't kill us sigprocmask(SIG_BLOCK, &wait_set, NULL); printf("Waiting for SIGUSR1 (send with: kill -USR1 %d)\n", getpid()); int sig; sigwait(&wait_set, &sig); // Blocks until signal arrives printf("Received signal %d\n", sig);} // ========================================// Real-time signals with data// ========================================void realtime_signal_handler(int sig, siginfo_t *info, void *context) { char buf[128]; int len = snprintf(buf, sizeof(buf), "RT signal %d, value %d, sender pid %d\n", sig, info->si_value.sival_int, info->si_pid); write(STDOUT_FILENO, buf, len);} void realtime_signals() { // Set up handler for SIGRTMIN struct sigaction sa; sa.sa_sigaction = realtime_signal_handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaction(SIGRTMIN, &sa, NULL); pid_t child = fork(); if (child == 0) { sleep(1); // Send real-time signal with data union sigval value; value.sival_int = 42; sigqueue(getppid(), SIGRTMIN, value); exit(0); } // Parent waits pause(); // Wait for any signal wait(NULL);}Signal handlers are notoriously error-prone. Key rules: (1) Only call async-signal-safe functions (write(), _exit()—not printf(), malloc(), or most library functions). (2) Use volatile sig_atomic_t for flags shared with main code. (3) Keep handlers minimal—set a flag and return. (4) Be aware of race conditions with main code. Modern code often prefers signalfd() or self-pipe tricks for safer signal handling.
signalfd() — Signals as File Descriptors
Linux provides signalfd() to receive signals through poll()/select()-able file descriptors, enabling integration with event loops:
int signalfd(int fd, const sigset_t *mask, int flags);
This allows handling signals synchronously in the main event loop rather than asynchronously in signal handlers—much safer and easier to reason about.
With multiple IPC mechanisms available, choosing the right one depends on communication patterns, performance requirements, and system constraints:
| Mechanism | Best For | Avoid When |
|---|---|---|
| Anonymous Pipe | Parent-child data streams, shell pipelines | Unrelated processes, bidirectional |
| Named Pipe (FIFO) | Simple unrelated process streams, daemons | High-performance, structured data |
| Unix Socket | Local client-server, fd passing, credential passing | Pure network scenarios |
| TCP Socket | Network communication, established protocols | Same-host IPC (slower than Unix) |
| UDP Socket | Broadcasts, multicast, real-time data | Reliability required, large messages |
| Shared Memory | Large data, zero-copy, high frequency | Simple synchronization, untrusted parties |
| Message Queue | Discrete messages, priorities, async | Large data, byte streams |
| Signals | Async events, control signals | Data transfer, reliable delivery |
For new development: Unix domain sockets are often the best default for local IPC—fast, well-supported, support fd passing, and integrate with standard I/O multiplexing. Shared memory + eventfd is optimal for high-performance scenarios. Message queues are excellent when message boundaries and priorities matter. Only use pipes for simple parent-child scenarios or shell integration.
Communication system calls enable the cooperation that makes complex software systems possible. We've explored the diverse mechanisms through which processes exchange data and coordinate:
Module Complete
With communication system calls, we complete our comprehensive survey of system call types. From process control through file management, device management, information maintenance, and communication, you now understand the fundamental vocabulary through which application programs request operating system services. This knowledge forms the foundation for systems programming, understanding operating system internals, and debugging complex multi-process applications.
You have now mastered the complete taxonomy of system calls: process control for managing process lifecycles, file management for persistent I/O, device management for hardware access, information maintenance for system state, and communication for inter-process cooperation. This comprehensive understanding enables you to reason about how applications interact with the operating system and to design robust, efficient systems software.