Project #2: Basic Multiprogramming and System Calls in NACHOS

Due 11:59:59pm on Feb 23, 2006

Contents

Overview
Exercises
How to turnin
Grading Criteria
Issues to Consider
Suggestions
FAQ
Part 1 Implementation Guide
Part 2 Implementation Guide
NOTE: This assignment is much more difficult than the previous one. It is very important to start early.

Overview

The NACHOS code you have been given is capable of executing user application programs, but in an extremely limited way. In particular, at most one user application process can run at a time, and the only system call that has been implemented is the halt system call that shuts down NACHOS. In this assignment, you will correct some of these deficiencies and turn NACHOS into a multiprogramming operating system with a working set of basic system calls.

For this assignment, in the first part, you are to modify NACHOS so that it can run multiple user applications at once. You are to implement the Fork(), Yield(), Exit(), Exec(), and Join() system calls. The detailed specifications for these system calls are given below.

In the second part of this assignment, you are to implement the Creat, Open, Read, Write, and Close system calls. For this assignment, you will be working on the version of NACHOS in the userprog directory. You will also need to write some simple user application programs, compile them using the MIPS cross compiler, and run them under NACHOS to test that your modifications to NACHOS actually work. User application programs are written in ANSI C. To write and compile them go to the test subdirectory of NACHOS. Several sample applications are provided in that directory. The only one that will run properly on an unmodified NACHOS is the halt program. To demonstrate this program, first go to the test directory. Type make halt to cross compile the halt.c source file and create the executable NACHOS user application halt. Then go to the userprog directory and type gmake nachos. When NACHOS is built, type nachos -x halt. This will start NACHOS and ask it to load and run the halt program. You should see a message indicating that NACHOS is halting at the request of the user program.

In brief, what happens when you type nachos -x halt is as follows:

NACHOS starts. The initial thread starts by running function StartProcess() in file progtest.cc.

A new address space is allocated for the user process, and the contents of the executable file halt are loaded into that address space. This is accomplished by the constructor function AddrSpace::AddrSpace() in the file addrspace.cc.

MIPS registers and memory management hardware are initialized for the new user process by the functions AddrSpace::InitRegister() and AddrSpace::RestoreState() in addrspace.cc.

Control is transferred to user mode and the halt program begins running. This is accomplished by the function Machine::Run(), which starts the MIPS emulator.

The system call Halt() is executed from user mode (now running the program halt). This causes a trap back to the NACHOS kernel via function ExceptionHandler() in file exception.cc.

The exception handler determines that a Halt() system call was requested from user mode, and it halts NACHOS by calling the function Interrupt::Halt().

Trace through the NACHOS code until you think you understand how program halt is executed.

In this assignment, you will also need to know the object file formats for Nachos. This is how NOFF (NACHOS Object File Format) looks like.

-----------
| DATA    |
-----------
| ....    |         -----
-----------             |
| ....    |             |
-----------             |
| bss     | segment     |
-----------             |---- CODE Section
| data    | segment     |
-----------             |
| code    | segment     |
-----------             |
| magic # | 0xbadfad    |
-----------         -----


NOFF files have only code and data section. Inside CODE sections are segments pointing to the real location of code, data, and bss sections.

--------------
|virtual addr|  points to the location in virtual memory
--------------
|in file addr|  points to a location inside the DATA part of NOFF file
--------------
|size        |  size of a segment in bytes
--------------
This information about the NOFF can be found in /bin/noff.h file.

When you create user programs and compile them using the MIPS compiler (cross compile) you get COFF (common object file format) file. This is a normal MIPS object (executable) file that has DATA, TEXT and CODE sections. For this file to be runable under Nachos it has to be turned into NOFF. This is done by using coffnoff translator.

Exercises

In the first part of this assignment, you are to implement the Fork(), Yield(), Exit(), Exec(), and Join() system calls that act as follows:
The Fork(func) system call creates a new user-level (child) process, whose address space starts out as an exact copy of that of the caller (the parent), but immediately the child abandons the program of the parent and starts executing the function supplied by the single argument. Notice this definition is slightly different from the one in the syscall.h file in Nachos.

The Yield() call is used by a process executing in user mode to temporarily relinquish the CPU to another process.

The Exit(int) call takes a single argument, which is an integer status value as in Unix. The currently executing process is terminated. For now, you can just ignore the status value. Later you will figure out how to get this value to an interested process.

The Exec(filename) system call spawns a new user-level thread (process), but creates a new address space and begins executing a new program given by the object code in the Nachos file whose name is supplied as an argument to the call. It should return to the parent a SpaceId which can be used to uniquely identify the newly created process.

The Join() call waits and returns only after a process with the specified ID (supplied as an arguemnt to that call) has finished.

Test your code by creating several user programs that exercise the various system calls while we also provide you the sample test programs . Be sure to test each of the system calls, and to try forking up to three processes (since each has a 1024 byte stack, that's all that will fit in NACHOS' 4K byte physical memory) and have them yield back and forth for awhile to make sure everything is working. Since the facility for I/O from user program will also be implemented during this assignment, you may initially have to rely on using debugging printout in the kernel to track what is happening. Use the DEBUG macro for this, and make sure that debugging printout is disabled by default when you submit your code for grading.

In the second part of this assignment, yoou are to implement the file system calls: Creat, Open, Read, Write, and Close. The semantics of thes calls are specified in syscall.h. You should extend your file system implementations to handle the console as well as normal files.

To support the system calls that access the console device, you will probably find it useful to implement a SynchConsole class that provides the abstraction of synchronous access to the console. The file progtest.cc has the beginning of a SynchConsole implementation.

Required Output Prints

In order for us to see how your program works, some debugging information must be added in your code. You should print out the following information:

  1. Which system call is called? Whenever a system call is invoked, print:
    System Call: [pid] invoked [call]
    where [pid] is the identifier of the process (SpaceID) and [call] is the name of one of the system calls that you had to implememt. Just give the name without parentheses (e.g., Fork, Create, Exit).
  2. How many pages are allocated to the user program when it is loaded? The following line should be printed when a program is loaded:
    Loaded Program: [x] code | [y] data | [z] bss
    where [x], [y] and [z] are the sizes of the code, (initialized) data and bss (uninitialized) data segments in bytes.
  3. Which process has been forked and how many pages are allocated to the forked process? Whenever a new process is forked, print the following line:
    Process [pid] Fork: start at address [addr] with [numPage] pages memory
    where [pid] is the process identifier (SpaceID) of the parent process, [addr] is the virtual address (in hexadecimal format) of the function that the new (child) process starts to execute and [numPage] is the number of pages that the new process gets.
  4. What is the file name in the Exec system call? When a new process should be executed, print the following line:
    Exec Program: [pid] loading [name]
    where [pid] is the identifier of the parent process that executes a new process and [name] is the name of the executable file that is being loaded
  5. When a thread exists, print the following line:
    Process [pid] exists with [status]
    where [pid] is the identifier of the exiting process and [status] is the exit code.

What to Submit

You have to provide a writeup in a file HW2_WRITEUP in which you have to mention which portions of the assignment you have been able to complete. And for the ones that are incomplete, what is their current status. The description of status for completed exercises or those that you did not start should not be more than 1 line. However, when you submit code that is not working properly (partial solution), make sure that your documentation is very verbose so that the TAs can understand what you have achieved and which parts are missing. This is necessary to receive at least partial credit. When you just submit something that does not work and give no explanations, expect to receive no credit. Also include both group members' names and a short note listing all office hours that both group members can attend. Make sure that the writeup file is in the top level code directory. You may or may not be asked to arrive during that hour for a 10 minute interview (See grading policy for details). The sample test programs for this project are available under ~cs170/Spr03-Section1.test/proj2. You can provide your own test programs.

  1. Go to your 'nachos' directory.
  2. Turn in your 'code' directory with the command:


