CS360 Final Exam - May 3, 2012 - James S. Plank

Answers and Explanations

Question 1

Grading:

5 points for the first two and 5 points for the second two.


Question 2

Part A: You need to set up a signal handler for SIGPIPE that prints "Got it" and exits. Then you call pipe() to get a pipe and close its read end. Finally, you write to it. Here it is: q2a.c

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void sigpipe_handler(int i)
{
  printf("Got it\n");
  exit(0);
}

main()
{
  int fd[2];

  signal(SIGPIPE, sigpipe_handler);
  pipe(fd);
  close(fd[0]);
  write(fd[1], "Hi\n", 3);
}

Part B: You need a program that is going to write to a pipe whose read end is closed. The simple example is:

UNIX> cat f1.txt | head
f1.txt should have at least 4K bytes after its first 10 lines, in case the operating system does some buffering. head will print out the first 10 lines and then exit. At that point, there is no read end to the pipe to which cat is writing. While the operating system may buffer some of the write calls to the pipe before head exits, the buffer is typically 4K, so cat will be put into the situation where it is writing, and the read end of the pipe is gone. That generates SIGPIPE.

Part C: There are five potential outputs:

  1. Suppose file descriptor 10 is not open, or it is opened for reading. Then, the write() call returns -1, because the write fails.
  2. Suppose file descriptor 10 has been opened for writing to a file. Then the write succeeds and returns 2. The output is "2".
  3. Suppose file descriptor 10 has been opened for writing to a socket. Sockets are finicky and can potentially only write one byte. In that case, the output will be 1.
  4. Suppose file descriptor 10 has been dup'd from standard output. Then it will write the "J\n" before "2".
  5. Finally, suppose file descriptor 10 is the write end of a pipe whose read ends have been closed. Then the write can generate SIGPIPE. There will be no output (or the shell may tell you that there is a broken pipe).
Segmentation violation is not a valid output -- system calls do not generate segmentation violations. Instead, they return with error codes.

Grading

Six points for part A, two for part B, four for part C (you only had to get four of five to get full credit).


Question 3

Command A: Since i equals 2, setjmp() is called, which returns zero: "T: 0". Next, simons_code() is called, and since j equals 2, setjmp() is called again, which again returns zero: "S: 0". Next, alvins_code() is called, and since a equals 2, it prints A: 2 and returns 2. All procedures return 2, so the main() procedure prints "M: 2". The final output:
UNIX> a.out 2 2 2
T: 0
S: 0
A: 2
M: 2
UNIX> 
Command B: Again i equals 2, so setjmp() is called, which returns zero: "T: 0". Next, simons_code() is called, and since j does not equal 2, s is simply set to 1: "S: 1". simons_code() now calls longjmp() which returns from setjmp() in theodores_code, with a value of 2: "T: 2". theodores_code() now returns 2 to main(): "M: 2". The final output:
UNIX> a.out 2 1 2
T: 0
S: 1
T: 2
M: 2
UNIX> 
Command C: This will print "T: 1", and then call longjmp(). However, no one has called setjmp(), so you're begging for a seg fault. That's what I got on my Macintosh:
UNIX> a.out 1 0 0
T: 1
Segmentation fault
UNIX> 
Command D: Now, theodores_code() prints "T: 0" and then calls simons_code(). This calls setjmp(), prints "S: 0" and calls alvins_code(). alvins_code() prints "A: 0" and calls longjmp(SharedBuf, 1). This ends up returning from the setjmp() call in simons_code() with a return value of 1: "S: 1". Now simons_code() calls longjmp(SharedBuf, 2), which again returns from the setjmp() in simons_code(), this time with a return value of 2: "S: 2". Now it returns 2 to theodores_code, which returns 2 to main(): "M: 2". The whole output:
UNIX> a.out 0 2 0
T: 0
S: 0
A: 0
S: 1
S: 2
M: 2
UNIX> 
Command E: On this last command, theodores_code() calls setjmp(), prints "T: 0" and then calls simons_code(). simons_code() prints "S: 0" and calls alvins_code(). alvins_code() prints "A: 0" and calls longjmp(SharedBuf, 1). This ends up returning from the setjmp() call in theodores_code() with a return value of 1: "T: 1". Now theodores_code() calls longjmp(SharedBuf, 1), which again returns from the setjmp() in theodores_code() with a return value of 1: "T: 1". And you should see that we're in an infinite loop:
UNIX> a.out 2 0 0
T: 0
S: 0
A: 0
T: 1
T: 1
..... repeating infinitely

