Lecture 5: April 12, 2012.
By: Corey Quon, Maria Tio, Rhys Yu, & Naseem Makiya
Ways of Getting Hard Modularity:
1) Client/Service Organization
- send messages to communicate.
Suppose we want to impose hard modularity on a factorial program:
Client:
send({"1", 5}, factserver);
receive(factserver, response);
if (response.code == "ok")
print(response.val);
else
print("error", response.code);
Server:
for (;;)
{
receive(ANYBODY, request);
if (request.opcode == "!")
{
n = fact(request.val);
response = {"ok", n};
}
else
{
response = {"ng", 0};
}
send(request.send, response);
}
Advantages (+) and Disadvantages (-):
- (+) Limits error propagation
- client is insulated from errors from the server
- no shared state; client and server are independent from each other
- (+) works even if other side loops
- (-) performance is much worse
- package/unpackage data into network code
- network delays
- (-) more administrative hassle such as IP and/or port setup
2) Virtualization
- server contains a virtual machine (with a 'fact' instruction) that executes client code
Client:
a = fact(b);
int fact(int n)
{
asm("fact, %d, %eax");
}
To get this virtual machine, write a C program to emulate x86:
C program:
int ecp;
int eax;
char mem[1024 * 1024 * 1024];
for (;;)
{
char insn = mem[eip++];
switch (insn)
{
case 221: eax = fact(eax); //<--code added
}
}
Advantages of virtualization:
Suppose the client does bad things:
- over flows its stack
- divides by zero
- loops
- catch loops with timeout i.e. by failing after 1 trillion instructions
- this isn't practical but works in many scenarios
Disadvantages of virtualization:
- emulation overhead (still too slow)
- even with timeout, there is a problem deciding what this static timeout value should be
We (server maintainers) need a virtualizable processor
- normally runs at full speed to execute client code
- lets us take control whenever client code does something dangerous, including
- access memory it should not
- execute a non-existent instruction like 'fact'
- execute a dangerous instruction
- examples: arithmetic exception, halt instruction, privileged instructions
We need protected transfer control
- server is in charge afterwards
By convention on x86
- example: int 5 -> privileged interrupt instruction
- On Linux, 0x80
- RETI (return from interrupt)
- RETI:INT :: RET:CALL
Processor has a 'privileged bit'
- enabled by default
- to change it, you need a privileged instruction
When the hardware traps, it enables this bit.
To do protected transfer control:
On a trap, the x86 hardware does the following...
- pushes 6 words on stack to do protected transfer control
- pushes
- ss - stack segment
- esp - stack pointer
- eflags - privileged bit, etc.
- cs - code segment
- eip - instruction pointer
- error code - trap type 'details'
- sets ip = isy[n];
- sets privileged bit
- goes on the Interrupt Service Vector and checks which code to execute using the isvn[n]
- syscall convention on Linux,
- result in %eax
- %eax = syscall#
- %ebx = arg1
- %ecx = arg2
- %edx = arg3
- %esi = arg4
- %edi = arg5
- slower than a function call
- On a virtualized system if it tries to execute privileged code the system pushes syscall stack above to the proper address in the home kernel to verify what is allowed.
Home kernel decides how virtual kernel should proceed
- Ring-based systems (4 levels)
- Application
- Drivers
- Virtual Memory
- Kernel
- ABI (application binary interface)
We want to support more than one application "simultaneously" even on a one-core CPU.
To do this, the kernel records each app's state
- Interrupt service routine
- Save registers into process descriptor for current process
- Restore registers from some other process
- RETI
Syscalls for manipulating processes!
- API
- void_exit(int status) - Noreturn;
- _Noreturn - C11 - C function that guarantees that it never returns
- pid_t fork(void);
- pid_t is a signed integer
- returns -1 if there aren't enough resources
- returns 0 if you are a child process (new process)
- returns >0 if you are a parent, value is child_pid
- pid_t wait_pid(pid_t pid, int *status, int flags);
- pid - which process you want to wait for
- status
- flags - how eager you're waiting for the process
- returns -1 if it fails
- Functions that don't return:
- void _exit _Noreturn;
- void abort() _Noreturn;
- void longjump(env, i) _Noreturn;
- A process can only wait on its own children
- flag WNOHANG - wait for 0 seconds
- flag 0 - wait indefinitely
- When wait_pid succeeds, it removes an entry from the process table.
- When fork succeeds, it adds an entry to the process table.