You are expected to understand this. CS 111 Operating Systems Principles, Fall 2006
You are here: CS111: [[2006fall:notes:lec3]]
 
 
 

Lecture 3 notes

By Danny Cheung, Ahmed Shama, Peter Wu
Thursday Oct 5, 2006

Introduction and Recap

Today's Big Topics Include:

  • Client/Service Abstractions
  • Virtualization

Recap from previous lecture: what do interfaces need to be designed for? Performance, Robustness, Neutrality, Simplicity.

Different Kinds of Layering

  1. Buffered I/O
  2. Buffer Cache
  3. Bus, Direct Memory Access

The first two are associated with caching and prefetching.

Side note: malloc in C is similar to new in C , while ''free'' in C is similar to ''delete'' in C++. ===== Von Neumann Computer ===== The task at hand: - Read a file from a disk - Translate it into French - Print the N most common words (this was taught in discussion section) We want to do these three tasks on a **Von Neumann Computer**. First of all, what does a Von Neumann Computer contain? The chart below shows the corresponding components between a Von Neumann Computer and a Turing Machine: ^ Von Neumann Computer ^ Turing Machine ^

     int fact(int x) {
          if (x == 0)
               return 1;
          else
               return x * fact(x - 1);
     }

In memory, we would use the stack to implement this. To get a better picture of the memory layout and the different types of memory, refer to the following diagram of a 4GB block of byte-addressed memory. As the heap and stack memories expand, they grow towards each other. lec3_2006_memorylayout.jpg In most languages (C, C, Java, etc), every function can only return once (In other words, return at most one value). For example, if we call fact(2,400,000), then that function calls fact(2,399,999) and suspends the previous one. See call/cc (alternate link) for examples from Scheme, where functions can actually return more than once!

On an x86 machine, factorial in assembly looks like this. (The addresses are not real, but the code is.)

   10:     fact:     cmpl     4(%esp), $0     # if argument does not equal to 0, jump to .L2
   11:               jne      .L2             
   12:               movl     $1, %eax        # set %eax, which we use for the return value, to 1
   13:               ret                      # pop the argument of the stack
   14:     .L2       movl     4(%esp), %eax   # copy argument to register %eax
   15:               decl     %eax            # decrement the register; subtract from the argument
   16:               pushl    %eax            # push argument onto stack
   17:               call     fact            # recursive call to fact
   18:               addl     $4, %esp        # gets rid of argument
   19:               mull     4(%esp), %eax   
   20:               ret

In line 10, the cmpl instruction compares whether the argument, 4(%esp), equals 0. The esp register represents the stack pointer, and 4(%esp) represents 4 bytes off the stack pointer.

Now let's call fact(5)

   0:                pushl    $5              # argument pushed onto stack
   2:                call     fact
   6:                addl     $4, %esp

This returns the value 120. The following figure shows how the stack would look like after all the values have been popped onto the stack. This stack grows down.

stack.jpg

The call command does the following:

  1. Pushes the return address onto the stack (the address from which we resume when the called function has finished)
  2. Changes the stack pointer
  3. Sets the instruction pointer

The ret command:

  1. Pops the stack
  2. Returns a value

Now suppose that in the assembly program of factorial, we inserted the following line between lines 10 and 11:

   movl     $0, 12(%esp)

This would replace the '5' that's located 12 bytes from the current stack pointer with a '0'. Now the function would return the value 0. Or, suppose instead that we inserted the line:

   movl     $0, 8(%esp)

This would move the stack pointer and replace '6' (the return address) with 0, changing the return value to 0

Because functions can tamper with other functions' state, function call modularity isn't looking like hard modularity!

Why Function Call Modularity is Soft

  • A stack overflow can occur when there are too many function calls, causing the stack to overlap other memory.
  • A function can write over other function's arguments, which is called a buffer overflow.
  • It's possible to run into an infinite loop, where the calling function never retains control from the called function again.
  • There might be privileged instructions that can break modularity.

A safe language like Java prevents buffer overflows. It does not, however, solve the infinite loop issue, because of the halting problem.

Buffer Overflow

We saw an example of this earlier, in the above scenario where we inserted extra lines of x86 code and moved the stack pointer. In another example, someone submitting a comment on microsoft.com could manage to take control of the entire Web site. Suppose the code to process the comment was as follows:

