Operating Systems, Fall 2001

Programming Assignment 2 - An Example Solution


Table of Contents

Introduction

This page presents a solution to the second programming assignment, which involves writing a device managers for the disk and terminal.

Only the portions of the solution which represent additions or significant changes to the solution for the first programming assignment are described here. Everything not described here is essentially identical the the solution for the first programming assignment.

Design

The device drivers have two main tasks: to share the devices among processes wanting to use the device, and to present a reasonable abstraction of the device to the processes.

Sharing the terminal is simple, largely because the terminal is a serially-reusable resource, and so can only be used by one process at a time. Sharing in this case consists of figuring out which process gets the terminal next, which, assuming you're not worried about fairness, isn't a complicated task.

Sharing the terminal is also simple because it only needs to be shared among user processes; the operating system doesn't make use of the terminal. The disk, however, is shared between the user processes and the operating system, and this makes sharing a bit more complicated. Some of the complication is an consequence of the two-part simulator, but some is due to the privileged position kernel code holds over user processes. Fortunately, this second form of complication can be controlled by designing the operating system so that the differences between user and kernel code are reduced, which, in addition to reducing complications, is a generally useful design guideline.

The Disk Driver

The disk driver presents a simple interface to the rest of the system. The driver needs to be initialized once at system boot-up: and clients of the disk driver need to be able to make disk I-O requests, passing along the following information When a disk I-O request has finished, the disk driver will call the callback function associated with the request, passing in the status returned by the disk-io operation and the argument passed in with the request. A client's disk I-O request information is kept in a structure, the fields of which have the same interpretation as the corresponding parameters in the disk_dvr::request() procedure. Pending I-O requests are hung on a queue. The driver needs to keep track whether or not a disk I-O request has been issued but not completed. Every once in a while (such as when a new disk I-O request gets hung on the queue), the disk driver needs to be poked to make sure it's handling outstanding requests.

If there's a I-O request in progress, or there's no pending requests, there's nothing for the disk driver to do. Otherwise, it should start the disk I-O operation requested at the head of the pending queue. The pending request is left on the queue so it can be dealt with when the request is finished.

A disk interrupt marks the end of a disk I-O request made by the head of the pending queue. After notifying the requester via the supplied call-back, the disk driver starts the next request, if any. Initializing the disk driver involves setting the disk interrupt handler. The information associated with a disk I-O request gets hung on the pending requests queue and the driver gets poked in case it was idle, waiting for a new request. This roundabout way can be optimized slightly by issuing the request directly if the queue is empty, but the extra code is more complex than the code below, and the time saved is immeasurable compared to the disk I-O time anyway.

The Disk Manager

The disk manager presents a simple interface to its clients. The parameters for an open are passed via the registers used to make the system call; the return values are passed the same way. Closing a connection to the disk requires the disk id associated with the connections. A disk I-O operation needs to know the operation (op), the disk I-O channel involved with the operation (dev), the user-space address involved with the operation (loc), and the number of the disk block involved (n). Finally, a predicate that determines whether or not the given value is a valid disk id. Each instance of an opened disk is referenced by an unsigned integer called the Disk_id. The set of currently active disk ids is stored in, naturally, a set.

This is not a particularly intelligent or safe approach to take, but it has the great virtue of being simple. It can be made safer against spoofing by keeping track of a (pid, did) pairs, where did is an disk id and pid is the id of the process to which did was allocated. A disk id did would be valid if and only if the id of the process holding did is pid and (pid, did) is a recognized pair.

When a process's disk I-O request is done, the disk manager needs to know

to finish processing the request.

When a process's disk I-O request is complete, the disk manager needs to store the return values in the process's registers and return the process on the ready queue. Assuming the disk id is valid, closing a disk connection involves just removing the disk id from the set of currently valid disk ids and returning ok to the closing process. A disk id is valid if it's in the set of valid disk ids. (How convenient is that?) Because any process can establish a connection to the disk and the disk can support any number of connections, disk opens always succeed. After checking the arguments to make sure they're reasonable, the disk I-O request is forwarded to the disk driver and the calling process is suspended until the request is completed.

The Terminal Manager

