Fundamentals 48 min read

Unlock Linux IPC: Deep Dive into Pipes, Signals, Shared Memory and More

This comprehensive guide explores Linux inter‑process communication (IPC), explaining why processes need to communicate, the underlying kernel mechanisms, and detailed coverage of pipes, FIFOs, signals, files, shared memory, message queues, and sockets with practical code examples and real‑world use cases.

Deepin Linux
Deepin Linux
Deepin Linux
Unlock Linux IPC: Deep Dive into Pipes, Signals, Shared Memory and More

1. Linux Inter‑Process Communication Basics

In everyday life we constantly exchange information, just as processes in a Linux system need to communicate and cooperate. Without IPC, each process would be an isolated island, leading to system chaos.

Selecting the appropriate IPC method is crucial for performance, stability, and scalability. Future IPC technologies will aim for higher performance, better security, and smarter operation, especially in distributed systems, AI, and IoT.

1.1 Why Communicate?

Processes, like people, have communication needs and are isolated in separate address spaces. To exchange information and coordinate tasks, IPC is essential.

The choice of IPC method depends on requirements, data volume, and implementation constraints, similar to how humans choose fire‑signals, carrier pigeons, letters, telegrams, phone calls, or instant messages.

1.2 Why Is Communication Possible?

The kernel space is shared; although each process has its own user space, they can access the kernel via system calls, which act as a bridge for communication.

Linux processes are like independent rooms; the kernel is a central control room that mediates communication.

2. Linux IPC Framework

2.1 Structure of IPC Mechanisms

The IPC mechanism consists of a communication core in kernel space and user‑space interfaces, analogous to a post office (core) and letters or phones (interfaces).

2.2 Types of IPC Mechanisms

(1) Shared‑Memory‑Based : After the kernel creates a channel, both sides can communicate directly without further kernel assistance, similar to opening a door between two rooms. Synchronisation (e.g., semaphores) is required to avoid data races.

(2) Message‑Passing : Every communication still passes through the kernel, like a messenger delivering letters between rooms.

2.3 Interface Design

Message‑passing is typically used for asymmetric communication (client‑server).

Shared memory suits symmetric communication but can also support asymmetric patterns.

Three typical interfaces are needed: creating the channel, locating/joining the channel, and using the channel for read/write operations.

3. In‑Depth Analysis of Common IPC Methods

3.1 Pipes

Pipes are a fundamental IPC tool that act like a data conduit between processes. They come in two forms:

Anonymous Pipes

