Without privileged instructions: Without privileged instructions, any process could execute any instruction (e.g., instructions for interacting with I/O devices, masking interrupts, manipulating virtual memory state, etc.). As a result, a buggy or malicious process could violate memory protection (access data in the address space of another process or the operating system), I/O protection (issue requests to disk for someone else's data), fairness (turning off interrupts to gain full control over the CPU), etc.
Without memory protection: Similar to above, without memory protection any process can potentially access the data of any other process (of any user), as well as the data maintained by the operating system. [Early personal computers lacked such memory protection, and any application could potentially disrupt other applications or the operating system with stray pointers.]
Without timer interrupts: Fundamentally, the timer interrupt is the essential mechanism that enables the operating system to regain control over the CPU on a regular basis. Without timer interrupts, a buggy or malicious process could run indefinitely on the CPU and deny the CPU to other processes and the operating system (e.g., an infinite loop).
Which of the following instructions should be privileged? (Also give a one-sentence explanation for why.)
a) Set value of timer
b) Read the clock
c) Clear memory
d) Turn off interrupts
e) Switch from user to kernel mode
a) Set value of timer: Yes, otherwise the user program can manipulate it such that the OS never gains control
b) Read the Clock: No, as a user can't really do anything harmful by simply reading the clock.
c) Clear Memory: Yes, since a user program shouldn't be able to clear arbitrary memory. (Exception: No, if interpreted as simply clearing memory belonging to the process.)
d) Turn off interrupts: Yes, same reasoning as for setting the value of the timer.
e) Switch from user to kernel mode: Yes, since otherwise a user program could simply switch to kernel mode to execute instructions it wouldn't otherwise be able to, and defeat security.
open: File does not exist.
read: Invalid file descriptor.
fork: No more processes (OS out of memory).
exec: Program file does not exist; file exists, but does not represent a valid executable file (e.g., it is a spreadsheet); file exists, but the user does not have permission to execute it (e.g., the file does not have the exec bit set, or the user does not have permission to execute the file); ...
unlink: File does not exist; user does not have permission to access the file.
On Unix, two signals that cannot be caught and handled by an application are SIGKILL and SIGSTOP.
a) SIGKILL immediately terminates a process. What is a scenario that motivates having the OS make it impossible for an application to handle SIGKILL?
One scenario is a buggy program with an infinite loop. If a program with an infinite loop can handle (and ignore) SIGKILL, then it may not be possible to ever terminate the process without rebooting.
Another scenario is if the process is malware. If malware applications could handle SIGKILL then they could prevent users from terminating them.
b) SIGSTOP immediately pauses a process (which can later be resumed with SIGCONT). Why is it safe (in terms of application correctness) for an application to be arbitrarily paused using SIGSTOP?
It is safe in terms of correctness because applications are already written assuming that they can be paused/suspended at any time. The effect of SIGSTOP is the same as if a process were paused by the OS as a result of a timer interrupt. The difference is that the OS would automatically schedule the process to run again in the future with a timer interrupt, whereas with SIGSTOP a process is paused until it receives a SIGCONT.
Below is a small C program that prints the passage of time in seconds using the simple Unix alarm interface (see man 2 alarm on ieng6):
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
volatile int seconds;
void handler (int signum) {
seconds++;
alarm (1); // schedule next SIGALRM
}
int main (int argc, char *argv[]) {
int last = seconds = 0;
signal (SIGALRM, handler); // SIGALRM is the timer signal
alarm (1); // schedule SIGALRM handler callback, arg is # of seconds
while (1) { // normally we would do useful work instead of spinning
if (last != seconds) {
printf ("%d sec\n", seconds);
last = seconds;
}
if (last == 5) return 0;
}
}
a) What is the output of this program? To verify your answer, you can create a file on ieng6, copy the code into it, and then compile and execute it:
a) The output is:$ ssh account@ieng6.ucsd.edu [do _not_ prep for CSE 120, we need to use the normal system compiler] $ cc q5.c [assuming you put the program contents into q5.c] $ ./a.out
1 sec 2 sec 3 sec 4 sec 5 sec
b) Generally it is not possible to predict how long a program will take to execute by inspecting its code. However, for this program we can. Approximately how long does it take to execute? (You can run it on ieng6 with time ./a.out.)
b) Approximately 5 seconds
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
volatile int seconds;
void handler (int signum) {
seconds++;
alarm (1); // schedule next SIGALRM
}
int main (int argc, char *argv[]) {
int last = seconds = 0;
signal (SIGALRM, handler); // SIGALRM is the timer signal
alarm (1); // schedule SIGALRM handler callback, arg is # of seconds
while (1) { // normally we would do useful work instead of spinning
if (last != seconds) {
printf ("%d sec\n", seconds);
last = seconds;
}
if (seconds == 5) return 0; // line changed from problem 5
}
}
Only one line is changed from Problem 5: the "if (...) return 0" statement in the while loop. When executing the program, sometimes the program prints out "n sec" four times and sometimes five times. Briefly explain why. (Again you can compile the program and experiment with it, although you may need to run the program a handful of times to see different outputs.)
Even though it is a single-threaded program, the timer interrupt introduces a race condition on the variable "seconds". A timer interrupt can invoke the handler in between the two "ifs": the first "if" sees seconds as 4; the timer interrupt fires, invokes the handler, and the handler increments seconds to 5; the second "if" now sees seconds as 5 and exits.
The status value that the child process passes to exit allows the child to indicate to the parent what happened when it executed — in its most basic form, the status value allows the child to indicate to the parent whether the child was successful or not. From this perspective, you can think of the child process as a large, coarse-grained procedure. The parent "invokes" the child as a process, and the child calls exit to pass a return value back to the parent.
Shell scripts frequently take advantage of this functionality, such as when a shell script wants to take a different action depending on whether a command was successful or not. The man pages for command line tools typically end by describing their exit status values and their interpretations. For instance, here is the list from man ls on ieng6:
Exit status:
0 if OK,
1 if minor problems (e.g., cannot access subdirectory),
2 if serious trouble (e.g., cannot access command-line argument).
#include <stdlib.h>
int main (int argc, char *arg[])
{
fork ();
if (fork ()) {
fork ();
} else {
char *argv[2] = {"/bin/ls", NULL};
execv (argv[0], argv);
fork ();
}
}
a) How many total processes are created (including the first process running the program)? (Note that execv is just one of multiple ways of invoking exec, see man 3 exec for all possibilities.)
Hint: You can always add debugging code, compile it, and run the program to experiment with what happens.
6 processes are created, including the first process. For reference, an example process tree looks like:
30938
30938 -> 30939
30939 -> 30940
30940 -> 30942
30938 -> 30941
30941 -> 30943
b) How many times does the /bin/ls program execute?
The /bin/ls program executes twice.
Note that exec does not create a new process, it overwrites the currently running process with a new program. As a result, the branch in the program that calls exec will not call fork after the exec.