Chapter 74:IPC: Named Pipes (FIFOs) for Com. bw Unrelated Processes
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamental concept of a FIFO (First-In, First-Out) special file and its role in Linux Inter-Process Communication (IPC).
- Explain the key differences between unnamed pipes and named pipes (FIFOs), particularly their persistence and use cases.
- Implement robust communication between two or more unrelated processes using the
mkfifo()
system call and standard file I/O operations. - Configure and manage blocking and non-blocking I/O behavior when opening and interacting with FIFOs.
- Debug common issues related to FIFO implementation, such as deadlocks, permission errors, and improper cleanup.
- Apply these concepts to build a practical, multi-process embedded application on a Raspberry Pi 5.
Introduction
In the world of embedded Linux, systems are rarely monolithic. They are often composed of multiple, specialized processes working in concert. A process might be dedicated to reading sensor data, another to managing a network connection, a third to updating a display, and a fourth to logging system events. The critical challenge lies in making these independent processes communicate reliably and efficiently. This is where Inter-Process Communication (IPC) mechanisms become indispensable.
This chapter delves into one of the most elegant and foundational IPC mechanisms: the named pipe, or FIFO (First-In, First-Out). While previous discussions may have covered unnamed pipes, which are limited to communication between related processes (like a parent and its child), FIFOs shatter this limitation. Because a FIFO exists as a special file within the Linux filesystem, any process with the correct permissions can find it, open it, and use it to communicate, regardless of its lineage.
This capability is profoundly important in embedded design. Imagine an embedded web server that must display real-time temperature data. The process reading from the temperature sensor and the process serving web pages are completely unrelated. A FIFO provides a simple, robust, and efficient “rendezvous point” in the filesystem for them to exchange this data. The sensor process writes temperatures into the FIFO, and the web server process reads them out. Throughout this chapter, we will use the Raspberry Pi 5 as our development platform to transform this theory into practice, building a tangible understanding of how to create, manage, and utilize FIFOs for robust communication in your own embedded projects.
Technical Background
To truly grasp the power and utility of FIFOs, one must first understand their place within the Linux filesystem and the specific rules that govern their behavior. A FIFO is not a regular file; it contains no persistent data on the disk. Instead, it is a transient conduit, a kernel-managed buffer that connects one process’s output directly to another’s input.
From Unnamed Pipes to Filesystem Nodes
The concept of a pipe—a unidirectional channel for data flow—is a cornerstone of the Unix philosophy. The unnamed pipes created with the pipe()
system call are highly efficient but ephemeral. They exist only within kernel memory and are accessible only through file descriptors inherited by child processes. Once those processes terminate, the pipe vanishes without a trace. This tight coupling to process lineage makes them unsuitable for many common embedded architectures where independent, long-running daemons or services must communicate.
graph TB subgraph "Unnamed Pipe (Ephemeral)" direction TB A[Parent Process] -- pipe() --> K_Pipe["Kernel Memory<br><i>(No Filesystem Entry)</i>"]; A -- fork() --> B[Child Process]; A -- "write(fd[1])" --> K_Pipe; K_Pipe -- "read(fd[0])" --> B; end subgraph "Named Pipe (FIFO - Persistent)" direction TB P1["Process A<br><i>(e.g., sensor_writer)</i>"] P2["Process B<br><i>(e.g., data_reader)</i>"] FS_FIFO["<br><b>/tmp/my_fifo</b><br><i>(Filesystem Node 'p')</i>"] P1 -- "write()" --> FS_FIFO; FS_FIFO -- "read()" --> P2; end style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style K_Pipe fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style P1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style P2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style FS_FIFO fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
FIFOs solve this problem by giving a pipe a name and a place in the filesystem. This is achieved with the mkfifo()
system call or the equivalent mkfifo
command-line utility. When you create a FIFO, you are creating a special file, a persistent inode on the disk. You can see it with ls -l
, note its unique file type indicated by a ‘p’ character, and manage its permissions just like a regular file.
This filesystem presence is the key differentiator. Because the FIFO has a path (e.g., /tmp/my_fifo
), any process, whether it’s a shell script, a Python application, or a C program, can locate and use it, provided it has the necessary permissions. The filesystem becomes the namespace for discovering and establishing communication channels.
The mkfifo()
System Call and File Permissions
The primary programmatic tool for creating a FIFO is the mkfifo()
function, declared in <sys/stat.h>
. Its prototype is straightforward:
int mkfifo(const char *pathname, mode_t mode);
The pathname
is the familiar null-terminated string representing the desired location and name of the FIFO file. The mode
argument specifies the file permissions (read, write, execute for user, group, and others), which are modified by the process’s umask
in the standard way. For instance, to create a FIFO that is readable and writable by the owner and the group, but not by others, one might use a mode of 0660
.
These permissions are crucial for security and system integrity. In an embedded system, you can use standard file permissions to restrict access to a FIFO, ensuring that only authorized processes can write data to it or read from it. For example, a sensor-reading process might run as a specific user, and the FIFO it writes to could be configured to only allow that user (and perhaps a specific group) to write data, preventing rogue processes from injecting spurious information.
The Blocking Nature of FIFO Operations
A defining characteristic of FIFO behavior is its blocking nature during the open()
call. This is a fundamental synchronization mechanism that prevents data loss and simplifies process coordination. The rules are symmetric and elegant:
- Opening for Reading: If a process opens a FIFO for reading (
O_RDONLY
), theopen()
call will block (i.e., pause execution) until another process opens the same FIFO for writing. This ensures that a reader doesn’t start listening to an empty pipe with no potential writers, which would lead to an immediate end-of-file condition. - Opening for Writing: Conversely, if a process opens a FIFO for writing (
O_WRONLY
), theopen()
call will block until another process opens it for reading. This prevents a writer from sending data into a void where there are no listeners, which would result in aSIGPIPE
signal and process termination.
sequenceDiagram actor Reader actor Writer participant Kernel as Kernel Reader->>+Kernel: open("/tmp/my_fifo", O_RDONLY) Note right of Reader: Execution BLOCKED<br>Waits for a writer. par Writer->>+Kernel: open("/tmp/my_fifo", O_WRONLY) Note left of Writer: Call initiated. and Note over Reader, Kernel: Kernel sees both<br>reader and writer. end Kernel-->>-Reader: Returns file descriptor (Success) Note right of Reader: Execution RESUMED Kernel-->>-Writer: Returns file descriptor (Success) Note left of Writer: Execution RESUMED Reader->>Writer: Communication Channel Established
This blocking behavior is often exactly what is needed. A data-logging service can start up and patiently wait at the open()
call, consuming no CPU cycles, until the sensor-reading process is ready to send data. However, this can also be a source of deadlocks if not managed carefully. If two processes each try to open a FIFO for reading without a writer present, they will both block forever.
Non-Blocking I/O with O_NONBLOCK
There are scenarios where blocking is undesirable. An application might need to check if a process is ready to communicate without getting stuck, or it might need to manage multiple communication channels simultaneously. For these cases, the O_NONBLOCK
flag can be bitwise-OR’d with the mode in the open()
call (e.g., open(pathname, O_WRONLY | O_NONBLOCK)
).
The effect of O_NONBLOCK
changes the open()
behavior significantly:
- Opening for Reading: If a FIFO is opened with
O_RDONLY | O_NONBLOCK
, theopen()
call will succeed immediately, even if there is no writer. - Opening for Writing: If a FIFO is opened with
O_WRONLY | O_NONBLOCK
, theopen()
will fail with anENXIO
error (“No such device or address”) if there is no process that has the FIFO open for reading.
Furthermore, O_NONBLOCK
also affects subsequent read()
and write()
calls. A read()
on a non-blocking, empty FIFO will not block; it will return -1
immediately with errno
set to EAGAIN
(“Try again”). Similarly, a write()
to a non-blocking FIFO that is full (because the reader isn’t consuming data fast enough) will also return -1
with errno
set to EAGAIN
. This allows a process to poll the FIFO or use it in an event loop (e.g., with select()
or epoll()
) to handle I/O efficiently without being tied down by a single blocking call.
graph TD Start["Start: open() with O_NONBLOCK"] --> OpType{Opening for?}; OpType -- "Reading (O_RDONLY)" --> ReadSuccess[("open() succeeds immediately<br>Returns file descriptor")]; OpType -- "Writing (O_WRONLY)" --> CheckReader{Reader already exists?}; CheckReader -- "Yes" --> WriteSuccess[("open() succeeds immediately<br>Returns file descriptor")]; CheckReader -- "No" --> WriteFail{"open() fails<br><i>errno = ENXIO</i>"}; subgraph Subsequent I/O ReadSuccess --> ReadOp{"read() from FIFO"}; ReadOp --> IsData{Data available?}; IsData -- "Yes" --> ReadData["Read data successfully"]; IsData -- "No" --> ReadAgain["read() returns -1<br><i>errno = EAGAIN</i>"]; WriteSuccess --> WriteOp{"write() to FIFO"}; WriteOp --> IsSpace{Buffer has space?}; IsSpace -- "Yes" --> WriteData["Write data successfully"]; IsSpace -- "No" --> WriteAgain["write() returns -1<br><i>errno = EAGAIN</i>"]; end style Start fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style OpType fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style CheckReader fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style IsData fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style IsSpace fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style ReadSuccess fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style WriteSuccess fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style ReadData fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style WriteData fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style WriteFail fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style ReadAgain fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 style WriteAgain fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 style ReadOp fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style WriteOp fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
Data Flow, Atomicity, and Capacity
Once the FIFO is successfully opened by both a reader and a writer, data can flow. The process is identical to working with standard files: the writer uses the write()
system call, and the reader uses read()
. The kernel handles moving the data from the writer’s buffer into the FIFO’s kernel buffer, and then from that buffer to the reader’s buffer. The “First-In, First-Out” name describes this behavior perfectly: the order of bytes written is the order of bytes read.
An important guarantee provided by the kernel is write atomicity. If a process writes a chunk of data to a FIFO that is smaller than the defined pipe buffer size (PIPE_BUF
), the kernel ensures that this write operation is atomic. This means the data from this write()
call will not be interleaved with data from any other process that might also be writing to the same FIFO. This is critical for preventing message corruption when multiple writers might be sending data to a single reader. If a write is larger than PIPE_BUF
, the kernel may break it into smaller chunks, and interleaving could occur. The value of PIPE_BUF
is defined by the system and is guaranteed by the POSIX standard to be at least 512 bytes.
The FIFO itself has a limited capacity, determined by the kernel (typically 65,536 bytes on modern Linux systems). If a writer attempts to write to a FIFO and the kernel buffer is full, the write()
call will block until the reader has consumed enough data to make space. This provides a natural form of flow control, also known as backpressure. The writer cannot overwhelm the reader; its speed is automatically throttled to match the reader’s consumption rate.
Lifecycle and Cleanup
Since a FIFO is a filesystem entry, it persists until it is explicitly removed. A common mistake is to assume the FIFO file will disappear when the processes using it terminate. This is not the case. It will remain in the filesystem, and a subsequent attempt to create a FIFO at the same path will fail (unless the existing file is first removed).
Proper cleanup is therefore essential. The standard tool for removing any file, including a FIFO, is the unlink()
system call or the rm
command-line utility. Typically, the process responsible for creating the FIFO (often a server or manager process) is also responsible for calling unlink()
when it shuts down cleanly. This ensures the system remains in a clean state, ready for the next time the application runs.
Practical Examples
Theory provides the foundation, but true understanding comes from hands-on implementation. In this section, we will build a classic client-server system on the Raspberry Pi 5 using a FIFO. We will create two separate C programs:
sensor_writer
: This program will simulate a sensor. It will periodically generate a “data packet” containing a timestamp, a device ID, and a random temperature value, and write this structured data into a FIFO.data_reader
: This program will act as a data logger or consumer. It will open the FIFO, read the data packets written bysensor_writer
, and print them to the console in a human-readable format.
File Structure and Setup
First, let’s establish a clean working directory on your Raspberry Pi 5.
# On your Raspberry Pi 5
mkdir ~/fifo_project
cd ~/fifo_project
All our files (sensor_writer.c
, data_reader.c
, and eventually the compiled binaries) will reside in this directory. The FIFO itself will also be created here.
The FIFO and Data Structure
We need to define a common data structure that both processes understand. A C struct
is perfect for this. Let’s create a header file named fifo_common.h
to share this definition.
fifo_common.h
#ifndef FIFO_COMMON_H
#define FIFO_COMMON_H
#include <time.h>
// The name of our FIFO in the filesystem
#define FIFO_PATH "/tmp/sensor_fifo"
// The data structure we will pass through the FIFO
// This represents a single reading from our simulated sensor
typedef struct {
time_t timestamp; // Time of the reading
int device_id; // ID of the sensor device
float temperature; // The temperature reading
} SensorData;
#endif // FIFO_COMMON_H
Tip: Using a shared header file is a best practice. It ensures that both the writer and reader are compiled with the exact same data structure definition and FIFO path, preventing subtle and hard-to-debug errors related to data misalignment or path mismatches. We are placing the FIFO in
/tmp
which is a standard location for temporary runtime files.
The Writer Process: sensor_writer.c
This program is responsible for creating the FIFO, generating sensor data, and writing it into the pipe.
sensor_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include "fifo_common.h"
int main() {
int fifo_fd;
SensorData data;
// Set up a seed for the random number generator
srand(time(NULL));
printf("Writer: Creating FIFO at %s\n", FIFO_PATH);
// Create the FIFO (named pipe) with read/write permissions for owner and group
if (mkfifo(FIFO_PATH, 0660) == -1) {
// EEXIST is expected if the FIFO already exists, which is not an error for us.
if (errno != EEXIST) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
printf("Writer: FIFO already exists.\n");
}
printf("Writer: Opening FIFO for writing... (will block until reader opens)\n");
// Open the FIFO for writing. This will block until a reader opens the other end.
fifo_fd = open(FIFO_PATH, O_WRONLY);
if (fifo_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("Writer: FIFO opened. Starting to send data.\n");
// Main loop to generate and send data
for (int i = 0; i < 10; ++i) {
// Prepare the data packet
data.timestamp = time(NULL);
data.device_id = 101;
// Generate a random temperature between 15.0 and 25.0
data.temperature = 15.0f + (rand() / (float)RAND_MAX) * 10.0f;
printf("Writer: Writing data packet %d: Temp = %.2f C\n", i + 1, data.temperature);
// Write the entire struct to the FIFO
if (write(fifo_fd, &data, sizeof(SensorData)) == -1) {
perror("write");
close(fifo_fd);
exit(EXIT_FAILURE);
}
// Wait for 2 seconds before sending the next packet
sleep(2);
}
printf("Writer: Finished sending data. Closing FIFO.\n");
close(fifo_fd);
// The writer could optionally remove the FIFO, but typically the "server" (reader) does.
// unlink(FIFO_PATH);
return 0;
}
Code Explanation:
mkfifo(FIFO_PATH, 0660)
: This is the core call to create the FIFO. We check forEEXIST
because if the reader was run before and didn’t clean up, or if this writer is restarted, the file might already be there. We treat that as a non-fatal condition.open(FIFO_PATH, O_WRONLY)
: The writer opens the FIFO in write-only mode. Execution will pause here until thedata_reader
process opens the same FIFO for reading.write(fifo_fd, &data, sizeof(SensorData))
: Inside the loop, we write the entireSensorData
struct in a single, atomic operation. Becausesizeof(SensorData)
is much smaller thanPIPE_BUF
, this write is guaranteed not to be interleaved with other writers.close(fifo_fd)
: After sending all data, we close our end of the pipe. This will signal an end-of-file (EOF) condition to the reader.
The Reader Process: data_reader.c
This program opens the FIFO, reads the SensorData
structs, and displays them. It is also responsible for cleaning up the FIFO.
data_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include "fifo_common.h"
int main() {
int fifo_fd;
SensorData data;
ssize_t bytes_read;
char time_str[30];
// The reader assumes the writer will create the FIFO.
// A more robust implementation might create it here if it doesn't exist.
printf("Reader: Opening FIFO for reading... (will block until writer opens)\n");
// Open the FIFO for reading. This will block until a writer opens the other end.
fifo_fd = open(FIFO_PATH, O_RDONLY);
if (fifo_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("Reader: FIFO opened. Waiting for data.\n");
// Loop to read data from the FIFO
while ((bytes_read = read(fifo_fd, &data, sizeof(SensorData))) > 0) {
if (bytes_read == sizeof(SensorData)) {
// Convert timestamp to a readable string
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&data.timestamp));
printf("Reader: Received data -> [Timestamp: %s, Device ID: %d, Temperature: %.2f C]\n",
time_str, data.device_id, data.temperature);
} else {
// This could happen if the writer closes mid-write, though unlikely with our small struct
fprintf(stderr, "Reader: Received a partial record (%zd bytes), discarding.\n", bytes_read);
}
}
// read() returns 0 on EOF (writer closed the pipe), or -1 on error.
if (bytes_read == 0) {
printf("Reader: Writer has closed the FIFO. End of data.\n");
} else if (bytes_read == -1) {
perror("read");
}
printf("Reader: Closing and cleaning up FIFO.\n");
close(fifo_fd);
// The reader, acting as the server, cleans up the FIFO from the filesystem.
if (unlink(FIFO_PATH) == -1) {
perror("unlink");
}
return 0;
}
Code Explanation:
open(FIFO_PATH, O_RDONLY)
: The reader opens the FIFO in read-only mode. Like the writer, it will block here until the other process is ready.while ((bytes_read = read(...)) > 0)
: This is the standard loop for reading from a file or pipe.read()
will block until data is available. It returns the number of bytes read,0
when the writer closes its end (EOF), or-1
on an error.if (bytes_read == sizeof(SensorData))
: We perform a crucial check to ensure we read a complete data record. This guards against partial reads, although they are unlikely in this specific example.unlink(FIFO_PATH)
: After the loop terminates (when the writer closes the pipe), the reader performs the cleanup by removing the FIFO file from/tmp
.
Build, Flash, and Boot Procedures
On the Raspberry Pi 5, compilation is straightforward. We don’t need a cross-compiler since we are building and running on the same machine.
1. Compile the Programs:
Open a terminal in your ~/fifo_project directory and run the following commands:
# Compile the writer
gcc -o sensor_writer sensor_writer.c -Wall
# Compile the reader
gcc -o data_reader data_reader.c -Wall
The -Wall
flag enables all compiler warnings, which is a good habit for catching potential bugs.
2. Run the Application:
This requires two separate terminal windows, as both processes will block until the other starts.
- In Terminal 1: Start the reader process first.
cd ~/fifo_project
./data_reader
You will see the output:Reader: Opening FIFO for reading… (will block until writer opens)The cursor will just sit there, waiting. This is the blocking open() call in action.
- In Terminal 2: Now, start the writer process.
cd ~/fifo_project
./sensor_writer
3. Observe the Output:
As soon as you run sensor_writer in Terminal 2, both processes will unblock.
Terminal 2 (Writer) Output:
Writer: Creating FIFO at /tmp/sensor_fifo
Writer: FIFO already exists.
Writer: Opening FIFO for writing... (will block until reader opens)
Writer: FIFO opened. Starting to send data.
Writer: Writing data packet 1: Temp = 21.43 C
Writer: Writing data packet 2: Temp = 18.91 C
...
Writer: Finished sending data. Closing FIFO.
Terminal 1 (Reader) Output:
Reader: Opening FIFO for reading... (will block until writer opens)
Reader: FIFO opened. Waiting for data.
Reader: Received data -> [Timestamp: 2025-07-30 15:30:05, Device ID: 101, Temperature: 21.43 C]
Reader: Received data -> [Timestamp: 2025-07-30 15:30:07, Device ID: 101, Temperature: 18.91 C]
...
Reader: Writer has closed the FIFO. End of data.
Reader: Closing and cleaning up FIFO.
After the writer finishes, the reader detects the EOF, prints its final message, and terminates. If you check the /tmp
directory (ls -l /tmp/sensor_fifo
), you will see that the FIFO file has been successfully removed by the reader.
Common Mistakes & Troubleshooting
While FIFOs are powerful, their blocking nature and filesystem presence can lead to several common issues. Understanding these pitfalls is key to building robust systems.
Exercises
- Modify for Bidirectional Communication:
- Objective: Create a system where the reader process can send an acknowledgment back to the writer.
- Guidance:
- Create a second FIFO (e.g.,
/tmp/ack_fifo
). - Modify the
sensor_writer
to open the acknowledgment FIFO for reading after it sends a data packet. - Modify the
data_reader
to write a simple acknowledgment string (e.g., “OK”) to the acknowledgment FIFO after successfully processing a packet. - The writer should read and print this acknowledgment before sending the next packet.
- Create a second FIFO (e.g.,
- Implement Non-Blocking Read:
- Objective: Change the reader to be non-blocking, allowing it to perform other tasks while waiting for data.
- Guidance:
- Modify
data_reader
to open the FIFO with theO_NONBLOCK
flag (open(FIFO_PATH, O_RDONLY | O_NONBLOCK)
). - In the main
while
loop, ifread()
returns-1
anderrno
isEAGAIN
, it means no data is available. Instead of blocking, print a “Waiting for data…” message andsleep()
for one second before trying again. - Observe how the reader’s behavior changes compared to the original blocking version.
- Modify
- Multiple Writers, Single Reader:
- Objective: Demonstrate the atomicity of FIFO writes.
- Guidance:
- Modify
sensor_writer
to accept a device ID from the command line (argv
). - Run the
data_reader
process in one terminal. - In two other terminals, start two separate instances of
sensor_writer
simultaneously, giving each a unique ID (e.g.,./sensor_writer 101
and./sensor_writer 202
). - Observe the output of the reader. You should see a clean, interleaved stream of data packets from both writers, with no corrupted or mixed records, thanks to atomic writes.
- Modify
- Error Handling and Robustness:
- Objective: Make the reader more resilient to writer failures.
- Guidance:
- Run the
data_reader
. - Run the
sensor_writer
. - While the writer is sending data, kill it prematurely using
Ctrl+C
in its terminal. - Observe what happens in the reader. The
read()
call should return0
(EOF), and the reader should shut down cleanly. - Now, modify the reader’s logic: instead of exiting when the writer closes, it should clean up the old FIFO and then try to re-open a new one in a loop, effectively waiting for a new writer to start.
- Run the
- Integrate a Real Sensor:
- Objective: Replace the simulated data with real hardware data on the Raspberry Pi 5.
- Guidance:
- Connect a simple sensor like a DS18B20 temperature sensor or a BMP280 pressure/temperature sensor to the Raspberry Pi’s GPIO pins.
- Install the necessary libraries to read from the sensor (e.g., using kernel modules for 1-Wire or a user-space library for I2C/SPI).
- Modify
sensor_writer.c
to read the actual temperature from the hardware instead of generating a random number. - Run the
sensor_writer
anddata_reader
to see a live stream of real-world sensor data being passed between processes.
Summary
This chapter provided a comprehensive exploration of named pipes (FIFOs) as a powerful IPC mechanism in embedded Linux. We have moved from theory to practical application, solidifying a critical skill for any embedded developer.
- FIFOs are Filesystem-Based IPC: Unlike unnamed pipes, FIFOs have a name and exist as a special file (
p
type) in the filesystem, allowing unrelated processes to communicate. - Creation and Cleanup: The
mkfifo()
system call creates a FIFO, and theunlink()
system call is required to remove it. Proper cleanup is essential to avoid leaving stale files. - Blocking I/O for Synchronization: By default,
open()
calls on a FIFO will block until both a reader and a writer are present, providing a simple and effective synchronization mechanism. - Non-Blocking I/O for Flexibility: The
O_NONBLOCK
flag allows processes to open and interact with FIFOs without blocking, which is useful for polling and integration into event loops. - Atomic and Ordered Data Flow: FIFOs guarantee that data is read in the same order it was written. Writes smaller than
PIPE_BUF
are atomic, preventing data corruption from multiple writers. - Practical Application: We successfully implemented a multi-process sensor monitoring application on the Raspberry Pi 5, demonstrating how to pass structured data between a writer and a reader process.
Mastering FIFOs gives you a simple yet robust tool for designing modular, multi-process embedded systems, a common and powerful paradigm in modern device development.
Further Reading
- The Linux Programming Interface by Michael Kerrisk – Chapter 44, “Pipes and FIFOs,” provides an exhaustive and authoritative reference on this topic.
- Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago – Chapter 15, “Interprocess Communication,” covers FIFOs in the broader context of classic UNIX IPC.
fifo(7)
Linux Programmer’s Manual Page – The official man page (man 7 fifo
) offers a concise technical overview of FIFO behavior and characteristics. https://man7.org/linux/man-pages/man7/fifo.7.htmlmkfifo(3)
Linux Programmer’s Manual Page – The man page for themkfifo()
system call (man 3 mkfifo
) details its arguments, return values, and potential errors.- POSIX.1-2017 Standard, Section 4.4, FIFOs – The official IEEE standard defining the behavior of FIFOs, for those who need the most precise technical specification. Available from the The Open Group’s website. https://en.wikipedia.org/wiki/List_of_POSIX_commands
- Raspberry Pi Documentation – While not specific to FIFOs, the official hardware and software documentation is essential for any low-level development on the platform. https://www.raspberrypi.com/documentation/