Anonymous pipes exist only in memory and are unidirectional, mainly used between related processes (e.g., parent‑child). The pipe() call returns two file descriptors: one for reading, one for writing.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 1024
int main() {
    int pipe_fd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];
    if (pipe(pipe_fd) == -1) { perror("pipe creation failed"); exit(1); }
    pid = fork();
    if (pid == -1) { perror("fork failed"); exit(1); }
    else if (pid == 0) { // child
        close(pipe_fd[1]);
        ssize_t bytes = read(pipe_fd[0], buffer, BUFFER_SIZE-1);
        if (bytes == -1) { perror("read failed"); exit(1); }
        buffer[bytes] = '\0';
        printf("Child received: %s
", buffer);
        close(pipe_fd[0]);
        exit(0);
    } else { // parent
        close(pipe_fd[0]);
        const char *msg = "Hello, child!";
        write(pipe_fd[1], msg, strlen(msg));
        close(pipe_fd[1]);
        wait(NULL);
        printf("Parent sent: %s
", msg);
    }
    return 0;
}

Advantages: simple, fast, no disk I/O. Disadvantages: half‑duplex, limited to related processes, limited buffer size.

Named Pipes (FIFO)

Named pipes have a pathname in the file system, allowing unrelated processes to communicate.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "myfifo"
#define BUFFER_SIZE 1024
int main() {
    int fd;
    if (mkfifo(FIFO_NAME, 0666) == -1 && errno != EEXIST) { perror("mkfifo"); return 1; }
    fd = open(FIFO_NAME, O_WRONLY);
    const char *msg = "Hello, client!";
    write(fd, msg, strlen(msg));
    close(fd);
    return 0;
}

Named pipes are suitable for long‑lived communication, such as logging systems.

3.2 Signals

Signals are asynchronous notifications from the kernel to a process (e.g., Ctrl‑C generates SIGINT). Processes can ignore, use default handling, or install custom handlers.

SIGINT (2): interrupt from keyboard.

SIGTERM (15): graceful termination.

SIGKILL (9): forced termination.

SIGCHLD (17): child status change.

import signal, os, sys, time

def handle_sigint(signum, frame):
    print(f"
Received SIGINT({signum}) – user interrupt")
    print("Cleaning up…")
    time.sleep(1)
    print("Cleanup done, exiting")
    sys.exit(0)

def handle_sigterm(signum, frame):
    print(f"
Received SIGTERM({signum}) – termination request")
    sys.exit(0)

def handle_sighup(signum, frame):
    print(f"
Received SIGHUP({signum}) – terminal hangup")
    print("Reloading config…")
    time.sleep(1)
    print("Config reloaded")

def handle_sigchld(signum, frame):
    print(f"
Received SIGCHLD({signum}) – child state change")
    try:
        while True:
            pid, status = os.waitpid(-1, os.WNOHANG)
            if pid == 0:
                break
            print(f"Child {pid} exited, status {status}")
    except OSError:
        pass

def main():
    signal.signal(signal.SIGINT, handle_sigint)
    signal.signal(signal.SIGTERM, handle_sigterm)
    signal.signal(signal.SIGHUP, handle_sighup)
    signal.signal(signal.SIGCHLD, handle_sigchld)
    signal.signal(signal.SIGPIPE, signal.SIG_IGN)
    print(f"PID: {os.getpid()}")
    print("Running – press Ctrl+C or send signals…")
    while True:
        time.sleep(1)

if __name__ == "__main__":
    main()

Signals are lightweight but can only carry a small amount of information.

3.3 Files

Processes can exchange data by reading and writing the same file, similar to using a shared notebook. File locks (shared or exclusive) are required to avoid race conditions.

3.4 Shared Memory

Shared memory provides the fastest IPC because data is not copied between processes. Synchronisation (e.g., semaphores) must be added for safe concurrent access.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define TEXT_SZ 2048
struct shared_use_st { char text[TEXT_SZ]; };
int main() {
    int shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1) { perror("shmget"); exit(1); }
    void *shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1) { perror("shmat"); exit(1); }
    struct shared_use_st *shared = shm;
    strcpy(shared->text, "Data to be shared");
    printf("Read from shared memory: %s
", shared->text);
    shmdt(shm);
    shmctl(shmid, IPC_RMID, 0);
    return 0;
}

3.5 Message Queues

Message queues act like a mailbox where processes can post and retrieve messages, supporting asynchronous communication and message categorisation.

#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <string.h>
#define MSG_SIZE 128
typedef struct { long mtype; char mtext[MSG_SIZE]; } message_buf;
int main() {
    key_t key = ftok(".", 'a');
    int msgid = msgget(key, IPC_CREAT | 0666);
    message_buf msg = {1, "Hello, message queue!"};
    msgsnd(msgid, &msg, strlen(msg.mtext)+1, 0);
    printf("Message sent: %s
", msg.mtext);
    msgrcv(msgid, &msg, MSG_SIZE, 1, 0);
    printf("Message received: %s
", msg.mtext);
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

3.6 Sockets

Sockets provide network‑level communication and can also be used for local IPC (UNIX domain sockets).

import socket, os, signal, sys
SOCKET_PATH = "/tmp/local_ipc_socket"

def handle_sigint(signum, frame):
    print("
Interrupt received, cleaning up…")
    if os.path.exists(SOCKET_PATH): os.unlink(SOCKET_PATH)
    sys.exit(0)

def run_server():
    signal.signal(signal.SIGINT, handle_sigint)
    if os.path.exists(SOCKET_PATH): os.unlink(SOCKET_PATH)
    server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    server.bind(SOCKET_PATH)
    server.listen(1)
    client, _ = server.accept()
    while True:
        data = client.recv(1024)
        if not data: break
        print(f"Received: {data.decode()}")
        client.sendall(f"Server got: {data.decode()}".encode())
        if data.decode().lower() == 'exit': break
    client.close(); server.close(); os.unlink(SOCKET_PATH)

if __name__ == "__main__":
    run_server()

4. Linux IPC Case Studies

4.1 Pipe Example

Parent creates a pipe, writes a message, child reads and prints it.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 1024
int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];
    if (pipe(pipefd) == -1) { perror("pipe"); exit(1); }
    pid = fork();
    if (pid == 0) { // child
        close(pipefd[1]);
        ssize_t n = read(pipefd[0], buffer, BUFFER_SIZE-1);
        buffer[n] = '\0';
        printf("Child read: %s
", buffer);
        close(pipefd[0]);
        exit(0);
    } else { // parent
        close(pipefd[0]);
        const char *msg = "Hello from parent!";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);
        wait(NULL);
        printf("Parent done
");
    }
    return 0;
}

4.2 Message Queue Example

Sender process posts a message; receiver reads until an "exit" message is received.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
struct msg_buffer { long msg_type; char msg_text[1024]; };
int main() {
    key_t key = ftok("msg_queue_demo", 65);
    int msgid = msgget(key, 0666 | IPC_CREAT);
    struct msg_buffer msg = {1, "Hello from sender!"};
    msgsnd(msgid, &msg, sizeof(msg.msg_text), 0);
    strcpy(msg.msg_text, "exit");
    msgsnd(msgid, &msg, sizeof(msg.msg_text), 0);
    sleep(1);
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
struct msg_buffer { long msg_type; char msg_text[1024]; };
int main() {
    key_t key = ftok("msg_queue_demo", 65);
    int msgid = msgget(key, 0666);
    struct msg_buffer msg;
    while (1) {
        msgrcv(msgid, &msg, sizeof(msg.msg_text), 1, 0);
        printf("Received: %s
", msg.msg_text);
        if (strcmp(msg.msg_text, "exit") == 0) break;
    }
    return 0;
}

4.3 Shared Memory Example

Writer creates shared memory, writes a string, and pauses for the reader.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
#define SHM_KEY 0x123456
int main() {
    int shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
    char *addr = shmat(shmid, NULL, 0);
    const char *msg = "Hello from shared memory writer!";
    strncpy(addr, msg, SHM_SIZE-1);
    printf("Wrote: %s
", msg);
    sleep(5);
    shmdt(addr);
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
#define SHM_KEY 0x123456
int main() {
    int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    char *addr = shmat(shmid, NULL, 0);
    printf("Read: %s
", addr);
    shmdt(addr);
    return 0;
}

4.4 Signal Example

Receiver registers handlers for SIGUSR1 and SIGUSR2; sender sends these signals to the receiver’s PID.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handle_usr1(int s){ printf("Got SIGUSR1, PID %d
", getpid()); }
void handle_usr2(int s){ printf("Got SIGUSR2, exiting
"); exit(0); }
int main(){
    signal(SIGUSR1, handle_usr1);
    signal(SIGUSR2, handle_usr2);
    printf("Receiver PID: %d
", getpid());
    while(1) pause();
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]){
    if(argc!=2){ fprintf(stderr,"Usage: %s <PID>
",argv[0]); exit(1); }
    pid_t pid = (pid_t)atoi(argv[1]);
    kill(pid, SIGUSR1);
    sleep(1);
    kill(pid, SIGUSR2);
    return 0;
}

4.5 Socket Example

Server listens on TCP port 8080, client connects, sends a message, and receives a reply.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main(){
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = { .sin_family=AF_INET, .sin_addr.s_addr=INADDR_ANY, .sin_port=htons(PORT) };
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 5);
    int client = accept(server_fd, NULL, NULL);
    char buf[BUFFER_SIZE];
    read(client, buf, BUFFER_SIZE);
    printf("Received: %s
", buf);
    const char *resp = "Server got your message";
    send(client, resp, strlen(resp), 0);
    close(client); close(server_fd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main(){
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv = { .sin_family=AF_INET, .sin_port=htons(PORT) };
    inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr);
    connect(sock, (struct sockaddr*)&serv, sizeof(serv));
    const char *msg = "Hello, server!";
    send(sock, msg, strlen(msg), 0);
    char buf[BUFFER_SIZE];
    read(sock, buf, BUFFER_SIZE);
    printf("Server replied: %s
", buf);
    close(sock);
    return 0;
}
Process communication diagram
Process communication diagram
IPC framework diagram
IPC framework diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

message queuesLinuxSystem Programmingshared memoryIPCSocketssignalspipes
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.