CS360 Final Exam - May 15, 2024 - Answers and Grading

These are answers to the example exam.

Question 3

Here are the answers for the different banks:

 Variables   |    A    |    B    |    C    |    D    |    E    |    F   
 ----------- | ------- | ------- | ------- | ------- | ------- | ------ 
 a,b         |   16    |    88   | unknown |  0xb000 |  0x6000 |   0x0  
 c,d         |   16    |   104   | unknown |  0xc000 |  0x7000 |   0x0  
 p,q         |   16    |    96   | unknown |  0xa000 |  0x5000 |   0x0  
 x,y         |   16    |   112   | unknown |  0xd000 |  0x8000 |   0x0  

Grading

Two points per question. I was pretty lenient with partial credit.


Question 4

Suppose that the first chunk on the freelist had a size of 24 bytes. Then you wouldn't be able to carve off 16 bytes and put the remainder on the freelist. In that case, you get all 24 bytes, even though you only need 16. The answer is 24.

Grading

Four points -- there wasn't much wiggle room on this one, and answers like "maybe the machine is big-endian" or "maybe the machine has 64-byte pointers" were not correct.


Question 5

(Again, this pertains to the example question for Q3): You have increased the size of a's chunk from 16 to 104. When you call free(a), it will put a chunk of size 104 on the freelist. That is unfortunate, because the bytes from 0x6010 to (0x6000+104) are already either allocated or free or some combination. When you subsequently call malloc() for a small amount, it will return bytes in this range, and now they are being used for two purposes -- their original purpose and as the return value of this new malloc() call. This is a disaster, and very hard to debug unless you use a tool like valgrind to help you figure it out.

Grading

Four points -- again, this had a very precise answer, so vague hand-waving didn't get you much. There were some excellent answers from students. For example: I also liked the comments, "leading to a menagerie of undefined behavior", and "you will encounter issues with the might of a thousand winds."

I labeled a lot of answers as "vague", because they use some good words, but lack the detail to show that they understand the answer. For example:

That got a point.
ChatGPT flails on this question, because it is answering with respect to general implementations of malloc() rather than the one specific to this class. It does some hedging at the end. The only part of its answer that is good is the part about "double-free errors", which of course is its third hedging answer to the question.


Question 6 - 15 points

A mutex is a data structure supported by the pthreads library that threads may lock and unlock. Only one thread may "hold" the lock at any one time. So:

Grading

A successful answer had these 9 components:
By the way, this is one of those questions that ChatGPT gets perfectly.


Question 7 - 15 points


Grading

Your answer needed some flavor of the following components: Obviously, not everyone's answers fit this pattern, so I tried to adjust accordingly.
ChatGPT does a decent job with this question too, although, since it uses many more sources than my lecture notes, it handles the question is a much different way than I do above. In particular, its answer to tha last part focuses on more general advantages of the stdio library, rather than how I advocate its use in this class. So I would take off for ChatGPT's answer here.

Regardless, if your answers to questions 6 and/or 7 were too ChatGPT-like, I concluded that you were cheating, because none of you write like this, and ChatGPT's lingo is not influenced by my teaching, and in this instance is quite different.


Question 8 - 45 points

Yes, a big question. I'm going to answer it in pieces. Here is my data structure. Obviously, yours can differ, but this is pretty much the bare minimum of what is needed in the data structure:

typedef struct {
  FILE *sock;        // A stdio buffer for the socket connection.
  JRB t;             // The red-black tree to hold output
  JRB pipes;         // A red-black tree: Key = process id's of children.
                     //                   Val = read end of the pipe from the child's stdout.
  int nprocs;        // Number of simultaneous processes (the command line argument)
  int cur_procs;     // Number of processes currently running.
} Mystruct;

Obviously, you could just have the file descriptor for the socket connection rather than the stdio buffer, but the buffer makes life so much easier and less bug-prone. initialize_v() allocates this data structure and initializes everything. Straightforward:

void *initialize_v(int fd, JRB t, int nprocs)
{
  Mystruct *ms;

  ms = (Mystruct *) malloc(sizeof(Mystruct));
  if (ms == NULL) { perror("malloc"); exit(1); }
  ms->sock = fdopen(fd, "r");
  ms->t = t;
  ms->pipes = make_jrb();
  ms->nprocs = nprocs;
  ms->cur_procs = 0;
  return (void *) ms;
}

get_next_command_line() simply reads a line from the socket. It needs to allocate space for the line. I do that by calling strdup() when I return. Alternatively, you could allocate the line in Mystruct. In fact, that would be more efficient. However, I kind of prefer the strdup() solution, because I think it will prevent bugs -- I just have to remember to free() it when I'm done with it. Either solution is fine.

