Skip to content

Inter-Process Communication (IPC)

Overview

IPC mechanisms allow processes to exchange data and coordinate actions. Linux provides several IPC methods, each with different characteristics and use cases.

Pipes

Pipes provide unidirectional data flow between processes.

Unnamed Pipes

Created with pipe() system call, only between related processes (parent-child).

#include <unistd.h>

int pipefd[2];
pipe(pipefd);  // pipefd[0] = read end, pipefd[1] = write end

if (fork() == 0) {
    // Child writes
    close(pipefd[0]);
    write(pipefd[1], "Hello", 5);
    close(pipefd[1]);
    exit(0);
} else {
    // Parent reads
    close(pipefd[1]);
    char buf[10];
    read(pipefd[0], buf, 5);
    close(pipefd[0]);
}

Characteristics:

  • Unidirectional: One-way communication
  • Related processes only: Must share file descriptors
  • Buffer size: Typically 64KB (check with ulimit -p)
  • Blocking: Reads block until data available, writes block when full
  • Anonymous: No filesystem entry

Shell Pipes:

ls -l | grep ".txt" | wc -l
# Three processes connected by two pipes

Named Pipes (FIFOs)

Pipes with filesystem entries, allowing unrelated processes to communicate.

# Create FIFO
mkfifo /tmp/mypipe

# Process 1 (writer)
echo "Hello" > /tmp/mypipe

# Process 2 (reader)
cat /tmp/mypipe
#include <sys/stat.h>
#include <fcntl.h>

// Create FIFO
mkfifo("/tmp/mypipe", 0666);

// Writer
int fd = open("/tmp/mypipe", O_WRONLY);
write(fd, "Hello", 5);
close(fd);

// Reader
int fd = open("/tmp/mypipe", O_RDONLY);
char buf[10];
read(fd, buf, 5);
close(fd);

Characteristics:

  • Filesystem entry: Has a pathname
  • Unrelated processes: Any process can open
  • Persistent: Exists until unlink()
  • Blocking opens: Writer blocks until reader, vice versa

How Pipes Work at Kernel Level

graph LR
    A[Process A] -->|write| B[Kernel Buffer]
    B -->|read| C[Process B]

Kernel implementation:

  1. Ring buffer: Circular buffer in kernel memory
  2. File descriptors: Both ends are file descriptors
  3. Synchronization: Kernel handles blocking/waking
  4. Size limit: Default 64KB (can be adjusted)
# View pipe buffer size
ulimit -p
cat /proc/sys/fs/pipe-max-size

# Adjust pipe size (in code)
fcntl(fd, F_SETPIPE_SZ, 1048576);  // 1MB

Signals

Signals are asynchronous notifications sent to processes.

Common Signals

Signal Number Default Action Description
SIGHUP 1 Terminate Hangup (terminal closed)
SIGINT 2 Terminate Interrupt (Ctrl+C)
SIGQUIT 3 Core dump Quit (Ctrl+\)
SIGILL 4 Core dump Illegal instruction
SIGTRAP 5 Core dump Trace trap (debugger)
SIGABRT 6 Core dump Abort (assert failure)
SIGBUS 7 Core dump Bus error (alignment)
SIGFPE 8 Core dump Floating point exception
SIGKILL 9 Terminate Kill (cannot be caught)
SIGSEGV 11 Core dump Segmentation fault
SIGPIPE 13 Terminate Broken pipe
SIGALRM 14 Terminate Timer alarm
SIGTERM 15 Terminate Termination request
SIGCHLD 17 Ignore Child process status changed
SIGCONT 18 Continue Continue if stopped
SIGSTOP 19 Stop Stop (cannot be caught)
SIGTSTP 20 Stop Terminal stop (Ctrl+Z)

Signal Handling

#include <signal.h>

// Signal handler
void handler(int signum) {
    printf("Caught signal %d\n", signum);
}

// Register handler
signal(SIGINT, handler);
// or better:
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);

Sending signals:

# Send signal to process
kill -TERM <PID>
kill -9 <PID>      # SIGKILL
killall program

# From code
kill(pid, SIGTERM);
raise(SIGTERM);    // Send to self

Signal Safety

Async-Signal-Safe Functions

Signal handlers can only call async-signal-safe functions. Most library functions are NOT safe!

Safe functions include:

  • write(), read()
  • _exit()
  • signal(), sigaction()

NOT safe:

  • printf(), malloc(), free()
  • Most library functions
// BAD: printf is not async-signal-safe
void bad_handler(int sig) {
    printf("Signal %d\n", sig);
}

// GOOD: use write()
void good_handler(int sig) {
    const char msg[] = "Signal caught\n";
    write(STDOUT_FILENO, msg, sizeof(msg)-1);
}

Signal Masking

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);

// Block SIGINT
sigprocmask(SIG_BLOCK, &set, NULL);

