Operating Systems, Fall 2001

Programming Assignment 3 - An Example Solution


Table of Contents

Introduction

This page presents a solution to the third programming assignment, which involves writing a paging virtual-memory manager.

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

Design

Because the paging manager is constrained from below by the paging hardware and constrained from above by the actions required to handle a page falut, there aren't too many design decisions to be made in the page manager proper.

Probably the most important decision to be made is the page replacement algorithm, but even here the interface to the algorithm is simple and fixed, which means it's easy to just cobble something up to meet the interface spec using, for example, random page replacement. Once everything else is in place and working, the replacement algorithm can be upgraded to something more acceptable, should that be necessary.

Another interesting design problem is determining how to wake up the user process that threw the page fault once the page lands in primary store. This would be a difficult design problem if it wasn't for the design of the disk manager done in the second assignment. However, the situations in both cases are the same, and so the solution used for the disk manager - callbacks - can also be used here.

That pretty much wraps the major design issues associated with the page manager proper. There are some minor, local design issues remaining, but these are best handled when they come up during code development below.

In the overall scheme of things, the page manager fits between the disk driver and the process manager, and receives its input from the interrupt handler on addressing faults. The page manager uses the disk driver to page to and from the disk, and uses the process manager to suspend and resume processes involved in a page fault.

The Virtual-Memory Manager

The page manager presents a simple interface to the rest of the operating system. When initialized, the page manage needs to know the number of the first disk block making up the paging area on the disk. When process pid throws a page fault by accessing unmapped virtual address vaddr, the page manager needs to page-in the proper page. When the page has landed in primary store, the page manager calls f(arg). When process pid exits, its mapped in pages have to be freed to make them available for other processes. A primary-store page frame is either free, allocated, or in transition from being free to being allocated. The number of the fist block in the paging area on disk. When a requested page-in or page-out completes, the disk driver calls back to the page manager, passing along the following information: Return a pointer to a page frame suitable for receiving a page. A free page frame is most suitable; a page frame in transition (that is, in the process of receiving a page from disk) is completely unsuitable. Of the remaining page frames, a page frame that's been unaccessed for the longest period of time is must suitable. When a page-in request is complete, the disk driver will call page_in_cb() to complete the page-in, which involves completing the page-table initialization and calling the requester of the page in. Schedule a page-in request with the disk driver, using the information pointed to by prip. Schedule a page-out request for the page being held in the page frame associated with ptep.

If this is the first time this page's getting paged out, the process's page-block location for this page points to the page's location in the program file. Bump the page-block location up to move the page to the paging area.

The page-out request doesn't include a call-back function, which means the page manager won't know when the page-out has been completed. This turns out not to be a problem because the disk driver handles requests in FIFO order; as long as the page-in request is issued after the page-out request, disk I-O is handled in the proper order. This is, however, an incredibly dangerous assumption to make, and represents bad, bad programming.

Page manager initialization involves remembering the first disk block of the paging area and setting the page table state. Page in the virtual address vaddr on behalf of the process pid. If the allocated page frame has a dirty page, schedule a page out before the page in. The process with process number pid has exited. Free up all the page frames allocated to the process, and mark them all as being invalid.

The Rest of The OS

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

The Program Loader

With the advent of virtual memory, the program loader is essentially out of a job. The operating system's going to use pure demand paging, which brings programs into primary store page by page as needed. The only job left to the program loader is to read the disk to figure out where the programs are, and hang them on the process ready queue.

To keep things simple, the program loader hangs all programs on the ready queue before running any of them. Because the program loader runs to completion and stops, there's no longer any need for the done() predicate and poke() procedure in the interface; only the init() procedure remains as the program loader's interface.

There is, however, another reason besides simplicity for delaying program execution until all programs have been loaded. Because the paging area follows the programs on disk, the page manager can't be initialized until the total size of the programs are known.

Keeping track of a program on disk involves recording its starting block and the number of blocks it occupies, something for which a pair of unsigneds is well suited. Keeping track of all such programs on the disk is easiest done with a vector of such pairs.

Because processes don't run until all programs are read, the program loader can read disk blocks into primary store without clobbering some process's pages. This is actually half-way to pre-fetching; the other half is keeping track in the page table where each initial block lands in primary store. However, that's too much work, so the page manager just reads into the first block of User Space and lets it go at that. get_block() handles a block that's been read from the disk. If the block is the first block in a program, save the program information and adjust the next-block address. If there's more programs to read, then issue the next read request; otherwise, initialize the page manager and hand the programs over to the process manager using the auxiliary procedure start_program(). Just in case there's no programs on the disk, the program loader should be prepared to shut down the system. To initialize the program loader, set the next disk-block to read to the first disk block and go read it.

The Disk Manager

The disk manager copies data from the disk to a process's address space. Devices, such as the disk, only work with physical addresses. With processes now running in virtual memory, the disk manager has to be changed to translate from physical to virtual address (or vice versa) while copying the data.

There are a couple of ways the disk manager can implement the physical-virtual address translation, but the easiest way is to assign a one-block disk buffer to the disk manager. The disk manager does all disk I-O to and from the its buffer, and does virtual-physical address translation when copying data between its (physical address) buffer and the process's (virtual) address space.

