CS360 Final Exam - May 15, 2024

The exam was given on campus and used question banks for some questions. This is an example exam.

Quiz Instructions

Please do all questions. Note the point values and allocate your time accordingly. Below are prototypes of system calls and procedures that you may find useful.

Prototypes of various useful system calls:

int fork();
int wait(int *stat_loc);
int dup2(int fildes, int fildes2);
int pipe(int fildes[2]);

int open(const char *path, int oflag, ...);
int close(int fildes);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
off_t lseek(int fd, off_t offset, int whence);

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

int  setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
int  sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

int execl(const char *path, const char *arg, ...);  /* End the argument list with NULL */
int execlp(const char *file, const char *arg, ...);  /* End the argument list with NULL */
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

Prototypes of Standard IO Library Calls

char  *fgets(char *s, int size, FILE *stream); /* Returns NULL on EOF */
int    fputs(const char *s, FILE *stream);     /* Returns EOF when unsuccessful */
int    fflush(FILE *stream);                   /* Returns 0 on success, EOF on failure */
FILE  *fdopen(int fd, char *mode);             /* Returns NULL on failure */

int fgetc(FILE *stream);                       /* Returns EOF on EOF */
int fputc(int c, FILE *stream);                /* Returns EOF when unsuccessful */

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);

int atoi(char *s);   /* Converts a string to an integer - returns zero if unsuccessful */

Prototypes from Pthreads

typedef void *(*pthread_proc)(void *);
int pthread_create(pthread_t *thread, pthread_attr_t *attr, 
                   pthread_proc start_routine, void *arg);

int       pthread_join(pthread_t thread, void **value_ptr);
void      pthread_exit(void *value_ptr);
int       pthread_detach(pthread_t thread);
pthread_t pthread_self();

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

Prototypes from sockettome.h

int serve_socket(int port);
int accept_connection(int s);
int request_connection(char *hn, int port);

Prototypes from libfdr

typedef struct inputstruct {
  char *name;               /* File name */
  FILE *f;                  /* File descriptor */
  int line;                 /* Line number */
  char text1[MAXLEN];       /* The line */
  char text2[MAXLEN];       /* Working -- contains fields */
  int NF;                   /* Number of fields */
  char *fields[MAXFIELDS];  /* Pointers to fields */
  int file;                 /* 1 for file, 0 for popen */
} *IS;

IS new_inputstruct(/* FILENAME -- NULL for stdin */);
IS pipe_inputstruct(/* COMMAND -- NULL for stdin */);
int get_line(/* IS */); /* returns NF, or -1 on EOF.  Does not close the file */
void jettison_inputstruct(/* IS */);  /* frees the IS and fcloses the file */

typedef struct dllist {
  struct dllist *flink;
  struct dllist *blink;
  Jval val;
} *Dllist;

Dllist new_dllist();
void free_dllist(Dllist);

void dll_append(Dllist, Jval);
void dll_prepend(Dllist, Jval);
void dll_insert_b(Dllist, Jval);
void dll_insert_a(Dllist, Jval);

void dll_delete_node(Dllist);
int dll_empty(Dllist);

typedef struct jrb_node {
  /* stuff... */
  struct jrb_node *flink;
  struct jrb_node *blink;
  struct jrb_node *parent;
  Jval key;
  Jval val;
} *JRB;

JRB make_jrb();   /* Creates a new rb-tree */

JRB jrb_insert_str(JRB tree, char *key, Jval val);
JRB jrb_insert_int(JRB tree, int ikey, Jval val);
JRB jrb_insert_dbl(JRB tree, double dkey, Jval val);
JRB jrb_insert_gen(JRB tree, Jval key, Jval val, int (*func)(Jval,Jval));

JRB jrb_find_str(JRB root, char *key);
JRB jrb_find_int(JRB root, int ikey);
JRB jrb_find_dbl(JRB root, double dkey);
JRB jrb_find_gen(JRB root, Jval, int (*func)(Jval, Jval));

JRB jrb_find_gte_str(JRB root, char *key, int *found);
JRB jrb_find_gte_int(JRB root, int ikey, int *found);
JRB jrb_find_gte_dbl(JRB root, double dkey, int *found);
JRB jrb_find_gte_gen(JRB root, Jval key, int (*func)(Jval, Jval), int *found);

void jrb_delete_node(JRB node);  /* Deletes and frees a node (but not the key or val) */
void jrb_free_tree(JRB root);  /* Deletes and frees an entire tree */


Question 1 - 0 Points

Please enter your UTK username. For example, mine is jplank.

______________________________


Question 2 - 0 Points

If you feel compelled to write me a note about any of your answers, please do it here, rather than in the answer boxes of the various questions. You don't need to do this; however, I've learned that some students feel that it is necessary.

______________________________


Question 3 - 12 points

In this question, please assume that malloc() is implemented as described in class, and that pointers are 32 bits. Take a look at the following snippet of code:

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

