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.
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 accesses to system resources (like 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!",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!");
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, but these take time to complete. So, all methods of Disk return immediately. When the desired operation is complete, the Disk will cause an interrupt and the Kernel can pick up where it left off.
When the Kernel starts, it looks for a program to run called Shell. If it cannot be found, the Kernel stops right away. (No point in having an OS without a program to run!).
Programs can launch off other programs by calling Library.exec() or Library.execAndWait(). These functions are not identical to their UNIX conterparts, so make sure to read the Library reference manual.
Try out the MiniKernel by running
java Main 100 10You should see something like this:
Main: Starting kernel. Kernel: Buffer size is 10 blocks Kernel: Disk is 100 blocks Kernel: Loading initial program. Shell>The last line is printed by the Shell program. The Shell uses Library.input to get input from the user, parses it into words, and then calls Library.execAndWait to run the correct program. You can now type in the name of other Java programs and they will execute under the MiniKernel. A trivial example is Count.
Prohibited classes include, but are not limited to:
Now, on the other hand, the purpose of the MiniKernel is to simulate a real operating system, so it is perfectly acceptable to use these procedures inside the kernel.
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 -- we need to instead return a long. To accomplish this, we will make a GET_TIME system call which modifies a Temp object sent 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, let's make a new temporary class:
public class Temp { public long value; }Now, add a function to Kernel to put the current time into a Temp:
private static int doGetTime( Temp t ) { t.value = System.currentTimeMillis(); return 0; }We must add a unique system call number:
public static final int SYSCALL_GET_TIME=5;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 a Temp. If the object sent is not a Temp, an invalid cast exception will be thrown here. This exception is caught at the end of interrupt(), which will return ERROR_BAD_ARGUMENT_TYPE.
switch(i1) { ... case SYSCALL_GET_TIME: return doGetTime((Temp)o1); ... }If you like, a program can use the system call exactly the way it is:
Temp t = new Temp(); e = Kernel.interrupt( Kernel.USER_INTERRUPT, 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:
public static long getTime() { Temp t = new Temp(); int error = Kernel.interrupt( Kernel.USER_INTERRUPT, Kernel.SYSCALL_GET_TIME, 0, t, null, null ); if( error==0 ) return t.value; return error; }Now, any program can issue:
t = Library.getTime();Notice that every stage of the system call returns a result. This is the 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.
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 has completed.
So, in order to safely control access to the disk, you must cause requesting process to wait until it is their turn, call beginRead or beginWrite, and then wait until the Disk causes an interrupt.