There's plenty of free device space available, so allocate a one-block buffer there.

The buffer scheme has the great virtue of being (relatively) simple, but it does have a number of drawbacks. First, the buffer becomes a bottleneck between processes and the disk because the buffer must be involved in every disk I-O operation. This can be relieved somewhat by increasing the number of buffers available to the disk manager, but as a practical matter the number of buffers is finite and you can't outrun the bottleneck problem.

The disk manager also has to keep a queue of disk I-O requests because the buffer is a serially reusable resource available to only one process at a time (this is the source of the bottleneck). Also, the need to copy between the buffer and the process essentially doubles the I-O time.

The interface presented by the disk manager is unchanged; only the internals need to be changed to accommodate virtual memory.

The disk manager needs to be able to queue up requests and issue them one at a time when the buffer is available.

If the disk manager's working on a disk I-O request, or if there's no pending requests, then the disk manager shouldn't start a new request. Otherwise, the disk manager should start the disk I-O operation at the head of the pending queue. When the current disk I-O request has completed, its request should be popped off the head of the pending queue, the disk manager should be set to idle, and next pending request should be started. The disk manager needs two steps to perform a write from a process to the disk:

  1. Copy the data to write from the process to the buffer.

  2. Write the buffer to the disk.

The second step is easy, involving a call to disk_dvr::request(). The first step is more complicated, because it may involving paging.

A disk block is the same size as a page (what a coincidence!), so the data block to write may be split across at most two pages, which means the copy to the buffer may require at most two page-ins. When copying the data, the disk manager has to first check each virtual address to make sure it's resident in primary store, and page it in if it's not.

The analysis given in the previous paragraph is a bit simple minded because it's only necessary to check at most two addresses: the address at the start of the data block and the address on the other side of the page boundary crossed by the data block (if it doesn't fit all in one page). However, exploiting this fact complicates the code, so it will be ignored in what follows.

write_dm_buffer() is responsible for performing a disk write; it also is the call-back for the page manager on page ins. The pass-along argument is the offset into the data block being written; the rest of the arguments are kept at the head of the pending queue. Once the data block has been copied to the buffer, the actual disk write can begin, and the disk write request is wrapped-up by write_buffer_cb().

When a process's write request has completed, store the status and make the process ready to run. Then go start the next disk I-O request, if any. A disk read is handled much like a disk write, except the steps are reversed: first read the disk into the buffer and then copy the buffer into the process's address space. read_dm_buffer() assumes the read's already been done and the data's ready to be copied into the process's address space. Once the copy is complete, the process can be made ready to run and the disk manager can begin de novo. The requested disk read was either successful, in which case control moves on to copying the data from the buffer to the process, or it failed, in which case the requesting process wakes up immediately with the bad news. The implementation of the disk-manager interface procedures is unchanged, except for operation(), which now hangs the I-O request on the pending queue, rather than issuing the disk I-O commands directly.

The Terminal Manager

The terminal device, like the disk, deals only in physical addresses, and the changes required in the terminal manager are similar to those required in the disk manager. There are, however, two points of difference between disk and terminal I-O; one of the differences complicates things, and the other simplifies things.

Unlike the disk, from which a process can only read a fixed-sized data block, the terminal can supply a process with an arbitrarily long sequence of words. This complicates buffering. On the other hand, also unlike the disk, data come from the terminal a word at a time, which, assuming you're not interested in high performance, simplifies buffering.

The interface presented to by the terminal manager is unchanged.

The terminal manager now needs to deal with call-backs from the page manager in addition to interrupts from the terminal. It seems a wise design decision to devote a procedure to handle each. The write-interrupt handler, for example, look as it did before, except the actual character handling code has been moved to character_from_process(), which also handles the page-manager call-back.

character_from_process() fetches the next character from the process (which may involve requesting a page in) and writes it to the terminal. read_terminal_ih() reads the incoming character from the terminal and, if it doesn't indicate the end-of-file, passes it on to character_to_process() for for storage, which may involve a page in. Store the character arg read from the terminal, which may involve requesting a page in.

The Interrupt Handler

The only interrupt handler that the operating system hasn't explicitly defined is the bad-address interrupt handler, and now's the time to fix that.

When a process throws a bad (virtual) address, it could be because the address is outside the range of legal addresses for the process, or because the address isn't resident in physical storage. In the latter case, schedule a page in for the address; end the process in the former case.

When a page lands in primary store, restart the process that caused the page fault.

The Process Manager

The process manager deals with User Space, and so has to be changed to accommodate virtual memory. The changes are minor, and involve mostly replacing User-Space chunks of memory with pages of virtual memory.

A new process is defined by its starting location on the disk and the number of disk blocks it occupies.

Given the number of a page belonging to a process, the page manager needs to be able to indicate in which disk block the page is stored, and retrieve the disk block number holding a process's page. A process-table entry needs to be extended by a page map, which maps between a process's pages and the location of the pages on disk. The procedures to read and write a process's page map. The process::add() procedure changes slightly to set the register context properly for virtual memory. The exit() procedure needs to call the page manager to clear the exiting process's pages from the page frame.

Index


This page last modified on 17 January 2002.