CSE 451, Introduction to Operating Systems, Spring 2012

Assigned: Wednesday April 4
Due: Wednesday April 18 at 11:59 p.m. (electronic submission)
Project group information here

Project Setup

There are two parts to setup: establishing your kernel build environment, and establishing your kernel test environment. The department labs have been set up to make this easy, if you work in them. It is also possible to work on, say, your home machine. (High speed connectivity is almost certainly a requirement, though.) Details for getting either environment set up are on the VM information page.

Obtaining the Linux Source

The version of the source we'll be using is on attu at:

/cse/courses/cse451/12sp/linux-2.6.38.2-CSE451.tar.gz

This is the same as the one in the course VM under /project1/. You should use this version of the kernel source no matter what -- no matter what platform it is you build on, no matter what year it is, not matter what the latest Linux distribution is, no matter what version you are running, no matter what Google says, etc.

Your Build Environment

This project involves writing both user-level code and kernel hacking. Exactly where this work can be done is pretty flexible. Because the situation is complicated, though, we describe only two:

  1. Use the virtual machine we provide to do everything.
  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, either on your own personal computer or on a Windows lab computer. This is the preferred approach as compilation on forkbomb will be faster than inside the VM.

For both setup situations, you need to copy the source files you've created or modified if you want them to be backed up. Just assume neither forkbomb nor the VM is backed up. The number of source files you will be editing is small, so you can easily back them up to your CSE home directory. forkbomb mounts your home directory under /homes/iws/, though from the VM, you have to move files to the host you're running on and then to your home directory, as the VM can communicate only with the host, not the internet as a whole.

Using forkbomb

You can login to forkbomb.cs.washington.edu using SSH. Due to the usual disk space quotas, you will not have enough quota in your home directory to copy and compile a kernel, so we have instead provided each of you with local disk space under /cse451/username, where username is your unix account name. When you make your copy of the linux kernel and when you edit or compile it, you should do so in this local directory, not your network home directory. To build the kernel source on forkbomb, execute:

ssh username@forkbomb.cs.washington.edu
cd /cse451/username
cp /cse/courses/cse451/12sp/linux-2.6.38.2-CSE451.tar.gz . # note that final .
tar -xvf linux-2.6.38.2-CSE451.tar.gz
cd linux-2.6.38.2
linux32 make bzImage -j2

Make sure that you have linux32 before make. If you do not, you will be prompted with configuration options, which is not what you want (Ctrl+C to cancel). The kernel will now be compiled. Once it finishes, you will have a bzImage file located under arch/x86/boot/bzImage. From a Windows lab machine, copy the bzImage to your home directory using the file transfer client under:

Start->Programs->Internet and Remote Connections->SSH->Secure File Transfer Client.

If you are working from your own personal machine, you can use WinSCP, scp, etc. to copy the bzImage file to your machine. Once you have the file on the computer that you are using, copy it into the running VM image that you set up from the directions on the VM information page, again using a file transfer utility. Assuming that the bzImage file is now in the home directory of your VM, you can install it by running:

sudo mv ~/bzImage /boot/vmlinuz-2.6.38.2-CSE451

Now restart the VM and test out your changes. If the kernel fails to boot, you can restart the VM and pick another kernel from the Grub menu in order to get back to a working VM environment, then run sudo cp /boot/bzImage /boot/vmlinuz-2.6.38.2-CSE451 to restore the default kernel once it has booted.

Using the Course VM

The kernel source is located under /project1kernel/. cd to /project1kernel/linux-2.6.38.2/ and then run make bzImage -j2 to build the kernel. Use sudo make install to install the kernel, then restart to test your changes. If the kernel fails to boot, restart and pick a different kernel.

Now, the assignment!


Assignment Goals

  • C, beautiful C, ...
  • To understand the roles of and relationships among applications, OS command interpreters (shells), library calls, system calls, and the kernel.
  • To become familiar with the tools and skills needed to understand, modify, compile, install, and debug the Linux kernel.
  • To design and implement a simple shell, a simple system call, and a simple library routine.

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: 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) that 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:

  • The code is provided in compiled form - .o files rather than .c.
  • A library is actually a single file that (typically) contains many .o files within it (just as Winzip, jar, and tar files contain other files within them).

Many languages come with some kind of "standard library" that provides commonly used functionality. C does, and 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 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, or at least easier.

Step 1: Build a new shell (fsh.c)

