UP | HOME

Redirection
Processes
COP-3402

Table of Contents

Recall: redirection in bash

We can use bash to change (redirect) where stdio goes to and comes from.

cat > outfile.txt
grep define < /usr/include/stdio.h

How does bash implement this?

What's the role of standard I/O in making this possible?

Goal

Replace the original standard I/O with a different file.

Let's see how bash implements redirection

Diagram:

  • ls
  • original stdout
  • open a new file
  • replace stdout
  • exec ls

Why std i/o here? We don't have to or want to rewrite ls, instead we can alter standard i/o, then execute ls as usual.

Recall: file I/O

  • open() - open a file for I/O
  • read() - read bytes from an open file
  • write() - write bytes from an open file

After we open a file, how do we reference that open file?

File descriptors

LPI Figure 5-2

  • Index into kernel table of open files
  • Returned by open
  • Used in calls to read/write

Writing to standard out

write_stdout.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // read, write
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen

int main(int argc, char **argv) {
  char *msg1 = "hello, world!\n";
  ssize_t msg1c = strlen(msg1);

  if (-1 == write(STDOUT_FILENO, msg1, msg1c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }
}

What's the difference between stdout and STDOUT_FILENO?

printf vs. write?

Why don't we have to open STDOUT_FILENO?

Duplicate a file descriptor

man 2 dup  # LPI chapter 5

Copies the file descriptor, so that we have two.

Both can write to the same open file.

duplicate_stdout.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen

int main(int argc, char **argv) {
  char *msg1 = "hello, world!\n";
  ssize_t msg1c = strlen(msg1);

  if (-1 == write(STDOUT_FILENO, msg1, msg1c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }

  int dupfd = dup(STDOUT_FILENO);
  if (-1 == dupfd) {
    perror("dup");
    exit(EXIT_FAILURE);
  }

  sleep(2);

  char *msg2 = "goodbye!\n";
  ssize_t msg2c = strlen(msg2);

  if (-1 == write(dupfd, msg2, msg2c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }
}

Notice how we write to the new file descriptor, yet it still leads to the original stdout (our terminal in this case).

(Optional) duplicate_fd.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen

int main(int argc, char **argv) {
  char *path = "dupfd.out";
  int fd = open(path, O_RDWR | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
  if (-1 == fd) {
    perror("open");
    exit(1);
  }

  char *msg1 = "hello, world!\n";
  ssize_t msg1c = strlen(msg1);

  if (-1 == write(fd, msg1, msg1c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }

  int dupfd = dup(fd);
  if (-1 == dupfd) {
    perror("dup");
    exit(EXIT_FAILURE);
  }

  char *msg2 = "goodbye!\n";
  ssize_t msg2c = strlen(msg2);

  if (-1 == write(fd, msg2, msg2c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }
}

This is the same as duplicating stdout, but in this case we open a file and duplicate that open file's descriptor.

Why does dup exist?

Redirection is one use!

The dup2 variant

  • dup2 replaces an existing file descriptor
man 2 dup2  # LPI chapter 5

Redirection is such a common use for dup, dup2 does the replacement for you.

  • First it closes any open file at newfd, then updates newfd to point to the same open file as oldfd

See LPI Figure 5-2 for hints about how this could be implemented

Standard I/O file descriptors

What are the file descriptors for stdin, stdout, and stderr?

/usr/include/unistd.h

#define STDIN_FILENO    0       /* Standard input.  */
#define STDOUT_FILENO   1       /* Standard output.  */
#define STDERR_FILENO   2       /* Standard error output.  */

This is the unix convention. The macros abstract away the use of magic numbers for the file descriptors.

Replacing a file descriptor

dup2(oldfd, newfd)

To replace stdout:

dup2(oldfd, STDOUT_FILENO)

redirect_stdout.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen

int main(int argc, char **argv) {
  char *msg1 = "write to original stdout\n";
  ssize_t msg1c = strlen(msg1);

  if (-1 == write(STDOUT_FILENO, msg1, msg1c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }

  char *path = "newfile_redirect_stdout.txt";
  int fd = open(path, O_RDWR | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
  if (-1 == fd) {
    perror("open");
    exit(1);
  }

  if (-1 == dup2(fd, STDOUT_FILENO)) {
    perror("dup2");
    exit(EXIT_FAILURE);
  }

  char *msg2 = "written to redirected stdout\n";
  ssize_t msg2c = strlen(msg2);

  if (-1 == write(STDOUT_FILENO, msg2, msg2c)) {
    perror("write");
    exit(EXIT_FAILURE);
  }

  printf("where will printf go now?\n");
}

Why does printf now go to the file instead of the terminal?

(Diagram)

  • bash creates process for redirect_stdout, giving it the terminal as stdout
  • redirect_stdout writes to stdout (the terminal)
  • redirect_stdout opens a new file (redirect_stdout.out)
  • redirect_stdout redirects stdout to that file (dup2)
  • redirect_stdout writes to stdout, which is now the file

(Optional) Redirecting stdin

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen

int main(int argc, char **argv) {
  pid_t pid;

  if (argc < 2) {
    printf("USAGE: %s file_to_read\n", argv[0]);
    exit(EXIT_FAILURE);
  }
  char *path = argv[1];

  const int BUFSIZE = 10;
  char buf[BUFSIZE];
  ssize_t bytes = read(STDIN_FILENO, buf, BUFSIZE);
  if (-1 == bytes) {
    perror("read");
    exit(1);
  }

  printf("bytes from original stdin: %ld\n", bytes);

  for (int i = 0; i < bytes; i++) {
    putchar(buf[i]);
  }
  putchar('\n');

  int fd = open(path, O_RDONLY);
  if (-1 == fd) {
    perror("open");
    exit(1);
  }

  if (-1 == dup2(fd, STDIN_FILENO)) {
    perror("dup2");
    exit(EXIT_FAILURE);
  }

  bytes = read(STDIN_FILENO, buf, BUFSIZE);
  if (-1 == bytes) {
    perror("read");
    exit(1);
  }

  printf("bytes from redirected stdin: %ld\n", bytes);

  for (int i = 0; i < bytes; i++) {
    putchar(buf[i]);
  }
  putchar('\n');
}

Redirecting a child process's I/O

  • now let's redirect a child's process
    • open file in parent
    • fork
      • gets inherited
    • replace stdout
    • then call exec
      • if program just writes to whatever stdout is, this will redirect to the file
      • this is the beauty of stdio and why it works
      • programs are written to just use whatever stdio file descriptors are there (which the program can assume have already been opened, which fork guarantees (as long as parent keeps them open), and which the shell controls where to direct i/o to)
      • try with ls to illustrate this

child.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write, fork
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen
#include <inttypes.h>  // intmax_t
#include <sys/wait.h>  // wait

int main(int argc, char **argv) {
  pid_t pid;

  if (argc < 2) {
    printf("USAGE: %s file_to_write\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  char *path = argv[1];
  int fd = open(path, O_RDWR | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
  if (-1 == fd) {
    perror("open");
    exit(1);
  }

  switch (pid = fork()) {
  case -1:
    perror("fork");
    exit(EXIT_FAILURE);
    break;
  case 0:
    // child

    if (-1 == dup2(fd, STDOUT_FILENO)) {
      perror("dup2");
      exit(EXIT_FAILURE);
    }

    printf("child process\n");
    exit(EXIT_SUCCESS);
    break;
  default:
    // parent
    printf("parent process\n");
    exit(EXIT_SUCCESS);
    break;
  }
}

child_exec.c

#include <fcntl.h>     // O_RDONLY, open
#include <unistd.h>    // dup, dup2, read, write, fork
#include <stdlib.h>    // exit
#include <stdio.h>     // perror, fprintf
#include <string.h>    // strlen
#include <inttypes.h>  // intmax_t
#include <sys/wait.h>  // wait

int main(int argc, char **argv) {
  pid_t pid;

  if (argc < 2) {
    printf("USAGE: %s file_to_write\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  char *path = argv[1];
  int fd = open(path, O_RDWR | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
  if (-1 == fd) {
    perror("open");
    exit(1);
  }

  switch (pid = fork()) {
  case -1:
    perror("fork");
    exit(EXIT_FAILURE);
    break;
  case 0:
    // child

    if (-1 == dup2(fd, STDOUT_FILENO)) {
      perror("dup2");
      exit(EXIT_FAILURE);
    }

    char *prog = "/usr/bin/ls";
    char *newargv[] = { prog, "/", NULL };
    char *newenv[] = { NULL };
    execve(prog, newargv, newenv);
    perror("execve");
    _exit(EXIT_FAILURE);
    break;
  default:
    // parent
    printf("parent process\n");
    exit(EXIT_SUCCESS);
    break;
  }
}

Author: Paul Gazzillo

Created: 2025-10-15 Wed 10:00