2. Nachos Machine

Nachos simulates a real CPU and harware devices, including interrupts and memory management. The Java package nachos.machine provides this simulation.

2.1. Configuring Nachos

The nachos simulation is configured for the various projects using the nachos.conf file (for the most part, this file is equivelant to the BIOS or OpenFirmware configuration of modern PCs or Macintoshes). It specifies which hardware devices to include in the simulation as well as which Nachos kernel to use. The project directories include appropriate configurations, and, where neccessary, the project handouts document any changes to this file required to complete the project.

2.2. Boot Process

The nachos boot process is similar to that of a real machine. An instance of the nachos.machine.Machine class is created to begin booting. The hardware (Machine object) first initializes the devices including the interrupt controller, timer, elevator controller, MIPS processor, console, and file system.

The Machine object then hands control to the particular AutoGrader in use, an action equivelant to loading the bootstrap code from the boot sector of the disk. It is the AutoGrader that creates a Nachos kernel, starting the operating system. Students need not worry about this step in the boot process - the interesting part begins with the kernel.

A Nachos kernel is just a subclass of nachos.machine.Kernel. For instance, the thread project uses nachos.threads.ThreadedKernel (and later projects inherit from ThreadedKernel).

2.3. Nachos Hardware Devices

The Nachos machine simulation includes several hardware devices. Some would be found in most modern computers (e.g. the network interface), while others (such as the elevator controller) are unique to Nachos. Most classes in the machine directory are part of the hardware simulation, while all classes outside that directory are part of the Nachos operating system.

2.3.1. Interrupt Management

The nachos.machine.Interrupt class simulates interrupts by maintaining an event queue together with a simulated clock. As the clock ticks, the event queue is examined to find events scheduled to take place now. The interrupt controller is returned by Machine.interrupt().

The clock is maintained entirely in software and ticks only under the following conditions:

  • Every time interrupts are re-enabled (i.e. only when interrupts are disabled and get enabled again), the clock advances 10 ticks. Nachos code frequently disables and restores interrupts for mutual exclusion purposes by making explicit calls to disable() and restore().

  • Whenever the MIPS simulator executes one instruction, the clock advances one tick.

Note: Nachos C++ users: Nachos C++ allowed the simulated time to be advanced to that of the next interrupt whenever the ready list is empty. This provides a small performance gain, but it creates unnatural interaction between the kernel and the hardware, and it is unnecessary (a normal OS uses an idle thread, and this is exactly what Nachos does now).

Whenever the clock advances, the event queue is examined and any pending interrupt events are serviced by invoking the device event handler associated with the event. Note that this handler is not an interrupt handler (a.k.a. interrupt service routine). Interrupt handlers are part of software, while device event handlers are part of the hardware simulation. A device event handler will invoke the software interrupt handler for the device, as we will see later. For this reason, the Interrupt class disables interrupts before calling a device event handler.

Caution

Due to a bug in the current release of Nachos, only the timer interrupt handler may cause a context switch (the problem is that a few device event handlers are not reentrant; in order for an interrupt handler to be allowed to do a context switch, the device event handler that invoked it must be reentrant). All interrupt handlers besides the timer interrupt handler must not directly or indirectly cause a context switch before returning, or deadlock may occur. However, you probably won't even want to context switch in any other interrupt handler anyway, so this should not be a problem.

The Interrupt class accomplishes the above through three methods. These methods are only accessible to hardware simulation devices.

  • schedule() takes a time and a device event handler as arguments, and schedules the specified handler to be called at the specified time.

  • tick() advances the time by 1 tick or 10 ticks, depending on whether Nachos is in user mode or kernel mode. It is called by setStatus() whenever interrupts go from being disabled to being enabled, and also by Processor.run() after each user instruction is executed.

  • checkIfDue() invokes event handlers for queued events until no more events are due to occur. It is invoked by tick().

The Interrupt class also simulates the hardware interface to enable and disable interrupts (see the Javadoc for Interrupt).

The remainder of the hardware devices present in Nachos depend on the Interrupt device. No hardware devices in Nachos create threads, thus, the only time the code in the device classes execute is due to a function call by the running KThread or due to an interrupt handler executed by the Interrupt object.

2.3.2. Timer

Nachos provides an instance of a Timer to simulate a real-time clock, generating interrupts at regular intervals. It is implemented using the event driven interrupt mechanism described above. Machine.timer() returns a reference to this timer.

Timer supports only two operations:

  • getTime() returns the number of ticks since Nachos started.

  • setInterruptHandler() sets the timer interrupt handler, which is invoked by the simulated timer approximately every Stats.TimerTicks ticks.

The timer can be used to provide preemption. Note however that the timer interrupts do not always occur at exactly the same intervals. Do not rely on timer interrupts being equally spaced; instead, use getTime().

2.3.3. Serial Console

Nachos provides three classes of I/O devices with read/write interfaces, of which the simplest is the serial console. The serial console, specified by the SerialConsole class, simulates the behavior of a serial port. It provides byte-wide read and write primitives that never block. The machine's serial console is returned by Machine.console().

The read operation tests if a byte of data is ready to be returned. If so, it returns the byte immediately, and otherwise it returns -1. When another byte of data is received, a receive interrupt occurs. Only one byte can be queued at a time, so it is not possible for two receive interrupts to occur without an intervening read operation.

The write operation starts transmitting a byte of data and returns immediately. When the transmission is complete and another byte can be sent, a send interrupt occurs. If two writes occur without an intervening send interrupt, the actual data transmitted is undefined (so the kernel should always wait for a send interrupt first).

Note that the receive interrupt handler and send interrupt handler are provided by the kernel, by calling setInterruptHandlers().

Implementation note: in a normal Nachos session, the serial console is implemented by class StandardConsole, which uses stdin and stdout. It schedules a read device event every Stats.ConsoleTime ticks to poll stdin for another byte of data. If a byte is present, it stores it and invokes the receive interrupt handler.

2.3.4. Disk

The file systems project has not yet been ported, so the disk has not been tested.

2.3.5. Network Link

Separate Nachos instances running on the same real-life machine can communicate with each other over a network, using the NetworkLink class. An instance of this class is returned by Machine.networkLink().

The network link's interface is similar to the serial console's interface, except that instead of receiving and sending bytes at a time, the network link receives and sends packets at a time. Packets are instances of the Packet class.

Each network link has a link address, a number that uniquely identifies the link on the network. The link address is returned by getLinkAddress().

A packet consists of a header and some data bytes. The header specifies the link address of the machine sending the packet (the source link address), the link address of the machine to which the packet is being sent (the destination link address), and the number of bytes of data contained in the packet. The data bytes are not analyzed by the network hardware, while the header is. When a link transmits a packet, it transmits it only to the link specified in the destination link address field of the header. Note that the source address can be forged.

The remainder of the interface to NetworkLink is equivalent to that of SerialConsole. The kernel can check for a packet by calling receive(), which returns null if no packet is available. Whenever a packet arrives, a receive interrupt is generated. The kernel can send a packet by calling send(), but it must wait for a send interrupt before attempting to send another packet.