Tom Thumb found the Lab 1 "shell" project just too hard, so he decided
to write a "mini-shell" instead. His mini-shell can only run one
sub-program, with mandatory input redirection. For example, the command
"minish wc -l foo.txt
" should run the command "wc -l
< foo.txt
". When the sub-command exits, the mini-shell will
print out "Done!
". Here's his code:
int main(int argc, char **argv) { int infd; pid_t p; // Open the last argv[] argument for redirection infd = open(argv[argc - 1], O_RDONLY); dup2(infd, 0); if ((p = fork()) == 0) { argv[argc - 1] = NULL; // clear out redirection execvp(argv[1], &argv[2]); // run subcommand! } printf("Done!\n"); }
Needless to say, Tom Thumb isn't a very good programmer! Help the poor fellow out.
open()
can fail for many reasons, including
file not found and permission denied.
fork()
can fail, in which case, the shell should notify the
user of the error.execvp()
can fail -- for instance, if the user misspelled
the application name.dup2()
can fail, causing unexpected behavior.open()
, and the arguments
to execvp()
are out of bounds.execvp()
should look like
execp(argv[1], &argv[1])
, since the subcommand's 0th
argument must be the command name.close()
d the infd
file
descriptor after calling dup2()
.dup2()
inside the if
statement. As it is, the parent process will have a file descriptor open
for the redirected input. This is harmless in this minishell, but would not
be harmless if the shell had to run multiple processes.Chastened, Tom returns to the drawing board. Here's his second try.
void sigchld_handler(int signal_value) { int status; (void) wait(&status); printf("Done!\n"); exit(0); } int main(int argc, char **argv) { int infd; pid_t p; // Open the last argv[] argument for redirection infd = open(argv[argc - 1], O_RDONLY); dup2(infd, 0); if ((p = fork()) == 0) { argv[argc - 1] = NULL; // clear out redirection execvp(argv[1], &argv[2]); // run subcommand! } signal(SIGCHLD, &sigchld_handler); while (1) /* Do nothing */; }
This is better, but not much.
Question. Does Tom's code contain any race
conditions, or "Heisenbugs"? If yes, describe a sequence of events that
will trigger a race condition. If no, explain why not.
Answer. Yes, the code contains a race condition. For example, if
the child finishes executing before the signal handler can be installed,
the parent will loop forever, because it will never receive a signal from
the child.
Question. Describe the performance consequences
of Tom's waiting strategy (the while
loop in
main
). Could Tom do better? Why and how? Refer to specific
operating system structures discussed in class.
Answer.
The parent is busy-waiting while the child executes. This means the parent
is competing with the child for CPU cycles even though it is not doing
anything productive. It would be better if the parent blocks until the child
finishs. Tom can do this using a wait()
or waitpid()
call in place of signal()
.
fork()
fails, then no signal will ever get sent to the signal
handler.execvp()
fails, then the child will fall out of the
if
statement and loop forever, so no signal will ever get sent
to the parent's signal handler.open()
/dup()
fails for some reason, the child
process may be stuck waiting for input from stdin. As long as the child process
is doing this, the parent will hang around waiting for the child.All your CS 111 TAs love mutexes. In fact, they love mutexes to an insane degree. Last weekend on a late-night skim-milk bender, they wrote three programs that attempt to synchronize on three different mutexes. Here they are:
mutex_t l1, l2, l3; process_1() { acquire(l3); acquire(l1); release(l1); release(l3); } process_2() { acquire(l2); acquire(l3); acquire(l2); release(l2); release(l2); release(l3); } process_3() { acquire(l3); acquire(l1); acquire(l2); release(l1); release(l3); release(l2); }
The TAs were supposed to figure out whether these processes contained a deadlock, but cleverly, they've decided to pass off this work to you.
mutex_t
is a simple mutual-exclusion lock.mutex_t
is a recursive mutual-exclusion lock.mutex_t
is a semaphore allowing at most two
processes to hold the lock at a time.l2
, and then process 3 can
acquire l3
and l1
. At this point, no process can
proceed. Process 1 and 2 are trying to acquire l3
, held by
process 3. Process 3 is trying to acquire l2
, held by process
2.l2
multiple times.
However, the circular wait condition as previously described will still cause
a deadlock.l1
. This means no
sequence of acquires will cause a process to block on l1
.
Additionally, process 1 will run to completion if it can grab l3
.
The only time process 1 can block is when both process 2 and 3 hold
l3
. Knowing this, proving process 2 and 3 will never deadlock
among themselves is sufficient to prove the whole system will not deadlock.
l3
,
thus l3
cannot cause either process to block. For l2
,
if process 2 acquires l2
twice, then it eventually runs to
completion. If process 2 acquires l2
once, then process 3 can
acquire l2
and run to completion.
mutex_t l1, l2, l3; process_1() { acquire(l3); acquire(l1); release(l1); release(l3); } process_2() { acquire(l3); acquire(l2); acquire(l2); release(l2); release(l2); release(l3); } process_3() { acquire(l3); acquire(l2); acquire(l1); release(l1); release(l2); release(l3); }
Extra Credit. Put a box around the lame joke in the preceding question.