thecodingidiot.com

The PipelineRedirects

Redirects

A pipe connects two processes. File redirects connect the pipeline to the filesystem: infile becomes cmd1's stdin; outfile becomes cmdN's stdout. Both use the same dup2 mechanism from the previous page, preceded by open.

open

open opens a file and returns a file descriptor. It takes a path, a set of flags, and — when creating a file — a permission mode:

#include <fcntl.h>
 
int fd;
 
fd = open("infile.txt", O_RDONLY);
fd = open("outfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

If open fails, it returns -1 and sets errno. Use perror to print the error:

fd = open("infile.txt", O_RDONLY);
if (fd < 0) {
    perror("infile.txt");
    exit(1);
}

Flags

FlagMeaning
O_RDONLYOpen for reading only
O_WRONLYOpen for writing only
O_CREATCreate the file if it does not exist (requires mode)
O_TRUNCTruncate the file to zero length on open

O_WRONLY | O_CREAT | O_TRUNC is the standard combination for an output file: create it if missing, overwrite it if present. The mode 0644 gives the owner read-write permission and the group and others read-only permission — the same as most files created by shell redirects.

The mode argument is only consulted when O_CREAT is specified. When opening a file for reading, omit it.

Redirect infile to stdin

The child process that will run cmd1 reads from stdin. To make it read from a file instead, dup2 the file's descriptor onto STDIN_FILENO (0) before execing:

int infd;
 
infd = open(infile, O_RDONLY);
if (infd < 0) {
    perror(infile);
    exit(1);
}
dup2(infd, STDIN_FILENO);
close(infd);    /* STDIN_FILENO now refers to the file; infd is redundant */
exec_cmd(cmd1_argv);

After dup2(infd, STDIN_FILENO), file descriptor 0 reads from the file. close(infd) removes the redundant reference — the file is still open through STDIN_FILENO.

Redirect stdout to outfile

The child that runs cmdN writes to stdout. To redirect its stdout to a file:

int outfd;
 
outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd < 0) {
    perror(outfile);
    exit(1);
}
dup2(outfd, STDOUT_FILENO);
close(outfd);   /* STDOUT_FILENO now refers to the file; outfd is redundant */
exec_cmd(cmdN_argv);

The command does not know it is writing to a file. It writes to STDOUT_FILENO as always; the kernel routes those bytes to the file.

Error handling order

Open both files before forking. If infile cannot be opened, there is nothing to pipe — exit before creating any child processes:

infd = open(infile, O_RDONLY);
if (infd < 0) {
    perror(infile);
    return (1);   /* or exit(1) — no children yet, nothing to wait for */
}
outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd < 0) {
    perror(outfile);
    close(infd);
    return (1);
}
/* fork here */

This matches what the shell does: bash -c "< missing cmd" exits immediately with an error and does not exec cmd. Testing file access before forking means you never have a zombie child waiting on a parent that will never call waitpid.

The parent does not use these fds

After forking the children that will use infd and outfd, the parent closes both:

close(infd);
close(outfd);

The same close-everything-you-do-not-use rule from the pipe page applies here. A file descriptor opened by the parent is inherited by every child. If outfd stays open in the parent, the file stays open even after the last child closes it — the parent's copy holds the reference count above zero until it too closes.

A redirect test

Extend main.c to open infile and outfile from argv and run a two-command pipeline between them:

int main(int argc, char **argv)
{
    int     infd;
    int     outfd;
    char    *cmd1[] = { "cat", NULL };
    char    *cmd2[] = { "wc", "-l", NULL };
 
    if (argc != 3) {
        tci_printf("usage: ./pipeline infile outfile\n");
        return (1);
    }
    infd = open(argv[1], O_RDONLY);
    if (infd < 0) { perror(argv[1]); return (1); }
    outfd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);  /* create or overwrite; 0644 = rw-r--r-- */
    if (outfd < 0) {
        perror(argv[2]);
        close(infd);    /* already open; release before returning */
        return (1);
    }
 
    run_two_redirect(cmd1, cmd2, infd, outfd);
 
    close(infd);
    close(outfd);
    return (0);
}

Where run_two_redirect is a version of run_two from the previous page extended to accept and apply infd and outfd in the appropriate children.

Test:

make re
echo -e "banana\napple\ncherry" > fruits.txt
./pipeline fruits.txt counts.txt
cat counts.txt

Expected: 3 — three lines in fruits.txt, counted by wc -l, written to counts.txt.

Compare with the shell:

< fruits.txt cat | wc -l > counts.txt
cat counts.txt

Same result. The next page assembles all the pieces into the full two-command pipeline function that main.c will call.