Applying C - Pipes
Written by Harry Fairhead   
Monday, 03 January 2022
Article Index
Applying C - Pipes
Anonymous Pipes

You can use files to communicate between two processes but pipes are a much better solution. Find out how they work in this  extract from my  book on using C in an IoT context.

Now available as a paperback or ebook from Amazon.

Applying C For The IoT With Linux

  1. C,IoT, POSIX & LINUX
  2. Kernel Mode, User Mode & Syscall
  3. Execution, Permissions & Systemd
    Extract Running Programs With Systemd
  4. Signals & Exceptions
    Extract  Signals
  5. Integer Arithmetic
    Extract: Basic Arithmetic As Bit Operations 
  6. Fixed Point
    Extract: Simple Fixed Point Arithmetic
  7. Floating Point
  8. File Descriptors
    Extract: Simple File Descriptors 
    Extract: Pipes 
  9. The Pseudo-File System
    Extract: The Pseudo File System 
  10. Graphics
    Extract: framebuffer 
  11. Sockets
    Extract: Sockets The Client 
  12. Extract: Socket Server ***NEW
  13. Threading
    Extract:  Condition Variables
    Extract:  Deadline Scheduling
  14. Cores Atomics & Memory Management
  15. Interupts & Polling
  16. Assembler
    Extract: Assembler 

Also see the companion book: Fundamental C

<ASIN:1871962609>

<ASIN:1871962463>

<ASIN:1871962617>

<ASIN:1871962455>

In chapter but not in this extract:

  • Files
  • File Descriptors
  • A Random Access Example
  • Descriptors and Streams
  • fcntl
  • Sharing Files – Locking

Pipes

The previous example (in sharing files) uses a file as a way of sending data between two processes. In most cases a better way to do the same job is to use a named pipe. This is also another example of how a file descriptor can apply to things other than standard files.

A named pipe, or FIFO, behaves like a file that can be opened, written to, and read by any process that cares to open it. The reason is it called a FIFO is that it operates like a First In First Out stack. The writing program can write bytes to it and they are stored until a reading program starts to read in the order that the bytes were written. You can think of the writing program pushing bytes into the pipe and the reading program taking them out at the other end.

There is a subtlety, however. When a process writes to a named pipe the call blocks until a process has opened the pipe for read. Notice that the process doesn't actually have to perform the read to unblock the write call, it is enough that it is ready to read to move the pipe on. In the same way, an attempt to read from a pipe will block until there is some data to read.

To create a named pipe you first use the mkfifo function:

mkfifo(filename,permissions);

where filename is the name of the pipe and it is a full path including directories, and permission is the usual file access permissions. Notice that the mkfifo really does behave as if it is inserting a file into the filing system in that the directories have to exist, the file has to not exist as a standard file, and the process has to have permission to create a file.

Once the named pipe has been created it can be used just like a standard file - it can be opened and you can use read and write to work with it. However, you can only open a named pipe for read or for write. If you want two-way communication you need to use two named pipes. It is possible to use C file handling, i.e. streams, with a named pipe, but as these are buffered it is easier to use file descriptors. It is also worth knowing that there can be multiple readers and writers of a named pipe.

You can see the file that corresponds to the pipe in the directory list and it is indicated as a pipe file by a p next to its permissions. The pipe file remains available until you explicitly remove it. Notice that although the named pipe looks like a standard file, the operating system makes use of memory to store the data passing through it, hence it is faster.

A Named Pipe Example

For example, a named pipe version of the previous file writer is:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> 
#include <fcntl.h> 
#include <sys/stat.h> 
int main(int argc, char** argv) {
    int value = 0x55555555;
    mkfifo("/tmp/myfifo", 0666);
    int fd = open("/tmp/myfifo", O_WRONLY);
    for (;;) {
        value = ~value;
        write(fd, &value, 4);
        printf("W%X", value);
        fflush(stdout);
        sleep(1);
    }
    return (EXIT_SUCCESS);
}

Notice the file has to be opened for writing. If you run this program you will see nothing printed until the reading program opens the pipe:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h> 
#include <unistd.h> 
#include <fcntl.h>
int main(int argc, char** argv) {
    int value;
    int fd = open("/tmp/myfifo", O_RDONLY);
    for (;;) {
        read(fd, &value, 4);
        if ((value != 0x55555555) &&
(value != ~0x55555555)){ printf("%x\n", value);
} printf("R%X", value); fflush(stdout); sleep(1); } return (EXIT_SUCCESS); }

Notice that the reader doesn't have to create the named pipe using mkfifo as the pipe is available to other processes as soon as it is created and remains available until it is explicitly deleted. Things go wrong if the pipe file hasn't been added to the file system as the reading program will then try to treat the file as a standard file. In most cases it is a good idea to use mkfifo, even if you know the pipe file already exists.

The final subtlety is that the writing process will block until a reader connects, but after this, if the reader closes the pipe or terminates, the writer will not block but fail with a runtime error. In fact, a SIGPIPE signal is generated and you can handle it if you want the writer to detect the closing of the pipe. That is, if you want to handle the closing of the pipe by the reader you need to change the writer's main program to:

int main(int argc, char** argv) {
    int value = 0x55555555;
    
    struct sigaction psa={0};
    psa.sa_handler = signalHandler;
    sigaction(SIGPIPE, &psa, NULL);
    
    mkfifo("/tmp/myfifo", 0666);
    int fd = open("/tmp/myfifo", O_WRONLY);
    for (;;) {
        value = ~value;
        if(write(fd, &value, 4)<0){
            close(fd);
            fd = open("/tmp/myfifo", O_WRONLY);            
        }
        printf("W%X", value);
        fflush(stdout);
        sleep(1);
    }
    return (EXIT_SUCCESS);
}

The handling of the signal is standard, see Chapter 4, but notice that when the handler returns you have to check that the write returned an error to close and re-open the pipe. This way the pipe is ready to be connected to by another reader. A reader can detect a pipe closed by the writer by testing for zero bytes returned by read - which normally blocks until data is available.

Notice that you can open named pipes in non-blocking mode, simply use O_NONBLOCK in the open function. In this case both read and write return at once and they return an error if the other end of the pipe isn't open.

All of this works just as well with threads as with separate processes. You could easily convert the reader and writer code into a single process with two threads.

You might be wondering why no locks are needed when using a named pipe as they were in the case of a real file? The answer is that writing to a pipe is an atomic operation, that is it cannot be interrupted, as long as the amount of data is less than PIPE_BUF defined in linux/limits.h (currently 4096 bytes). If you keep writes to less than this size there is no need to lock.

This extract is from my  book on using C in an IoT context.This extract is from my  book on using C in an IoT context.



Last Updated ( Monday, 03 January 2022 )