CS 111 Lecture 4 Scribe Notes (Spring 2012)

Prepared by Nivita Tharanipathy, Nancy Hsieh, Jason Wu, and Camille Hidalgo


Table of Contents
-   Modularity
o   Client Services Organization
o   Virtualization
-   Processes
-   TODO (next lecture)

Modularity

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:

1) Client Services Organization

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);

}

}

This option has a few advantages:
-   It limits error propagation
-   There are no shared states
-   Works even if the other side loops
However, there are a few disadvantages, as well:
-   More administrative hassle
-   Performance is much worse
o   You have to package/unpackagedata into network order
o   You have to worry about network delays

2) Virtualization

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:

Interupt Service Vector

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:

x86 Ring Based System

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


Processes

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:

Application Diagram

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 */


TODO (next lecture)

pipe, open - interprocess communication

exec* - run other programs