Nachos
Home ] Syllabus ] [ Nachos ]

 

  1. Overview
  2. Compiling
  3. Running
  4. User Programs
  5. System Calls and their Implementation
  6. Clock Device and Timer Interrupts

Overview

Nachos is a teaching operating system developed at UC Berkeley. It has been extensively modified (both simplified and extended) to suit our purposes. The Nachos program consists of (1) an operating system kernel, and (2) a machine emulation, emulating a MIPS cpu, main memory, address translation, real-time clock, timer device, interrupts, and devices like consoles and disks. The Nachos program runs on top of the native machine (e.g., SPARC/Solaris). Since the emulated machine is a reasonably complete machine, it can run Nachos user programs (which are like normal C programs), on top of this emulated machine. These programs can make Nachos system calls to request services from the Nachos kernel. On the other hand, they cannot use native operating system's interface, and hence none of the standard libraries.

Nachos is a multi-threaded program; one thread is created for each user process that we wish to execute. Each thread executes a fetch-execute cycle of the emulated CPU. The fetch execute cycle uses emulated registers (most notable the program counter) and an address-space map (pageTable) to execute instructions from one user program. As part of this fetch-execute cycle, if a syscall execution is executed, then a call is made to the Nachos kernel to handle the system call. Likewise, the Nachos kernel is called when an exception occurs.

The Nachos hardware emulation also emulates other peripheral devices (console, disk, timer) and manages interrupts from those devices. For example, it emulates a real-time clock, using which a timer device is programmed to generate a clock/timer interrupt at regular intervals (defined by the quantum size). The interrupt handlers are also implemented in the kernel. The interrupts are checked after each instruction execution, just like in real CPUs.

Compiling

Directory Structure

