CSE 120 Discussion Notes: Week 6

Project 2: Multiprogramming in Nachos

The high order bits:

You'll want to take a look at the Project 2 Description as well.

User Mode and Kernel Mode

At the start of the quarter, we saw that operating systems rely on the processor to support two modes of operation: user and kernel (or privileged and unprivileged). In kernel mode, you can do just about whatever you want. Code running in user mode has restrictions placed on it so that it cannot directly access hardware, interfere with other running programs, and so on—assuming the operating system is implemented correctly. The hardware provides the basic mechanisms, and the operating system has to make it all work.

Nachos is no different. In Project 1, all the code you wrote ran in kernel mode. In Project 2, you'll finally start to run code in Nachos's "user mode", though most of the code you write will still run in "kernel mode". How do the two modes actually work in Nachos?

  1. User mode in Nachos consists of an emulated CPU (a MIPS). Since the processor is emulated, Nachos can implement all necessary details such as address translation, checking for and handling faults, etc., that are needed to properly protect kernel code from misbehaving user programs. Programs in the code/test/ subdirectory are meant to run in user mode.
  2. Kernel mode consists of just about everything else in Nachos. All the C++ code you write (except for things in the test subdirectory run without emulation, and hence can do anything Nachos will let you—thus, the equivalent of kernel mode.

To provide proper protection, there is quite a bit of separation between code running in the two modes:

Nachos System Calls

A user program in Nachos performs a system call (such as exec or read) by loading arguments into registers and executing the system call instruction. This causes a trap to ExceptionHandler in the kernel. ExceptionHandler looks to see what happens, notices that the cause is SyscallException, then examines the processor registers to determine exactly which system call was requested.

Question: Why do we need to keep the userspace registers separate from the kernel registers here? Why can't we simply treat this like a function call into the kernel, with the usual rules that the called function may overwrite some of the registers?

Memory and Address Translation

In Nachos, all the "physical memory" in the system (the memory used for userspace code) is stored in Machine::mainMemory. But user code cannot acces this memory directly. Instead, Nachos uses page tables that map virtual addresses (used by user code) into physical addresses (addresses in mainMemory).

The AddrSpace class has a pageTable array (and numPages variable giving its size). Element i of this array gives the physical page (location in mainMemory) corresponding to virtual page i. By setting up the pageTable array properly, you can make a user's pages actually be stored anywhere in memory you want, without the user program knowing. You can even (and will, in Project 3) provide virtual memory, reading in pages from disk when a process needs them.

An Aside: Context Switches

With support for user mode programs, each user process is associated with a kernel thread. So there's no need to change the way that scheduling is done—scheduling the kernel threads also schedules the user threads for us.

The Machine class can only store one pagetable and set of user registers at a time. Since each user process should have its own pagetables and registers, we store the registers and a pointer to the pagetables in the Thread object. When we context switch, both the user and kernel state are swapped. So, everything just works.

(For details, look at Thread::RestoreUserState and AddrSpace::RestoreState.)

Questions to Consider

  1. fork() is very often followed almost immediately by exec() on Unix. How can modern systems reduce the cost of copying all the memory of a process when forking? What might you do on older systems lacking support for virtual memory? For fun, try looking up the vfork() system call.
  2. The x86 architecture provided a simple form of segmentation even before it provided protection features. How can this be used to overcome limited bits for addressing (just 16)?