CS360 Final Exam - April 30, 2015 - James S. Plank
Answers and Explanations
Question 1
Just because the code is here on an exam doesn't mean that you should write code like
this. This is bad code. But the purpose is to allow you to demonstrate your
understanding of malloc().
As described in the lecture notes, the size of the allocated chunk is stored
8 bytes behind the pointer returned from malloc(). Therefore, after
the XXX statement, p and q point to the sizes of their allocated
chunks:
- A: Round up to a multiple of eight and add eight bytes of padding: 16.
- B: Same thing: 96.
- C: Suppose you have a chunk of 24 bytes, and you want to carve off 16. The remainder
is too small to put back on the free list, so you simply return the 24 bytes to the user.
The answer is 24, and there's not really a reasonable other answer.
- D: The problem is that you are adding 96 to the size of p's chunk. That
means that when you call free(r), you are freeing 96 extra bytes, and those bytes are
going to be either allocated or already on the free list. When they are allocated again as
part of the 112-byte chunk, they will be in use for two different purposes. That is
a disaster. Secondarily, there is a memory lead with q. That's really less of a problem.
- E: If q is 0x5018, then the p and q chunks are adjacent. Now,
the fact that q is a memory leak is ok, because we've changed p's size to encompass
q exactly. So, there's no problem in this case.
Grading: 3 points per part, (4 on part D).
Question 2
This was the only code-writing of the exam. It's a pretty straightforward
fork()/dup()/exec() program, with a socket connection thrown in:
Call pipe and fork. In the child, dup standard output onto the pipe and
call exec for /home/boss/del. In the child, close standard input (so that the
child can see EOF), and then read from the pipe. Send all lines from the pipe
to both standard output and the socket connection:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "sockettome.h"
main()
{
int p[2];
char buf[1000];
int fd;
FILE *fin;
FILE *fsock;
pipe(p);
if (fork() == 0) {
dup2(p[1], 1);
close(p[1]);
close(p[0]);
execl("/home/boss/dez", "dez", NULL);
perror("Execl failed");
exit(1);
}
fd = request_connection("boss.evilcorp.com", 6666);
fsock = fdopen(fd, "w");
fin = fdopen(p[0], "r");
close(p[1]);
while (fgets(buf, 1000, fin) != NULL) {
fputs(buf, stdout);
fflush(stdout);
fputs(buf, fsock);
fflush(fsock);
}
exit(0);
}
|
Grading: The following components made up the grade:
- Calling pipe before fork. (2 points)
- Calling one of the exec variants in the child. (1 point)
- Calling dup2 of P[1] in the child. (1 point)
- Closing P[0] in the child. (0.5 points)
- Closing P[1] in the child. (0.5 points)
- Closing fd 0 in the parent (1 point)
- Calling request_connection() in the parent (1 point)
- Closing P[1] in the parent. (1 point)
- Reading lines from p[0] in the parent. (1 point)
- Detecting when p[0] is done in the parent. (2 points)
- Writing lines to the socket (1 point)
- Writing lines to standard output (1 point)
- Getting the writes correct (1 point)
- Nothing extraneous (like extra dup2 calls, accept_connecetion, etc). 2 points
You got the "close fd 0" point if you called dup2(p[0], 0), but you didn't get the point
if you read from p[0] rather than 0 afterwards.
Question 3
The code is in
q3.c.
I was hoping that the code here was straightforward enough. We fork off as many
threads as there are words on the command line. The characters of the
words then become commands to the threads.
- Run 1: There are two threads, and they are going to print their id's twice
each. There's no guarantee which thread runs first, but when a thread locks the mutex,
it will do all of its printing before the other thread. Therefore, there are two answers:
"A A B B" and "B B A A":
c and s.
- Run 2: There are three threads. The first two will print and block on the
first condition variable. The third thread will print and exit the program. There
are many orders in which the threads can run -- you need to account for all of them that
end when the third thread gets control:
h, i,
p,
t and v.
- Run 3: Now things are completely deterministic: Thread A will block,
while threads B and C sleep. Thread B wakes up, unblocks thread A, prints and exits.
Threads A and C wake up, and both go back to blocking. For thread A, this is on the
second condition variable. For thread C, it's another sleep call. Thread C wakes up
a second later, wakes up thread A and prints. Thread A will then get the mutex and
print. The answer is "B C A":
u.
- Run 4: This is very similar to the case above, only now thread B is doing
both wakeups. The answer is "B B A":
r.
- Run 5: This is does one of two things, depending on which thread gets
control first. If thread A gets control, then it will block, and thread B will get control,
print, and wake up thread A. Thread A prints while thread B sleeps, and then the process exits:
"B A". If instead thread B gets control first, it will call pthread_cond_signal(), but
there is no thread waiting on the condition variable, so the call does nothing. Thread
B prints and sleeps. Now thread A starts and blocks. When thread B wakes up from sleeping,
it kills the process while thread A is still blocked. The answer then is "B":
k and
l.
- Run 6: Thread A will get control first, print and block. Thread B will wake up
from sleeping, print and exit: "A B":
d.
- Run 7: Threads A and C will both block, on condition variables one and two
respectively. Thread B sleeps, prints, and wakes up the two other threads. Although it
wakes up thread A first, the order in which threads A and C run depends on which one gets
the mutex first after thread B unlocks it. Therefore, there are two answers: "B A C" and "B C A":
p and
u.
If you want to verify, I have two shell scripts -- q3-manyrun.sh that runs
q3 a given number of times -- and q3-script.sh that runs each test case and pipes
the answer to sort -u. Running it on my macintosh gives every answer described above:
UNIX> sh q3-script.sh
Run 1
A A B B
B B A A
Run 2
A B C
A C
B A C
B C
C
Run 3
B C A
Run 4
B B A
Run 5
B
B A
Run 6
A B
Run 7
B A C
B C A
UNIX>
Grading: Runs 2 and 7 were worth 3, and the rest were worth 2. Divide the points by the number
of correct answers per run, and that's what each answer is worth. Incorrect answers are negative
half that amount. You couldn't get negative for a part.
Question 4
This one is similar in flavor to Question 3, in that we are using the strings of argv as
commands. This time, though, it is to two communicating processes. The process with
id equal to 1 is the child, and the pricess with id equal to 2 is the parent.
The processes share a pipe in the variable p.
Each process has an output file in f, which is either O1.txt (for the child) or O2.txt
(for the parent).
The commands that each process can execute are:
- 'A': Close the read end of the pipe.
- 'B': Close the write end of the pipe.
- 'C': Write a line with BSIZE-1 'A' characters followed by a newline, to the pipe.
- 'D': Read BSIZE characters from the pipe and write it to the output file.
- 'S': Sleep for a second.
You are asked, for each run, how the processes exits, and what their output files are.
- Run 1: The child writes 16,000 characters to the pipe and exits.
The parent reads those characters and prints them. The parent tries to read again, but
since the write end of the pipe hasn't been closed, the parent will hang. The answer
to this is:
a, g, i and n.
I have put some print statements into
q4.c so that you can see what happens:
UNIX> q4 C DD
Parent is pid 46682
Child is pid 46683
Pid 46683: Normal Exit
^C
UNIX> wc OC.txt ; wc OP.txt
0 0 0 OC.txt
1 1 16000 OP.txt
UNIX>
- Run 2: The child now closes the read end of the pipe, and the
parent closes the write end. The child writes 16,000 characters three times,
and the parent reads only once. The parent will now exit normally. The
child's second write() call will fill up the pipe buffer, but the third
will block. When the parent exits, there will be no read end of the pipe.
Therefore, the operating system will note that the child is writing to a pipe
with no read end. That means the child will exit on SIGPIPE:
d, e, i and n.
UNIX> q4 ACCC BD
Child is pid 46736
Parent is pid 46735
Pid 46735: Normal Exit
Pid 46736: Caught sigpipe
UNIX> wc OC.txt ; wc OP.txt
0 0 0 OC.txt
1 1 16000 OP.txt
UNIX>
- Run 3: The child sleeps for a second. The parent performs two writes
into the pipe. The second of these will block, because the pipe buffer is filled.
The child wakes up a second later, reads 16,000 bytes from the pipe and exits.
The parent will unblock at a this point and close the write end of the pipe.
it reads 16,000 bytes, and tries to read again. As there is no write end of the
pipe, the parent will get a return value of 0 from the read() call. It will
exit with a return code of 1:
a, f, j and n.
UNIX> q4 SD CCBDD
Parent is pid 46779
Child is pid 46780
Pid 46780: Normal Exit
Pid 46779: Exit on read from p[0]
UNIX> wc OC.txt ; wc OP.txt
1 1 16000 OC.txt
1 1 16000 OP.txt
UNIX>
- Run 4: The parent goes to sleep instantly. The child writes into the pipe,
and then reads what it wrote. The parent does the same thing. This one is pretty
straightforward:
a, e, j and n.
UNIX> q4 CD SCD
Parent is pid 46798
Child is pid 46799
Pid 46799: Normal Exit
Pid 46798: Normal Exit
UNIX> wc OC.txt ; wc OP.txt
1 1 16000 OC.txt
1 1 16000 OP.txt
UNIX>
- Run 5: The parent is the simple one -- it writes to the pipe and exits.
The child closes the write end of the pipe and tries to read twice. The first read is
successful and the second returns 0, so the child exits with a return code of 1:
b, e, j and m.
UNIX> q4 BDD C
Parent is pid 46822
Pid 46822: Normal Exit
Child is pid 46823
Pid 46823: Exit on read from p[0]
UNIX> wc OC.txt ; wc OP.txt
1 1 16000 OC.txt
0 0 0 OP.txt
UNIX>
- Run 6: The child writes four times, and the parent reads twice.
The parent will then exit normally. The third of the child's write() calls
will return, because the buffer is not full, but the fourth will block. Because the
child has not closed the read end of the pipe, the child will block forever:
c, e, i and o.
UNIX> q4 CCCC DD
Parent is pid 46845
Child is pid 46846
Pid 46845: Normal Exit
UNIX> ps | grep q4
46846 ttys001 0:00.00 q4 CCCC DD
46848 ttys001 0:00.00 grep q4
UNIX> kill 46846
UNIX> wc OC.txt ; wc OP.txt
0 0 0 OC.txt
2 2 32000 OP.txt
UNIX>
- Run 7: The child writes twice, and the parent reads once. The parent will
exit normally. The child's second write() will complete, because the buffer is full,
and because there is still an open read end of the pipe. The child then exits normally:
a, e, i and n.
UNIX> q4 CC D
Parent is pid 46894
Child is pid 46895
Pid 46895: Normal Exit
Pid 46894: Normal Exit
UNIX> wc OC.txt ; wc OP.txt
0 0 0 OC.txt
1 1 16000 OP.txt
UNIX>
- Run 8: The child closes the write end of the pipe and reads. The parent closes
the read end of the pipe and writes. Therefore, the child's read() call will return,
and the child exits normally. The parent sleeps for a second and then tries to write to
the pipe. However, the child is now dead, so there is no read end. That will generate
SIGPIPE in the parent:
a, h, j and m.
UNIX> q4 BD ACSC
Parent is pid 46905
Child is pid 46906
Pid 46906: Normal Exit
Pid 46905: Caught sigpipe
UNIX> wc OC.txt ; wc OP.txt
1 1 16000 OC.txt
0 0 0 OP.txt
UNIX>
Grading: 0.5 per correct answer.