Last lecture, we talked about function calls and some of the problems where you would want modularity. There are 2 basic techniques to get hard modularity:
You can go to a client services organization. They cooperate by sending messages to communicate. Here is an example of how that works:
Client:
send( {"!",5}, factserver);
receive(factserver, response, timeout=1000);
if (response.code == "ok") print(response,val);
else print("error", response.code);
Server:
for(;;) {
receive(ANYBODY, request);
if (receive.opcode == "!") {
n = fact(request.val);
response = {"ok", n};
} else {
response = {"ng",0};
send(request.sender, response);
}
}
Here, the server contains a virtual machine that executes the client code. The following is an example:
Client:
a = fact(b); /* where ‘fact(b)’ is the following definition
-> int fact (int n) { asm(“fact, %d, %eax”); }
-> write a C program to emulate x86:
inteip;
inteip;
inteax;
intmem[1024x1024x1024];
...
for(;;)
charinsn = mem[eip++];
switch (insn) { case 221: eax = fact(eax); } }} */
(A few examples of emulators are gemu, Bochs, etc.)
There are positives in this client’s implementation:
- The emulator can catch overflows in its stack
- The emulate can catch dividing by zero (0)
o How? Possibly by loops/with timeouts (eg. fail after 1 trillion instructions)
o But, there has to be a better way as it might stop the program when it shouldn’t
(ie. there is a long running execution that involves more than 1 trillion instructions)
§ Possibility: We can look at the state and see if it stays the same…
· But, why would you want to keep a snapshot of everything? Not so good…
· However, for the most part, it works, so we keep it
With the positive, come negatives:
- There is emulation overhead (~10x greater)
To compensate, we (server maintainers) need a virtualization processor:
- This normally runs at full speed to execute client code
- It will let us take control whenever client code does something dangerous.
Examples:
o When memory is accessed that shouldn’t be
o Executes a non-existent instruction like ‘fact’
o Arithmetic exceptions
o Executes a dangerous instruction, like the halt instruction
· Also known as privilege instructions (where you have to be in the kernel to execute)
o If the client code tries to pull a fast one (overflows stack, divides by zero, etc.)
To avoid these issues, we need protected transfer control. This means that the server is in charge after a dangerous situation occurs.
By convention on x86:
int5 insn (privileged ‘interupt’)
//on Linux; INT 0x80; RETI (return from interrupt) RETI is to INT as RET is to CALL
- The Processor has a privileged bit (enabled by default).To change it, you need a privileged instruction.
- When the hardware traps, it enables this bit:
On a trap, the x86 hardware does the following:
Pushes: ss   stack segment
esp stack pointer
eflags privileged bit, etc.
cs code segment
eip instruction ptr
error code trap type details
//Pushed six words on the stack in order to do this control
setsip = isw[n];
sets privilege bit…;
Compared to our client server code, this is much faster! - faster than through a socket. However, compared to function call, it is not so good (due to extra overhead)
> syscall convention on Linux,
%eax = syscall #
%ebx = arg 1
%ecx = arg 2
%edx = arg 3
%esi = arg 4
%edi = arg 5
%ebp = arg 6
INT 0x80 insn
//result in %eax
So far we have a model where system code runs privileges but we might have a system that has levels:
So, instead of having privilege bits, we can have privilege levels (as illustrated above).
In order to enter next level, you trap.
- The next inner system is treated as if its own kernel
- You can even make it fancier by going from level 0->3
x86 uses this ring-based system of 4 levels with 2 privilege bits
Say, we want to support more than (>) 1 application ‘‘simultaneously’’ even on a one-core CPU. To do this, the kernel records each application’s state:
process = code that’s running
process descriptor contains:
UID - 1000
Regs - %eax, %edx, %ecx… []<- whatever RETI needs
Now, the interrupt service routine:
…(at some point)
has to save registers into the process descriptor for the current process
…(and later on)
restore registers from some other process
RETI
- The kernel can decide to start running some other application whenever you issue an interrupt.
- Once you’re in kernel mode, the kernel can decide what it wants to do.
- Going back and looking at the process table, you will see many different virtual machines.
- If there 100 entries, we have 100 different applications running.
o Each one of them thinks they have the machine, but only one is running at a time.
o While this is happening, the other states are frozen and are to be used later
Syscalls for manipulating processes:
API void_exit(int status) _Noreturn;
Exit kills a process in affect; the opposite of exit is:
pid_t fork(void); //pid_t is a signed integer
/* By convention, ‘fork’ returns one of three possibilities:
-1 not enough system resources to create new process
0 you are child process (new process)
> 0 you are parent, value is child pid */
//fork bomb - creating processes exponentially fast DON’T MESS WITH SEAS SERVERS
while (fork() >= 0)
continue;
Waiting for a child process to exit:
pid_t waitpit(pid_t pid, int *status, int flags);
/* some flags:
0 - wait indefinitely
WNOHANG - wait 0 sec */
pipe, open - interprocess communication
exec* - run other programs