Mastering UNIX pipes, Part 2
By Kamil Rytarowski
- 4 minutes read - 666 wordsA pipe is a first-in-first-out interprocess communication channel. In the previous article we had introduced the reader to the UNIX pipe concept and presented the basic characteristics of this interprocess communication channel. In this part, we will dig into the examples of combining two processes and managing the byte transfers.
Text file viewer with pipes
Let’s start with an example of spawning a $PAGER
program with the pipe(2)
+fork(2)
pair.
We could implement from scratch a program for reading text listings, but wouldn’t it be just
more convenient to reuse the prior art and reuse more(1)
or less(1)
for this task?
We will present the full code and then explain it chunk by chunk.
/* CC0 1.0 Universal (CC0 1.0) - Public Domain Dedication */
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <err.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MYPAGER "less"
static char *
get_pager(void)
{
static char *pager;
pager = getenv("PAGER");
if (pager == NULL || *pager == '\0')
pager = MYPAGER;
return pager;
}
static char *
get_argv0(char *pager)
{
static char *argv0;
if ((argv0 = strrchr(pager, '/')) == NULL)
argv0 = pager;
else
argv0++;
return argv0;
}
int
main(int argc, char **argv)
{
struct stat sb;
FILE *fp;
char *pager, *argv0;
char *buf;
size_t size, n;
int status;
pid_t child;
int fildes[2];
int oldout;
if (argc != 2)
errx(EXIT_FAILURE, "usage: %s FILE", argv[0]);
if (pipe(fildes) == -1)
err(EXIT_FAILURE, "pipe");
if ((child = fork()) == -1)
err(EXIT_FAILURE, "fork");
if (child == 0) {
/* child */
if (close(fildes[1]) == -1)
err(EXIT_FAILURE, "close");
if (dup2(fildes[0], STDIN_FILENO) != STDIN_FILENO)
err(EXIT_FAILURE, "dup2");
if (close(fildes[0]) == -1)
err(EXIT_FAILURE, "close");
pager = get_pager();
argv0 = get_argv0(pager);
execlp(pager, argv0, NULL);
err(EXIT_FAILURE, "execlp");
}
/* parent */
signal(SIGPIPE, SIG_IGN);
if (close(fildes[0]) == -1)
err(EXIT_FAILURE, "close");
if ((oldout = dup(STDOUT_FILENO)) == -1)
err(EXIT_FAILURE, "dup");
if (dup2(fildes[1], STDOUT_FILENO) != STDOUT_FILENO)
err(EXIT_FAILURE, "dup2");
if (close(fildes[1]) == -1)
err(EXIT_FAILURE, "close");
if ((fp = fopen(argv[1], "r")) == NULL)
err(EXIT_FAILURE, "fopen");
if (fstat(fileno(fp), &sb) == -1)
err(EXIT_FAILURE, "fstat");
size = sb.st_size;
buf = malloc(size);
if (fread(buf, 1, size, fp) != size)
errx(EXIT_FAILURE, "fread");
fclose(fp);
if (fwrite(buf, 1, size, stdout) != size)
errx(EXIT_FAILURE, "fwrite");
fflush(stdout);
if (close(STDOUT_FILENO) == -1)
err(EXIT_FAILURE, "close");
/* wait for the child process termination */
if (wait(&status) == -1)
err(EXIT_FAILURE, "wait");
if (dup2(oldout, STDOUT_FILENO) != STDOUT_FILENO)
err(EXIT_FAILURE, "dup2");
if (close(oldout) == -1)
err(EXIT_FAILURE, "close");
printf("Quitting");
for (n = 0; n < 3; n++) {
sleep(1);
printf(".");
fflush(stdout);
}
printf("\n");
printf("Bye\n");
return EXIT_SUCCESS;
}
Save this file as pager.c
and build:
$ cc pager.c -o pager
Now run it with:
$ ./pager /etc/protocols
We should see the $PAGER
to pop up on the window.
Once you exit from the $PAGER
program,
the parent process reinitializes the standard output stream and
prints a bye message.
This program has been verifed to build and work correctly on:
- NetBSD 9.99.69,
- FreeBSD 12.0,
- OpenBSD 6.6,
- Linux 5.6.14 (Fedora 32).
Code walk-through
Let’s see the code, chunk by chunk.
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <err.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
Include the standard POSIX headers and portable <err.h>
,
which formatted error messages routines.
#define MYPAGER "less"
Define your default pager fallback.
The default value is prompted from the environment value PAGER
and in case of being unavailable
or empty, going back to the compiled in program specified as MYPAGER
.
static char *
get_pager(void)
{
static char *pager;
pager = getenv("PAGER");
if (pager == NULL || *pager == '\0')
pager = MYPAGER;
return pager;
}
This utility function scans the environment and picks $PAGER
once defined and non-empty, otherwise it fallbacks to MYPAGER
.
static char *
get_argv0(char *pager)
{
static char *argv0;
if ((argv0 = strrchr(pager, '/')) == NULL)
argv0 = pager;
else
argv0++;
return argv0;
}
Later in the code we need to pass well-formed argv[0]
argument, with the basename of the program.
Summary
We have introduced the reader to the UNIX pipe concept and presented the basic characteristics of this interprocess communication channel. In the next part, we will dig into the examples of combining two processes and managing the byte transfers.