Comp111: Operating Systems
Classroom Exercise 4 Answers
Implementing glue
Fall 2017

In class we have discussed the basic pattern of fork and exec, and how system calls allow us to fool processes into writing to files instead of terminals. Let's make sure we understand the important issues. The exercise refers to the following program:

 
  1. Precisely what does this program do?

    Answer: The child runs ps -ef with its stdout connected to the file descriptor read in the parent. The ps command prints the process table to the parent, which prints it, with an appended "child says: " on the front of each line.

    The printf in the child is never executed.

  2. What is the impact of omitting the close(fd[READ]) and close(fd[WRITE]) statements?

    Answer: Absolutely nothing changes. The fd[WRITE] file descriptor is not used by the parent, so its existence does nothing there. In the child, the fd[READ] file descriptor are not used by ps, so its existence after the execl has no impact.
  3. What is the impact of omitting the close(fileno(stdout)) statement?

    Answer: The dup will still execute but will place the copy in the first open file descriptor, which in this case will be 3! Just after the pipe call: After close(fd[READ]), we have: After the dup, we will have because that is the first open slot! Thus ps will execute with an unmodified stdin and stdout, and will thus print its output to the terminal rather than through the parent. So the "child says: " prefix will disappear.

    Note that what ps doesn't use won't hurt it. The fact that there is a lot of garbage in the file descriptor table has no effect, because ps doesn't try to print to it or read from it.

  4. (Advanced) One disadvantage of pipes is that they are block buffered; the output is blocked in (typically 4096-byte) blocks rather than by line. Why does this not mess up this example?
    Answer: The reason this works is that -- independent of buffering -- the parent eventually gets all of the characters that the child printed. The reason for this is that the child "ps" closes its standard output before exit, which causes a flush of the buffer to the parent.
  5. (Advanced) Explain exactly how the shell implements
    ps -ef |& ./a.out
    
    by modifying the above program to do the same thing. Hint: you need to dup stderr.

    Answer: We want ps to write to the standard input of a.out when it thinks it is writing to stderr. Additionally we want our process to read stdin and actually get input from fd[READ]!

    In the above example, ps writes to the read file pointer of a.out when it thinks it is writing to stdout. So we have to write to the same place from stderr. In the child, we have to include another close and dup:

    close(fileno(stderr)); 
    dup(fd[WRITE]); 
    
    so that all output from stdout and stderr goes to fd[WRITE].

    In the parent, we add a close and dup for input to map the output of the child to our stdin:

     
    close(fileno(stdin)); 
    dup(fd[READ]); 
    
    instead of the fdopen that we used in the example. Then we read stdin (or perhaps execl another process in the parent) and all works as required.

    In practice, the shell makes one pipe and forks twice to make two children that share the pipe, after which it closes both sides of the pipe in the shell itself, to wit, something like this:

      
    #define SIZE 256
    #define READ 0
    #define WRITE 1
    main()
    { 
        pid_t pid1, pid2, pid3; int status;
        int fd[2]; struct rusage usage;
        pipe(fd); // create the pipe first 
        if (!(pid1=fork())) {        // child 1 is the reader: "| proc2"
            close(fd[WRITE]);        // close unused side
            close(fileno(stdin)); 
    	dup(fd[READ]); 
    	close(fd[READ]);         // don't leave the unduped descriptor 
    	execl(proc2, proc2, NULL) 
        } else if (!(pid2=fork())) { // child 2 is the writer: "proc1 |"
            close(fd[READ]);         // closed unused side
            close(fileno(stdout)); 
            dup(fd[WRITE]); 
            close(fd[WRITE]);        // don't leave the unduped descriptor
            execl(proc1, proc1, NULL); 
        } else {  // parent waits (or reaps) twice!
            pid3=wait3(&status, 0, &usage);       
            pid3=wait3(&status, 0, &usage);       
        } 
    }
    
    Note that a dup creates a complete copy, and closing the original does not affect the copy! Dup is very fast; it is just a matter of changing an integer index!
  6. I claim that the programming patterns what I just demonstrated are pretty much the opposite of functional programming. Why?
    Answer: dup functions by side effects, i.e., it modifies its environment rather than producing a result. The basic tenet of functional programming is to write code that is free of such side effects. So the entire function of dup is totally counter to tbe most basic principle of functional programming.
`