MiniKernel User's Guide


This document will help you get started using the MiniKernel. You will also need to carefully read and use:
  • API Reference Manual
  • Source Code
  • Introduction

    Writing a real operating system is a gargantuan task. The MiniKernel allows you to write code to deal with the interesting problems presented inside a real operating system without the headache of debugging a real, crashable, system.

    This Kernel simulates the services provided by a real operating system, and draws a distinction between user-level code and kernel-level code. All interaction with the kernel is done by causing interrupts. In addition, simulated devices attached to the computer will also cause spontaneous interrupts when they require service.

    The ugliest part -- implementing Threads and loading programs -- has already been built. Each user-level program is represented by one Java Thread. You are free to extend the system by adding system calls and by implementing such things as file systems.

    Overview

    There are several components to a working MiniKernel system. Each component sees the world a little differently. There can be several user-level programs asking for service from the Kernel by going through the Library. The Kernel must deal with simultaneous requests from all the programs and also pay attention to the Disk.

    From the point of view of the user-level programs, the system is quite simple. A program does not have access to system resources such as disks and it cannot affect other programs. To communicate with the kernel, it simply causes an interrupt and supplies arguments to indicate the service it wants. For example, to output some text on the console, a program causes this interrupt:

    
        Kernel.interrupt(Kernel.INTERRUPT_USER,
                         Kernel.SYSCALL_OUTPUT,
                         0, "Hello, world!\n", null, null);
    

    It would be awfully clumsy to fill your code with commands like this, so the Library class has a convenience function for every system call. A program can accomplish the same thing by doing this:

    
        Library.output("Hello, world!\n");
    

    From the Kernel's point of view, things are a little trickier. Every program and device in the system talks to the kernel by invoking interrupt(). The Kernel must determine who is asking for the service and respond appropriately.

    The Kernel can issue instructions to the Disk. Although Disk operations take time to complete, all methods of Disk return immediately. When a requested operation is complete, the Disk causes an interrupt and the Kernel can pick up where it left off.

    User Programs

    The main program is called Boot. It creates a simulated disk drive, starts it running, and the gets the MiniKernel running by causing a POWER_ON interrupt. The interrupt gives the kernel a reference to the disk drive, the name of a shell program to run, and one integer parameter. The MiniKernel can run as a “shell” program any Java program that implements public static void main(String args[]). It cannot run conventional UNIX programs like ls and emacs. Because the Kernel is going to be simulating operating system services, we won't allow user-level programs to use certain Java functions (see below).

    When the Kernel starts, it looks for the shell program. If it cannot be found, the Kernel stops right away (no point in having an OS without a program to run!). The shell starts other programs by calling Library.exec().

    Try out the MiniKernel by running

    
        java Boot 10 Disk 100 Shell
    
    You should see something like this:
    
        % java Boot 10 Disk 100 Shell Count
        Restored 512 bytes from file DISK
        Boot: Starting kernel.
        Kernel: Disk is 100 blocks
        Kernel: Disk cache size is 10 blocks
        Kernel: Loading initial program.
        Shell>
    
    The Shell> prompt is printed by the Shell program by a call to Library.ouput. Type the name of a Java program. Included with the MiniKernel is a trivial program called Count. The Shell uses Library.input to get input from the user, parses it into words, and then calls Library.exec to run the correct program.
    
        Shell> Count
        Counting: 10
        Counting: 9
        Counting: 8
        Counting: 7
        Counting: 6
        Counting: 5
        Counting: 4
        Counting: 3
        Counting: 2
        Counting: 1
        *** Blast off ***!
        Kernel: Shell Count has terminated.
        Saving contents to DISK file...
        0 read operations and 0 write operations performed
        Boot: Kernel has stopped.
        Shell>
    
    Finally, type Control-D to deliver an end-of-file indication to the Shell. It exists, and the Kernel, seeing that it has nothing more to do, also exits. You can now type in the name of other Java programs and they will execute under the MiniKernel.

    Limitations of User Programs

    Java is full of all sorts of classes and methods which call real OS procedures. Because the MiniKernel is simulating an operating system, user-level programs are prohibited from using methods which call the real OS. For example, if you must output some text, you must call Library.output(), and not System.out.println().

    Prohibited classes include, but are not limited to:

  • java.lang.System
  • java.lang.Thread
  • java.io.*
  • (I don't know of a technical way to enforce this, so you're on the honor system!)

    On the other hand, the purpose of the MiniKernel is to simulate a real operating system, so it is perfectly acceptable to use System.out.println() etc. inside the kernel.

    Adding a System Call

    Most operating systems provide a “wall clock” function for user programs. For example, UNIX has a system call gettimeofday(). Let's add a function called getTime() to the MiniKernel.

    Getting the current time, measured in milliseconds, is easy; we simply call System.currentTimeMillis(). This function is prohibited from user-level programs (see above), so it must be wrapped up inside the Kernel.

    There will be a little bit of a trick to this. Kernel.interrupt() returns an int, which isn't big enough to return the number of milliseconds since January 1, 1970, so we need instead to return a long. To return this value to the caller, we have the new GET_TIME system call pass a one-element array of long as an argument. This format is somewhat inconvenient for the user, so we will make the corresponding library function return a plain long.

    Let's walk through this procedure:

    First, add a function to Kernel to put the current time into an array of long

    
        private static int doGetTime(long[] t) {
            t[0] = System.currentTimeMillis();
            return 0;
        }
    
    We must add a unique system call number. The largest syscall number is current 3 (for SYSCALL_JOIN) so we add
    
        public static final int SYSCALL_GET_TIME = 4;
    
    And, we must modify interrupt() to dispatch the appropriate system call to the function doGetTime(). Interrupt has lots of arguments, we'll just pick o1 to store a pointer to an array of long.
    
        switch (i1) {
        ...
        case SYSCALL_GET_TIME:
            return doGetTime((long[])o1);
        ...
        }
    
    If the object sent is not such an array, an invalid cast exception will be thrown here. This exception is caught at the end of interrupt(), which will return ERROR_BAD_ARGUMENT_TYPE. Similarly, if the argument is null or the array passed in is not big enough (it must have length at least 1), an exeption will be thrown, and caught and converted to an error return from interrupt(). (It's ok if the array is bigger; only the first element will be modified and the rest will remain unchanged). If you like, a program can use the system call exactly the way it is:
    
            long[] t = new long[1];
            e = Kernel.interrupt( Kernel.INTERRUPT_USER,
                                  Kernel.SYSCALL_GET_TIME,
                                  0, t, null, null );
            /* The time can now be found in t.value */
    
    But this is quite ugly, so let's make a Library function to simplify things. Add the following method to Library.java.
    
        public static long getTime() {
            long[] t = new long[1];
            int rc = Kernel.interrupt(Kernel.INTERRUPT_USER,
                            Kernel.SYSCALL_GET_TIME,
                            0, t, null, null );
            if (rc < 0) {
                return rc;
            } else {
                return t[0];
            }
        } // getTime
    
    Now, any program can issue:
    
        t = Library.getTime();
    
    Notice that every stage of the system call returns a result. This is the customary UNIX way of indicating an error. A negative value means a particular kind of error which is defined as a constant in Kernel. In Java, we prefer to use exceptions, but for this assignment, you'll have to follow the UNIX tradition and look at the return value of every system call.

    To test the new function, write a simple program that calls Library.getTime() and displays the result.

    
        import java.util.Date;
    
        /** Simple test of the getTime() kernel call.
         * CS537, Spring 2007.
         */
        public class TimeTest {
            /** Main program.
             * @param args ignored.
             */
            public static void main(String[] args) {
                long now = Library.getTime();
                if (now < 0) {
                    Library.output("Error: " + Library.errorMessage[(int) -now] + "\n");
                } else {
                    Library.output(
                        "Current time is " + now
                        + " = " + new Date(now) + "\n");
                }
            } // main
        } // TimeTest
    

    Compile this class and then type

    
        java Boot 10 Disk 100 Shell TimeTest
    

    Some Notes About the Disk

    Just as in a real OS, a “user” program does not have direct access to the various devices attached to the computer; it must ask the kernel to perform operations on its behalf. So, if you want user programs to perform disk operations, you must add system calls to do so.

    The interface to Disk simply allows one to read and write individual blocks, so if you want programs to access a named file system, you'll have to write that, too.

    The tricky part about using disks is that they are slooooow. Generally, the OS will start a disk operation and then turn its attention elsewhere for a while. The disk will respond when it is done by causing an interrupt with kind set to INTERRUPT_DISK.

    Furthermore, a disk gets ornery if you are not patient. The MiniKernel Disk can only perform one operation at a time, and it will crash the system if you start a new operation before the previous one has completed.

    So in order to safely control access to the disk, you must put each requesting process to sleep until it is its turn, call beginRead or beginWrite, and then wake up the process when the Disk indicates, by calling Kernel.interrupt, that the operation is complete.


    Copyright © 2007 by Doug Thain and Marvin Solomon. All rights reserved.