Sitemap

Inter Process Communication : Pipes

6 min readFeb 13, 2025

--

Inter process communication is an important mechanism of operating system that enables process to communicate with each other to exchange data and coordinate their actions.

When a process needs to communicate with another process, it can be via various methods such as pipes, shared memory, message queues, semaphores, sockets etc.

Let’s look at how to establish inter process communication using pipes in this article.

Each process has three Input-Output streams by default : stdin, stdout and stderr, represented by a file descriptor. File descriptor table of a process has stdin = 0, stdout = 1, and stderr = 2 set by default.

Press enter or click to view image in full size
default state of a process : https://www.rozmichelle.com/pipes-forks-dups/

What are pipes ?
Pipe is a buffered stream, represented by two file descriptors, one to write data into and the other descriptor to read data from. Together, the two files act as a single unit i.e a pipe and form communication path between processes.

Pipe is unidirectional in nature, where the data flows from write end to read end.

Pipes can be created using the syscall pipe(). It finds the next two file descriptors available to form the input and output end of the pipe.

Above program results in creation of a pipe with fds[0] and fds[1] assigned the immediately next available file descriptors as the read and write end of the pipe.

Press enter or click to view image in full size
process with a pipe(fds) call : https://www.rozmichelle.com/pipes-forks-dups/

Note : The pipe() syscall creates pipe which are temporary and exist only as long as the process that created them is running.

Introducing pipes in process is useful in various situations

  • Pass input/command from one process to another.
  • Form chain of commands , ex: ls -l | grep “.txt”
  • Combine output of multiple processes.

Inter process communication using Unnamed Pipe
Communication between a parent and child process formed via fork() is easy via shared and unnamed pipe. A pipe created before a child’s creation is shared by both parent and child process. In each of these processes, the input and output ends of the shared pipe can be manipulated to communicate data between the two processes.

When a parent process with an existing pipe, calls a fork, the view of pipe in each process (parent and child) is as follows

Press enter or click to view image in full size
pipe state in parent and child process : https://www.rozmichelle.com/pipes-forks-dups/

Parent and child process both share a single pipe but have different view of it in their respective processes.

Get Priii’s stories in your inbox

Join Medium for free to get updates from this writer.

One way communication between parent and child
To pass data from parent to child process via shared pipe,

  • Create a pipe in parent process, followed by a fork()
  • At parent process, close the read end of shared pipe, here fds[0].
  • At child process, close the write end of shared pipe, here fds[1].
  • At child, redirect the read end of shared pipe, here fds[0] to stdin of child.

This forms a channel such that parent process can write to the shared pipe and the child can receive this data at it’s stdin.

Press enter or click to view image in full size
communicate between parent to child via shared pipe : https://www.rozmichelle.com/pipes-forks-dups/
#include <iostream>
#include <unistd.h>

using namespace std;

int main(int argc, char *argv[])
{
pid_t pid;
int fds[2]; //fds for pipe
if (pipe(fds) < 0)
{
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();

if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
char data_from_parent[30];
// child process
close(fds[1]); // close write end of pipe
// dup2(fds1[0],STDIN_FILENO); //redirecting pipe's read end to STDIN doesn't work consistently when paired with execl
read(fds[0], data_from_parent, sizeof(data_from_parent)); // reading data, instead of redirecting to STDIN
close(fds[0]);
cout << "Data received at child from parent is " << data_from_parent << "\n";
}
else
{
// parent process
char in;
close(fds[0]);
cout << "Enter data to be sent from parent to child\n";
do
{
in = getchar();
write(fds[1], &in, sizeof(in));
} while (in != '\n');
close(fds[1]); // close write end of pipe to signify end of input from parent process
wait(NULL);
cout << "Exiting parent \n";
}
}

Note : If a process tries to read from an empty pipe, it will be temporarily suspended until data is written to the pipe.