Grading

Three points for each of the first two commands, two points each for the remaining three.


Question 4

Outline: You are going to call fork() to create a child process, which will run chpmnk. To communicate with it, you'll need two pipes -- one that you can use to write to its standard input, and one that you can use to read from its standard ouptput. So, you'll call pipe() twice before calling fork().

The child will make the appropriate dup2() calls to have one pipe go to standard input and the other go to standard output. It will then close all the pipe file descriptors and call execl() or execv() on chpmnk.

The parent will close parts of the pipes that it won't use, and set up standard IO streams (using fdopen() for the write end of the first pipe and the read end of the second pipe. Then it reads lines from standard input, calls preprocess_line() and fputs() or fprintf() to send the lines to chmpnk. It calls fgets() to read output from chmpnk, then calls postprocess_line() and prints out the resulting line. That's it.

The whole code is in q4a.c. In this code, I make up a preprocess_line() that converts all lowercase characters to uppercase, and postprocess_line() that prepends "Output: "to the strings, if there is room. You didn't need to do anything with preprocess_line() or postprocess_line() -- I just wrote them so that the program would compile completely.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

void preprocess_line(char *s)
{
  int i;

  for (i = 0; s[i] != '\0'; i++) {
    if (s[i] <= 'z' && s[i] >= 'a') s[i] += ('A'-'a');
  }
}

void postprocess_line(char *s)
{
  int i;
  char buf[500];

  if (strlen(s) > 490) return;
  sprintf(buf, "Output: %s", s);
  strcpy(s, buf);
  return;
}

main()
{
  char line[500];
  int p1[2], p2[2];
  FILE *fin, *fout;

  pipe(p1);
  pipe(p2);

  if (fork() == 0) {
    dup2(p1[0], 0);
    dup2(p2[1], 1);
    close(p1[0]);
    close(p1[1]);
    close(p2[0]);
    close(p2[1]);
    execl("./chpmnk", "chpmnk", NULL);
    perror("execl chpmnk");
    exit(1);
  } 

  close(p1[0]);
  close(p2[1]);
  fin = fdopen(p2[0], "r");
  fout = fdopen(p1[1], "w");
  
  while (fgets(line, 500, stdin) != NULL) {

    preprocess_line(line);
    fputs(line, fout);
    fflush(fout);

    if (fgets(line, 500, fin) == NULL) {
      fprintf(stderr, "Alvin, Simon and Theodore messed up again.\n");
      exit(1);
    }
 
    postprocess_line(line);
    fputs(line, stdout);
  }
  exit(0);
}

(This is just for people studying from this question for future exams): You can test this out as is -- the child process will die because there is no chpmnk, but the parent doesn't die until you put a line of input into it. How does it die? On SIGPIPE, since there is no read end of p1:

UNIX> q4a
execl chpmnk: No such file or directory
Example input
Broken pipe
UNIX> 
You can test it for real by copying /bin/cat to chmpnk:
UNIX> cp /bin/cat chpmnk
UNIX> q4a
Example input
Output: EXAMPLE INPUT
You probably hated answering this question, but it's a fun one!
Output: YOU PROBABLY HATED ANSWERING THIS QUESTION, BUT IT'S A FUN ONE!
UNIX> 
Some of you called fork() for each line of standard input. While technically that would be correct, it's very inefficient. You also had to do it correctly (e.g. some of you set up a pipe before the loop to read standard input. That cannot work.).

Grading

Outline = 7. Code = 13. I took off for the obvious things: