IPC - pipes in Unix
Interprocess Communication with pipes

Introduction
Section titled โIntroductionโIPC stands for Interprocess Communication. It is a way to allow communication between processes. There are many IPC mechanisms:
- Shared files
- Shared memory (with semaphores)
- Pipes (named and unnamed)
- Message queues
- Sockets
- Signals
A pipe connects the standard output of a process to the standard input of another. It is a unidirectional communication channel between:
- producer: writer process
- consumer: reader process
It synchronizes producer and consumer processes.
A process thatโs reading from a pipe freezes if the pipe is empty and keeps waiting for data.
A process thatโs writing to a pipe freezes if the pipe is full, waiting for free space on which to write.
Processes work on pipes as if they were files: they use the same primitives shown in Manage Files in Unix article.
Communication
Section titled โCommunicationโFrom now on, fd variable will be assumed to be a two-element array storing the pipeโs file descriptors:
void main() { int fd[2];}The array above holds:
- reader process:
fd[0] - writer process:
fd[1]
A process usually opens a pipe, then create a child process which has the same file descriptors of the parent and send data to (or wait data from) the child process.
Since both parent and child processes could read and write, each process close a pipeโs end: child close fd[0] and parent close fd[1] or vice-versa.
A huge limit of pipes is that allow communication only between processes that have at least a common relative.
Please Note: another common use is when a (parent) process opens a pipe to make two children processes communicate to each other.
Operations over a pipe
Section titled โOperations over a pipeโCreate a pipe
Section titled โCreate a pipeโIf the data written by the writer process have been read, the writing on the pipe restart from the beginning: the memory is managed like a circular buffer.
#include <stdio.h>#include <unistd.h>
void main() { int fd[2];
if(pipe(fd) == 0) printf("Pipe created successfully\n");}Close a pipe
Section titled โClose a pipeโclose() system call closes one end of the pipe.
After fork() is called, the parent and child processes must both close the pipeโs end that they will not intend to use.
Closing a pipeโs end does not trigger any actions until thereโs at least another process that has the same pipeโs end opened.
If a process closes the last reference of a pipeโs end, there could be the following scenarios:
- reading from a closed pipe:
read()returns0 - writing on a closed pipe:
write()triggers writer process termination (sendsSIGPIPEsignal)
#include <stdio.h>#include <unistd.h>
void main() { int fd[2]; if(pipe(fd) == 0) printf("Pipe created successfully\n");
// close reader file descriptor if(close(fd[0]) == -1) { perror("reader file descriptor close"); exit(EXIT_FAILURE); }
// close writer file descriptor if(close(pipe_p1p2[1]) == -1) { perror("writer file descriptor close"); exit(EXIT_FAILURE); }}Read/Write on a pipe
Section titled โRead/Write on a pipeโBecause of synchronization between producer and consumer processes:
read()blocks the reader if the pipe is emptywrite()blocks the writer if thereโs no space left in the pipe
Moreover, thereโs no I/O pointer: read and write are FIFO only (First In First Out).
Pipes VS Files
Section titled โPipes VS FilesโDifferences between pipes and files are measured in terms of:
- Lifetime
- pipe has process lifetime
- file has file system lifetime
- Data Management
- Dimension
A pipe uses a FIFO management of data, while a file can be accessed at each position.
The pipeโs dimension is fixed at 4 KiB (4096 bytes).
Redirect stdout and stdin
Section titled โRedirect stdout and stdinโRedirecting standard output is useful when a process needs to communicate its output to another process through a pipe.
On the other side, redirecting standard input is useful when a process needs to get data from another process through a pipe.
To implement redirection youโll need to use dup() system call.
It creates a copy of the file descriptor passed as parameter using the lowest-numbered unused file descriptor for the new descriptor.
Given the following scenario:
- Processes P1 and P2 (both child of P0) communicate through a pipe
- P1 is the writer process
- P2 is the reader process
Given the pipe:
void main() { int pipe_fd[2]; if (pipe(pipe_fd) != 0) exit(1);}If P1 wants to redirect its stdout to the pipe connected to P2, it will have to:
- Close standard output:
close(1)orclose(STDOUT_FILENO) - Duplicate writer file descriptor:
the lowest-numbered unused file descriptor is nowstdout
dup(pipe_fd[1]) - Close writer file descriptor:
close(pipe_fd[1])
While if P2 wants to redirect its stdin to the pipe connected to P1, it will have to:
- Close standard input:
close(0)orclose(STDIN_FILENO) - Duplicate reader file descriptor:
the lowest-numbered unused file descriptor is nowstdin
dup(pipe_fd[0]) - close reader file descriptor:
close(pipe_fd[0])
In Two Commands Piping example you can see it in action: stdout and stdin redirection is used to implement piping between processes which have a relative in common.
FIFO are named pipes. They have the same semantic of pipes:
read()blocks the reader if the FIFO is emptywrite()blocks the writer if thereโs no space left in the FIFO- reading from a closed pipe returns
0 - writing on a closed pipe triggers
SIGPIPEsignal (which terminate the writer process by default)
Moreover, open() blocks the process waiting for the opening of both sides (write and read).
The biggest difference between pipes and FIFO is that since a FIFO have a name on the file system, they can be used by processes that donโt have relatives in common. Both FIFO and pipes require however that processes run on the same hardware machine.
FIFO Operations
Section titled โFIFO OperationsโTo create a FIFO you need to use mkfifo() system call, passing the name in the file system and the permission bits.
Once created, you need to open() the FIFO before using it.
To close a FIFO you need to use close() passing the file descriptor as parameter.
To destroy a FIFO you need to use unlink passing its file systemโs name.
#include <sys/types.h>#include <sys/stat.h>
// ...
int mode_t mode;const char *pathname;
int return_value = mkfifo(pathname, mode);
int fd = open(pathname, O_RDONLY);// int fd = open(pathname, O_WRONLY);
// ...
close(fd);unlink(pathname);Examples
Section titled โExamplesโPipe Examples
Section titled โPipe ExamplesโNote that stdlib.h defines:
EXIT_FAILURE = 1: Failing exit status.EXIT_SUCCESS = 0: Successful exit status.
While unistd.h library defines standard file descriptorsโ numbers:
STDIN_FILENO=0: Standard inputSTDOUT_FILENO=1: Standard outputSTDERR_FILENO=2: Standard error output
Print argument on stdout
Section titled โPrint argument on stdoutโThe snippet below prints on stdout the passed argument.
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/wait.h>
enum {BUF_SIZE = 4096};
/** * Print on the standard output the string passed as first argument * * Parent writes the string retrieved from the command line argument and then waits for the child to terminate. * The child process reads from the pipe and prints the read bytes to the console. * Both child and parent close the one end of the pipe because the child process inherits the parent's file descriptors. * After the sent bytes are read, the read end of the pipe is closed by the child, terminating with an exit call. */int main(int argc, char *argv[]) { int pipe_fd[2]; // store pipe's descriptors char buf[BUF_SIZE]; // allocate a buffer ssize_t num_read; // number of characters to read
// if not string is passed, terminate the program if (argc != 2) { fprintf(stderr, "Usage: %s string\n", argv[0]); exit(EXIT_FAILURE); }
// create the pipe, terminate the program if an error occurs if (pipe(pipe_fd) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
// fork process into child and parent processes switch (fork()) { case -1: { perror("fork"); // error while forking exit(EXIT_FAILURE); } case 0: { // child process
// child closes writer file descriptor if (close(pipe_fd[1]) == -1) { perror("close - parent"); exit(EXIT_FAILURE); }
// exhausts the data in the pipe while(1) { // read from the pipe num_read = read(pipe_fd[0], buf, BUF_SIZE);
if(num_read == -1) { perror("read"); exit(EXIT_FAILURE); }
if (num_read == 0) break;
// write on the standard output if(write(STDOUT_FILENO, buf, num_read) != num_read) { perror("write - partial/failed write"); exit(EXIT_FAILURE); } }
// write the new line character on the standard output write(STDOUT_FILENO, "\n", 1);
// child closes reader file descriptor if (close(pipe_fd[0]) == -1) { perror("close"); exit(EXIT_FAILURE); }
// terminate program execution with the low-order 8 bits of STATUS. _exit(EXIT_SUCCESS); } default: { // parent process
// parent closes reader file descriptor if (close(pipe_fd[0]) == -1) { perror("close - parent"); exit(EXIT_FAILURE); }
// write on the pipe the first argument passed if (write(pipe_fd[1], argv[1], strlen(argv[1])) != strlen(argv[1])) { perror("write - partial/failed write"); exit(EXIT_FAILURE); }
// parent closes writer file descriptor if (close(pipe_fd[1]) == -1) { perror("close"); exit(EXIT_FAILURE); }
wait(NULL); exit(EXIT_SUCCESS); } }}Two Commands Piping
Section titled โTwo Commands PipingโCreate piping.c as follows:
#include <stdio.h> // ISO C99 Standard: 7.19 Input/output#include <stdlib.h> // ISO C99 Standard: 7.20 General utilities#include <string.h> // ISO C99 Standard: 7.21 String handling#include <unistd.h> // POSIX Standard: 2.10 Symbolic Constants#include <sys/wait.h> // POSIX Standard: 3.2.1 Wait for Process Termination
#define SEPARATOR "@" // must be different from | (pipe sign)
void join(char *cmd1[], char *cmd2[]) { int status, p1_pid, p2_pid, pipe_p1p2[2];
// CREATE PIPE if (pipe(pipe_p1p2) != 0) { fprintf(stderr, "error during pipe creation\n"); exit(EXIT_FAILURE); }
// FORK 1st CHILD p1_pid = fork();
if(p1_pid < 0) { perror("1st child fork"); exit(EXIT_FAILURE); } else if (p1_pid == 0) { // close reader file descriptor if (close(pipe_p1p2[0]) == -1) { perror("reader file descriptor close"); exit(EXIT_FAILURE); }
// REDIRECT STDOUT
// close stdout // close(1); if (close(STDOUT_FILENO) == -1) { perror("stdout file descriptor close"); exit(EXIT_FAILURE); }
// first available file descriptor number is 1 // dup() creates a copy of the file descriptor // passed as parameter using the lowest-numbered // unused file descriptor for the new descriptor, which is stdout // output on the pipe if(dup(pipe_p1p2[1]) == -1) { perror("duplicate stdout"); exit(EXIT_FAILURE); }
// close writer file descriptor if (close(pipe_p1p2[1]) == -1) { perror("write file descriptor close"); exit(EXIT_FAILURE); }
// EXECUTE THE 1st COMMAND execvp(cmd1[0], cmd1);
// if child process arrives here // it means that an error occurred perror("1st command error"); exit(EXIT_FAILURE); }
// FORK 2nd CHILD p2_pid = fork();
if(p2_pid < 0) { perror("2nd child fork"); exit(EXIT_FAILURE); } else if (p2_pid == 0) { // close writer file descriptor if (close(pipe_p1p2[1]) == -1) { perror("writer file descriptor close"); exit(EXIT_FAILURE); }
// REDIRECT STDIN
// close stdin // close(0); if (close(STDIN_FILENO) == -1) { perror("stdin file descriptor close"); exit(EXIT_FAILURE); }
// first available file descriptor number is 0 // dup() creates a copy of the file descriptor // passed as parameter using the lowest-numbered // unused file descriptor for the new descriptor, which is stdin // input from pipe if(dup(pipe_p1p2[0]) == -1) { perror("duplicate stdin"); exit(EXIT_FAILURE); }
// close reader file descriptor if (close(pipe_p1p2[0]) == -1) { perror("reader file descriptor close"); exit(EXIT_FAILURE); }
// EXECUTE THE 2nd COMMAND execvp(cmd2[0], cmd2);
// if child process arrives here // it means that an error occurred perror("2nd command error"); exit(EXIT_FAILURE); }
// PARENT PROCESS // close the non-necessary file descriptors close(pipe_p1p2[0]); close(pipe_p1p2[1]);
// wait for children to terminate wait(&status); wait(&status);}
/** * Provide two different commands separated by SEPARATOR character. * It's not possible to use | (pipe sign) in place of SEPARATOR * because it will be interpreted by the shell as a pipe. */int main(int argc, char **argv) { int i, j; char *cmd1[10], *cmd2[10];
if (argc <= 3) { fprintf(stderr, "usage: <cmd_1> SEPARATOR <cmd_2>\n"); exit(EXIT_FAILURE); }
// 1st COMMAND for (i = 1; i < argc && strcmp(argv[i], SEPARATOR); i++) cmd1[i - 1] = argv[i];
// argv terminated by 0 cmd1[i - 1] = (char *) 0; i++;
// 2nd COMMAND for (j = 1; i < argc; i++, j++) cmd2[j - 1] = argv[i];
// TERMINATOR cmd2[j - 1] = (char *) 0;
join(cmd1, cmd2);
return 0;}Compile into piping file:
gcc -o piping piping.cPipe the result of ls -lha to wc and get the result on standard output:
./piping ls -lha @ wc 9 74 427FIFO Example
Section titled โFIFO ExampleโLetโs simulate the communication between a client and a server with a couple of separated programs.
They both include messages-exchange.h header file to avoid duplicating some common code.
Note that the FIFO name must be passed as first parameter.
Client code also handles Ctrl + C signal in order to stop the server in the right way.
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <string.h>#include <signal.h>#include <sys/stat.h>
#define MSG_SIZE 50#define PERM 0744#define MSG_END "end"client.c code is:
#include "messages-exchange.h"
int fd;
void client_close();void handler(int);
int main(int argc, char **argv) { if (argc != 2) { printf("Error: FIFO name not specified.\n"); exit(1); }
// when Ctrl + C signal is triggered, send END signal(SIGINT, handler);
char msg[MSG_SIZE]; const char *pathname = argv[1];
if ((mkfifo(pathname, PERM)) < 0) { perror("fifo already exists, proceed"); }
if ((fd = open(pathname, O_WRONLY)) < 0) { perror("fifo error"); exit(1); }
printf("Client: insert messages for the server (prompt \"%s\" to exit)\n", MSG_END);
while (1) { scanf("%s", msg); if ((write(fd, msg, strlen(msg) + 1)) < 0) { perror("writing error"); exit(2); } if (strcmp(msg, MSG_END) == 0) { client_close(); } }}
void handler(int signal) { printf("\tReceived signal: %d\n", signal);
if ((write(fd, MSG_END, strlen(MSG_END) + 1)) < 0) { perror("MSG_END writing error"); exit(2); }
client_close();}
void client_close() { printf("Client ends\n"); close(fd); exit(0);}
// gcc -o client-main client.c && ./client-main test-fifoserver.c code is:
#include <stdio.h>
int main(int argc, char **argv) { if (argc != 2) { printf("FIFO name not specified"); exit(1); }
int fd; char buffer[MSG_SIZE]; const char *pathname = argv[1];
if ((mkfifo(pathname, PERM)) < 0) { perror("fifo already exists, proceed"); }
if ((fd = open(pathname, O_RDONLY)) < 0) { perror("error opening fifo"); exit(1); }
printf("I am the server.\n");
while (1) { if ((read(fd, buffer, MSG_SIZE)) < 0) { perror("writing error"); exit(2); }
printf("> server: %s\n", buffer);
if (strcmp(buffer, MSG_END) == 0) { printf("server ends\n"); close(fd); unlink(pathname); exit(0); } }
return 0;}
// gcc -o server-main server.c && ./server-main test-fifoStart client and server programs in different shells:
# Terminal 1gcc -o client-main client.c./client-main test-fifoClient: insert messages for the server (prompt "end" to exit)Insert messages into FIFO:
HelloCatPietro# Terminal 2gcc -o server-main server.c./server-main test-fifoI am the server.Start reading from FIFO looking for messages:
> server: Hello> server: Cat> server: PietroConclusion
Section titled โConclusionโUseful links: