In previous tutorials, we’ve explored file I/O, process creation, shell implementation and memory management. However, processes in Linux are generally isolated from one another. Each process has its own address space and cannot directly access another process’s memory.
This raises an important question:
How do processes communicate?
The answer is Inter-Process Communication (IPC).
Linux provides several mechanisms that allow processes to exchange data, synchronize execution and notify each other about events. In this tutorial, we’ll examine the most commonly used IPC techniques and the system calls that make them possible.
1. What is IPC?
Inter-Process Communication (IPC) refers to mechanisms that allow separate processes to:
- Exchange data
- Coordinate actions
- Share resources
- Signal events
Common Linux IPC mechanisms include:
- Pipes: Stream data between related processes
- FIFOs (Named Pipes): Pipes between unrelated processes
- Signals: Send notifications
- Shared Memory: Share memory regions
- Sockets: Local or network communication
Each mechanism serves a different purpose and has different performance characteristics.
2. Pipes
A pipe is one of the simplest IPC mechanisms. A pipe creates a unidirectional communication channel between processes.
#include <unistd.h>
int pipe(int pipefd[2]);
The kernel provides two file descriptors:
- pipefd[0]: Read End
- pipefd[1]: Write End
Basic Pipe Example
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int pipefd[2];
pipe(pipefd);
write(pipefd[1], "Hello", 5);
char buffer[16];
read(pipefd[0], buffer, sizeof(buffer));
printf("%s\n", buffer);
return 0;
}
Output:
Hello
While this example uses a single process, pipes are most useful after a fork().
3. Pipes Between Parent and Child Processes
The most common pipe usage is communication between a parent and child.
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
int main()
{
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0)
{
close(pipefd[0]);
write(pipefd[1], "Message from child", 18);
close(pipefd[1]);
}
else
{
close(pipefd[1]);
char buffer[64];
read(pipefd[0], buffer, sizeof(buffer));
printf("%s\n", buffer);
close(pipefd[0]);
wait(NULL);
}
return 0;
}
This pattern appears frequently in shells and process managers.
4. Named Pipes (FIFOs)
Regular pipes only work between related processes. A FIFO (First-In First-Out) allows unrelated processes to communicate.
Create a FIFO:
#include <sys/stat.h>
mkfifo("myfifo", 0666);
The FIFO appears in the filesystem:
ls -l myfifo
You can then open it like a normal file.
Writing to a FIFO
int fd = open("myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);
Reading from a FIFO
int fd = open("myfifo", O_RDONLY);
char buffer[64];
read(fd, buffer, sizeof(buffer));
printf("%s\n", buffer);
close(fd);
Named pipes are useful for simple communication between independent programs.
5. Signals
Signals provide lightweight process notifications.
A signal is not designed to transfer large amounts of data. Instead, it communicates that an event occurred.
Examples:
- SIGINT: Interrupt (Ctrl+C)
- SIGTERM: Request termination
- SIGKILL: Force termination
- SIGCHLD: Child exited
- SIGUSR1: User-defined
- SIGUSR2: User-defined
Sending Signals
#include <signal.h>
kill(pid, SIGTERM);
Despite the name, kill() sends signals of any type.
Receiving Signals
#include <signal.h>
#include <stdio.h>
void handler(int sig)
{
printf("Signal received: %d\n", sig);
}
int main()
{
signal(SIGUSR1, handler);
while (1);
return 0;
}
From another terminal:
kill -USR1 <pid>
The handler executes immediately.
6. Reliable Signal Handling with sigaction()
Modern Linux programs prefer sigaction().
#include <signal.h>
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
Advantages:
- More reliable
- Better portability
- Supports advanced options
Most production code uses sigaction() instead of signal().
7. Shared Memory Using mmap()
Shared memory is one of the fastest IPC mechanisms because data is not copied through the kernel after setup.
Example:
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
Multiple processes can map the same region and those processes can read and write directly.
8. Example: Parent and Child Sharing Memory
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int *shared = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*shared = 0;
pid_t pid = fork();
if (pid == 0)
{
*shared = 42;
}
else
{
wait(NULL);
printf("Value: %d\n", *shared);
}
return 0;
}
Output:
Value: 42
Both processes access the same memory.
9. IPC Mechanisms Compared
| Method | Speed | Complexity | Typical Use |
|---|---|---|---|
| Pipe | Medium | Low | Parent-child communication |
| FIFO | Medium | Low | Independent processes |
| Signals | Very Fast | Low | Notifications |
| Shared Memory | Very Fast | Medium | Large data exchange |
| Sockets | Medium | High | Local and network communication |
There is no universally best IPC mechanism. The choice depends on the problem you’re solving.
10. TL/DR
- IPC enables processes to communicate and coordinate.
- Pipes provide simple communication channels.
- FIFOs extend pipes to unrelated processes.
- Signals notify processes about events.
- Shared memory provides extremely fast data exchange.
- Different IPC mechanisms are suited to different use cases.
At this point, you understand the core IPC mechanisms available in Linux and the system calls that support them.