CMPS 111: Introduction to Operating Systems

Programming Assignment #3: Multiprogramming and User Processes

Due Friday, May 10, at midnight.

Remember: your programming assignment must be turned in online.

Ground rules

The basic rules and project information are the same for this assignment as they were for the first assignment. However, there's one major change: you may work on this assignment either by yourself or with a single project partner (i.e., a group of two people doing a single project). If two people partner on a project, include both names and CATS accounts in any files either of you modified. A project team should only hand in one copy of their assignment; the person whose handin directory isn't being used should submit a single file called partner containing the name and CATS account of both project members into her directory. Partners will be required to do a little extra work: the items designated [Partners/Extra Credit] are required for people working with partners and extra credit for those working alone. The grade received by each project partner will be the same.

Also, I am making an experimental change: you may now discuss your design with other members of the class. However, if you do so, you must follow the Gilligan's Island rule - you may bring no notes of any kind to or from the discussion and you must wait at least 1/2 hour before sitting down to work on your code. Furthermore, you must note in your design document any discussions that you had, with whom you had them, and what you discussed. Reminder: you cannot share code, view other's code, or allow others to view your code. Failure to comply with this will be considered a violation of the ethics portion of this class, with all that implies.

As with other projects, the following suggestions apply:

The Basics

There are several related goals for this assignment:

To do this, you'll need to understand traps, process creation and deletion, basic file I/O, and keyboard I/O. You'll also have to do some work on synchronizing user processes using the Locks and Condition variables provided in synch.working.h.

The Details

Spawn() and Waitpid()

You'll need to implement two system calls, Spawn() and Waitpid(). Both calls deal with user processes, a necessity for a timeshared system. Spawn() takes two arguments — the name of the program to run in the new process and a single integer to be passed to the new process (as is done in Unix with argc and argv[]). Spawn() returns an positive integer (the process ID) that can be used to identify the process in future calls to the operating system. For now, assume that the memory allocated to a process by ProcessFork() is sufficient to run it; this assumption will change in Project #4. The current allocation is 64 KB of memory; please make sure your user programs aren't any larger (including data space!). If you really need more space for each user process, let me know and I'll show you how to increase the allocation.

Spawn() has to load user programs into memory. ProcessFork() already does this, and can be called from a trap. Passing the -u userprog argument to the basic dlxos will create a (single) user process; the same code can be used to create your user process. However, Spawn() must return a negative number if the call fails—the file isn't found, there aren't any process slots left, or any other errors such as running out of memory. Each error should return a different number so a user program can differentiate the causes of the system call failure. You may need to modify ProcessFork() to return a negative number (rather than exiting) for some of these error conditions.

Waitpid() takes a single argument—a process ID returned from an earlier Spawn() call . The process calling Waitpid() is suspended until the process identified by the argument to Waitpid() completes. When it completes, the process that called Waitpid() is resumed. As with Spawn(), it's possible for Waitpid() to fail for reasons such as non-existence of the process whose ID is passed, in which case a negative number should be returned to indicate an error. You'll probably want to use locks and condition variables to implement Waitpid()....

Input & output

Your operating system should support simple I/O routines—the ability to read & write a single character at a time. This functionality is provided to user programs via traps, but the OS has to do some work to make the traps work properly. Keep in mind that characters may come in before the user program has actually asked for them, so you may need to use a buffer to communicate between the keyboard trap handler and the trap handler that deals with requests to get a character. Also, your user programs may not use printf(); they may only use Putchar(). You may want to write the code for puts() to write out strings; this code should call Putchar().

Disk input and output must use the file system calls provided in dlxos; see the sample code in process.c for examples of how to open, read, and close a file. You may use the existing code in ProcessFork() as an example of how to use the OS read and write calls.

User shell

Your user shell should allow a user to type in commands and have them execute. The commands are actually DLX programs you've compiled separately, and are loaded in and run when the command is typed. There are two basic ways to run a program: foreground and background. To run a program in the foreground, type

program 1234

This will fork off a new process running "program", and pass the numeric argument 1234 to it. The shell will then wait for program to finish using the Waitpid() system call. Programs run in the background are called like this:

program 5678 &

This will run program in the background, allowing it to run at the same time as the shell (and perhaps other background processes). Immediately after forking off this process, the shell should return and allow the user to enter more commands.

Your shell should buffer up characters that are typed in, taking action only when a <RETURN> is pressed. You should correctly implement backspace (<CONTROL-H>), which should delete the last character from the buffer. To implement backspace on the display, output the character sequence <BACKSPACE><SPACE><BACKSPACE>. For information about which characters correspond to which, you can test keyboard input using the default version of the OS (it prints out the keys it receives).

The shell will use the Spawn() and Waitpid() traps, of course. It'll also need to use traps to get and put characters in the simulator— Getchar() and Putchar(). You'll need to write these traps as well, though the OS code for both of them is partially done already.

For consistency's sake, please write the code for your user shell in shell.c.


There are already some basic traps provided for you, such as Open() (though the code for Open() doesn't do anything useful. Yet...) You should look at these traps as examples for the traps you'll need to implement. If you create code for new traps, you should put them into a separate file called usertraps.s, and compile this file along with every user program you write. A sample usertraps.s file with sample code for calling traps has been provided for you. Make sure that the trap numbers in traps.h agree with those in usertraps.s.

Remember that every trap (user or system) must be caught explicitly by the operating system. When the OS catches a trap, it should make sure the arguments are valid and not assume anything. Parameters to traps are passed in the user's address space, but need to be accessed by the operating system in kernel space. See the code for the Open and Putchar traps for examples of how to copy arguments from user space to kernel space. Finally, when a trap wants to return a value to the user program, it should call ProcessSetResult() to set the return value from the trap (again, examples are in the existing code in traps.c).

You'll need at least the following traps to implement your shell

Design hints

The shell is a user program, and isn't compiled into the operating system. As such, you shouldn't compile it with the rest of the OS files. Instead, compile it separately and load it in. Since the basic operating system comes with the ability to execute a user-level process (via the -u option), you might want to experiment with them first and use that information to design your shell.

The operating system learns of incoming keystrokes (characters) by being interrupted. When it gets a keyboard interrupt, the interrupt handler (in traps.c) should place the incoming character into an array. When it's called, Getchar() removes a character from this array if one is available. If none is ready, it should wait (using semaphores or locks & condition variables) until one becomes available. Note that this is a variation on the producer-consumer problem we studied in class. There's only one difference—it would be impossible for the producer (the interrupt handler for the keyboard) to wait until the array became non-full, so throw away characters if there's no space in the array for them.

There are several miscellaneous functions in misc.c and misc.h that implement functions from the standard C library (which isn't available in DLX). Feel free to use these functions in your user programs (or OS) and (if you like) add more functions. In particular, you may find the dstrtol() function (which converts strings to integers) and string functions useful for your shell.


What to turn in

A compressed tar file of your project directory, including your design document. You must do "make clean" before creating the tar file. In addition, include a README file to explain anything unusual to the TA — testing procedures, etc. Your code and other associated files must be in a single directory so they'll build properly in the submit directory.

REMEMBER: Do not submit object files, assembler files, or executables. Every file in the submit directory that could be generated automatically by the compiler or assembler will result in a 5 point deduction from your programming assignment grade.