You can turnin up to 3 times per project and not more than that! The earlier versions will be discarded.

Note: only one turnin per group is accepted!

Grading Criteria

Grade Distribution (out of 200 Possible Points)

120 Points (60%): Implementation 80 Points (40%): Test Case Performance

Issues to Consider

Here is an outline of the the major issues you will have to deal with to make NACHOS into a multiprogrammed system:
In order to handle the various system calls, you will need to modify the ExceptionHandler() function in exception.cc to determine which system call or exception occurred, and to transfer control to an appropriate function. You might want to consider introducing ``stubs'' (functions with empty bodies or with debugging printout so you can tell when they are called) for all the system calls right away, and then postpone their actual implementation until a bit later. This strategy will help you understand better how control is transferred from user mode to system mode when a system call is executed.

The NACHOS code you have been given is extremely simple-minded about memory management. In particular, the constructor function AddrSpace::AddrSpace() simply determines the amount of memory that will be required by the application to be run and then allocates that much space contiguously starting at address zero in physical memory. The page tables (which control the address translation hardware) are set up so that the logical addresses (what the user program sees) are identical to the physical addresses (where the data is actually stored).

The above scheme is inadequate for running more than one application at a time. You will need to design and implement a scheme for allocating and freeing physical memory, and you will need to arrange to set up the page tables so that the logical address space seen by a user application is a contiguous region starting from address zero, even though the data is stored at different physical addresses. You will want to implement a memory management scheme that is flexible enough to extend to virtual memory later in the semester. We suggest implementing a C++ class with methods for allocating and freeing physical memory one page at a time. By setting up the page tables properly, you can give the user application a contiguous logical address space even though each page of actual data might be stored anywhere in physical memory.

The Fork() system call is the most difficult part of this assignment. It is different from the system call Exec in that Fork will start a new process that runs a user function specified by the argument of the call, while Exec will start a process that runs a different executable file. The parameter types for Fork() and Exec() also differ. Fork(func) takes an argument func which is a pointer to a function. The function must be compiled as part of the user program that is currently running. By making this system call Fork(func), the user program expects the following: a new thread will be generated for use by the user program; and this thread will run func in an address space that is an exact copy of the current one. This implementation of Fork makes it possible to have and to access multiple entry points in an executable file.

To make the system call Fork(func) work for the user program, you will need to know how to find the entry point of the function that is passed as the parameter. The parameter convention is determined by the cross-compiler which produces executable code from the user source program. Look at the file exception.cc to see that this entry point, which is an address in the executable code's address space, is already loaded into register 4 when the trap to the exception handler occurs. All you need to do is to insert code into the exception handler (or call a new function of your own) which does the following: set up an address space which is a copy of the address space of the current thread, and load the address that is in register 4 into the program counter. After these steps, use Thread::Fork() to create a new thread, initialize the MIPS registers for the new process, and have both the new and old processes return to user mode. The parent should return to user mode by returning from the exception handler, the child process should continue to run from the address that is now in the program counter, which is the entry point of the function. To implement Fork, you will need to introduce modifications to the AddrSpace class in addrspace.cc so that you can make a ``clone'' of a running user application program. We suggest adding a function AddrSpace::Fork(). In brief, calling this function will create a new address space that is an exact copy of the original. You will have to allocate additional physical memory for this copy, set up the page tables properly for the new address space, and copy the data from the old address space to the new. Once the physical memory has been allocated and the page tables set up, you will use Thread::Fork() to create a new kernel thread, initialize the MIPS registers for the new process, and then have both the old and the new processes return to user mode. The child process should continue by finishing the Fork() system call. The parent should return to user mode merely by returning from the ExceptionHandler() function.

The Exit() system call should work by calling Thread::Finish(), but only after deallocating any physical memory and other resources that are assigned to the thread that is exiting.

