Appearance
Pipes and Message Queues
The OS provides several mechanisms to communicate between processes. Knowing how to select the correct one is imperative to build efficient and reliable systems. Two such mechanisms are Pipes and Message Queues.
Pipes are like a simple water pipe - data flows in one direction, and it's just a stream of bytes. Message queues are more like a mailbox system - each message is separate, and you can even prioritize them.
Let's start with understanding every IPC mechanism.
Unnamed Pipes: The Simple Stream
Think of an unnamed pipe like a water pipe between two processes. You pour water (data) in one end, and it comes out the other end. Simple, right?
How Unnamed Pipes Work
When you create a pipe, you get two file descriptors - one for reading and one for writing. The writing process puts data into the pipe, and the reading process takes it out.
cpp
int pipe_fds[2];
pipe(pipe_fds); // Creates the pipe
// pipe_fds[0] is for reading
// pipe_fds[1] is for writing1
2
3
4
2
3
4
The key insight is that pipes are unidirectional - data only flows in one direction. If you need two-way communication, you need two pipes.
The Byte Stream Nature
Pipes treat data as a continuous stream of bytes. There are no message boundaries. If you write "Hello" and then "World" to a pipe, the reader might get "HelloWorld" in a single read, or "He" and "lloWorld" in two reads, or any other combination.
This is both a strength and a limitation. It's simple and efficient, but you have to handle message boundaries yourself if you need them.
Process Relationship Limitation
Unnamed pipes only work between related processes - typically a parent and child, or processes created by the same parent. This is because the pipe is created in the parent's process and inherited by the child.
If you try to use an unnamed pipe between unrelated processes, it won't work. They can't see each other's file descriptors.
Named Pipes (FIFOs): Persistent Communication
What if you want to communicate between unrelated processes? That's where named pipes come in.
How Named Pipes Work
Named pipes (also called FIFOs) are like unnamed pipes, but they exist as files in the filesystem. You create them with mkfifo() and they persist until you remove them.
cpp
mkfifo("/tmp/my_pipe", 0644); // Creates a named pipe
int fd = open("/tmp/my_pipe", O_RDWR); // Opens it1
2
2
The beauty of named pipes is that any process with the right permissions can open and use them. They don't need to be related.
The Blocking Behavior
Named pipes have an interesting property: by default, opening a FIFO blocks until another process opens it for the opposite operation (read/write). This can be useful for coordination.
For example, if Process A opens a FIFO for writing, it will block until Process B opens the same FIFO for reading. This ensures that both processes are ready before communication begins.
Use Cases for Named Pipes
Named pipes are great for:
- Simple client-server communication: Multiple clients can write to a server's named pipe
- Configuration updates: One process signals another to reload configuration
- Persistent communication channels: The pipe exists even if the creating process terminates
Message Queues: Structured Communication
Now let's talk about message queues. If pipes are like water pipes, message queues are like a sophisticated mail system.
How Message Queues Work
Message queues store messages in the kernel. Each message has a type and a priority. When you send a message, it goes into the queue. When you receive a message, you can specify which type you want.
cpp
int msgid = msgget(key, IPC_CREAT | 0644); // Create or get queue
msgsnd(msgid, &message, sizeof(message), 0); // Send message
msgrcv(msgid, &message, sizeof(message), msg_type, 0); // Receive message1
2
3
2
3
Message Boundaries
Unlike pipes, message queues preserve message boundaries. If you send two messages, you'll receive two messages. This makes them much easier to use for structured communication.
Priority Support
Messages can have priorities. High-priority messages are delivered before low-priority ones. This is incredibly useful for systems where some messages are more urgent than others.
For example, in a trading system:
- High priority: Order cancellations, risk alerts
- Medium priority: Order confirmations
- Low priority: Market data updates
Type-based Filtering
You can receive messages of specific types. This allows multiple processes to use the same queue but only receive messages relevant to them.
Performance Comparison
Let's compare these mechanisms:
Latency
- Pipes: Very low latency for simple data transfer
- Message Queues: Higher latency due to kernel overhead and message management
Throughput
- Pipes: High throughput for large data transfers
- Message Queues: Lower throughput due to per-message overhead
Memory Usage
- Pipes: Minimal memory overhead
- Message Queues: Higher memory usage due to kernel buffering
Real-World Examples
Let's look at some concrete examples to understand when to use each.
Example 1: Log Processing Pipeline
You're building a log processing system:
- Process A: Reads log files and extracts events
- Process B: Filters events by type
- Process C: Writes filtered events to a database
Solution: Use pipes in a chain
cpp
Process A → pipe1 → Process B → pipe2 → Process C1
This works perfectly because:
- Data flows in one direction
- You don't need message boundaries (each log line is separate)
- Processes are related (created by the same parent)
Example 2: Configuration Management
You have a system with multiple components that need to reload configuration when it changes:
- Configuration monitor: Watches for config file changes
- Web server: Serves web pages
- Database server: Handles database operations
- Cache server: Manages caching
Solution: Use a named pipe
cpp
Configuration monitor → named pipe → All other processes1
When the config changes, the monitor writes to the pipe, and all other processes read the notification and reload their configuration.
Example 3: Trading System
You're building a trading system with different types of messages:
- Market data updates (high volume, low priority)
- Order requests (medium volume, high priority)
- Risk alerts (low volume, highest priority)
- System status updates (low volume, low priority)
Solution: Use message queues with priorities
cpp
Market data → Queue (priority 1)
Order requests → Queue (priority 3)
Risk alerts → Queue (priority 5)
Status updates → Queue (priority 1)1
2
3
4
2
3
4
The order manager can receive high-priority messages first, ensuring that risk alerts and order requests are processed before market data updates.
When to Use Each Mechanism
Choose Pipes When:
- You have a simple, unidirectional data flow
- Message boundaries don't matter (or you can handle them yourself)
- Performance is critical
- Processes are related (for unnamed pipes)
- You want the simplest possible solution
Choose Named Pipes When:
- You need communication between unrelated processes
- You want the pipe to persist beyond the creating process
- You need simple coordination between processes
- You want the simplicity of pipes but with process independence
Choose Message Queues When:
- You need message boundaries preserved
- You need priority-based message processing
- You need to filter messages by type
- You want reliable message delivery
- You're building a complex communication system
Common Patterns
Once you understand the basics, you'll see these patterns emerge:
Pipeline Pattern: Use pipes to chain processes together
- Data processing pipelines
- Log processing chains
- Image processing workflows
Broadcast Pattern: Use named pipes to notify multiple processes
- Configuration change notifications
- System-wide events
- Service discovery
Priority Queue Pattern: Use message queues for priority-based processing
- Trading systems
- Event processing systems
- Real-time monitoring systems
Integration with Other IPC
Pipes and message queues are often used together with other IPC mechanisms:
Pipes + Shared Memory: Use pipes for control messages, shared memory for bulk data
- Example: Video processing (control via pipes, video frames via shared memory)
Message Queues + Sockets: Use message queues for local communication, sockets for network communication
- Example: Local event processing via queues, network requests via sockets
Pipes + Signals: Use pipes for data transfer, signals for urgent notifications
- Example: Log data via pipes, "system shutdown" via signals
The Bottom Line
Pipes and message queues are fundamental IPC mechanisms that serve different purposes. Pipes are simple and fast, perfect for straightforward data flow. Message queues are more sophisticated, offering structured communication with priorities and filtering.
The key is understanding your requirements. Do you need speed and simplicity? Use pipes. Do you need structure and priorities? Use message queues. Do you need process independence? Use named pipes.
In many systems, you'll use a combination of these mechanisms, choosing the right tool for each communication pattern in your application.
Questions
Q: What is the main limitation of unnamed pipes?
Unnamed pipes are unidirectional - data flows in one direction only.
Q: Which IPC mechanism provides the highest throughput for large data transfers?
Shared memory provides the highest throughput as it eliminates data copying.
Q: What is the key advantage of message queues over pipes?
Message queues preserve message boundaries, unlike pipes which are byte streams.
Implement a producer-consumer pattern using named pipes (FIFOs). The producer should write integers to a pipe, and the consumer should read and process them.
cpp
#include <unistd.h>
#include <string>
#include <vector>
const char* FIFO_PATH = "/tmp/producer_consumer_fifo";
// TODO: Implement producer function
// Should:
// 1. Create a named pipe (FIFO) using mkfifo()
// 2. Open the FIFO for writing
// 3. Write each integer from the vector to the FIFO
// 4. Close the file descriptor when done
void producer(const std::vector<int>& numbers) {
// USER CODE HERE
}
// TODO: Implement consumer function
// Should:
// 1. Open the FIFO for reading
// 2. Read integers from the FIFO
// 3. Store them in a vector and return it
// 4. Close the file descriptor when done
// 5. Remove the FIFO using unlink()
std::vector<int> consumer(int count) {
// USER CODE HERE
return {};
}
#include <sys/stat.h>
#include <fcntl.h>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29