Two way communication between parent and child
As a single pipe is uni directional, we need a set of pipes to communicate bi-directionally, i.e send data from parent to child and vice-versa.

  • Create two pipes in parent process before calling fork().
  • For pipe-1, to communicate from parent to child,
    - At parent process, close the read end of pipe- 1, here fds1[0].
    - At child process, close the write end of pipe-1, here fds1[1].
    - At the child, redirect the read end of pipe-1, fds1[0] to stdin of child.
  • For pipe-2, to communicate from child to parent,
    - At child process, close the read end of pipe - 2, here fds2[0].
    - At parent process, close the write end of pipe-2, here fds2[1].
    - At parent, redirect the read end of pipe-2, fds2[0] to stdin of parent.

This forms two channels for bi-directional communication to/fro parent and child.

#include <iostream>
#include <unistd.h>

using namespace std;

int main(int argc, char *argv[])
{
pid_t pid;
int fds1[2]; //pipe-1; parent to child
int fds2[2];//pipe-2; child to parent
string command;

if (argc < 2)
{
cout << "Enter command to be passed from parent to child\n";
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; i++)
{
command = command + argv[i] + " ";
}

if (pipe(fds1) < 0)
{
perror("pipe1");
exit(EXIT_FAILURE);
}
if (pipe(fds2) < 0)
{
perror("pipe2");
exit(EXIT_FAILURE);
}

pid = fork();

if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
//child process
char c;
char command[30];
close(fds1[1]); //close write end of pipe-1
close(fds2[0]); //close read end of pipe-2
// dup2(fds1[0],STDIN_FILENO); //redirecting fds1 to STDIN doesn't work consistently when paired with execl
read(fds1[0], command, sizeof(command)); // reading data, instead of redirecting to STDIN
close(fds1[0]);
cout << "Command received at child is " << command << "\n";
dup2(fds2[1], STDOUT_FILENO); //redirect stdout of child to pipe-2's write end
close(fds2[1]);
execl("/bin/sh", "sh", "-c", command, NULL);
}
else
{
//parent process
char buf[1024];
close(fds1[0]); //close the read end of piep-1
close(fds2[1]); //close the write end of pipe-2
write(fds1[1], &command, sizeof(command)); //write command to write end of pipe-1
close(fds1[1]);
wait(NULL);
read(fds2[0], buf, sizeof(buf));//read output of command executed at child from pipe-2's write end
cout << "\nSTDOUT of child at parent \n"
<< buf << "\n";
cout << "Exiting parent \n";
}
}

Named pipes
Inter process communication between child and parent processes is easy as they are aware of a shared pipe’s existence between them due to resource inheritance upon fork.
In case of two completely independent processes, named pipes are required for inter process communication.

Named pipes are special, persistent files in the filesystem, which can be listed with “ls” unlike the unnamed pipes in that exists only during runtime.

mkfifo() system call is used to create named pipes. They are bi-directional in nature i.e a single pipe can be used to both read and write simultaneously.

#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

int main(int argc, char *argv[])
{
int fd;
char arr[80], arr1[80];
errno = 0;
// FIFO file path
char *myfifo = "/tmp/namedPipe";
// Creating the named file(FIFO)
// mkfifo(<pathname>, <permission>)
int ret = mkfifo(myfifo, 0666);
if (ret < 0)
{
cout << "Error creating file " << errno << "\n";
}
return 0;
}

The named pipe so created by mkfifo() can be referenced by multiple independent processes simultaneously to write and read data from, like a regular file.

Some use cases of pipe

  • Delegating task via popen() : popen() is a function provided by libc that creates a pipe, forks a process and invokes the shell in the new process. This enables creation of background processes and delegating tasks by a process and communicate with it based on the type argument(read or write) of popen() [0]
  • Command chaining : pipes are used to connect the output of one command to the input of another, enabling complex operations to be executed by combining simple commands.

--

--

Priii
Priii

Written by Priii

Product Security Engineer | Documenting and sharing my learnings here!