Case Study: IPC (Parent/Child) Over Unnamed Pipe#
Pipe Creation#
Pipe
Buffer of limited capacity
Unidirectional communication channel
Two ends: read and write
⟶ described by file descriptors (
read_end
andwrite_end
in the remainder code)
#include <unistd.h>
#include <stdio.h>
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
[[maybe_unused]] int read_end = pipe_ends[0];
[[maybe_unused]] int write_end = pipe_ends[1];
return 0;
}
Child Creation#
Process has two file descriptors
At
fork()
, these are inherited by the child
#include <unistd.h>
#include <stdio.h>
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
[[maybe_unused]] int read_end = pipe_ends[0];
[[maybe_unused]] int write_end = pipe_ends[1];
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // <-- child
// ...
}
else { // <-- parent
// ...
}
return 0;
}
Close Unneeded Ends#
In our program, communication direction will be from child to parent
⟶ Parent only reads, child only writes
Unneeded file descriptors; parent closes
write_end
, child closesread_end
#include <unistd.h>
#include <stdio.h>
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
int read_end = pipe_ends[0];
int write_end = pipe_ends[1];
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
close(read_end); // <-- unneeded
}
else {
close(write_end); // <-- unneeded
}
return 0;
}
Implementing Infinite Data Flow#
Child infinitely writes into the pipe’s
write_end
Parent infinitely reads from
read_end
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <print>
struct data
{
int sequence_number;
};
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
int read_end = pipe_ends[0];
int write_end = pipe_ends[1];
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
close(read_end);
// ****** <-- produce data once a second
int cur_seq = 0;
while (true) {
data data = { cur_seq++ };
ssize_t nwritten = write(write_end, &data, sizeof(data));
if (nwritten == -1) {
perror("write");
exit(1);
}
sleep(1);
}
}
else {
close(write_end);
// ****** <-- consume data
while (true) {
data data;
ssize_t nread = read(read_end, &data, sizeof(data));
if (nread == -1) {
perror("read");
exit(1);
}
std::println("parent received: {}", data.sequence_number);
}
}
return 0;
}
Finite Data Flow#
Child only produces, say, 5 items
How to notify parent, over the pipe, that no data will come anymore?
⟶ Child closes
write_end
⟶ Parent then sees an end-of-file condition (
read()
returns 0)
Note
Technically, the child does not need to close write_end
. This
is done by the system, implicitly, when the child calls exit()
.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <print>
struct data
{
int sequence_number;
};
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
int read_end = pipe_ends[0];
int write_end = pipe_ends[1];
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
close(read_end);
int cur_seq = 0;
for (int i=0; i<5; i++) { // <-- produce only 5 items
data data = { cur_seq++ };
ssize_t nwritten = write(write_end, &data, sizeof(data));
if (nwritten == -1) {
perror("write");
exit(1);
}
sleep(1);
}
close(write_end); // <-- end-of-file for consumer
exit(0); // <-- terminate (don't leak into unconditional code)
}
else {
close(write_end);
while (true) {
data data;
ssize_t nread = read(read_end, &data, sizeof(data));
if (nread == -1) {
perror("read");
exit(1);
}
if (nread == 0) { // <-- end-of-file seen
std::println("seen EOF");
break;
}
std::println("parent received: {}", data.sequence_number);
}
close(read_end);
exit(0);
}
return 0;
}
Unneeded ⟶ Harmful!#
Yes, closing unneeded file descriptors is mandatory!
What if parent hadn’t closed its write-end file descriptor?
⟶ Pipe’s write-end is referenced twice: once from parent and once from child
⟶ When child closes its
write_end
, the pipe’s in-kernel write-end is still referenced by the parent⟶ Pipe is not shutdown
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <print>
struct data
{
int sequence_number;
};
int main()
{
int pipe_ends[2];
if (pipe(pipe_ends) == -1) {
perror("pipe");
return 1;
}
int read_end = pipe_ends[0];
int write_end = pipe_ends[1];
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
close(read_end);
int cur_seq = 0;
for (int i=0; i<5; i++) {
data data = { cur_seq++ };
ssize_t nwritten = write(write_end, &data, sizeof(data));
if (nwritten == -1) {
perror("write");
exit(1);
}
sleep(1);
}
close(write_end);
exit(0);
}
else {
// close(write_end); // <-- NOT closing -> pinned
while (true) {
data data;
ssize_t nread = read(read_end, &data, sizeof(data));
if (nread == -1) {
perror("read");
exit(1);
}
if (nread == 0) {
std::println("seen EOF");
break;
}
std::println("parent received: {}", data.sequence_number);
}
close(read_end);
exit(0);
}
return 0;
}
What If Receiver (Parent) Terminates?#
A process that writes to a pipe (or a socket, for that matter) whose receiver end is closed is delivered a
SIGPIPE
⟶ Default action: terminate
This means that the child will terminate at its next write()
operation.