Project 1 - The Shell and System Calls

Out: Wednesday January 16th

Due (code and writeup): Friday, January 25, 10:00 a.m. (electronic) and in class (hard-copy)

You must get started on this promptly, or you won’t finish.  “Slow and steady wins the race!”


Assignment Goals


Background: The Shell

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 out 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
  1. 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 being 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 easier.

 

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

 

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

<executable name> <arg0> <arg1> .... <argN>

Your shell uses the fork() standard library call, and some version 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 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.

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:

#define __NR_execcounts someNumber

#include <sys/syscall.h>
#include <unistd.h>

...
   
int ret = syscall(__NR_execcounts, ...);

man syscall will tell you more.

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 instance,

 

execcnts find . -name '*.c'

 

would print the numbers of each of the four syscalls involved in executing that find command.


Some Hints from Last Year

The C libraries might never have invoked one/some/most of the fork-like system calls. Moreover, the kernel code translates some of the calls into one or more of the others (so that only one of the fork-line calls ever really got used). This can be confusing. (On the other hand, the fact that this could happen is part of what this version of the assignment is about -- the role of the runtime libraries, for instance.)

Another common problem is not understanding the difference between '. myscript.sh' and 'myscript.sh'. (This is exactly the point of having it in the assignment, of course.)

Finally, note that '. myscript.sh' could read a script that also used a '.' command -- your implementation should handle that. Consider also the situation where myscript.sh does a '. myscript.sh'.


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 (bzImage) 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. gotos are generally considered bad programming style, but these are used frequently in the Linux kernel, why could 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 that 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.


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

Additionally, hand in printed copies of the following in class on Friday, January 19:

  1. Your write-up.
  2. The code for your shell implementation (fsh.c).
  3. The code for your library routine (getexeccounts.c and getexeccounts.h).
  4. The code for your utility program (execcnts.c).

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: We will be using the turnin(1L) program.

  1. Get all of your files onto one of the general-purpose instructional Linux machines (attu). (No object code, backup copies, or other cruft, please.) scp(1) may be useful. You can also use the WinXP machines to access your instructional Linux home directory. See Lab Support's NT Dfs Page; you want to mount \\ntdfs\cs\unix\homes\iws\LOGIN.
  2. Create a directory named your_username-project1 and copy the files to be submitted into this folder. Use turnin these instructions to submit the directory.


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