Linux IPC mechanisms are how processes talk to each other, and pipes are the simplest way to do it.

Imagine two programs, a sender and a receiver. A pipe is like a one-way street between them. The sender writes data into one end, and the receiver reads it from the other. It’s a stream of bytes, ordered and reliable.

Let’s see it in action. We can create a pipe using pipe() in C, which gives us two file descriptors: one for reading and one for writing.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buf[30];

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {    /* Child process reads from pipe */
        close(pipefd[1]); // Close the write end, not needed by child
        read(pipefd[0], buf, 30);
        printf("Child received: %s\n", buf);
        close(pipefd[0]); // Close the read end
        exit(EXIT_SUCCESS);
    } else {            /* Parent process writes to pipe */
        close(pipefd[0]); // Close the read end, not needed by parent
        const char *msg = "Hello from parent!";
        write(pipefd[1], msg, strlen(msg));
        printf("Parent sent: %s\n", msg);
        close(pipefd[1]); // Close the write end
        wait(NULL); // Wait for child to finish
        exit(EXIT_SUCCESS);
    }
}

When you compile and run this, you’ll see the parent send a message and the child receive it. The write call blocks until there’s space in the pipe, and read blocks until there’s data. This synchronization is a key feature.

Pipes are fundamentally implemented using the kernel’s page cache. When you write to a pipe, the data is copied into kernel memory buffers. When another process reads, the data is copied from those buffers to the reader’s user space. This buffering ensures data isn’t lost and provides flow control. The kernel manages these buffers, allocating and deallocating them as needed.

Beyond simple one-way communication, we have named pipes, or FIFOs (First-In, First-Out). Unlike anonymous pipes created by pipe(), FIFOs exist as special files in the filesystem. This allows unrelated processes to communicate, as long as they know the path to the FIFO.

You can create a FIFO with mkfifo myfifo. Then, one process can open it for writing and another for reading, just like a regular file.

# Terminal 1 (writer)
echo "This is a named pipe message" > myfifo

# Terminal 2 (reader)
cat < myfifo

The fundamental difference from anonymous pipes is the persistence in the filesystem and the ability for processes that didn’t start with a shared parent to connect. The kernel still uses underlying buffering mechanisms, but the access point is a file inode.

Sockets offer a more versatile communication paradigm, moving beyond the local machine. They are the backbone of network programming. A socket is an endpoint for communication. You can think of them as a generalized pipe that can operate across networks.

There are different types of sockets: stream sockets (like TCP, providing reliable, ordered, byte-stream communication) and datagram sockets (like UDP, providing unreliable, message-based communication).

Consider a simple TCP echo server and client. The server listens on a port, and clients connect to it.

// Server (simplified snippet)
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>

// ... socket creation, bind, listen ...

int client_sock = accept(server_fd, (struct sockaddr *)&address, &addrlen);
// ... read from client_sock, write back to client_sock ...
// Client (simplified snippet)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

// ... socket creation, connect to server_ip:port ...
// ... write to socket, read response from socket ...

The kernel manages socket buffers, but the complexity extends to network protocols, routing, and connection state. For stream sockets, the kernel guarantees delivery and order, retransmitting lost packets. For datagram sockets, it’s a "fire and forget" approach, with no guarantees.

Shared memory is the fastest IPC method. Instead of copying data between processes via the kernel, two or more processes map the same region of physical memory into their own address spaces.

Using System V IPC or POSIX shared memory, you can create a segment, attach it, and then read/write directly to that memory.

// POSIX Shared Memory (simplified)
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>

// ... open shared memory object ...
// ... mmap the object into address space ...
// ... write/read directly to the mapped memory ...
// ... munmap, close ...

The beauty here is that data written by one process is immediately visible to another without any kernel involvement in the data transfer itself. The kernel’s role is primarily in managing the mapping and synchronization primitives (like mutexes or semaphores) that processes use to coordinate access to the shared memory to prevent race conditions.

The surprising thing about shared memory is that while it bypasses kernel data copying, it doesn’t inherently solve the problem of coordination. Two processes writing to the same memory location simultaneously without any locking will lead to corruption. The speed comes from eliminating the transport layer, not from magically making concurrent access safe.

Shared memory requires explicit synchronization mechanisms. Without them, you can easily corrupt data. This is often done using semaphores or mutexes, which are themselves IPC mechanisms, to ensure only one process modifies the shared data at a time.

The next hurdle is dealing with the complexities of concurrent access and avoiding deadlocks when using shared memory with synchronization primitives.

Want structured learning?

Take the full Linux & Systems Programming course →