int process_comment(int fd) {     // fd is a file descriptor
     char buf[100];
     int i = 0, c;
     while ((c = readc(fd)) != '\n') {
          buf[i++] = c;
     }
     /* format using ASP XHTML... blah blah; */
     display;
}

Someone could take over the Web site by overflowing the buffer. Instead of submitting English text, one could submit x86 code that overflows buf, overwriting the stack's return address and other information stored on the stack. The website is compromised!

Privileged Instructions

Another example comes from the MIPS-X machine, designed from 1984-1987, which had the following command:

   0xCFC00000 == hsc            Halt and Spontaneously Combust

The semantics are "The processor stops fetching instructions and self destructs. Note that the contents of Reg(31) are actually lost."

This is a joke (although it really appears in the MIPS-X instruction set manual), but the point still stands. What happens when you call a function that executes an instruction that can cause damage to your program or to the machine?

Soft Modularity = Fate Sharing. A bug in one module affects others; this is another example of propagation of effects. We want hard modularity.

Hard Modularity

Hard modularity can be accomplished through the following.

Client/Service Interaction

Clients and services interact only by sending messages to one another. Clients send requests, and services respond to those requests.

This accomplishes the following:

  • Limits error propagation
  • Defines and limits interactions
  • Requires the programmer to carefully program messages.

For example, if a client sends a message and does not receive a response, a timeout protocol will prevent the client from waiting indefinitely. This prevents the previous problems with soft modularity from occurring.

One suggestion for the counting French words problem is to get 3 computers, with each one accomplishing a different task:

  1. Read a file from a disk
  2. Translate it into French
  3. Print the N most common words

Here, the links abstraction would be the message-sending channels. This setup limits error propagation; if one machine crashes or goes into an infinite loop, the other machines can still operate. The interactions are clearly defined and limited, and machines can disregard any interaction/message that violates the predefined conventions. Yet, is there a problem with this setup? Yes, modularity is determined by resources (i.e. money). This is very bad: esepcially when processing power is so abundant, good robust programs should be available to everyone, not just people with mucho cash! The solution to this is virtualization.

Virtualization

Virtualization is the process of simulating another object's interface. A virtual version behaves roughly as the real version. We need three abstractions for each virtual machine:

  1. Interpreter
  2. Links
  3. Memory

In our example with the three computers, virtualization would eliminate two computers by replacing them with processes and virtualize them into one machine. However, the modularity is not 100% perfect, because the three virtual machines are physically tied to each other. For example, a power outage would cause all three processes to fail. Nevertheless, the modularization is much harder than in the simple function call case: for instance, the processes will not be able to scribble over each other's memory.

In order to achieve virtualization, we need help from the processor. We need a virtualizable processor. For example:

  • hsc and other privileged instructions have to trap into the kernel. In other words, the kernel gets control when the virtual computer executes.
  • If one virtual computer goes into an infinite loop, the virtualization layer (i.e. the kernel) would need to use a timer interrupt. A timer interrupt is a mechanism in which the virtual computer gives the kernel control periodically. This prevents infinite loops.

We have now virtualized every part of the Von Neumann computer. Each of these virtual computers are called a process.

Process

A process is like a virtual computer. A process is: a program in execution in an isolated domain. A program is a collection of instructions that achieve a specific function.

In this setup, the message channel are the files. The same interface (i.e. read and write) that applies to the disk can be used for message channels! Unix's big idea: Everything is a file.

Setting up Communication Channels in UNIX

     translate < file | wordcount

The bad way to do it:

  1. Start translation
  2. Start word count
  3. Change translate program to read from file
  4. Change translate program to write to word count
  5. Change word count program to read from translate

Steps 3 through 5 are bad, because we're changing the processes' state after they have already started. This violates virtualization; the process could already be in a different state from its original state. Instead, we change the message channel using two Unix functions:

     pid_t fork()          // makes a copy of the current process called the child process
     int execvp(const char *file, char *const argv[]); // replaces current process with a new program (EXCEPT: file descriptors)

To distinguish between the two copies (parent and child) of the process, fork returns different values in both programs. The child process will return a 0 and the parent process returns the child pid. If a failure occurs, the return value will be -1.

execvp takes two arguments: *file should point to the file name to be executed, and argv[] is an array of pointers to arguments of the file that we want to execute. If execvp returns -1, then an error has occurred.

 
2006fall/notes/lec3.txt · Last modified: 2007/09/28 00:25 (external edit)
 
Recent changes RSS feed Driven by DokuWiki