The terminal manager presents the same interface to its clients as does the disk manager. Closing a connection to the terminal requires the terminal id associated with the connection. The parameters for an open are passed via the registers used to make the system call; the return values are passed the same way. A terminal I-O operation needs to know the operation (op), the terminal I-O channel involved with the operation (dev), the user-space address involved with the operation (loc), and the number of bytes involved (n). Finally, some way of determining whether or not a word holds a valid terminal id. Because there can be at most one outstanding terminal I-O request, handling the request data is considerably simpler than it is for the disk, which may have to keep track of the data for an arbitrary number of outstanding I-O requests.

The terminal manager uses a set of global variables rather than define a structure and a queue. The globals are

Check the previous terminal I-O operation to make sure it went off without an error; die if there was an error. Do the terminal I-O operation _op on behalf of procedure _c. Store into _c the character found at address _l on behalf of procedure _w. This is a little bit tricky because if the _l is in the process's register set, the character has to come from the process's saved context (the process having been suspended until the requested I-O operation is finished). When an interrupt signaling the end of the previous terminal write arrives, then either all the characters have been written to the terminal and the requesting process can be hung back on the ready queue, or there's still more characters to write, in which case the next character should be fetched and written to the terminal. When an interrupt signaling the end of the previous terminal read arrives, then either an actual character was read or the terminal sent an eof. An actual character has to be stored wherever the caller specified (keeping in mind that characters going into the register set have to be stored in the process's saved context).

If all the characters requested have been read, or there's an eof, then the requesting process can be hung back on the ready queue. Otherwise, go read the next character from the terminal.

Closing the connection frees the terminal to be re-opened. terminal_mgr::is_valid_id() is too simple, because it can be spoofed. Oh well. Either the terminal is available for opening, in which case it's allocated to the requestor, or it already open, in which case the requestor gets an error. After checking the arguments to make sure they're reasonable, terminal_mgr::operation() suspends the calling process until the request is completed and issues the requested terminal I-O operation. Each I-O operation has a different interrupt handler; the proper interrupt handler is installed before the operation is issued. *
<terminal-mgr.cc>=

#include "terminal-mgr.h"
#include "os.h"
#include "process.h"

<terminal manager data structures>

<terminal manager macros>

<terminal manager procedures>

namespace terminal_mgr {
  <terminal manager interface procedures>
  }
Defines terminal-mgr.cc (links are to index).

The Rest of the OS

The rest of the operating system requires no major changes, just adjustments to changes in the disk interface and the addition of the disk and terminal managers.

This section describes the changes made to the rest of the operating system. Only examples of the changes will be described here; see the solution for the first assignment for full details of the remaining parts.

The Program Loader

The most significant modifications to the remaining pieces of the assignment 1 operating system occurred in the program loader, because it was written under the assumption that it was the only code accessing the disk. This assumption is no longer true in the assignment 2 operating system, where the disk is shared with user processes.

Despite the significant change in the assumption about disk ownership, the actual code changes needed in the program-loader code are minor. The program loader no longer directly manipulates the disk-control registers to perform disk I-O, but rather makes a disk-driver request.

Also, the disk status comes back as an argument to the callback function; no need to access the disk-control register for the status. The disk-interrupt handlers have to be rewritten as disk-driver callback functions, but that involves changing the handler signature to match that of the disk-driver callback prototype. For example, the read_rest_program() interrupt handler now has the signature

void read_rest_program(status::responses r, void *)

Because the program-loader has at most one outstanding disk I-O request at a time, the information on the request can be kept as global variables, and the pass-along argument to the callback isn't used. The signatures of the other interrupt handlers used by the program loader are changed similarly; the body of each program loader remains unchanged.

Interrupt Handling and Initialization

The system-call interrupt handler needs to be extended to deal with the four new system calls open, close, read, and write. The only slightly tricky part is remembering that the last three system calls have to check the device-id type to make sure the proper device handler gets called.

For example, the read and write system calls are folded into one case-statement branch and dealt with using a general-purpose device-io procedure.

After retrieving the system-call arguments, do_dev_io() figures out what the device-type is and calls the proper operation procedure. The reboot interrupt handler also has to be changed to initialize the disk and program loader separately.

Utilities

Both the device managers need to check a block of user space defined by a start address a and a size in words n to make sure it's contained within the running process's range of valid address (remembering, of course, that the registers below the program counter are part of the range of valid addresses).

Index


This page last modified on 9 January 2002.