void a()
{
  int *a, *b, *orig_a, *orig_b;

  a = (int *) malloc(6);
  b = (int *) malloc(76);

  printf("Line 1: 0x%lx\n", (unsigned long) a);
  printf("Line 2: 0x%lx\n", (unsigned long) b);

  orig_a = a;
  orig_b = b;

  b -= 2;
  a -= 2; 

  printf("Line 3: %d\n", *a);
  printf("Line 4: %d\n", *b);

  *a += *b;

  free(orig_a);
  free(orig_b);

  printf("Line 5: 0x%x\n", *(a+1));
  printf("Line 6: 0x%x\n", *(a+2));
  printf("Line 7: 0x%x\n", *(b+1));
  printf("Line 8: 0x%x\n", *(b+2));
}

When we run a(), the first two lines are:

Line 1: 0x6008
Line 2: 0xb008

Please answer the following fill-in-the-blank questions. The questions that follow will ask more about this program. In any question, if you can't determine the answer, then answer "unknown."

A: What is the most likely value printed on line 3? [L3]
B: What is the most likely value printed on line 4? [L4]
C: What is the value printed on line 5? [L5]
D: What is the value printed on line 6? [L6]
E: What is the value printed on line 7? [L7]
F: What is the value printed on line 8? [L8]


Question 4 - 4 points

In question 3, I say "most likely value" Please me another possible value than can be printed, and why it would have that value instead of your answer to question 3A?

Question 5 - 4 points

In Question 3, when procedure a() returns, please explain to me exactly why subsequent malloc() and free() calls are going to be problematic.

Question 6 - 15 points

Please explain the exact semantics, and the purpose of the following pthreads routines:
pthread_mutex_lock(pthread_mutex_t *l);
pthread_mutex_unlock(pthread_mutex_t *l);
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *l);
pthread_cond_signal(pthread_cond_t *c);
When it doubt, write more rather than less. Vague and imprecise answers demonstrate that you don't really understand these procedures.

Question 7 - 15 points

You are on a job interview, and the interviewer asks you the following questions. Please answer them in the best possible way, so that you can get the job!

As with question 6, err on the side of more rather than less -- you want this job!

Question 8 - 45 points

You are doing optimization research, which is quite CPU intensive. Multiple researchers in your group have written optimization programs, which are called as follows:
UNIX> program_name key
These are single-threaded programs that can take a while to run -- anywhere from a second or two to minutes. When one of these is done, it will print two words of output on a single line: the key, and a floating point value. For example:
UNIX> ./optimize Fred
Fred 35.582403 
UNIX>
You are running on a machine with multiple processors, and you need to write a driver program that will keep all of those cores busy with optimizations. Here's a picture of how it's going to work:

Your program will first connect to a server over a socket. The server will send lines of text on the socket, where each line is composed of two words, separated by a single space. The first word is the name of a program, and the second is a key. Each is guaranteed to be less than 100 characters.

Your driver program is going to read a line from the server and then create a process that has the program running with the key as its first command line argument. Standard output of the process is connected to a pipe to your driver program. One parameter to your driver is nprocs, the number of processors on your computer. You want to have that many optimizations going on simultaneously. In the picture above nprocs is four.

When a process finishes, your program will detect it, and then read the output over the pipe. Recall from above that the output is the key and a value. In the picture below, the third process has finished, and writes "79a40d91 23.457263" to its standard output, which your driver will read over the pipe:

Your driver will store the output in a red-black tree, keyed on the floating point value (use a double, not a float). The val should be the string ("79a40d91" in the example above). It should then start a new process based on the next line from the server. This is reflected in the next picture:

When the socket connect is closed and all of the optimization processes are done, the driver prints the red-black tree and exits.

Now, I've written the driver main() in the file program.c. I include it below:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "jrb.h"
#include "sockettome.h"
#include "program.h"

int main(int argc, char **argv)
{
  int port;
  char *host;
  int nprocs;
  int fd;
  int done;
  JRB tmp, t;
  char *cl;
  void *v;

  if (argc != 4 || sscanf(argv[2], "%d", &port) == 0 
                || sscanf(argv[3], "%d", &nprocs) == 0) {
    fprintf(stderr, "usage: ./driver host port nprocs\n");
    exit(1);
  }
  host = argv[1];

  fd = request_connection(host, port);
  t = make_jrb();

  v = initialize_v(fd, t, nprocs);
  done = 0;

  while (!done) {
    cl = get_next_command_line(v);
    if (cl == NULL) {
      done = 1;
    } else {
      do_next_test(cl, v);
    }
  }

  finish_up(v);

  jrb_traverse(tmp, t) printf("%lf %s\n", tmp->key.d, tmp->val.s);
  return 0;
}

As you can see the driver calls four procedures, defined in program.h:

void *initialize_v(int fd, JRB t, int nprocs);
char *get_next_command_line(void *v);
void do_next_test(char *cl, void *v);
void finish_up(void *v);

It should be pretty clear from program.c above what these do. However, I'll describe each of them below:

In case it's not clear, you need to define your own data structure, which is what you'll return from initialize_v(), and which is passed to get_next_command_line(), do_next_test() and finish_up(). In the text box below, you need to define this data structure, and write the four procedures defined in program.h. To help -- I also wrote a procedure called wait_for_next_process(), which waited for the next child process to exit, read its output and added it to the red-black tree. I called this in both do_next_test() and finish_up().

(Remember to set your formatting to "preformatted" to get a fixed-width font. Also remember how convenient fdopen() can make your life!).