The Nachos directory structure is as follows:
  • kernel: Nachos kernel code.
  • machine: The emulated machine code.
  • user: Nachos user programs.
  • utils: Utility functions for use in kernel and machine.
  • bin: An executable file converter for user programs. (You should not have to worry about what's in here. )
  • doc:documentation
  • include: Include Files (including syscall.h, which must be included in user programs).
For the most part you will have to worry about the kernel code (and to some extent the utils code). You can look into, but not modify, any of the machine code. After all, this is supposed to be a piece of hardware, and we are not in the business of manufacturing hardware. You may, however, increase main memory size, change the quantum, etc. You can write additional user programs along the lines of the ones provided here.

Makefiles

Each directory has a Makefile, which is used to compile code in that directory. The Makefiles in the machine and utils directory are used to compile the code into libraries (libmachine.a and libutils.a respectively). The Makefile in kernel directory is used to compile the Nachos program (which includes the kernel source and the two libraries). The Makefile in user directory can be used to compile the Nachos program as well as the test user programs.

The Makefile consists of (1) a set of definitions, and (2) a set of targets. For each target, a set of "shell commands" can be specified, which are executed if the target is out of date with respect to its dependencies. You can do man make to find more details, but here is a quick example from the Makefile in the kernel directory.

In the beginning of the Makefile, you will see the following lines. These are just defining symbolic constants.

HOST = -DHOST_SPARC -DHOST_IS_BIG_ENDIAN -DHOST_SVR4 -DHOST_SunOS5
DEFINES = -DUSER_PROGRAM -DFILESYS_STUB -DFILESYS_NEEDED
INCPATH = -I../include  -I. -I../utils -I../machine

CC = g++
CCC=g++
LD = g++
AS = as

CPP_AS_FLAGS = -D_ASM
CFLAGS = -g -Wall -Wshadow -fwritable-strings $(INCPATH) $(DEFINES) $(HOST) -DCHANGED
CCFLAGS = $(CFLAGS)
LDFLAGS = -lsocket -lnsl
KERNEL_C = addrspace.cc\
        exception.cc\
        main.cc\
        memmgr.cc\
        procmgr.cc\
        scheduler.cc\
        syscall.cc\
        system.cc\
        thread.cc

KERNEL_S = switch.s

KERNEL_O = $(KERNEL_C:cc=o) switch.o

KERNEL = nachos
MACHINE = machine
UTILS =   utils
The KERNEL_C constant defines the source code files for the nachos kernel (there is also an assembly language file switch.s). The KERNEL_O constant is the corresponding set of object files (obtained by compiling each of the source files). If you add any extra kernel source files, make sure you add it in the KERNEL_C definition (Note: the \ is an escape character, it prevents the Makefile from thinking that the line is over).

After the definitions, you will find the rules for the targets. The first target is the kernel itself, which depends on the machine and utils libraries, as well as the kernel object files. The rule says how to update the target (kernel, i.e., the nachos program), which is just a linking command to create the executable. You can see that the rule for machine and utils libraries simply asks to run the make command in the corresponding directory. Note: Enclosing a command in parenthesis means that once the whole set of commands is completed, return back to the same directory.

$(KERNEL): $(MACHINE) $(UTILS) $(KERNEL_O)
        $(LD) $(KERNEL_O) $(LDFLAGS) -lmachine -lutils -o $(KERNEL)


machine: 
        (cd ../machine ; make)

utils: 
        (cd ../utils ; make)
In the user directory Makefile, you will notice the following line:
EXEC = kernel halt exit getpid fork exec yield sort init simple

all: $(EXEC)
when you type make on the shell prompt in user directory, it makes the target all, which makes all the other targets. These targets are the Nachos program itself, and all the user programs specified. If you add a new user program, you should add it to this target list as well as add extra entries in the Makefile (similar to entry for other user programs).

Running

The Nachos kernel only looks for program executables in the current directory, so its best to run Nachos from the user directory. The Nachos program can be invoked as follows:
../kernel/nachos [-d <flags>] [<initprog>]
or simply
./nachos [-d <flags>] [<initprog>]
where <initprog> is the initial program to execute (and can be chosen to be any of the user programs in the directory). If it is not specified, then the program init is executed. (cf. kernel/main.cc).

The flags are useful for debugging purposes. Some of the important flags are:

  • 'p': process management
  • 't': thread management
  • 's': system calls
  • 'a': address space
  • 'm': machine emulation
You should initially try to run with -d pts to turn on the debugging flags related to threads, process, and system calls. The output should be useful in tracing the execution. The flags for machine and address space generate a lot of output, so use them as needed. When you make modifications, you may want to add your own DEBUG statements in your modified code for debugging purposes.

Sample Execution

Here is a sample execution from running
../kernel/nachos -d tps getpid
The output is shown below with italicized text as my comments.
Nachos Main: Parsing Command Line Arguments
Nachos Main: Debug Flags pts
// Read the Debugging Flags correctly from the command line

Nachos Main: Booting Up...
Nachos Main: Boot Up Done ... Looking for Initial Process
Nachos Main: Starting Initial Program: getpid
// Initialization is done, its going to execute getpid now

System Call: GetPid
System Call: GetPid: returning id 1

// The process "getpid" makes the system call GetPid(), and is
// returned a value of 1, which is its process id

System Call: Exit
System Call: Exit Status: 0, Pid: 1

// The process makes the system call Exit(0) to terminate. Notice
// that the Exit(0) call is automatically added (from start.s). Change
// the program to add: Exit(pid) after the GetPid() call and see the
// output. You should see Exit Status 1 (the returned value from GetPid()

Terminating Process 1
Finishing thread "getpid"
Sleeping thread "getpid"

// The thread is dying now, an attempt is made to find another
// thread to run, but their is no more process or threads, so we finish
// the execution.  

No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!


// Some stats are being generated here, the first line shows the
// total time that has evolved since the beginning. You can also
// see that there is no activity on the console, disk, etc.

Ticks: total 100, idle 46, system 30, user 24
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

User Programs

The user programs are standard C programs, with the following exceptions:
  1. No floating point operations; these are not implemented in the MIPS simulator.
  2. No standard library. Standard library uses system calls of the native machine; we are running on top of an emulated machine, with the system calls supplied by nachos. Unless we implement all Unix system calls in nachos, this cannot be done. Some of the implications of this are that you cannot use input-output calls (e.g., printf()), or dynamic memory allocation (e.g., malloc() or new).
The user directory has some sample programs. You can modify them, or create new ones along those lines. Make sure that "syscall.h" is included in the program. Also, the compilation command should be as in the Makefile. For example, if you create a new program "myprog.c", then add the following lines in the Makefile:
myprog.o: myprog.c
        $(CC) $(CFLAGS) -c myprog.c
myprog: myprog.o start.o
        $(LD) $(LDFLAGS) start.o myprog.o -o myprog.coff
        ../bin/coff2noff myprog.coff myprog
Also, add myprog as a target in the target all Then, you can compile it using make myprog which will create the executable in file myprog.

To create an assembly language version of the file, you can do the following: /afs/cs.pitt.edu/public/gnu/decstation-ultrix/decstation-ultrix/bin/gcc -G 0 -S -I../include myprog.c, which will create the file myprog.s, in MIPS assembly format.

Feel free to change the user programs as you wish like. Remember that you can do no I/O -- which makes it difficult to debug your user programs. Keep them simple, using them only for debugging purposes. You can do all kinds of simple integer arithmetic, and make the system calls supported by Nachos.

System Calls and their Implementation

The Nachos kernel currently supports 7 system calls. These calls are defined in include/syscall.h, and their prototypes are given below.
void   Halt();               // halt the machine
void   Exit(int status);     // terminate the current process
int    Fork();               // fork a new process
int    Exec(char *file) ;    // execute the program (file)
int    GetPid() ;            // get the process id
void   Yield() ;             // yield cpu to another process
void   Print(char *string) ; // print the string (for debugging); no automatic newline
void   PrintInt(int value) ; // print the integer value (for debugging); no automatic newline
The behavior of the system calls mimics the behavior of the corresponding system calls in Unix. Each system call has a code, which is used to identify the system call. These codes are defined in include/syscall.h, and are given below.
#define SC_Halt         0
#define SC_Exit         1
#define SC_Fork         2
#define SC_Exec         3
#define SC_GetPid       4
#define SC_Yield        5 
#define SC_Print        6
#define SC_PrintInt     7
The system calls can be used in user programs just as you use them in Unix programs. The example user programs show you how to use these system calls.

User Level System Call Wrappers

The user programs are compiled with the file start.s, which is available in the user directory as well. This file includes implementations of the system calls (in assembly language). These procedures are simply wrappers for the system calls, since they actually trap into the kernel (using the syscall instruction), and the kernel does the actual work.

The following assembly language segment shows the implementation of the GetPid() system call.

        .globl GetPid
        .ent    GetPid
GetPid:
        addiu $2,$0,SC_GetPid // Put System Call Code in Reg 2 ($2)
        syscall               // syscall instruction (trap to kernel)
        j       $31           // return from procedure (Reg 31 has ret. addr.)
        .end GetPid
If you look at the procedures for other system calls, they do exactly the same things: (1) put the system call code in register 2, (2) trap to the kernel, and (3) return back from the procedure to the calling code. The main work for the system call is done in the kernel, which is explained below.

System Call Handling in Kernel

A user program executes in the Nachos machine, by a thread executing the CPU fetch-execute cycle (Machine::Run()). This code will (1) fetch an instruction from the emulated memory (whose address is known through the emulated program counter register), (2) decode the instruction to extract out the op code and the operands, and (3) execute the instruction, during which register values get updated etc., including the program counter. The code executes these three steps repeatedly, emulating real CPU behavior.

During the execution of the user program if the instruction is a syscall instruction, then the code makes a call to the ExceptionHandler(SyscallException) routine (defined in kernel/exception.cc), which is the kernel entry point. This routine is also called when there is an exception during the execution of an instruction. The exception handler recognizes that it is a system call, and uses register R2 to get the system call code, and then makes calls to internal routines to handle the system call. When the system call is completed, the kernel code returns from the ExceptionHandler() routine, and the CPU emulation code goes and executes the next user program instruction.

Passing Parameters in System Calls

So far we have seen how the control passes from the user program to the kernel in a system call, and how the control comes back from the kernel to the user program. The next question is how the parameters of a system call get passed to the kernel, and how does the kernel return values to the user programs.

The MIPS machine convention for passing parameters to procedures is that the first 4 parameters are passed in registers R4, R5, R6, and R7 (We will not worry about more parameters since none of our system calls need more than 4 arguments). Since the system call is invoked like a regular procedure call in user programs, the parameters are placed in these registers (this can be seen in the assembly language code generated from the user code source file). Therefore, when the control comes to the kernel, the kernel can extract the parameters of the call from these registers.

Integer Parameters

Integer parameters are the easiest to deal with. For example in the system call Exit(int status), an integer status is passed as a parameter. In the kernel code (see kernel/syscall.cc), the register value can be retrieved into a local variable as follows:
int R4 = machine->ReadRegister(4) ;
and then R4 can be used like a normal integer variable.

Pointer Parameters

The pointer parameters are a bit tricky. Consider the Exec(char *file) system call, whose argument is a pointer to a character (string). Thus, in this case register R4 would hold a memory address which indicates where the actual character (string) is placed in the user program's virtual/logical address space. There are two complications here:
  1. The memory address is a virtual address into the user program's address space. This virtual address is to be mapped to Nachos main memory physical address through a process page table.
  2. Once the virtual address is translated into the physical memory address, this address is still an address in the emulated Nachos main memory, and cannot be used directly like you would in normal C programs. To get the real character, we have to copy from the Nachos emulated main memory into some local variable.
In the specific case of Exec(), the character pointer is a pointer to a (null-terminated) string. The part of reading the string from user program address space into a local kernel variable is all encapsulated in the routine
int 
system_read_null(char *from_user_space, char *to_kernel_space, int maxlen)
where from_user_space is a virtual address pointing to the string in the user program address space, to_kernel_space is a pointer to some local variable in the kernel, and maxlen is used to ensure that the number of characters transferred is bounded (in case the null-character was never encountered). Thus, the implementation for Exec() uses this call to transfer the filename from the user program address space into a local variable, and then uses the local variable from then on.

Clock Device and Timer Interrupts

A clock device is simulated in the machine emulation. It is not perfect, in the sense that performance measurements will not always be very realistic. However, it works well enough to emulate advancement of time, and to use it to generate timer interrupts. The clock ticks once after the execution of a user program instruction. It also ticks once whenever interrupts are enabled in the kernel.

Using this emulated clock, a timer device is constructed in the kernel (see system.cc), using the call

timer = new Timer(TimerInterruptHandler, 0, randomYield);
This specifies that the interrupt handler for the Timer Device is the routine TimerInterruptHandler. The other two arguments are irrelevant for our purposes. A timer interrupt is generated once every 100 clock ticks (specified as a constant TimerTicks in machine/stats.h).

Timer Interrupt Handler

The timer interrupt handler code is given below.
static void
TimerInterruptHandler(int dummy)
{
    if (interrupt->getStatus() != IdleMode)
        interrupt->YieldOnReturn();
}
All it does (in a slightly convoluted way) is that it (eventually) results in the call to
currentThread->Yield();
which (defined in kernel/thread.cc) in turn simply uses the scheduler to schedule the next process, if any. If there is a next process, then the current process is made ready (and put to the Ready List). The scheduler currently (1) finds the next process from the head of the Ready List, and (2) puts a process at the end of the Ready List whenever a process is made ready. All of these combined result in a Round-Robin scheduling policy being implemented.

In the current implementation of the Nachos kernel, the operating system does not maintain current time, or the time elapsed etc. If any of this is needed (e.g., for scheduling or accounting purposes), then a System Clock may be defined, which ticks on each timer interrupt. Note that this System Clock ticks much more infrequently than the emulater hardware clock. This System Clock should be used for all timing related things within the kernel.

Manas Saksena
Last modified: Wed Feb 17 11:41:47 1999