[an error occurred while processing this directive]

Project 1: The Shell and System Calls

Out: Monday, October 10, 2011
Due (code and writeup): Friday, October 21 Monday, October 24, 11:59PM
Notes:


Setup Instructions

This project involves writing both user-level code and kernel hacking. Exactly where (on which systems) this work can be done is pretty flexible. Because the situation is complicated, though, we describe only two:
  1. Use a 32-bit virtual machine we provide to do everything. That virtual machine has to live on your personal machine.
  2. Use remote access to a CSE machine, forkbomb.cs.washington.edu, to do everything other than booting your hacked kernel. Boot your kernel on a virtual machine we provide.
For a variety of reasons (beyond those already mentioned), working in the VM on your machine is by far easiest, assuming it's feasible.

File backup is your responsibility. forkbomb is not backed up (although it mounts your CSE home directory, which is backed up). Your files on the VM are backed up only to the extent that you have made copies of them somewhere else.

I recommend reading all of the links below before making a decision about where you'll be working.


Assignment Goals


Background: The Shell

As we'll discuss in class, the OS command interpreter is the program that people interact with in order to launch and control programs. On UNIX systems, the command interpreter is usually called the shell[1]: it is a user-level program that gives people a command-line interface to launching, suspending, and killing other programs. sh, ksh, csh, tcsh, and bash are all examples of UNIX shells.

Every shell is structured as a loop that includes the following:

  1. print a prompt
  2. read a line of input from the user
  3. parse the line into the program name, and an array of parameters
  4. use the fork() system call to spawn a new child process
    • the child process then uses the exec() system call to launch the specified program
    • the parent process (the shell) uses the wait() system call to wait for the child to terminate
  5. once the child (i.e. the launched program) finishes, the shell repeats the loop by jumping to 1.
Although most of the commands people type on the prompt are the name of other UNIX programs (such as ls or cat), shells recognize some special commands (called internal commands) which are not program names. For example, the exit command terminates the shell, and the cd command changes the current working directory. Shells directly make system calls to execute these commands, instead of forking child processes to handle them.

Background: Library Routines

Library routines are just code that is made available to you. They differ from routines you write yourself in two ways, though:

Many languages come with some kind of "standard library" that provides commonly used functionality. C does. So does C++. The Java API might be thought of as the standard library for that language.

In C, many of the standard library functions serve as an interface between the code written for user applications and the operating system on which the application runs -- the library routines provide a standard interface to user code for IO (e.g, printf() and scanf()). This allows the applications to be somewhat portable (able to run on many different systems) - all you have to do is compile the application code on some system and link it with the standard library functions (written for and compiled on that system), and voila.

In Unix, libraries[2] are manipulated with the ar command. In a typical installation, standard libraries are located in /usr/lib, and have file names beginning with "lib". The name of the standard C library is usually /usr/lib/libc.a, for instance.

Linkers, like ld (used by gcc), will often automatically look in standard libraries for code required at link time. If you have a non-standard library (as you will in this assignment), you have to tell the compiler/linker about it, e.g.,

$ gcc -c sub.c # creates sub.o
$ ar r mylib.a sub.o # creates mylib.a with sub.o inside it
$ gcc main.o mylib.a # creates a.out
See documentation on gcc for more complicated uses of libraries, and man ar for more information on creating and examining them.

The Assignment

This assignment has four parts:
  1. implement a simple shell
  2. implement a new system call
  3. implement a library routine capable of invoking that system call, and a simple driver program that exercises it
  4. answer some questions about what you've done
The implementation approach we suggest has a slightly different order than that, though, the problem being that to test the new syscall you need a working library routine, and to test the library routine you need a working app. The suggested methodology minimizes the number of pieces of code in play not known to be working, thus making debugging easy(er).
Step 1: Build a new shell (fsh.c)
Write a shell program (in C), fsh.c, which has the following features: Assume that executable names are specified as they are using "a real shell," i.e., name resolution involves the PATH environment variable. Try to use the same prompt as in the following:

CSE451Shell% date
Sat Jan 6 16:03:51 PST 1982
CSE451Shell% /bin/cat /etc/motd /etc/shells
Pine, MH, and emacs RMAIL are the supported mailers on the
instructional systems.
[and on and on...]
/bin/sh
/bin/bash
[and on and on...]
CSE451Shell% ./date
./date: No such file or directory

Notes:
Step 2: Create a driver routine, and a dummy library routine (getexeccounts.c, getexecounts.h, getcounts.a, getDriver.c)
Our goal in this step is to debug the process of creating and linking with a library, as well as debugging a driver routine that will be used to test the library routine.

We start by writing getexeccounts.c and getexeccounts.h. The latter defines a single function:

int getExecCounts( int pid, int* pArray );
The former contains the implementation of that function. getExecCounts() returns 0 for success and non-zero for failure. The argument pArray is assumed to be an array of four ints. The implementation at this point is just a dummy routine: it assigns the value k to the kth element of the array, k=0,1,2,3, and always returns success.

Now write getDriver.c, an implementation of main() that invokes getExecCounts and prints the returned values like this:

pid 22114:
    0   fork
    1   vfork
    2   execve
    3   clone
Compile and link as usual. To this point, this step should take maybe 15 minutes (depending primarily on how fast you type).