// Critical section
// SIGINT will be pending, not delivered

// Unblock
sigprocmask(SIG_UNBLOCK, &set, NULL);

Sockets

Sockets provide bidirectional communication between processes.

Unix Domain Sockets

For IPC on same machine, more efficient than TCP.

#include <sys/socket.h>
#include <sys/un.h>

// Server
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysock");
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 5);
int client = accept(sock, NULL, NULL);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysock");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));

Advantages over network sockets:

  • Faster (no network stack)
  • Can pass file descriptors
  • Filesystem permissions
  • More secure

Network Sockets

See Networking Stack section for detailed coverage.

Message Queues

Ordered list of messages stored in kernel.

POSIX Message Queues

#include <mqueue.h>

// Create/open queue
mqd_t mq = mq_open("/myqueue", O_CREAT | O_RDWR, 0644, NULL);

// Send message
char msg[] = "Hello";
mq_send(mq, msg, sizeof(msg), 0);

// Receive message
char buffer[100];
mq_receive(mq, buffer, sizeof(buffer), NULL);

// Close and unlink
mq_close(mq);
mq_unlink("/myqueue");

Characteristics:

  • Ordered: FIFO within same priority
  • Priority: Messages have priorities
  • Persistent: Survives process termination
  • Limits: Max message size, queue size
# View message queues
ls /dev/mqueue/
cat /proc/sys/fs/mqueue/*

System V Message Queues

Older API, still widely used.

#include <sys/msg.h>

// Create queue
key_t key = ftok("/tmp/myfile", 'A');
int msgid = msgget(key, IPC_CREAT | 0666);

// Send
struct msgbuf {
    long mtype;
    char mtext[100];
} msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);

// Receive
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);

// Remove
msgctl(msgid, IPC_RMID, NULL);
# View System V message queues
ipcs -q
ipcrm -q <msgid>

Shared Memory

Fastest IPC method - processes map same physical memory.

POSIX Shared Memory

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

// Create shared memory
int shm_fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, 4096);

// Map into address space
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                 MAP_SHARED, shm_fd, 0);

// Use memory
strcpy((char*)ptr, "Hello");

// Cleanup
munmap(ptr, 4096);
close(shm_fd);
shm_unlink("/myshm");

System V Shared Memory

#include <sys/shm.h>

// Create segment
key_t key = ftok("/tmp/myfile", 'B');
int shmid = shmget(key, 4096, IPC_CREAT | 0666);

// Attach
void *ptr = shmat(shmid, NULL, 0);

// Use
strcpy((char*)ptr, "Hello");

// Detach and remove
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
# View shared memory
ipcs -m
ipcrm -m <shmid>

Important:

Synchronization Required

Shared memory provides no synchronization! You must use mutexes, semaphores, or other mechanisms to coordinate access.

Semaphores

Semaphores coordinate access to shared resources (covered in detail in Synchronization).

POSIX Semaphores

#include <semaphore.h>

// Named semaphore (inter-process)
sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 1);
sem_wait(sem);   // P operation (decrement)
// Critical section
sem_post(sem);   // V operation (increment)
sem_close(sem);
sem_unlink("/mysem");

// Unnamed semaphore (thread/process)
sem_t sem;
sem_init(&sem, 1, 1);  // pshared=1 for processes
sem_destroy(&sem);

System V Semaphores

#include <sys/sem.h>

// Create semaphore set
key_t key = ftok("/tmp/myfile", 'C');
int semid = semget(key, 1, IPC_CREAT | 0666);

// Initialize
semctl(semid, 0, SETVAL, 1);

// Wait (P)
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1);

// Signal (V)
op.sem_op = 1;
semop(semid, &op, 1);

// Remove
semctl(semid, 0, IPC_RMID);

Comparison of IPC Mechanisms

Mechanism Speed Use Case Complexity
Pipes Fast Related processes, streaming Low
FIFOs Fast Unrelated processes, streaming Low
Signals Fast Notifications, simple events Low
Unix Sockets Fast Local bidirectional comm Medium
Message Queues Medium Structured messages Medium
Shared Memory Fastest Large data, high performance High

Practice Questions

  1. What is the difference between named and unnamed pipes?
  2. Why can't you catch SIGKILL?
  3. What functions are async-signal-safe and why does it matter?
  4. Why is shared memory the fastest IPC mechanism?
  5. When would you use message queues instead of pipes?
  6. How do Unix domain sockets differ from network sockets?
  7. What happens if you write to a pipe with no readers?

Further Reading

  • man 7 pipe, man 3 mkfifo
  • man 7 signal, man 2 sigaction
  • man 7 unix, man 2 socket
  • man 7 mq_overview
  • man 7 shm_overview
  • "The Linux Programming Interface" Chapters 44-54