Exam 1, Solution

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:

  1. A process cannot read from any environment except its own, so there is no way for a child process to (directly) receive information from its parent's environment.
  2. If the parent sets its environment before forking 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:

  1. A process cannot modify the environment of another process, so it is not possible for a process to use an environment belonging to one of its children to (directly) communicate with that child.
  2. If the parent sets its environment before forking 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 forks, 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 31266
What 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.)