In order to implement the Exec() system call, you will need a method for transferring data (the name of the executable, supplied as the argument to the system call) between the user address space and the kernel. You are not to use functions Machine::ReadMem() and Machine::WriteMem() in machine/translate.cc. Instead, you will have to code your own functions that take into account the address translations described by the page tables to locate the proper physical address for any given logical address. (Recall that strings in C are stored as sequences of characters in successive memory locations, terminated by a null character.)

Once the name of the executable has been copied into the kernel, and the file has been verified to exist, the executable file should be consulted to determine the amount of physical memory required for the new program. This physical memory should be allocated and initialized with data from the executable file, the page tables thread should be adjusted for the new program, the MIPS registers should be reinitialized for starting at the beginning of the new program, and control should return to user mode. File progtest.cc contains a sample for executing a binary program.

If you use ``machine->Run'' to execute a user program, it terminates the current thread. Since Exec() needs to return a space ID to the caller, you should find a way to do that.

NOTE: The object code produced by the MIPS cross-compiler assumes that the data segment begins at the physical address immediately following the text segment. In particular, there is no page alignment, so that if the text segment ends in the middle of a page, then the data segment will start just after it and the page will contain both code and data.

Yield() system call will call Thread::Yield() after making sure to save any necessary state information about the currently executing process.

Be sure to synchronize your code correctly. You will need to put lock operations in your code to ensure that it will work properly. Your locking should be fine grained enough to eliminate any spurious latency problems caused by coarse grained locking. For example, any time a thread accesses the disk or the console it should not hold a lock that would prevent another thread from accessing some other I/O device or piece of data.

You should buffer user file reads in a disk buffer called diskBuffer (defined in system.cc). All of your user-level file I/O must go through the diskBuffer.

You will need to solve a synchronization problem that occurs when multiple processes try to read or write from a file at the same time.

Suggestions

