How to organize an OS


Objective: Hard Modularity

(More on modularity here)
  1. Client Server (to be covered in Lab 4)
  2. Virtualization (to be covered in Lab 2 and 3)
    1. The advatage of virtualization is that applications do not run on the actual hardware, instead they run on virtual machines, like a "sandbox." By not letting the applications run on the actual hardware, the applications are not able to trash anything. This approach mimics a "sandbox" because the app cannot affect anything outside the boundaries of the virtual machine.
    2. The simplest approach? Use an interpreter!
      • The interpreter implements and simulates all of the neccessary functionalities of a given OS (x86 for example) needed to run the application
    3. Pros:
      1. Apps can be for some other hardware
      2. Apps can't escape the sandbox. The interpreter checks if an app tries to go outside their specified domain, the interpreter halts it.
      3. Multiple virtual machines can run on a single physical machine
    4. Cons:
      1. Performance - it's slower (If written naively, it is about 10x slower; if written well, ~1.2x slower)


Virtualizable Processor

The goal of the virtualizable processor is to protect the transfer of control. This is accomplished by not letting people execute arbitrary privileged code, but instead may only execute privileged code at specific spots.

Image of instructions accessible to app and extra bit to determine privilege

Instruction categories:

Ordinary Instructions add, sub, call, ret, etc.
Privileged Instructions I/O operations, halting process, etc.

Features of a virtualizable processor

Image of trap vector redirection

Structuring virtual memory

Solely partitioning memoring into a section dedicated for apps, and a section for OS does not work. If this approach were used, an app could just jump to a location in the OS, bypassing any authorization checks. Instead, We need a Protected Transfer of Control.


When an app tries to execute a bad (unprivileged) instruction, the instruction in not actually executed

The Linux convention for getting the kernel's attention is to execute an invalid instruction. The interrupt instruction - INT - takes a byte that indicates which trap to execute. 128 or 0x80 indicates to the kernel that this trap was triggered intentionally. The trap vector then passes control over to the kernel, which attempts to execute the specific privileged instruction that the app was attempting to execute (the number of this specific instruction is stored in the %eax register when the call is made)

When you trap, the x86 pushes:

ssstack segment
espstack pointer (user)
eflagsprivileged bit
cscode segment
eipinstruction pointer
error

When trying to issue a system call(eg read())
A note on return instructions:
RET returns from the function, resetting the instruction pointer: ip=*sp++.
RETI is the converse of INT, because it pops off everything that was pushed on, and reassumes the prior state. It is a privileged instruction because it sets the privileged bit.

Image of a modular OS

Operating systems can be partitioned to make sure that applications access instructions properly. The kernel can have exclusive access to privileged ones, while ordinary instructions can be accessed by everyone. There's also the idea of recursively programmed kernels using the two privileged bits in x86. These bits could be used to organize into four levels.

Image of a ring-structured kernel OS

Combining a Virtualizable Processor with an OS

NOTE: Applications are at the kernel's mercy
Our process will have a number of (virtual) registers %eax, %esp, etc...
These are the same as real registers when running. However, when not running, these need to be saved. Where?
So we have a process table, where each row is a process

0
1
2  (UID, PIE, info about the process, virtual %eax, virtual %ebx )<-Process Table Entry(PTE)
3
4
5

During execution the entry corresponding to the running process is junk
Kernel decides when to switch process (context switch)

Processes are restricted and hence can't modify their own memory. The process descriptor table (PDT) lives in memory and hold the register values and information for all processes not at the foreground. If process 3 is running, then the values in its row in the PDT are garbage (performance reasons). We can't rely on them being accurate. If the kernel performs a context switch to another process, then the PTE for process 3 is updated to last accurate values but the values for the new process are now junk.

Process Descriptor Table
Unix or Posix syscalls to manipulate the process table

Note: Execution order of child/parent processes is not predictable/guaranteed. For example, if the parent prints "hello" and the child prints "goodby," you may get the result: "hgcololdoby."