Homework 2 Solutions

22C:116, Fall 1995

Douglas W. Jones
  1. Describe, briefly, what this program does.
    The program creates two processes, connected by a circular linkage of pipes. A single integer is circulated through these pipes between the two processes. The integer is initially zero. Each process increments this circulating integer before passing it on, and each process terminates as soon as the circulating integer has been incremented to a value greater than 4. One process is designated the parent, and the other is designated the child; each writes out its identity when it receives the circulating integer. This behavior means that there will be a total of 5 lines of output from parent and child (after the initial diagnostic), with one printing 3 and the other 2 lines of output.

  2. Translate the above C code to this high level parallel programming notation!
    main()
    {
            int p1[2], p2[2], pid, cc, buf[1];
            pipe(p1);  pipe(p2);  buf[0] = 0;
            cc = write( p1[1], buf, 4 );
            cc = write( 1, "pre-fork\n", 9 );
            cobegin
                    while (buf[0] < 4) {
                            cc = read( p2[0], buf, 4 );
                            cc = write( 1, "parent\n", 7 );
                            buf[0]++; cc = write( p1[1], buf, 4 );
                    };
                    while (buf[0] < 4) {
                            cc = read( p1[0], buf, 4 );
                            cc = write( 1, "child\n", 6 );
                            buf[0]++; cc = write( p2[1], buf, 4 );
                    }
            coend;
    }
    

  3. If you rewrite the above code using printf(...) instead of write(1,...) to produce output, the output is different. Why?
    The output is not necessarily different. The code for printf behaves differently when directed to an interactive terminal or to a non-interactive file or device. When directed to a terminal, printf flushes its buffers after every output of a newline, but when directed to a non-interactive file, buffers are only flushed when they fill or are explicitly flushed. The following two example runs illustrate this difference:
    % t1
    pre-fork
    child
    parent
    child
    parent
    child
    
    % t1 | cat
    pre-fork
    child
    child
    child
    pre-fork
    parent
    parent
    
    In the second example, the un-flushed buffer was copied and given to both processes when the fork was done, and then each added its own output. The buffers were only flushed when the process terminated, and since the parent waited for the child to terminate, the child's buffer was output first.

  4. If you rewrite the above code using fdopen() to convert each file descriptor to a stream, and then use the more common fprintf() and fscanf() routines instead of using read() and write on the two ends of the pipe, what change in behavior would you expect.
    I expect a deadlock. The reason is that streams are buffered until some event causes the buffer to be flushed. The basic stream operations putc and getc are macros, and you can inspect the code involved in doing the buffer flushing in the include file stdio.h.

    Merely opening the streams poses problems, because some versions of fdopen may do a preliminary read to fill the buffer, causing the deadlock to emerge even before any explicit reads or writes have been performed. Thus, only the parent's output stream should be opened prior to the fork, and the child should open its streams after the fork.

    With this precaution, the deadlock emerges after the fork, and this should be resolved with the insertion of an fflush operation after each fprintf. Unfortunately, even this does not solve the problem, and most UNIX implementors don't provide workable solutions, but instead suggest that programs that use pipes should use read and write instead of the higher level stream primitives.