Question 1: Devices and I/O (10 points)
Do device drivers contribute to horizontal abstraction, vertical abstraction, neither, or both? Explain your answer.
Device drivers contribute to both horizontal and vertical abstraction.
Device drivers allow people to write operating systems code that interacts with devices at a high level of abstraction. The code can treat all devices of a given catagory the same and let the driver code implement the low-level details that map those operations to a particular piece of hardware (a physical device). This is an example of vertical abstraction.
Similarly, because the OS code will be written to interact with entire catagories of devices by maintaining a high-level abstraction of them, it is possible for a device manufacturor to create a new device with its own hardware peculiarities, yet have that device useable by many different OSs simply by writing appropriate device drivers. This simple movement of components across systems is an example of horizontal abstraction.
Note that other examples of horizontal abstraction could be given as well. For example, the fact that different devices could easily be used on the same OS by writing device drivers for them, is another example of horizontal abstraction.
Question 2: Standard I/O and Resource Management (20 points)
A. We discussed the fact that the ANSI C Standard I/O library function
gets
is deprecated because it is a security hole and that the ANSI C
Standard I/O library function fgets
is, therefore, the better choice
to use in your code. What is different about gets
and fgets
that makes fgets
safer?
4 pts.
With gets
there is no way for the calling function to
specify the maximum number of characters (bytes) to be returned.
Therefore, it is very possible that gets
will return more
characters than there is space allocated to hold them. This is known as a
buffer overflow. Depending on the implementation, a buffer overflow may
variously cause data loss, data corruption, or even code corruption. Both
data and code corruption may occur when the data is placed into memory
addresses beyond those that had been allocated to hold the characters
returned by gets
. In the worst case, with a targeted attack
on code known to be subject to buffer overflows, the attacker may manage to
corrupt the code in such a way as to replace a part of it with the
attacker's own code, giving the attacker control over the functioning of
the process that suffered the buffer overflow.
With fgets
, in contrast, the calling function is required
to specify the maximum number of characters (bytes) to be returned.
Therefore, if fgets
is used properly, it will not be subject
to buffer overflows. (Note, however, that fgets
may be used
improperly. A programmer may mistakenly specify that more characters may
be returned than are bytes allocated for them and, therefore, buffer
overflows are still possible with fgets
. This is why the
question asks why fgets
is safer than
gets
, rather than asking why fgets
is safe
- it isn't. A good compiler will warn the programmer that he or she is
using fgets
improperly and unsafely but a poor programmer
may ignore these warnings.)
Despite the fact that gets
is a security hole, Stefan uses it
anyway in code that he is writing. He compiles and runs the code on an
Operating System that is properly fulfilling its role as a resource
manager. Stefan is an ordinary user of this system. Under these
circumstances, which of the following has Stefan put at risk? For each,
explain your answer.
B. The process running this code.
4 pts.
The process running Stefan's code that uses gets
is
certainly at risk. It is this code that may suffer from data loss, data
corruption, or even code corruption if gets
is called. A
general purpose OS should allow processes to do as they please with the
memory allocated to them, so there is no way for such an OS to prevent a
process from storing what is, from the programmer's perspective, the wrong
thing in the wrong place within that memory region. From the perspective
of the OS, the process is simply using the memory allocated to it; there is
no notion of right or wrong use of that resource.
C. Processes that are descendants of the process running this code.
4 pts.
Processes that are descendants of the process running this code are also
at risk. First, a descendant is created using fork
to be
virtually identical to the process that created it. This means that the
descendant's code also contains gets
and may suffer from a
buffer overflow if gets
is called. Second, if the parent
process has already called gets
and its data has already been
damaged by a buffer overflow, then the data of the descendant process will
also be damaged.
The descendant process may also be indirectly at risk as well. For
example, if it is using the same files as its parent, it may get faulty
data by reading a file into which its parent has written corrupt data.
D. Other processes Stephan is running at the same time as this code.
4 pts.
Other processes Stephan is running at the same time as this code are not
put directly at risk by Stephan's use of gets
in this code, at
least for the most part. An OS that is properly fulfilling its role as a
resource manager protects the memory allocated to one process from being
written into by any other process, unless the processes have explicitly
agreed to share memory. (Since we haven't covered shared memory in this
course yet, you weren't expected to include the exception regarding shared
memory in your answer.) Because other processes have their own memory
assigned to them, the buffer overflow that may occur in the process running
the code containing gets
cannot directly damage the data or
code of these other processes.
However, the other processes Stephan is running may still be indirectly at risk of getting bad data if they share information with the at-risk code. See the example above in part C.
(I don't know why Stefan changed his name to Stephan for parts D and E. It just happened.)
E. Other processes that other users are running on the system at the time that Stephan is running this code.
4 pts.
As with other processes that Stephan is running, other processes that
other users are running on the system at the time that Stephan is running
this code are not put directly at risk by Stephan's use of
gets
in this code, except for processes that have agreed to
share memory. (Again, you don't need to mention the shared memory
exception, since we haven't covered it yet in this course.) The reasons
are exactly the same.
However, the other processes that other users are running on the system at the time that Stephan is running this code may still be indirectly at risk of getting bad data if they share information with the at-risk code, just as with other processes that Stephan is running. Again, see the example above in part C.
Notes:
I was thinking of direct risks, rather than indirect risks when I wrote this question. I gave credit to people who gave answers relating to indirect risks but did not deduct points from those who only talked about direct risks. Any program that shares data with another program is at risk of getting bad data from the other program, regardless of things like buffer overflows.
Several people answered that Stefan's use of gets
put all
other processes on the system at risk because his buffer overflows could
crash the system. WRONG! While having the system crash would
certainly be a problem for these other processes, the question states that
"Stefan is an ordinary user of this system" and he "runs the code on an
Operating System that is properly fulfilling its role as a resource
manager," An OS that is properly fulfilling this role will not crash
because an ordinary user runs buggy code, no matter what the bug is. By
definition, any OS that crashes when running on adequate and properly
functioning hardware is a defective OS. It is probably a reflection of the
poor quality operating systems that students are used to using that they
believe that a crash due to buggy user code is not an indication of OS
failure when, in fact, it is.
Question 3: Daemons, Process Groups, and Sessions (20 points)
Recall that we discussed in class several steps that daemons commonly go
through when they are started up and that the first few of these are
typically to fork
a child, have the parent exit
, and have the
child continue as the daemon process. Now, note that while init
,
pageout
, and fsflush
are all daemon processes that run on the
CS network Solaris machines, the following output from the ps
command
shows that these processes did not go through the typical steps listed
above.
turing:~ $ ps -ef UID PID PPID C STIME TTY TIME CMD root 0 0 0 Aug 22 ? 0:01 sched root 1 0 0 Aug 22 ? 0:25 /etc/init - root 2 0 0 Aug 22 ? 0:01 pageout root 3 0 1 Aug 22 ? 556:08 fsflush . . .
A. How can we tell by the above output from ps
that init
,
pageout
, and fsflush
did not go through the typical start-up
steps listed above?
10 pts.
There are two ways that we can tell this from the output from the ps
command given above. You only needed to discuss one of these ways in
order to get full credit.
The most straightforward is to look at the PID column. As we have
discussed, every new process that enters the system gets the next available
process ID number. When the system is started, all process ID numbers are
available, so a counter is simply incremented for each new assignment. If
init
, pageout
, and fsflush
had gone through the typical
start-up steps listed above, each would have created a child process with
fork
. Each child process would, of course, get its own PID and each
parent would exit, taking its (the parent's) PID with it. This would leave
gaps in the PID numbers that would show up in the output above. Note,
however, the numbers are seqential. Hence, the typical start-up steps
listed above were not followed.
We can also tell this by looking at the PPID column. We also discussed
the idea of orphaned processes and said that they are inherited by
init
. As we discussed, and as shown above, init
has process ID 1. If init
, pageout
, and fsflush
had gone
through the typical start-up steps listed above, each would have created a
child process with fork
, each of these children would have been
orphaned when its parent exited, and each of these children would have been
inherited by init
- i.e., their PPID numbers would be 1.
However, each of these processes has a PPID of 0. Hence, the typical
start-up steps listed above were not followed.
(The idea of init
being orphaned and inherited by itself is
somewhat bizarre. Fortunately, we don't have to worry about how that would
happen, since it doesn't; the typical start-up steps for daemon processes
are not followed by init
.)
B. Why didn't these processes bother to go through the typical start-up steps listed above?
10 pts.
They didn't have to. As we discussed, the purpose of these steps is to
isolate the daemon process from a controlling terminal so that if the
terminal disconnects, the daemon process will continue to run. However,
since these processes were started up with no controlling terminal, there
is no reason for them to fork
children, exit, have the
children move into new sessions away from the controlling terminal, and so
forth.
You could infer this from the PPIDs shown in the output which show that
these processes regard the schedular itself as their parent - if
sched
were associated with a controlling terminal and were to
exit, then the system would stop functioning because there would be no
assignment of processes to the CPU. (Also, if you were used to reading the
output of ps
you would know that the question marks under the
TTY column heading indicate that no controlling terminal is associated with
those processes.)
Question 4: Scheduling (20 points)
Two scheduling performance measures that we discussed for processes are:
1. Turnaround Time The total amount of time that passes in the world between the time a process enters the system (makes the transition from the new state to the ready state) and the time that the process leaves the system (makes the transition from the ready state to the terminated state), having completed successfully.
2. Response Time The average time that a process spends in the ready and/or waiting states before being moved to the running state, after user input is given.
For each of the scheduling strategies listed below, say which of the performance measures listed above are reasonable measures to use. Explain your answers.
i. First Come, First Served.
4 pts.
First come, first served (FCFS) is a simple non-preemptive scheduling algorithm that is only suitable for non-interactive batch systems. It isn't appropriate for an interactive system because it allows for arbitrarily long delays before responding to user input. (For example, if the CPU is allocated to process A, it can keep using it indefinitely, despite the fact that a user is providing input to process B.) For this reason, it isn't reasonable to try to measure its performance with respect to response time (RT), which is only an appropriate performance measure for interactive systems - RT measures how quickly the system responds to user input.
On the other hand, turnaround time (TT) is an appropriate performance measure for non-interactive systems but not for interactive ones. In a non-interactive system, we can consider the the time it takes to get jobs done and, in many cases, doing more jobs more quickly is better. In an interactive system, however, TT will be dependent on user responses and the speed and order of them. This means that measuring TT in an interactive system is not reasonable. For these reasons, measuring TT for FCFS is reasonable, even thought it will not perform well on this measure.
ii. Shortest Job Next.
4 pts.
As with FCFS, shortest job next (SJN) is a non-preemptive scheduling algorithm that is only suitable for batch (non-interactive) systems and for the same reason. The same logic that we applied to FCFS can be applied to SJN to determine that it is reasonable to use TT as a performance measure with SJN but that RT is not a reasonable performance measure to use.
iii. Earliest Deadline First Scheduling.
4 pts.
The earliest deadline first scheduling strategy is used for real time systems, where TT & RT are irrelevant. What is important in real time systems is that deadlines are met. To try to use either TT or RT in this case is not reasonable.
iv. Round Robin.
4 pts.
Round robin (RR) is a preemptive scheduling strategy that can be used in both interactive and non-interactive batch systems. When it is used in an interactive system, RT is reasonable to measure, even though RR will not do well in that regard as compared to a priority scheduling strategy with higher priorities for interactive processes. When it is used in a batch system, it is reasonable to measure TT.
v. Priority Scheduling using Multi-Level Queues.
4 pts.
Priority scheduling using multi-level queues is another preemptive scheduling strategy that can be used in both interactive and non-interactive batch systems. As with RR, therefore, both RT and TT are reasonable performance measures to use.
Question 5: Process Environments (20 points)
A. Under what circumstances can a process use its own environment to communicate information to a child process? Explain your answer.
5 pts.
There are two equally valid ways to answer this question, depending on whether you are thinking of direct communication:
fork
ing and the
child reads from its environment, then the parent is using its own
environment to (indirectly) communicate information to a child process.
This is because, when the fork
happens, the OS makes a copy of
the parent's environment that is given to the child and from which the
child will subsequently read.B. Under what circumstances can a process use a child's environment to communicate information to that child process? Explain your answer.
5 pts.
As with part A, there are two equally valid ways to answer this question, depending on whether you are thinking of direct communication:
fork
ing and the
child reads from its environment, then the parent is (indirectly) using the
child's environment to communicate information to a child process. This is
because, when it fork
s, the parent process is requesting that
the OS create the child's environment as a copy of the parent's
environment. That copy is given to the child and the child will
subsequently read from it.C. Under what circumstances can a process use its own environment to communicate information to its parent process? Explain your answer.
5 pts.
Regardless of whether you are thinking of direct or indirect communication, there are no circumstances under which a process can use its own environment to communicate information to its parent process. The parent cannot read the child's environment, nor can the child request that the OS modify or set its parent's environment based on the child's environment.
D. Under what circumstances can a process use its parent's environment to communicate information to its parent process? Explain your answer.
5 pts.
As with part C, regardless of whether you are thinking of direct or indirect communication, there are no circumstances under which a process can use its parent's environment to communicate information to its parent process. The child process cannot modify its parent's environment, nor can it request that the OS modify or set its parent's environment on the child's behalf.
NOTE: Many people discussed using exec
-family system
calls in their answers to the various parts of this question. Partial
credit was given to these answers, since it is easy to get confused about
what constitutes a parent-child relationship. However, please note that an
exec
-family system call does not set up a parent-child
relationship because no new (child) process is created. Instead, when an
exec
-family system call is performed, the SAME process
continues to run. This process may now be running different code (or it
may be running the same code, for that matter) but it is still the same
function.
Question 6: exec
(10 points)
Examine the following code:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main (void){ pid_t mypid; mypid = getpid(); printf ("My process ID is %d\n", (int) mypid); execl ("exectest", "exectest", (char *) 0); perror ("Error calling execl:"); exit(1); }Assume that the executable is called
exectest
and that when I run it,
the first line printed by this code is
My process ID is 31266What is the remainder of the output of this code likely to be? Why?
The remainder of the output of the code is likely to be:
My process ID is 31266 My process ID is 31266 My process ID is 31266 . . .The same line will be printed over and over until the process is halted (e.g., if the user interrupts process execution by pressing
ctrl+c
).
The reason: When execl
is called in this code, it starts
executing the same code from the beginning. Thus, the same data is
initialized every time execl
is called.
Many students thought that the output of the code will like be something along the lines of:
My process ID is 31267 My process ID is 31268 My process ID is 31269 . . .This is wrong because
exec
-family functions do not create new
processes (new processes are created using fork
) and only new
processes have different process ID numbers. Because
exec
-family functions simply continue the same process, the
process ID number (and many other elements of the process control block)
remain unchanged. This is true even if they are used to run different
code, which is how they are generally used.
Some students also thought that the execl
call would fail
because fork
was not called first. This is also wrong. We
often use fork
before calling an exec
-family
function, in order to have a new process accomplish something on behalf of
an existing process. However, it is important to know that this is just a
common way of doing things - there is nothing manditory about it. If the
code of the existing process is done with what it needs to do, there is no
reason for it to fork
before calling execl
,
Instead, it can call execl
, which throws away the old
(completed) code and replaces it with the new code. (In this question, the
old and new code were simply the same code.)