CS 134 Homework Assignment #3: Basic System Calls

See the stages below for an assignment outline.

Wiki Answers Due: 10-30-2012
Design Documents Due: 11-6-2012
Preliminary Patches Due: 11-11-2012
Final Patches Due: 11-21-2012
Code Reviews Due: 12-3-2012, 7 PM

In this assignment you will be implementing a set of file- and process-related system calls. When you have completed the first coding part of the assignment, your operating system will be able to run multiple copies of a single user-space program, and allow that program to perform basic file I/O. You will make OS/161 able to run real programs! Once you have completed the final coding parts of the assignment, you will also add the ability to run a variety of programs concurrently.

A substantial part of this assignment is understanding how OS/161 works and determining what code is required to implement the required functionality. Expect to spend at least as long browsing and digesting OS/161 code as actually writing and debugging your own code.

Preliminaries

Recall that you must have your path correctly set to undertake all CS 134 assignments. If you haven't done so already, put the appropriate line in your .login or .bashrc.

Once we have chosen our patch for locks and condition variables, you will no longer need your code from Assignment 2. In the first week of the assignment, you will only be reading OS/161 code so it does not matter which code base you examine. Once the Assignment 3 code base is available, you can remove your Assignment 2 files. If you wish to save what you have done, you could run something along the lines of