FAQ

  1. Question: How can I test part one (why doesn't printf work in my test programs)?

    Answer: Until you have part 2 done you will have to test part 1 based on flow of execution. For example: to test if Fork/Exec works you Fork/Exec a function/executable and then call Halt() in the function/executable. If Nachos halts when you run your test program then your system call is probably working. Once you have the Write system call implemented you can further test part 1 by putting your Write statements where ever you wish to print something out. This will be useful for testing Join.
  2. Question: What does a process control block contain?

    Answer: A process control block (pcb) contains the attributes of a process. Some of the major attributes are the thread, the pid (SpaceID), open files, etc (remember that the thread has a pointer to the addrspace which is an indirect attribute of a pcb). There should be a global table of process control blocks as well. Remember that you will also want to be able to get a particular process's condition so that it can be waited on in Join (if necessary) and broadcast in Exit. It may not seem like you are using the process control blocks much in part 1 of the project (except for adding and deleting them) but you will use them more in part 2 of the project).
  3. Question: How do I add a new file for this project?

    Answer: You will want to create your .h and .cc file in userprog (every new file for this project can be added in this directory). Then you will want to add the .h, .cc, and .o to the list of files in the top level Makefile.common in the correct USERPROG section. Be sure not to add your file to the end of the list of files, but instead place it in the same section as the other userprog files. In each USERPROG section (ie _H, _C, and _O) all the files from each directory should be placed with the other files from that directory. Once this is done type "make depend" in the userprog directory and then you will be able to type "make" to compile as before.
  4. Question: How do I translate the name of the executable when implementing Exec?

    Answer: You will want to put a translate function in addrspace that is similar to the translate function in Machine. You will use this function to translate a virtual address into a physical address (one page at a time).
  5. Question: Is there a difference between the parameters of Exec and Fork?

    Answer: Yes there is. Exec takes the string representing the relative path (from where you run Nachos - userprog in this case) to the test program you wish to exec [ie. Exec("../test/myExecedProgram")]. Fork takes the name of the function you wish to fork. It is not a string, but a function pointer so it has the form Fork(myFunction) where you have implemented void myFunction() previously in you test program. In Exec you are translating the string that is passed in and in Fork you are using the pointer to myFunction as the PCReg value for when you run the new thread. If you try to pass a string to Fork or a name (not in string form) to Exec you will have serious problems
  6. Question: Does a Forked thread use the same addrspace as the thread who Forked it?

    Answer: No it does not. It uses a COPY of the addrspace from the thread who Forked it. This means you must create a new addrspace that has the same size page table as the Forking thread and then you must copy the Forking thread's pages in memory into the Forked threads pages in memory.
  7. Question: The project spec says we need to add a function ReadFile to Addrspace. What is this used for?

    Answer: This is used for copying the executable's code and data segments into memory (in the constructor for Addrspace (that Exec calls). noffH.code.virtualAddr is the logical address of the executable's code segment and noffH.initData.virtualAddr is the logical address of the executable's data segment. You no longer want to zero out (bzero) the memory. You need to copy these sections page by page into main memory. You will use your Addrspace::Translate to translate the logical data/code address to the physical address (in memory). Then you can use the C/C++/Unix "bcopy" to copy it over.
  8. Question: Where should we put the global structures/classes?

    Answer: You can put these in system.h in the threads directory. Only put the global variables (their declaration) in this file. Make sure you actually instantiate the global structures (like memory manager) in the system.cc file in the initialize function under the correct #ifdef's (for userprog in this case).
  9. Question: Does the Exit system call take a parameter? What is that parameter?

    Answer: The Exit system call should take an integer parameter. This parameter is the exit value of the process. This is the same as the exit values in Unix (0 -> good, 1 -> bad). For our purposes you can use any integer here. It only matters that if another process was "Join"ing on your process that the Join call would return the value that your process exited with (as per the project spec). This means that you need to save your exit value some how in the Exit system call so that Join can return it if necessary (even if you've already exited when someone calls Join on your process).
  10. Question: What should we do when we can't allocate enough physical pages in Addrspace constructor for a new thread?

    Answer: You should let Fork/Exec know that you don't have enough memory so that it can let the user know (return pid of -1) that it didn't Fork/Exec a new process. Make sure that you check if there is enough free pages in physical memory to facilitate the number of pages for your new process before trying to allocate the physical pages for each logical page. If you do not do this one of your allocates will return -1 and you will have to deallocate all the pages you just allocated before you can let Fork/Exec know of the error.
  11. Question: Do Exec and Fork call thread->Fork?

    Answer: Yes they do. After you have set all other information up (as in the project spec) you need to thread->fork a dummy function (that's in exception.cc). In this dummy function you will want to initialize and restore the registers (for Fork and Exec) and then set up the PC registers and return register address (for Exec only). After this you will call machine->Run() from the dummy function (for both Fork and Exec).

Part 1 Implementation Guide

Threads and Processes:

Thread::Fork() spawns a new kernel thread that uses the same AddrSpace as the thread that spawned it. If that address space is duplicated and not shared, it is no longer a kernel thread but a Forked Process. If that AddrSpace instead contains code and data loaded in from a separate file, it is no longer a forked process, but an executed process. The details of Fork() and Exec() are covered in steps 1) and 7) below.

System Calls:

10 Steps to a Multi-Programmed Nachos

  1. Implement Fork(). Fork will create a new kernel thread and set it's AddrSpace to be a duplicate of the CurrentThread's space. It sets then Yields(). The new thread runs a dummy function that will will copy back the machine registers, PC and return registers saved from before the yield was performed. You did save the PC, return and other machine registers didn't you?

    Duplicating the AddrSpace requires the implementation of a Memory Manager detailed in steps 2-4. Fork() will not work completely until the completion of step 4. Don't get stuck on step 1), steps 2-4 are much more important.

  2. Write a Memory Manager that will be used to facilitate contiguous virtual memory. The amount of memory available to the user space is the same as the amount of physical memory, it's not until project 3 that you will have to implement swapping virtual memory.

    You will need just two methods at first 1) getPage() allocates the first clear page and 2) clearPage(int i) takes the index of a page and frees it. You can use a bitmap (in code/userprog/bitmap.*) with one bit per page to track allocation or use your own integer array, which ever you prefer.

    Modify AddrSpace (code/userprog/addrspace.*) to use the memory manager. first, modify the page table constructors to use pages allocated by your memory manager for the physical page index. The later modification will come in step 4.

  3. Write the AddrSpace::Translate function, which converts a virtual address to a physical address. It does so by breaking the virtual address into a page table index and an offset. It then looks up the physical page in the page table entry given by the page table index and obtains the final physical address by combining the physical page address with the page offset. It might help to pass a pointer to the space you would like the physical address to be stored in as a paramter. This will allow the function to return a boolean TRUE or FALSE depending on whether or not the virtual address was valid. If confused, consult the text book on memory management and page table or Machine::Translate() in machine/translate.cc

  4. Write the AddrSpace::ReadFile function, which loads the code and data segments into the translated memory, instead of at position 0 like the code in the AddrSpace constructor already does. This is needed not only for Exec() but for the initial startup of the machine when executing any test program with virtual memory.

    You should buffer user file reads in a disk buffer called diskBuffer (defined in system.h). All of your user-level file I/O must go through the diskBuffer. Be sure to to under or over run the buffer during the copy. Also be sure not to write too much of the file into memeory. You can use the following prototype for the function.
       int AddrSpace::ReadFile(int virtAddr, 
                               OpenFile* file, 
                               int size, 
                               int fileAddr) {
       
    You will also need to use the functions: File::ReadAt(buff,size,addr) and bcopy(src,dst,num) as well as the memory locations at machine->mainMemory[physAddr].

    At this point, test programs should work the same as before. That is, the halt program and other system calls will still operate the way they did before you modified AddrSpace. Also Fork() should be working. Test your implementation appropriately.

  5. Write the PCB and a process manager. Create a PCB class that will store the necessary information about a process. Don't worry about putting everything in it right now, you can always add more as you go along. To start, it should have a PID, parent PID, and Thread*. The process manager should do the same thing as the memory manager - it has getPID and clearPID methods, which return an unused process id and clear a process id respectively. Again, use a bitmap or similar integer array. You'll also need an array of PCB* to store the PCBs. Modify the AddrSpace constructors to include a PCB as an attribute.

  6. Implement Yield(). Given that forked processes are almost the same as kernel threads, this one should be trivial.

  7. Implement Exec(). Exec is creating a new process (kernel thread) with new code and data segments loaded from the OpenFile object constructed from the filename passed in by the user. In order to get that file name you will have to write a function that copies over the string from user space. This function will start copying memory from the physical address pointed to by the virtual address in machine->ReadRegister(4). It should go until it hits a NULL byte.

    Fork the new thread to run a dummy function that sets the machine registers straight and runs the machine. The calling thread should yield to give control to the newly spawned thread. At this point you should be able to call test files from other test using Exec(someTestFile) from someOtherTestFile.c (in the test/ dir).

  8. Implement Join(). Join should force the current running thread to wait for some process to finish. The PCB manager can keep track of who is waiting for who using a condition variable for each PCB.

  9. Implement Exit(int status). This function should set set the status in the PCB being exited. It should also force any threads waiting on the exiting process to wake up.

  10. Test your system calls using your own test programs, then the ones in ~cs170/Spr03-Section1.test/proj2 that don't rely on part 2. If you have time, test using some programs to do crazy or malicious things.

Part 2 Implementation Guide

NACHOS Files:

We will be using NACHOS files exclusivly in part 2. This will be good preparation for project 3. NACHOS Files are similar to Unix files except they are stored on avirtual disk that is implemented as one big Unix file. The interface to Create, Open, Close, WriteAt and ReadAt to those files are defined in filesys/filesys.cc, filesys/openfile.cc and filesys/filehdr.h. You may want to look through those files when getting ready to call these functions for the first time.

10 Steps to an I/O Enabled NACHOS:

  1. Create a SysOpenFile object class that contains a pointer to the file system's OpenFile object as well as the systems (int)FileID and (char *)fileName for that file and the number of user processes accessing currently it. Declare an array of SysOpenFile objects for use by all system calls implemented in part 2.

  2. Create a UserOpenFile object class that contains the (char *)fileName, an index into the global SysOpenFile table and an integer offset represeting a processes current position in that file.

  3. Modify the AddrSpace's PCB to contain an array of OpenUserFiles. Limit the number to something reasonable, but greater than 20. Write a method (in PCBManager) that returns an OpenUserFile object given the fileName.

  4. Implement Create(char *fileName). This is a straight forward call that should simply get the fileName from user space then use fileSystem->Create(fileName,0) to create a new instance of an OpenFile object. Until a user opens the file for IO it is not necessary to do anything further.

  5. Implement Open(char *fileName);. This function will use an OpenFile object created previously by fileSystem->Open (fileName). Once you have this object, check to see if it is already open by some other process in the global SysOpenFile table. If so, incriment the userOpens count. If not, create a new SysOpenFile and store it's pointer in the global table at the next open slot. You can obtain the FileID by looking up the name in your SysOpenFile table.

    Then create a new instance of an OpenUserFile object (given a SysOpenFile object) and store it in the currentThread's PCB's OpenUserFile array.

    Finally, return the FileID to the user.

  6. Implement a function to Read/Write into MainMem and a buffer given a staring virtual address and a size. It should operate in the same way AddrSpace::ReadFile writes into the main memory one diskBuffer at a time.
    It may help to put the section of the code in ReadFile into a "helper" function called userReadWrite() that is general enough that both ReadFile(), myRead() and myWrite() can call. It need only be parameterized by the type of operation to be performed (Read or Write).

    When called by Write(), It will read from the MainMem addressed by the virtual addresses. It writes into the given (empty) buffer. Write(), will then put that buffer into an OpenFile. When called by Read(), It will write into MainMem the data in the given (full) buffer that Read() read from an OpenFile.

  7. Implement Write(char *buffer, int size, OpenFileId (int)id); First you will need to get the arguments from the user by reading registers 4-6. If the OpenFileID is == ConsoleOutput (syscall.h), then simply printf(buffer). Otherwise, grab a handle to the OpenFile object from the user's openfile list pointing to the global file list. Why can't you just go directly to the global file list?... becuase the user may not have opened that file before trying to write to it. Once you have the OpenFile object, you should fill up a buffer created of size 'size+1' using your userReadWrite() function. Then simply call OpenFileObject->Write(myOwnBuffer, size);

  8. Implement Read(char *buffer, int size, OpenFileId id); Get the arguments from the user in the same way you did for Write(). If the OpenFileID == ConsoleInput (syscall.h), use a routine to read into a buffer of size 'size+1' one character at a time using getChar(). Otherwise, grab a handle to the OpenFile object in the same way you did for Write() and use OpenFileObject->ReadAt(myOwnBuffer,size,pos) to put n characters into your buffer. pos is the position listed in the UserOpenFile object that represents the place in the current file you are writing to. With this method, you must explictly put a null byte after the last character read. The number read is returned from ReadAt().

    Now that your buffer is full of the read, you must write that buffer into the user's memory using the userReadWrite() function. Finally, return the number of bytes written.

  9. Test your system calls using your own test programs, then the ones in ~cs170/Spr03-Section1.test/proj2 Now that both parts are finished all test programs should work.

  10. Test some more.


Adapted from Divy Agrawal,Martin Rinard's and Christopher Kruegel's assignments as well as William Strathearn's section notes for CMPSC 170 at UC Santa Barbara