Operating Systems
CIS*3110 (W12)


Assignment #2

Due: March 4, 2012 at 23:55h
(please see deliverables section below)

Key concepts: Processes management; file descriptor table; system programming.


Read the following sections carefully to ensure that you know what is to be done and what is to be handed in. If the details aren't addressed successfully you will lose marks. Please refer to the coding style guidelines to provide direction regarding the format of the code you write. You are expected to work independently on this assignment (please see academic misconduct in computing for clarification if necessary).


Walking on an Egg-"shell"

Although we don't think about it all that often, the command shell in which you do most of your work is a marvel of presenting process and I/O management in a highly abstracted way, greatly simplifying the user's responsibilities in launching and managing processes. As we can learn a great deal about a number of fundamental process management and I/O issues, it's worth our time to build a specialized command shell of our own to see what else we could do with it if we choose.

The Problem

Using the C programming language, you are to implement a specialized command shell for spawning and managing the execution of processes on a workstation. Your shell will be highly simplified when compared to a workhorse such as bash; however it will support many of the features you would expect from the command shell such as launching programs, the ability to pipe the output of one program into another as part of the command invocation, but will also support a few higher level facilities of our own for process management. Your shell should take the form of a program called ossh. Note that once this program is executing, it must provide a prompt to the user (just like any shell) and respond to input as described below, while performing any monitoring features that are required at the same time.

Basic Operation

Once running your shell, the user should be able to launch programs as they do in their regular command shell. By way of reminder, a basic command to launch a program would look like:

progname arg1 arg2 ... argN

where progname is the name of a program to be executed, and arg1 arg2 ... argN are the arguments to be passed to that program (which are specific to whatever program is being launched obviously). In response to this command, the shell should arrange to spawn this process (using fork(2) and exec(2) system calls) and make whatever book-keeping updates it needs to in order to manage this process in future. Like a real shell, the shell itself should wait for the termination of this program before returning control (i.e. the prompt) to the user, while any output from that program is displayed in the shell normally.

Shell Commands

Your shell is required to support a number of more advanced features:

Piping
One program is invoked with its stdout connected to the stdin of another program (both of which are otherwise "standard commands" as described above). You will need to make use of pipe(2) and manipulation of the file descriptor tables using dup(2) or dup2(2) in order to accomplish this.

Syntax:
<standard_command> -> <standard_command>

Example:
ls -l -> sort -r

I/O redirection
You can cause a program to consume stdin from a file, or output its stdio to a file using ^ for input, and $ for output. You will need to make use of the open(2) low level I/O routine in addition to manipulation of the file descriptor table using dup(2) or dup2(2) to do this. Note it is possible to redirect either one of these, or both.

Syntax:
<standard_command> ^ <infile>
<standard_command> $ <outfile>
<standard_command> ^ <infile> $ <outfile>

Examples:
cat ^ excitingtextfile.txt
ls -l $ dirlisting.txt
sort ^ dirlisting.txt $ sorteddirlisting.txt

Note that redirection operations could occur in any order.