cd ~/courses/cs134/your-group-name
mkdir hw2-changes
mv hw2/handin hw2-changes/
mv hw2/src/*/{thread,synch}.{c,h} hw2-changes/
before removing the hw2 directory.

For this assignment, once the hw3 directory is available, svn copy it and run setup in the usual way, then configure and build the kernel using the SYSCALLS configuration file (see previous assignments for details).

Recall that the setup script builds all of the “userland” programs and libraries (such as /sbin/reboot). This assignment includes an additional program, /testbin/hw3test. After you've run setup and built the kernel, running this program (using the p command from the menu) will yield the following results:

Unknown syscall 6
Unknown syscall 6
Unknown syscall 6
...
Unknown syscall 0
When your assignment is complete, the results will be much more satisfying.

Overview

In the previous assignment, you may have thought of your solutions to the cats and mice problem as separate programs, but they were really just functions that were linked into the kernel itself and thus ran inside the kernel, in kernel mode. We used that approach because the kernel wasn't yet able to run meaningful user-space code. In this assignment, you'll be making OS/161 deal with true user-space programs for the first time.

At present, OS/161's support for user-space programs is limited and incomplete. The only working system call available to user-space code is reboot. Moreover, the kernel itself doesn't currently understand what a process is or maintain any per-process state. In this assignment, you will remedy this issue by

The first step is to read and understand the existing parts of OS/161. At present, OS/161 can run one user-level C program at a time—of course, the only programs that work are reboot, halt, and poweroff, which all (only) use the reboot system call. Understanding how the kernel runs these simple programs should help you in planning what parts of the code you will need to modify.

Code Reading

The code-reading component of this assignment is meant to direct your attention to those parts of OS/161 that are particularly relevant to this assignment.

kern/userprog/
Contains the files that are responsible for the loading and running of user-level programs. Currently, this directory only contains three source files (described below), although you may want to add more of your own during this assignment. Understanding these files is the key to getting started with the implementation of multiprogramming.
kern/userprog/loadelf.c
Contains the functions responsible for loading an ELF executable from the filesystem and into virtual memory space. (Of course, at this point OS/161's dumbvm code does not provide what is normally meant by virtual memory—although there is translation between the addresses that executables “believe” they are using and physical addresses, there is no mechanism for providing more memory than exists physically. The dumbvm system also leaks memory, so expect OS/161 to run out of memory after running a few programs.)
kern/userprog/runprogram.c
Contains only one function, runprogram, which is responsible for running a program from the kernel menu. It is a good base for writing the execv system call, but only a base—you will need to determine what more is required for execv that runprogram does not need to worry about. Additionally, once you have designed your process system, runprogram should be altered to start processes properly within this framework; for example, a program started by runprogram should have the standard file descriptors available while it's running.
kern/userprog/uio.c
Contains functions for moving data between kernel and user space. Knowing when and how to cross this boundary is critical to properly implementing user-level programs, so be sure to read this file very carefully. You should also examine the code in lib/copyinout.c.

Traps & System Calls

kern/arch/mips/mips/
Exceptions are the key to operating systems; they are the mechanism that enables the OS to regain control of execution and therefore do its job. You can think of exceptions as the interface between the processor and the operating system. When the OS boots, it installs an “exception handler” (carefully crafted assembly code) at a specific address in memory. When the processor raises an exception, it invokes this code, which sets up a “trap frame” and calls into the operating system (a “trap” is a machine exception that the operating system catches and handles). Interrupts are exceptions, and, more significantly for this assignment, so are system calls. Specifically, syscall.c handles traps that happen to be syscalls. Understanding the C code in this directory is key to being a real operating-systems junkie.
kern/arch/mips/mips/trap.c
mips_trap is the key function for returning control to the operating system. This is the C function that gets called by the assembly exception handler. md_usermode is the key function for returning control to user programs. kill_curthread is the function for handling broken user programs; when the processor is in user mode and hits something it can't handle (say, a bad instruction), it raises an exception. There's no way to recover from this, so the OS needs to kill off the process. Part of this assignment will be to write a useful version of this function.
kern/arch/mips/mips/syscall.c
mips_syscall is the function that delegates the actual work of a system call to the kernel function that implements it. Notice that reboot is the only case currently handled. You will also find a function, md_forkentry, which is a stub where you will place your code to implement the fork system call. It should get called from mips_syscall.

Userland

lib/crt0/
There's only one file in here, mips-crt0.S, which contains the MIPS assembly code that receives control when a user-level program is started. It calls main. Your execv implementation will interface with this code, so be sure to check what values it expects to appear in what registers and so forth.
lib/libc/
There's obviously a lot of code in the OS/161 C library (but dramatically less than, say, the GNU C library on a Linux system). We don't expect you to read it all, although it may be instructive in the long run to do so. For present purposes you need only look at the code that implements the user-level side of system calls.
lib/libc/errno.c
Defines the global variable errno.
lib/libc/syscalls-mips.S
Contains the machine-dependent code necessary for implementing the user-level side of MIPS system calls.
lib/libc/syscalls.S
This file is created from syscalls-mips.S at compile time and is the actual file assembled to put into the C library. The names of the system calls are placed in this file using a script, callno-parse.sh, that reads them from the kernel's header files. This approach avoids having to make a second list of the system calls.

Wiki Component

As with the previous assignment, the Wiki component is not graded in the conventional sense—completion of this part of the assignment is covered under the course's participation requirement. Every member of the class should should post or improve the answer to about two of the questions below on the course's Wiki and understand the answers to all of the questions. Some questions are easier than others, so if you have answered an easy one, you should try to correct or amplify a more difficult one.

In class, I may ask you about the answers to these questions, or ask you closely related questions whose answers you should know from answering the questions below.

  1. What is the difference between UIO_USERISPACE and UIO_USERSPACE?
    When should one use UIO_SYSSPACE instead?
  2. Why can the struct uio that is used to read in a segment be allocated on the stack in load_segment? (I.e., where does the memory read actually go?)
  3. In runprogram, why is it important to call vfs_close before going to user mode?
  4. What is the difference between md_usermode and mips_usermode?
  5. In what file are copyin and copyout defined? What about memmove?
    Why can't copyin and copyout be implemented as simply as memmove?
  6. What (briefly) is the purpose of userptr_t
  7. What is the numerical value of the exception code for a MIPS system call?
  8. Why does mips_trap set curspl to SPL_HIGH “manually,” instead of using splhigh?
  9. How many bytes does a MIPS instruction occupy?
    (Answer by reading mips_syscall carefully, not by looking somewhere else.)
  10. What is stored in struct trapframe, and what is the trapframe used for?
    Where is the trapframe that is passed into mips_syscall stored?
  11. Where is the struct thread defined? What does this structure contain?
  12. User-space code refers to a global variable called errno. How does errno get set?
  13. Why does the system call code need to add four to the instruction pointer stored in the trapframe before it returns?
  14. What would be required to implement a system call that took more than four arguments?
  15. The md_forkentry function exists to “return” from the fork() system call in a newly created child process. Why is return in quotes? When you write this function, what code will you use as your starting point? Where did the trapframe come from? How will md_forkentry come to be called?
  16. What is the purpose of the SYSCALL macro?
  17. What is the MIPS instruction that actually triggers a system call?
    (Answer this by reading the source, not by looking elsewhere.)
  18. How are vfs_open and vfs_close used? What other vfs_* calls are relevant?
  19. The path argument to vfs_open is not const. Investigate, and determine whether there are any potential gotchas with using vfs_open.
  20. What are VOP_READ and VOP_WRITE? How are they used?
    What does VOP_TRYSEEK do?

Design and Implementation Requirements

The first week of this assignment is devoted to design, with the remainder for testing, documenting, and cleaning up your code—although you can write some ancillary code even during the first week. Your first goal is to work out how you are going to achieve the objectives of this assignment, so that both members of the team will have a clear idea of what it is you are trying to achieve when you begin coding. At the end of the design phase, you will bring your design documents to class, ready to explain your design to others (ideally you should have electronic versions too). After class you may use your own design as is, or borrow from someone else's to use in synthesizing your final design.

You are required to provide an implementation plan and time budget in the file handin/schedule.txt. This requirement is intended to help you—if you are not disciplined in managing your time, you will have severe difficulties completing this assignment.

Basic File and Process System Calls

In this part, you will need to design and implement support for the following system calls:

  1. open, read, write, lseek, close, dup2
  2. fork, _exit
Your implementation of these system calls must

The manual pages in the OS/161 distribution (available in the src/man/bin directory of your source tree, and on-line on the CS134 Web site) contain a description of correct return values for various errors. If there are conditions that can happen that are not listed in the manual page, return the most appropriate error code from kern/errno.h. If none seem particularly appropriate, consider adding a new one.

If you're adding an error code for a condition for which Unix has a standard error-code symbol, use the same symbol if possible. Consult the Unix manual pages to learn about Unix error codes (see the intro page in Section 2 of the manual; use man man to find out how to get to Section 2). Note that if you add an error code to kern/include/kern/errno.h, you need to add a corresponding error message to the file src/lib/libc/strerror.c.

I/O-Related Calls

The OS/161 kernel already knows how to open files and character devices, and read and write from them. The problem is that this functionality is not exposed to user programs—no system calls are currently implemented that provide these facilities. Your task is to implement the necessary POSIX-style interface.

The open, read, write, lseek, close, and dup2 calls all manipulate file descriptors. From a user-code perspective, file descriptors are small integers returned by the operating system, which signify open files. On a POSIX system, the convention is that for any given process, the first three file descriptors (0, 1, and 2) are considered to be standard input (stdin), standard output (stdout), and standard error (stderr). In your solution, programs run directly from the OS/161 menu system should start out with these file descriptors attached to the console device (“con:”), but your implementation must allow programs to use dup2 to change them to point elsewhere.

Although these system calls may seem to be tied to the filesystem, in fact, they are really about manipulation of file descriptors, or process-specific filesystem state. A large part of this assignment is designing and implementing a system to track this state. You must determine what information is specific only to the process, what is specific to the file descriptor, and how the two relate. Don't rush your design. Think carefully about the state you need to maintain, how to organize it, and when and how it has to change. Important questions include:

Remember that you will not actually have to write any low-level file system code to implement these system calls. You will use the existing VFS layer to do most of the work. Your job is to construct the subsystem that implements the interface expected by user-level programs by invoking the appropriate VFS and vnode operations.

For consistency, please place most of your implementation of file-related system calls in the following files:

kern/include/file.h
Function prototypes and data types for your file subsystem
kern/userprog/file.c
Function implementations and variable instantiations

Process-Related Calls

In this part of the assignment, you should implement a simplified form of the fork system call. Your implementation should be the same as that described in the fork man page, except that it should return 1 to the parent (rather than the child's PID).

The amount of code to implement fork and _exit is quite small at this stage; the main challenge is to understand what needs to be done and where. In particular, you should design and implement the file-related system calls with fork in mind.

Some hints:

Robustness

Currently, if user-space code generates a fatal exception, the kernel panics. This placeholder solution is obviously not acceptable, and needs to be fixed now that you'll be running actual programs in user space. You'll need to replace kill_curthread with a new version that properly handles such issues more sanely. You should, of course, try to write it in as simple a manner as possible. Keep in mind that essentially nothing about the current thread's user-space state can be trusted if it has suffered a fatal exception—it must be taken off the processor in as judicious a manner as possible, but without returning execution to the user level.

Testing

You can run /testbin/hw3test to test your system calls. Example output (where most of the system calls except _exit are implemented) is shown below:
OS/161 kernel [? for menu]: p /testbin/hw3test
Operation took 0.000212160 seconds
OS/161 kernel [? for menu]:
**********
* File Tester
**********
* write() works for stdout
**********
* write() works for stderr
**********
* opening new file "test.file"
* open() got fd 3
* writing test string
* wrote 45 bytes
* writing test string again
* wrote 45 bytes
* closing file
**********
* opening old file "test.file"
* open() got fd 3
* reading entire file into buffer
* attempting read of 500 bytes
* read 90 bytes
* attempting read of 410 bytes
* read 0 bytes
* reading complete
* file content okay
**********
* testing lseek
* reading 10 bytes of file into buffer
* attempting read of 10 bytes
* read 10 bytes
* reading complete
* file lseek  okay
* closing file
**********
* testing fork
* Forked, in parent
* Forked, in child
Unknown syscall 0

Advanced Process-Related System Calls

In the final part of the assignment, you will implement the following system calls:

  1. getpid
  2. execv, waitpid, and _exit

Implementing Process IDs

A pid, or process ID, is a unique number that identifies a process (as such, it has much in common with a file descriptor; both are integers that let user-space programs refer to kernel data safely). The implementation of getpid is not terribly challenging, but pid allocation and reclamation are the important concepts that you must implement. It is not okay for your system to crash because over the lifetime of its execution you've used every possible pid once. Design your pid system, implement all the tasks associated with pid maintenance, and only then implement getpid.

Process Creation, Execution and Termination

In this part, you must revise fork so that a newly created process is given a new PID and that pid is returned to the parent of the process. You will want to think carefully through the design of fork and consider it together with execv to make sure that each system call is performing the correct functionality.

Although execv is “only” a system call, it is perhaps the most challenging part of this assignment. It is responsible for taking a process and making it execute a new program. Essentially, it must replace the existing address space with a brand new one for the new executable (created by calling as_create in the current dumbvm system) and then run it. While this is similar to starting a process straight out of the kernel (as runprogram does), it's not quite that simple. Remember that this call is coming out of user space, into the kernel, and then returning back to user space. You must manage the memory that travels across these boundaries very carefully. (Also, notice that runprogram doesn't take an argument vector—but arguments must of course be handled correctly in execv).

Although it may seem simple at first, waitpid requires a fair bit of design. Read the specification carefully to understand the semantics, and consider these semantics from the ground up in your design. You may also wish to consult the Unix manual page, although you should keep in mind that you are not required to implement all the things that Unix's waitpid supports—nor is the Unix parent/child model of waiting the only valid or viable possibility.

Your revised implementation of _exit is also intimately connected to the implementation of waitpid. They are essentially two halves of the same mechanism. In all likelihood, your code for _exit will be simple and your code for waitpid more complex—but it's perfectly viable to design it the other way around. If you find both are becoming extremely complicated, it may be a sign that you should rethink your design.

Shell

The system calls you have in OS/161 do not allow as full-featured a shell as Unix, but they are nevertheless sufficient to write a simple and useful command-line interface. This optional part of the assignment is recommended for your own sense of fun and achievement, but is not assessed. You may adapt code you wrote for 105, or collaborate more widely than with just your partner. Skeleton code for a shell is in bin/sh/sh.c.

Remember that because OS/161 implements a subset of the standard POSIX API, you can develop your shell under BSD, Linux, MacOS X, or Solaris and then compile it for OS/161. But keep in mind that OS/161 is really basic right now. No signals. No malloc. And no, you're not allowed to write those missing parts to make your shell better/easier. Yet.

Stages

To assist you in organizing and budgeting your time, this assignment is completed in stages.

Wiki Questions

The Wiki questions are due before class on Tuesday, October 30, 2012.

Design Documents

On Tuesday, November 6, you will need to bring your (printed) design documents to class. These documents will show how you plan to implement file I/O and processes. You do not need to have thought through your methods for handling process IDs or implementing waitpid, but it would be good if you have.

Your documents must include:

Preliminary Patch

As usual, you will make your changes to OS/161 available in the form of a patch. This patch is essentially a work in progress and need not be extensively commented or have explanatory text. It should, however, provide a complete working implementation of open, read, write, lseek, close, dup2, fork, and _exit.

You should generate your preliminary patch by running

cd ~/cs134/hw3
handin/makepatch > handin/prelim-syscalls.patch
svn add handin/prelim-syscalls.patch
svn commit

Your preliminary patch is due on the night of Sunday, November 11.

Final Patch

Your final patch will include code implementing all coding parts of the assignment.

You should generate your final patch by running

cd ~/cs134/hw3
handin/makepatch > handin/syscalls.patch
svn add handin/syscalls.patch
svn commit

You should also document your patch in handin/syscalls.txt.

The final patch is due the night of Wednesday, November 21.

Hints & Tips

You need to think about a variety of issues associated with implementing system calls. In this section, we raise some questions and offer a few tips.

Finally, remember to keep it simple. Both open and fork are allowed to return errors saying that you've opened too many files at once or forked too many processes at once, which, in turn, means that you are allowed to use fixed-sized tables.

Code Review

After all assignments have been submitted, we will perform code reviews on the submissions.