You also need to recognize when you've hit EOF.

char *get_next_command_line(void *v)
{
  Mystruct *ms;
  char buf[202];

  ms = (Mystruct *) v;
  if (fgets(buf, 1000, ms->sock) == NULL) return NULL;
  return strdup(buf);
}

As I said in the problem writeup, both do_next_test() and finish_up() have to wait for child processes to finish, so I wrote a common procedure to do that task. I called it wait_for_next_process(). Since it is implemented in the same place as the other procedures, I can simply pass the Mystruct rather than the (void *):

I chose to do the fdopen() here rather than when I insert the pipe connection into the red-black tree. It's up to you which you do. Once again, using the stdio buffer makes your life a lot easier.

void wait_for_next_process(Mystruct *ms)
{
  int pid, status;
  FILE *f;
  double v;
  JRB tmp;
  char name[100];

  /* Wait for a child to exit. */

  pid = wait(&status);

  /* Find that child in my tree of pipes. */

  tmp = jrb_find_int(ms->pipes, pid);
  if (tmp == NULL) { fprintf(stderr, "Couldn't find %d in tree.\n", pid); exit(1); }
  
  /* Use fdopen to read the name and the value from the pipe, and insert them into the tree. */

  f = fdopen(tmp->val.i, "r");

  if (fscanf(f, "%s %lf", name, &v) == 0) { fprintf(stderr, "bad fscanf\n"); exit(1); }
  jrb_insert_dbl(ms->t, v, new_jval_s(strdup(name)));

  /* Close the pipe and decrement the number of currently running processes. */

  jrb_delete_node(tmp);
  fclose(f);
  ms->cur_procs--;
}

Now, do_next_test() needs to call wait_for_next_process() if there are nprocs processes currently running. When that returns, it sets up a pipe, inserts it into the rb-tree and then creates the process for the optimization. To extract the program name and key, you can search for a space in cl, but since cl will have the newline character, it's kind of a pain. That's why I used sscanf(). I don't expect you to know this about fgets(), so if you searched for the space, that's fine.

If, like me, you allocated the string with strdup() in get_next_command_line(), you'll need to free() it here.

void do_next_test(char *cl, void *v)
{
  Mystruct *ms;
  int p[2];
  int pid;
  char s2[100], s1[100];

  ms = (Mystruct *) v;

  /* If there are too many processes, wait for one to exit. */

  if (ms->cur_procs == ms->nprocs) wait_for_next_process(ms);

  /* Create the pipe */

  pipe(p);
  ms->cur_procs++;
 
  /* Create the child process. */

  pid = fork();

  /* In the child, extract the program name and key, put the pipe on standard output,
     and close the pipe fd's.  Then use execlp() to run the program. */

  if (pid == 0) {
    sscanf(cl, "%s %s", s1, s2);
    dup2(p[1], 1);
    close(p[0]);
    close(p[1]);
    execlp(s1, s1, s2, NULL);
    perror("execlp");
    exit(0);
  }

  /* In the parent, put the read end of the pipe into the red-black tree, and close
     the write end. */

  jrb_insert_int(ms->pipes, pid, new_jval_i(p[0]));
  close(p[1]);

  /* Free the command line if you allocated it. */

  free(cl);
}

Finally, in finish_up(), you need to wait for all of the children to exit, and then free up your memory:

void finish_up(void *v)
{
  Mystruct *ms;

  ms = (Mystruct *) v;
  while (ms->cur_procs > 0) wait_for_next_process(ms);

  jrb_free_tree(ms->pipes);
  fclose(ms->sock);
  free(ms);
}


Grading

I broke the grading into 5 parts, each worth 10 points: However, I was not strict on the partitioning. More often than not, I took a holistic view of your answer, and assigned the points so that your total matched this holistic view.

There are a few things that damaged this holistic view:


I hate to say that ChatGPT did a decent job, but it would have gotten a good grade. The problems here, for me, are: ChatGPT uses the JRB/JVal library correctly -- that's likely because no one else on the net uses the jrb_ prefix. It's kind of chilling.

Actually, it's kind of chilling overall how well ChatGPT does, and whether or not that's a good thing. It's fine for someone like me who is seasoned enough to be able to spot the bugs quickly and fix them. Someone who isn't seasoned is not going to spot those bugs, and at best, it won't work and one can debug. At worst, the bugs go through to production because whoever is using ChatGPT isn't experienced enough to recognize them...


ChatGPT's solution: