Operating Systems Concepts, Fall 2004

Programming Assignment 1 - An Example Solution


Table of Contents

Introduction

This page presents a solution to the first programming assignment, which involves writing a process manager and implementing the exec, exit, yield, put_slot and get_slot system calls.

Design

The operating system is made up of managers for the disk, the CPU, User Space, and the interrupts: overall os structure Circles represent operating-system manager software; non circles represent resources, either hardware (such as the disk) or software (exited processes). Arrows between managers indicate interactions via subroutine calls; the caller is pointing and the called is being pointed at. The labels associated with each arrow gives the system event that triggers the subroutine call. For example the arrow labeled "Exit" from the Process Manager to the Disk Manager indicates that the Process Manager makes a subroutine call to the Disk Manager (via a routine provided by the Disk Manager) whenever a process exits the system.

The disk manager is responsible for loading programs. The interrupt handler is responsible for initializing the other managers on system boot-up; these interactions are not shown in the diagram.

The Interrupt Handlers

Whenever an interrupt is raised, the simulator calls interrupt_handler() passing along the raised interrupt's code. On boot-up, initialize the process, storage, and disk resource managers. When initialized, the disk resource manager starts reading in the first program. The system-call interrupt handler dispatches on the system-call code stored in register 0. That's it for interrupt handling.

The Process Manager

The Process-Manager Interface

A process is identified by a unsigned integer unique among all processes resident in the operating system. The first set of process-manager interface routines are the internal implementations of the system calls provided by the OS. The other parts of the process-manager interface are internal routines available to the rest of the OS.

Process Manager Data Structures and Data

A process-table entry contains all the information needed to manage a process, including the process's id, its status, its saved register context, and information about the User Space storage allocated to it. Because a process slot can only store one word of data, it's simple enough to store the slot data in the process-table entry.

The process table entry also contains code to allocate process ids and save and restore process register contexts.

Process-table entries are stored in a list, with special iterators indicating the entries for the currently running process and the idle process.

Process Scheduling

The scheduler is a simple round-robin scheduler. Although the scheduling could be done through the process table, it's easier to use a separate ring data structure with pointers into the process table. Stepping a current pointer through successive entries in the ring implements round-robin scheduling. Add the given process-table entry to the ready ring. If the ready ring's empty before the addition, the new process becomes the current process. Make the successor process to the current process the new current process and return a pointer to the new current process (which may be the old current process if there's only one process on the ready ring). Return the number of processes in the ready ring. Remove the currently executing process from the ready ring and, if there's a successor process in the ring, set the given reference to it and return true. If the ready ring's empty after the removal, return false. The qunatum is fixed at 15 ticks, but there is a little trick: if there's less than two process on the ready ring, then there's no need to set a quantum because there's no other process to switch to.
<process macros>=

#define set_quantum(_n) \
  store_mem(clock_countdown_register, \
            ready_ring::ready_count() > 1 ? 15 : 0, process::_n)
Defines set_quantum (links are to index).

Used below.

The countdown interrupt just calls process::yield(), as if the running process itself decided it was time to cede the CPU.

System-Call Implementations

Creating (execing) a process takes two steps. The first step, performed by process::exec(), creates a process table entry for the new process and starts loading the program associated with the new process. When the program is fully loaded, the program loader performs the second step by calling process::add().

The new process is added to the process table but not to the ready ring, so it won't be selected for execution.

Yield the running process to another waiting process. Return false if there's no other waiting process (in which case the calling process continues to run). Return true if the calling process has yielded the CPU to another process. Nothing is done if the calling process would yield to itself. The calling process exits. The only resource held by an exiting process is its allocated storage, which is returned to the storage manager. If the disk is suspended because there's not enough space to load a program, freeing up this space may enable the disk to continue loading; poke the disk so it can check. The running process wants to store a value in another process's slot. If the receiving proces exists and it slot is empty, go right ahead; otherwise the put call fails. The running process wants to retreive a value from its slot. If the the slot is empty, the call fails; otherwise store the value and the sending process's id in the given references.

Other Process-Manager Operations

Halting the system has pie-like easiness. Context switch to the given process. Loading a process into the CPU involves changing the process's state to executing and restoring the saved register state. Once a new process's program as been completly loaded, the program loader calls process::add() to finish creating the process. ready_proc() is called whenever the given process becomes ready to run. If the newly ready process is the only process in the ready ring, then the idle process is running now and can be rescheduled. If the newly ready process is the second process in the ready queue, then the other process is the current process and is running without a quantum; schedule one to initiate time sharing. The process manager's done when the idle process is the only process left to manage. However, that doesn't mean execution is done because the disk may still be at work loading programs. Process manager initialization defines and runs the idle process; the idle process is not stored in the ready ring. Store the given value in the given register of the given process's saved context. Look for a process with the given id. If found, set the given iterator reference to point to the process and return true; otherwise return false. Return an iterator to the process-table entry with id equal to pid; die with an error if there's no such entry.

Files

The Disk Manager

Because this version of the operating system doesn't make the disk available to the user processes, the disk manager only has two tasks: read in the index block and load programs as required by the process manager. Even though it uses the disk, it probably would have been clearer to call this code the program loader rather than the disk manager, but it's too late to change now.

The Disk-Manager Interface

The disk-manager interface reflects the simplicty of the disk manager's tasks:

Disk Manager Data Structures and Data

Once the index block is read in, its data (if any) is represented by the disk-block number at which the program starts and the size of the program in disk blocks. The collection of program-location data are stored in a vector indexed by the programs' index-block index. Program load requests can be issued faster than programs can be loaded, so the exess requests have to be queued up to be handled later. A program-load request contains

The program-load requests are queued in load_requests. If true, a program's being read in; The number of blocks to read from the disk. The disk index of the next block to read. The next disk block read gets stored at the address given by next_program_address. The chunk of user-space storage into which the program is being read.

Disk-Manager Procedures

On disk initialization, start a read of the batch-disk index block. Schedule a load of the program with the given index for the process with the given id. Return ok if the load was scheduled without error; otherwise return an error indication. If there's no programs to read or a read is in progress, there's no reason to start another read. Otherwise, there's more programs to read and reading isn't in progress, so start another read assuming there's enough User Space for the program. If there's no program-load requests queued and there's no read in progress then the disk manager's idle.

Disk Interrupt Handling

A disk interrupt can be caused either by the index block (only on the first read) or a program block (all other reads). Once the disk index block lands in User Space, copy the information into program_index so the disk manager has easy access to program information and start loading the first program (if it exists). Invalid program sizes panic the system; it's hard to know what to do otherwise. If there's at least one program on the batch disk, run the first program. This code pretends the idle process is doing the execing. Handle a disk-interrupt that's the result of reading a program block. If all the program's blocks have been read in, then the program is ready to run; hand it off to the process manager and go read then next program, if any. Otherwise, schedule a disk-read for the next block in the program. Once the new program's been completely read into User Space, make it ready to run, pop the request from the queue, and start the next load request (if any).

Queue and Disk Routines

Queue up a request to load program i make by process pid. Poke the disk manager just in case it was sittin' around, waitin' for something to do. Check for a disk-io error on behalf of _caller and die if found. Issue a disk read on behalf of _caller.

Files

Storage Management

The operating system's storage management characteristics are simple: programs don't change size during execution, program size can be determined statically before execution starts, and allocated space must be contiguous.

Correspondingly, the data representation for allocated storage is simple too. Each chunk of user space allocated to a program is represented by a pair of numbers giving the lowest user-space address and size of the chunk in disk blocks.

The storage manage needs to be initialized with both the lowest address and size in disk blocks of the allocatable storage. Allocate a chunk of User Space containing the given number of blocks. And it should be possible to return an allocated chunk to the free-storage Allocatable storage is organized into a sequence of blocks, each the size of a disk block. The whole of allocatable store is represented by a bit map, where bit i represents the availability of storage block i.

base_address keeps track of the lowest address in allocatable storage.

The internal representation of allocated storage is similar to the external representation. The only difference is using an index (into the bit map) instead of an address to indicate the start of the allocation. To avoid confusion between the representations, the internal representation is called an index range, and the external representation is called a storage chunk. There needs to be a way of converting between the internal and external representations of allocated storage. Return the next range of free blocks at or to the right of the block at start. Return a range of zero length if there's no free blocks. Set the free-map bits in the index range ir to the value bv. Die if any of the bits in the range are already equal to bv. The storage manager is initialized with the start (lowest address) and size of the storage area to be managed. Initially, all storage is free. Allocate and return an s-block chunk of User Space, or a chunk of 0 size if there's no such chunk. Return the allocated storage sc to the free list. A quick check to make sure the storage-management code is working without problems.

Files

Miscellaneous Definitions

Some odds and ends useful to all code throughout the operating system.

Index


This page last modified on 13 November 2004.