Linux provides multiple mechanisms for processes to exchange data and coordinate execution. These range from simple notifications to complex shared data structures, each optimized for specific use cases regarding speed, relationship between processes, and data volume.
Signal-Based Notification
Signals represent the asynchronous communication layer of the kernel. Unlike other IPC methods that transfer data, signals primarily convey event notifications. When a hardware exception occurs—such as division by zero or illegal memory access—the kernel generates corresponding signals (SIGFPE, SIGSEGV). Software events including timer expirations (SIGALRM) or explicit user requests via kill() also trigger signal delivery.
Standard signals (1–31) exhibit unreliable behavior under rapid successive delivery; the kernel coalesces multiple identical standard signals into a single instance. Conversely, real-time signals (34–64) maintain a queue, ensuring each emission results in distinct handling. Processes control signal disposition through handlers registered with sigaction() (preferred over the legacy signal() interface), choosing between default termination, explicit ignoring, or custom callback execution.
Signal handlers execute asynchronously, interrupting the normal flow of execution. Critical sections require protection via signal masks to prevent reentrancy issues:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
static volatile sig_atomic_t termination_requested = 0;
void handle_termination(int signo)
{
termination_requested = 1;
}
int main(void)
{
struct sigaction sa;
sigset_t block_mask, old_mask;
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGTERM);
sigaddset(&block_mask, SIGINT);
sa.sa_handler = handle_termination;
sa.sa_mask = block_mask;
sa.sa_flags = SA_RESTART;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (!termination_requested) {
sigprocmask(SIG_BLOCK, &block_mask, &old_mask);
/* Critical section: signal-safe operations only */
puts("Processing...");
sigprocmask(SIG_SETMASK, &old_mask, NULL);
sleep(1);
}
return 0;
}
Real-time signals support queuing and priority-based delivery, making them suitable for high-frequency event notification where signal loss is unacceptable.
Pipe Communication Channels
Pipes provide unidirectional byte streams betwean processes. The kernel implements pipes as circular buffers with atomic write guarantees for data sizes below PIPE_BUF (typically 4096 bytes on Linux).
Anonymous pipes require parent-child relationships. The creating process establishes file descriptors via pipe(), then forks, with each process closing the unused end:
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#define DATA_SIZE 64
int main(void)
{
int fd[2];
pid_t worker;
char outbound[DATA_SIZE] = "Payload from parent";
char inbound[DATA_SIZE] = {0};
if (pipe(fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
worker = fork();
if (worker == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (worker == 0) {
close(fd[1]);
read(fd[0], inbound, DATA_SIZE);
printf("Child received: %s\n", inbound);
close(fd[0]);
exit(EXIT_SUCCESS);
} else {
close(fd[0]);
write(fd[1], outbound, sizeof(outbound));
close(fd[1]);
wait(NULL);
}
return 0;
}
Named pipes (FIFOs) persist in the filesystem namespace via mkfifo(), enabling communication between unrelated processes. Unlike anonymous pipes, FIFOs support multiple simultaneous readers and writers, though atomic writes remain guaranteed only for transactions below the pipe buffer threshold. Log aggregation systems frequent employ FIFOs to collect events from disparate daemons into centralized processors.
System V Message Queues
Message queues store typed messages in kernel space, allowing selective retrieval based on message type rather than strict FIFO ordering. This deocuples message producers from consumers regarding timing—messages persist until explicitly received or until queue removal.
The API centers around four operations: msgget() for creation/access, msgsnd() for submission, msgrcv() for retrieval, and msgctl() for administrative control. Messages require a user-defined structure starting with a long type field:
#include <sys/msg.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_PAYLOAD 256
struct packet {
long priority;
char content[MAX_PAYLOAD];
};
int main(void)
{
int queue_id;
struct packet tx, rx;
key_t key = ftok("/tmp/queue_seed", 'A');
queue_id = msgget(key, IPC_CREAT | 0600);
if (queue_id == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
/* Producer segment */
tx.priority = 1;
strncpy(tx.content, "High priority data", MAX_PAYLOAD);
msgsnd(queue_id, &tx, sizeof(tx.content), 0);
/* Consumer segment */
msgrcv(queue_id, &rx, sizeof(rx.content), 1, 0);
printf("Received type %ld: %s\n", rx.priority, rx.content);
msgctl(queue_id, IPC_RMID, NULL);
return 0;
}
Unlike pipes, message queues retain data across process lifecycles, requiring explicit cleanup via ipcrm or msgctl(..., IPC_RMID, ...).
Semaphore Synchronization
Semaphores resolve race conditions when processes access shared resources. Acting as integer counters with atomic decrement (wait/P) and increment (signal/V) operations, they enforce mutual exclusion and counting constraints.
System V semaphores support complex operations through arrays and undo structures, though simpler POSIX unnamed semaphores often suffice for intra-process synchronization. The following demonstrates process synchronization using binary semaphore:
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
union sem_val {
int val;
struct semid_ds *stat;
unsigned short *array;
};
void semaphore_wait(int sem_id)
{
struct sembuf decrement = {0, -1, SEM_UNDO};
semop(sem_id, &decrement, 1);
}
void semaphore_signal(int sem_id)
{
struct sembuf increment = {0, 1, SEM_UNDO};
semop(sem_id, &increment, 1);
}
int main(void)
{
int sem_id;
union sem_val init;
pid_t child;
sem_id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
init.val = 0;
semctl(sem_id, 0, SETVAL, init);
child = fork();
if (child == 0) {
sleep(2);
puts("Child process completing task");
semaphore_signal(sem_id);
exit(0);
}
puts("Parent waiting for child synchronization");
semaphore_wait(sem_id);
puts("Parent proceeding after child signal");
wait(NULL);
semctl(sem_id, 0, IPC_RMID, init);
return 0;
}
The SEM_UNDO flag ensures that if a process terminates while holding a semaphore, the kernel automatically reverses the operation, preventing deadlocks.
Shared Memory Architecture
Shared memory offers the highest throughput IPC by mapping physical memory pages into multiple process address spaces. Eliminating kernel copying overhead, this mechanism achieves near-memory-speed data transfer suitable for high-frequency data exchange.
Processes attach to segments using shmat() after creation via shmget(). Synchronization requires external mechanisms—typically semaphores or mutexes placed in shared memory—to prevent concurrent modification:
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PROJECT_ID 'S'
struct shared_region {
int ready_flag;
char buffer[512];
};
int main(void)
{
int shmid;
key_t shm_key;
struct shared_region *shm_addr;
shm_key = ftok("/dev/null", PROJECT_ID);
shmid = shmget(shm_key, sizeof(struct shared_region), IPC_CREAT | 0644);
shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
/* Producer logic */
shm_addr->ready_flag = 0;
strcpy(shm_addr->buffer, "Zero-copy data transfer");
shm_addr->ready_flag = 1;
/* Consumer would check ready_flag before reading */
shmdt(shm_addr);
/* Cleanup performed by last process */
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
Network-Capable IPC: Sockets
Unix domain sockets extend the socket API for local machine communication, offering stream and datagram semantics with filesystem or abstract namespace addressing. While incurring higher overhead than shared memory, sockets provide bidirectional communication, message boundaries (in datagram mode), and portability to network environments.
Selection Criteria
Choose signals for exceptional condition notification, pipes for simple linear data flow between related processes, message queues for typed message persistence, semaphores exclusively for synchronization without data transfer, and shared memory when bandwidth dominates over latency concerns. Unix domain sockets provide the most flexible abstraction when multiple communication patterns coexist.