How to use the pipe() system call for inter-process communication

The pipe() system call is used for one-way communication between related processes, such as parent and child. Pipes cannot communicate with more than one process and are accessed like ordinary files. The system manages pipes as a FIFO queue.

Communication conditions

The pipe() system call allows inter-process communication with the following conditions:

  • Process A keeps the write end open and closes the read end of the pipe. It can only write to process B.

  • Process B keeps the read end open and closes the write end of the pipe. It can only read whatever is written by process A in the pipe.

  • If the process tries to read something when the pipe is empty, it is temporarily suspended until something is written in the pipe.

Working

Several commands are used to allow inter-process communication using pipes. All of the required commands are explained below:

Creating a pipe

The pipe() system call creates a pipe with two file descriptors: one for writing fd[1], and the other for reading fd[0].

The following command is used:

#include<unistd.h>
int pipe(int pipefd[2]);

Zero is returned by the pipe() system call. In case of failure, -1 is returned.

Note: Pipes behave like ordinary files, exept that they don't require an open() system call. The pipe() system call automatically opens the reading and writing file descriptors of a pipe.

Writing

The write() system call is used to write content to the pipe using the file descriptor pipefd[1].

The syntax of the write() method is as follows:

#include<unistd.h>
ssize_t write(int pipefd, void* buff, size_t s);

The system call has three arguments:

  • The file descriptor of the write end of the pipe, pipefd, is returned by the pipe() system call.

  • A buffer, buff, contains data that needs to be written to the pipe end.

  • The buffer can be dynamic or static in size, and the size is sent as a parameter, s.

On successful execution of the write() system call, the number of bytes written is returned. In case of error, -1 is returned.

Reading

The read() system call is used to write content to the pipe using the file descriptor pipefd[0].

The syntax of the read() method is as follows:

#include<unistd.h>
ssize_t returnsize = read(int pipefd, void* buff, size_t s);

The system call has three arguments:

  • The file descriptor of the read end of the pipe, pipefd, is returned by the pipe() system call.

  • A buffer, buff, is used to store the data read from the pipe.

  • The number of bytes that needs to be read from the pipe is sent as a parameter, s.

On successful execution of the read() system call, the number of bytes written is returned. In case of error, -1 is returned.

Closing the file descriptors

The pipe() system call automatically opens two file descriptors for reading and writing. Both the open file descriptors must be closed at the end of the process to execute the program successfully.

The following command is used to close the file descriptors:

#include<unistd.h>
int close(int pipefd);

Zero is returned on success by the close() system call. In case of failure, -1 is returned.

Sample program

A sample program below depicts the working of pipe() for one-way inter-process communication. The fork() system call is used to create a child process, and a pipe is used for communication between the parent and child process.

The fork() system call is executed after executing the pipe() system call. This allows both processes to share the file descriptors of the pipe.

#include<stdio.h>
#include<unistd.h>
#include<iostream>
using namespace std;
int main() {
int pipefd[2];
char msg1[20] = "Message 1";
char msg2[20] = "Message 2";
char readmsg[20] = "";
int returnstatus = pipe(pipefd);
if (returnstatus == -1)
{
cout<<"Pipe cannot be created\n";
return 1;
}
int pid = fork();
if (pid < 0)
{
cout<<"Unsuccessful fork command\n";
return 1;
}
if (pid == 0)
{ //child process
close(pipefd[1]);
read(pipefd[0], readmsg, 20);
cout<<"Child read: "<< readmsg<<endl;
read(pipefd[0], readmsg, 20);
cout<<"Child read: "<< readmsg<<endl;
close(pipefd[0]);
}
else
{ //parent process
close(pipefd[0]);
cout<<"Parent writing for the first time \n";
write(pipefd[1], msg1, 20);
cout<<"Parent writing for the second time \n";
write(pipefd[1], msg2, 20);
close(pipefd[1]);
}
return 0;
}

Explanation

A simple code has been written to depict the working of pipes between the parent and child process.

  • Lines 12–18: The pipe() system call is used to create a pipe. The file descriptors of the pipe are shared by both parent and child processes. An error message is displayed if the call fails.

  • Lines 20–26: The fork() system call creates a child process and an error message is displayed if the system call fails.

  • Lines 27–35: The writing end of the pipe pipefd[1] is closed because the child process is only meant to read messages from the pipe. The read() system call reads the two messages sent by the parent process in the readmsg array and displays them. The read end pipefd[0] is closed after process execution.

  • Lines 36–44: The reading end of the pipe pipefd[0] is closed because the parent is only supposed to write messages in the pipe. The write() system call writes two messages in the pipe, msg1 and msg2. The write end pipefd[1] is closed after process execution.

Two-way communication

One pipe is used for one-way communication between processes. If we want to establish two-way communication between the processes, two pipes must be used.

One pipe must be used to write in process A and read in process B. The second pipe would read in process A and write in process B.

The code below depicts two-way communication:

#include<stdio.h>
#include<unistd.h>
#include <iostream>
using namespace std;
int main() {
int pipefd1[2], pipefd2[2];
char readmsg1[80], readmsg2[80];
if (pipe(pipefd1) == -1)
{
cout<<"Pipe 1 creation error";
return 1;
}
if (pipe(pipefd2) == -1)
{
cout<<"Pipe 2 creation error";
return 1;
}
int pid = fork();
if (pid < 0)
{
cout<<"Fork error";
return 1;
}
if (pid == 0)
{ //child
close(pipefd1[1]);
read(pipefd1[0], readmsg1, 80);
cout<<readmsg1<<endl;
close(pipefd1[0]);
close(pipefd2[0]);
write(pipefd2[1], "Child writes to pipe 2. Parent reads and outputs from pipe 2.", 80);
close(pipefd2[1]);
}
else
{ //parent
close(pipefd1[0]);
write(pipefd1[1], "Parent writes to pipe 1. Child reads and outputs from pipe 1.", 80);
close(pipefd1[1]);
close(pipefd2[1]);
read(pipefd2[0], readmsg2, 80);
cout<<readmsg2<<endl;
close(pipefd2[0]);
}
return 0;
}

Explanation

  • Lines 10–19: Two pipe() system calls are used to create two pipes. Corresponding error messages are displayed if the system call fails.

  • Lines 31–34: The child process closes the write end of the first pipe because it is supposed to read from it. The read() system call reads and stores the message in readmsg1. The read end of the first pipe is then closed.

  • Lines 36–38: The child process closes the read end of the second pipe because it is supposed to write in it. The write() system call writes the message in the pipe and closes it afterward.

  • Lines 42–44: The parent process closes the read end of the first pipe because it is supposed to write in it. The write() system call writes the message in the pipe and closes it afterward.

  • Lines 46–49: The parent process closes the write end of the second pipe because it is supposed to read from it. The read() system call reads and stores the message in readmsg2. The read end of the second pipe is then closed.

Copyright ©2024 Educative, Inc. All rights reserved