Now create a library, getcounts.a, with a single member, getexeccounts.o, and make sure you can link getDriver.o against it.

Step 3: Add a new system call, and modify the library routine to use it

There are four system calls in Linux related to creating new processes: fork, vfork, execve, and clone.  (The man pages will describe for you the differences among them, although those details aren't important to the implementation portion of this assignment.)  Implement a new system call that returns to the calling program how many invocations of each of those four process creation calls have been performed by a specific process and all of its descendants.

To do this requires four things:

  1. Modify the process control block definition (struct task_struct in include/linux/sched.h) to allow you to record the required information on a per-process basis.
  2. Instrument the kernel to keep track of this information.

  3. Design and implement a new system call that will get this data back to the user application. Your syscall must have syscall number 341.

  4. Modify your library routine to invoke your new syscall and return the results.

You'll use your dummy app to test the syscall (as well, possibly, as other techniques, such as printk()).

Notes: Items 1-3

Notes: Item 4

Step 4: Implement a utility application (execcnts.c)
We want to implement a program, execcnts that is to process creation syscall statistics what time (man 1 time) is to seconds. execcnts takes a command invocation line as arguments, executes the command, and prints out the number of each of the four process creation syscalls made in executing the command line. For intance,
execcnts find . -name '*.c'
would print the numbers of each of the four syscalls involved in executing that find command.

execcnts has one additional feature not found in time: if you send it a SIGUSR1 signal, it prints out the current counts of the four syscalls and then continues waiting for the process it forked to terminate. You can use the kill command from another terminal window to send the SIGUSR1 signal. For example, if the execcnts process has pid 2248:

$ kill -SIGUSR1 2248
should cause it to print current counts. (You might find the use of command killall more convient than kill, as it lets you avoid figuring out pid's: man killall.)

Hints:man 2 wait
 man sigaction


What to Turn In

You should (electronically) turn in the following:
  1. The C source code of your shell (fsh.c).

  2. The C source code (getexeccounts.c and .h files) of your library routine implementation, and the library itself (getcounts.a).

  3. Your driver (getDriver.c).

  4. Your implementation of the utility program, execcnts.c.

  5. The source code of the files you changed in or added to the kernel.

  6. A compiled kernel image (/boot/vmlinuz-2.6.38.2) with your changes made.

  7. The names of all of the Linux kernel source files that you modified, and a written description of what you did to them and why you needed to do it (i.e. why was it necessary to modify this particular file).

  8. A transcript showing you using your new shell to invoke the /bin/date program, the /bin/cat program, and the exit and cd commands supported by your shell.  (/usr/bin/script might come in handy to generate this printout.  As always, do man script to find out how to use the command.)

  9. A brief write-up with the answers to the following questions.
    1. What is your syscall design? What arguments does it take? How does it return results? What restrictions are there, if any, on its use?
    2. Explain the calling sequence that makes your system call work. First, a user program calls <.....>. Then, <.....> calls <.....>. ... and so on. You can explain this using either text or a rough diagram (don't spend more than 15 minutes on a diagram).
    3. How could you extend your shell to support multiple simultaneous processes (foreground and background...)?
    4. Why must the three internal commands your shell supports be internal commands? (That is, why couldn't they be implemented as separate programs, like all other commands? In the specific case of '.', why couldn't you implement it by forking fsh <filename from the current shell, assuming you had already implemented input redirection in fsh?)
    5. I claim that the functionality provided by your new system call must be implemented in the kernel; that is, it couldn't be implemented through any combination of library routines, user applications, or the shell (without kernel support). Explain why that is true.
    6. Suppose that for whatever reason (e.g., you don't have access to the source), you couldn't modify the kernel implementation. I claim that, to a large extent, you could still obtain information about the number process creation system calls made by many applications, even if you don't have access to the source for those applications either. Explain why I'm right about this as well.

    Turn in your write-up electronically in a separate file along with your code.

Do not underestimate the importance of the write-up. Your project grade depends significantly on how well you understood what you were doing, and the write-up is the best way for you to demonstrate that understanding.

The grade on the project will be calculated as follows:

Submission instructions: Ideally, we'd like turnin to be a single <username>.tar.gz file, which expands to a sensible directory structure containing files with sensible names and not containing the superfluous mess the compiler leaves around (.o files, executables, etc.).


Footnotes

[1]
While we say "the shell," there are many different shell programs (e.g., sh, ksh, csh, tcsh, bash, the shell you're writing, etc.). As well, a single system, and a single user, can be running more than one shell process at a time. Because a shell is just a program, you can launch any shell from any other shell. On Unix systems, a user's login (default) shell is kept as the last data item in the line of information about that user in the /etc/passwd file, e.g.,
farnsworth:x:122:119:Ted &,411,35142:/homes/iws/farnsworth:/bin/bash
The command chsh has historically been used to change the login shell entry in your /etc/passwd line; our labs now use kchsh, which is a version of chsh adapted to use Kerberos password authentication.
[2]
At this point, the text is actually talking about static libraries, those whose code is linked to the application at link time (i.e., at the time the executable file is created). Dynamic (i.e., linked at run time) versions of libraries are available as well.
[3]
atoi() converts between ASCII representations of integers and ints: man atoi. (Note that it is incapable of indicating that the string argument does not represent an integer. However, sscanf() can do that. Sort of.)
[an error occurred while processing this directive]