Scribe: Dennis Li
How do we implement hard modularity?
I. Client/Server Organization - The modules are separated onto a client and server, which are placed on different machines that can either be real or virtual.Pros:
The client and server can be on different hosts. They are also protected from each other due to hard modularity.
Cons:
This approach is complicated and therefore is highly unreliable. The client/server model is slow due to the use of more cycles, power, and networking overhead.
// Example: Client/Server factorial call // Client code send (fact_name, (m){"!",5} }; // 5! a = get_response(fact_name); if (a.response_code == "OK") print (a.val); else return ERROR; // Server code for (;;) { receive (fact_name, request); if (request.opcode == "!") { n = request.val; int p = 1; for (int i = 1, i <= n, i++) //compute n! p *= i; response = (m){"OK", p}; } else response = (m){"No Good",0}; send (fact_name, response); }II. Virtualization - We create a virtual machine to run our untrusted code on.
Pros:
This approach also gives us hard modularity. The real and virtual machines can be of completely different architectures.
Cons:
This approach is even slower than the first approach due to the emulation overhead
Example: We can build a virtual machine by creating an x86 emulator that runs on top of another machine. We now can run the factorial code inside the emulator.
// Example: Factorial call using virtualization // x86 Emulator code // This code implements the virtual machine int ip; for (;;) { char ins = mem[ip++]; switch (decode(ins)) { case pop: //etc... } } // Place the factorial code inside virtual machine r = emul( ... ); //... is the address of the factorial code switch (r) { case STACK_OVERFLOW: ... case OK: //Get return value of 'factorial' case TOO_MANY_INSTRUCTIONS: ... }
This process is very slow since the physical machine must now execute multiple instructions for each instruction in the virtual machine
Since emulators are slow, how can we make virtualization faster?
Hardware AssistsAssuming similar architectures, we will need a virtualizable processor where:
These previous requirements essentially eliminates the need of an emulator!
How is control transferred?
When we have a application running inside the virtual machine, normal computations such as:a = b*b - c*ccan run at full speed. However what if the application need to issue a system call?
write(1,"hello, world!\n",13);These system calls are implemented via a delibrate crash! The INT instruction is used by convention.
//The following code is executed in the virtual machine write(int fd, char const * buf, size_t bufsize) { asm("int 128"); //128 signifies a system call ... }The interrupt service routine is a software routine that hardware invokes in response to an interrupt.
Application Binary Interface (ABI)
The ABI describes the low-level interface between an application program and the OS.Instructions:
Priviledged code should be able to access all instructions while unprivileged code should only have access to unpriviledged instructions.
ALU's and Registers:
The virtual machine should be given direct access to these
Primary Memory (RAM):
The virtual machine should be given selective direct access to RAM. Protecting memory is hard (virtual memory) and so we will cover this later in the course.
I/O Registers:
The virtual machine should be given indirect access to these through system calls. We also need to virtualize registers so that each program thinks it has its own set.
How is all this information tracked?
It is the job of the kernel to keep track of all the previous information by using process descriptors
Process descriptors are structures that help describe each process and contain information such as the registers that a process owns. It contains the process id, a copy of the stack, and a copy of the virtual registers. The virtual registers in the process desctriptor are only valid when the process is not running. Instead, when the process is running, the virtual registers in the process descriptor contain garbage values because the actual hardware registers are used. When the process is suspended, the current register values are then stored into the virtual registers in order to save the state.
On a single-core machine, at most one process can run at a given time.
n = getpid() // This command gets the pid of the calling process
To create a process:
p = fork()This clones the current process, except:
pid_t waitpid(pid_t p, int *status, int options)
// Example of using waitpid int* c; q = waitpid(p,c,0); // q = -1 if error, q = p if OKTo destroy a process: