Operating Systems Concepts, Summer 2002

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 exit, fork and join 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 the interrupt_handler() function, passing along the raised interrupt's code. The system-call interrupt handler dispatches on the system-call code stored in register 0. On boot-up, initialize the process, storage, and disk resource managers. When initialized, the disk resource manager starts reading in the first program.
<interrupt handlers>+= (U->) [<-D]

void reboot_ih(void) {
  process::init();
  storage::init(usr_base, usr_size/disk_block_size);
  disk::init();
  }
Defines reboot_ih (links are to index).

The Process Manager

A process is identified by a unsigned integer unique among all processes resident in the operating system. process::add() creates a new process from the program loaded into User Space at the given location and return the new process's id; the load request came from the process with the given id. process::exit() zombifies the running process, removing it from the ready queue and freeing up its resources. process::join() blocks the current process until the process with the given id exits, at which point the blocked process rejoins the ready queue. process::init() initializes the process manager; process::done() returns true if and only if the process manager has no more work to do. process::halt() halts the simulation. process::block() suspends the currently running process and schedules a replacement, returning the newly blocked process's id. process::ready() makes the blocked process identified by pid ready to run. Some of the other managers need to be able to store values into the saved register context for a particular process; process::put_register() makes that possible. A process may be blocked waiting for a non-cpu resource, ready to use the cpu, or executing in the cpu. It may also have exited but not yet been joined with. 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. 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 (which may, on occasion, be the same process). Loading a process into the cpu involves changing the process's state to executing and restoring the saved register state. Return a count of the number of process-table entries with a status matching any of those in the given status set. This should properly be done with std::count_if(), but that breaks CC (WorkShop 6 update 2 ver. 5.3 2001/05/15). Return an iterator to the process-table entry with id equal to pid; die with an error if there's no such entry. Return true if a process having the given id exists; false otherwise. A couple of helper predicates to find process-table entries of ready or running processes. If the idle process is running, check the process table for a ready process and run it if found. If no process is running (because the running process exited) find a ready process and run it (this should always be possible because the idle process is always around and always ready). The idle process is currently running. If there's some other process ready to run, find it and run it. If there is no running process (which there may not be if reschedule() was called after blocking the current process), look for and run a ready process (if there is a running process, then everything's hunky dory). Because the idle process is always ready and always available, something terrible happened if there's no ready process to run. When changing a process's status to ready, be sure to re-check the process-table to see if the new process can run immediately. Create a process from the program loaded into allocated space given by sc; return the new process's id. If this process is being forked, copy the values in parent's registers into the children's register set. The new process is ready to run. Block the currently running process and start another; return the ejected process's id. 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. A process exits with the exit value eval. 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.

If a process is waiting to join with the exiting process

If a process is waiting to join with the exiting process, wake up and ready the joiner and delete the exiter. Otherwise, zombify the exiting process and keep it around so it can be the target of a later join request. The running process wants to join with the process having the given id. If there's no such process or there's already a process waiting to join, the running process loses. Otherwise, do the join. If the current process joins with a process that's already exited, then just copy the exit value, clear the exited process, and let the current process go on it's merry way. Otherwise, block the current process.
<do a join>= (<-U)

if (ptei->status == proc_exited) {
  debugp(dbp_forkjoin, "At time %4d, process %2d joins with process %2d passing value %d.\n", 
         read_clock(), running_process->id, pid, ptei->exit_value);
  store_mem(0, status::ok, proc::join);
  store_mem(1, ptei->exit_value, proc::join);
  ptab.erase(ptei);
  }
else {
  ptei->joiner_waiting = true;
  ptei->joiner_pid = running_process->id;
  process::block(proc_blocked);
  }

Halting the system has pie-like easiness.

Process manager initialization defines and runs the idle process, which always occupies the last slot of the process table. Store the given value in the given register of the given process's saved context. When changing a process's status to ready, be sure to re-check the process-table to see if the new process can run immediately.

The Disk Manager

Because this version of the operating system doesn't make the disk available to the user processes, the only thing the code managing the disk has to do is to read programs into User Space, an activity which doesn't require too much in the way of an interface.

On disk initialization, start a read of the batch-disk index block. 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. The current process wants to fork the ith program on the batch disk. If the forked process doesn't exist, the forking process loses. Otherwise, the forking process blocks until the forked process has been loaded. There's no good reason to block the forking process; it's just slightly easier to allocate all process information all at once when the forked programs ready to run. The nonblocking alternative requires that some of the forked process information be allocated now (so the forking process can know the forked process's id) and the rest once the forked process is complete. 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. A disk interrupt can be caused either by the index block (only on the first read) or a program block (all other reads). 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. 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 read into User Space, make it ready to run, unsuspend the requesting process, and pop the request from the queue. If the new process is being forked, store the new process's id in the parent's context and ready the parent. Check for a disk-io error on behalf of _caller and die if found. Issue a disk read on behalf of _caller. A program's location in the batch disk is represented by the disk-block number at which the program starts and the size of the program in disk blocks.

The location information on programs in the batch disk are stored in a vector indexed by the program's index-block index.

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.

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.

Miscellaneous Definitions

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

Index


This page last modified on 25 June 2002.