Background Execution
Execute the program in the background (rather than foreground), returning the user to the prompt immediately while the process they created runs in the background. Note that you do not need to do anything special with stdout of background processes (it will be the same file descriptor as the shell's, so any output will appear in your shell anyway).

Syntax:
<standard_command> !

Examples:
find . -name "*.txt" !

Efficiency enforcement
It should be possible to demand a process being started keep the processor "sufficiently busy". The idea here is to ensure a process started, subject to this constraint, lives up to its billing. This is a real issue on many systems where inefficient use of the CPU resource is not acceptable. In this case, your shell will simply enforce it. It will be necessary for your program to periodically monitor the CPU load of any process started in this manner. If its CPU load falls below the specified threshold, it should be terminated (with a message to the terminal indicating this happened). Note that a process may take a short time before it hits a steady state in terms of processor utilization; you should take this into account and not kill a just started process that is still ramping up. This information can be trivially extracted from the output of ps; have a look at popen(3) as a means of making this output available withing your program as a FILE * stream, and alarm(2) as a mechanism for triggering a handler to do regular periodic monitoring.

Syntax:
<standard_command> > <percentage

Examples:
xhpl > 90

Job groups
It is possible to use a special notation prefix while executing a program that assigns that process to a job group that allows a collection of running processes to be treated as a group for specific job control commands. There are two aspects to this behaviour:
  1. Assigning a job group: applying a numbered prefix in square brackets before the standard command results in that process being assigned to that specific job group. Note that the number is arbitrary and isn't assumed to be sequential.

    Syntax:
    [<#>] <standard_command>

    Examples:
    [3] grep printf *.c

  2. Job control: the following are to be interpreted as shell commands. When they appear as the first non-whitespace content in a command-line, the entire command line is to be treated as a job control directive, and the specified action taken on all processes assigned to that job group. These are ultimately just commands to have signals sent to multiple processes with a single command, and as such will require the use of kill(2). The implementation of this functionality will be greatly eased by making use of group ids as you spawn processes (see setgid(2), and understand how you can use a single kill call to send a signal to an entire group). Job control commands to be supported include:

    • jkill: terminate all jobs in the group
    • jstop: pause all jobs in the group
    • jcont: start all jobs in the group

    Syntax:
    <jobcommand> <jobgroup#>

    Examples:
    jstop 4
    jkill 3

Note that because jkill, jstop and jcont are now treated as shell commands, it wouldn't be possible for a user to run programs by those names without providing the path (again, this is nothing you really need to support specifically, just pointing it out).


Bonus

ATTENTION: up to 5 bonus marks will be available, reflecting the extent to which your shell can combine the above elements in an arbitrary fashion. It is possible to get more than 30/30 on this assignment.

On the simpler side would be multiple stage pipelines:

prog1 -> prog2 -> prog3 -> prog4

[2] prog1 -> prog2 -> prog3 ^ infile.txt $ outfile.txt !

Obviously it would be prohibitive in terms of time to completely support arbitrary command variations (even the second example above requires some design thought - what support will you need in your shell in order to treat the entire command as a job group?); ensure you highlight what you've attempted to do in your documentation so we can properly evaluate what you've actually done. For example, how would you meaningfully apply the load constraint to a multi-process command-line? These are all design issues and need to be highlighted in documentation (or at least the README) so we can evaluate what you've done. If you leave it to us to discover what you may or may not have done, there is no possibility for bonus marks as we will not exhaustively test all assignments just in case.

Note that the idea to this bonus is not to reward people doing all kinds of extra work, but rather to encourage you to think about how you would do these things from the outset to make it easier. If you hack out something crude that works for the simple cases, it may be overly difficult or daunting to graft on some of these ideas as an afterthought. There, you've been warned. Make your choices.



Implementation Guidelines


Warning: always clean up after yourself!!!

This assignment involves you explicitly using the fork() system call to create new processes, and development and testing will involve experiments with possibly many CPU-bound jobs running in the background.

  1. If you make mistakes with the use of fork() it is quite possible to suck the machine on which you are running down to nothing with an exploding number of processes (the aptly-named "fork-bomb"). You are not to run this code on department equipment other than the REYN 001A lab machines which are provided for this purpose. You can run on your own computers if you like, just be aware that if you really hose them you may need to reboot.
  2. Please make sure no jobs are left behind (intentionally or by accident) when you leave a workstation---this could easily affect the next person using that machine. Note that if during our testing of your program, processes are improperly left running, your mark will be reduced by 10% for each such process!

Makefile

You are responsible for writing a Makefile to compile your code. Typing "make", "make ossh" or "make all" should result in the compilation of all required components with the result being your program (ossh). You should ensure that all required dependencies are specified correctly.


Deliverables



Last Modified: 2012 / 03 / 01