Write a shell program (in C), fsh.c, which has the following features:

  • It should recognize three internal commands:
    • exit [n] terminates the shell, either by calling the exit() standard library routine or causing a return from the shell's main(). If an argument (n) is given, it should be the exit value of the shell's execution. Otherwise, the exit value should be the value returned by the last executed command (or 0 if no commands were executed.)
    • cd [dir] uses the chdir() standard library routine to change the shell's working directory to the argument directory. If no argument is given, the value of the HOME environment variable is used.
    • . filename causes commands to be read from the file. When end-of-file is reached, the shell returns to reading commands from the keyboard.
  • If the command line doesn't invoke an internal command, the shell assumes it is of the form <executable name> <arg0> <arg1> .... <argN>
  • Your shell uses the fork() standard library call, and some flavor of exec(), to invoke the executable, passing it any command line arguments.

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 Mar 31 21:58:55 PDT 2012
CSE451Shell% /bin/cat /etc/motd /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/bin/tcsh
/bin/csh
CSE451Shell% ./date
./date: No such file or directory
Notes:
  • The words in bold in the sample session above were output by the shell, and those to the right of them were typed by the user. (Where did the non-bold, non-underlined words come from?)
  • Please take a look at the manual pages for execvp, fork, wait, and getenv.
  • To allow users to pass arguments to executables, you will have to parse the input line into words separated by whitespace (spaces and '\t' (the tab character), and then create an array of strings pointing at the words. You might try using strtok() for this (man strtok for a very good example of how to solve exactly this problem using it).
  • You'll need to pass the name of the command as well as the entire list of tokenized strings to one of the other variants of exec, such as execvp(). These tokenized strings will then end up as the argv[] argument to the main() function of the new program executed by the child process. Try man execv or man execvp for more details.
  • Users tend to have a lot of bugs, and consequently dealing with them in a robust way can require a lot of code, especially if you're feeling like being helpful about reporting just what the mistake might be. Except as explicitly noted in the "specs" above, or when trivial to implement and of significant value, we're not feeling very helpful in writing this shell.

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.

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.
  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

  • I suggest you wade, rather than dive, into this. In particular:
    1. If you've never compiled the kernel, go back through the project kernel information above.
    2. Now put a "printk()" somewhere in the code, and figure out how to find its output.  (Hints: /var/log and "man dmesg").
    3. Now implement a parameterless system call, whose body is just a printk() call. See this excellent walkthrough, though be aware it was written for a slightly different version of linux with fewer preexisting system calls; as one example, where it refers to entry.S, you'll want syscall_table.S instead.
  • Both the design and implementation aspects of steps 2 and 3 present several different ways to approach this problem. It is your job to analyze them from an engineering point-of-view and choose an appropriate set of trade-offs for your implementation.
  • Warning 1: Remember that the Linux kernel should be allowed to access any memory location, while the calling application should be prevented from causing the kernel to unwittingly read/write addresses other than those in its own address space. Details about this are here.
  • Warning 2 (Hint 0): Remember that it's inconceivable that this problem (warning 1) has never before been confronted in the existing kernel.
  • Warning 3: User programs tend to have a lot of bugs. Remember that the kernel must never, ever crash. This means that it cannot trust the application to know what it's talking about when it makes a request, particularly with respect to parameters passed in from the application to the kernel. (It also means the kernel cannot have a memory leak, as that would eventually cause a crash.)
  • Warning 4: Similarly, remember that you must be sure not to create security holes in the kernel with your code.

Notes: Item 4

  • You can't write C code that causes the compiler to generate a syscall instruction (meaning you can't directly invoke raw syscalls from C). Instead, you need to use the syscall() library routine. This code fragment show you how to do that:
  • #define __NR_execcounts [someNumber]
    #include <sys/syscall.h>
    #include <unistd.h>
    ...
    int ret = syscall(__NR_execcounts, ...);
    
    man syscall will tell you more.
  • In a more realistic situation, your new syscall number would be put in a system include files so that #include <unistd.h> would provide it to user programs. That's cumbersome in this classroom environment. As a workaround, we just #define the new value in the one file that needs it (the library routine).

Step 4: Implement a utility application (execcnts.c)

We want to implement a program, execcnts that is to process creation syscall statistics what time (for info, man 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.


What to Turn In

You should submit the following files under the directories as indicated under a username1-username2-username3 (or group-name) directory using the turnin instructions, omitting the files produced by compilation (such as .o files, executables, etc.). Only one member of your group should submit this file.
  1. The C source code of your shell (fsh.c) under shell/.
  2. The C source code (getexeccounts.c and .h files) of your library routine implementation, and the library itself (getcounts.a) under library/.
  3. Your driver (getDriver.c) under driver/.
  4. Your implementation of the utility program, execcnts.c under utility/.
  5. The source code of the files you changed in or added to the kernel under kernel/.
  6. A compiled kernel image (/boot/vmlinuz-2.6.38.2) with your changes under kernel/.
  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) in a file called files.txt under kernel/.
  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 in a file called sample_output.txt in the top-level directory of the tar.gz file. (/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 file called README at the top-level directory of the archive. This should contain the names and usernames of the members of your group as well as 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. gotos are generally considered bad programming style, but these are used frequently in the Linux kernel. Why might this be? This is a thinking question, so justification is more important than your answer.
    4. How could you extend your shell to support multiple simultaneous processes (foreground and background...)?
    5. 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?)
    6. How long does your new system call take? Explain your timing methodology.
    7. How is it that we can write test programs and scripts that will automate testing of your new syscall, even though we don't have any idea how it is you designed it (e.g., what the syscall number is, or what arguments it takes, or how it returns results)?
    8. 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 this is true.
    9. 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.

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. Please ensure that your group members' names and usernames are at the top of your write-up file.

The grade on the project will be calculated as follows:

  • Shell: 10 points
  • Library routine: 5 points
  • Utility program: 5 points
  • System call: 20 points
  • Write-up: 20 points

Additional notes/tips

Adding new files to the kernel source tree: If you need to add a new file to the Linux source tree, do it in the following manner:

  • Determine which existing directory the file(s) will live in.
  • Edit the Makefile in that directory, and add the name(s) of the .or file(s) that will be created to the definition of O_OBJS.

While we say "the shell," there are many different shell programs (e.g., sh, ksh, csh, tcsh, bash, the shell you're writing, etc.). Additionally, 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.

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.)