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

Lecture 4 notes

edited by: Eric Finlay, Sean Huckins, Mishali Naik
(note: all photos copyright Sean Huckins)
Tuesday, October 17th, 2006

Processes will be looked at from the following three perspectives:

  • Abstraction
  • Implementation
  • UNIX variant of the process

Processes

What is a process?

  • A virtual computer
  • A program in execution in an isolated domain

A process virtualizes primary memory space, the processor, and I/O devices (this is the basic Von Neumann architecture) for use.

kernel-layer-good.jpg

There are two basic questions involved with processes:

  1. (Process side) How can processes access the Kernel?
  2. (Kernel side) How are processes implemented?
    • Instructions
    • Hardware details
    • Interface definitions

Implementation of the processes should preserve PRNS (performance, robustness, neutrality, simplicity).

Kernel

How is the kernel implemented? The kernel's job is to allow processes access to system resources through virtualization. A scheduling algorithm must be created to allow processes access to the kernel in an fair and efficient way. Simply put, the kernel's main loop consists of picking a process, setting up the process's virtual memory space, and runnning that process.

To illustrate (in pseudocode):

main:   while (1) {
            pick a process;
            load process's register set;
            run process;
        }

Several factors must be considered when designing a kernel. It is inefficient to virtualize every instruction, since some instructions, such as addl (add two registers) and pushl (push a register on the stack), are relatively undangerous. (Imagine how expensive it would be if addl was implemented by a system call!) So, with some relatively basic support from the processor, such as virtual memory -- the subject of future lectures -- the kernel allows processes direct access to most instructions. This helps limit the total number of system calls that must be made to the kernel, thus allowing the kernel's resources for other processes. There are issues that arise if processes try to execute instructions that violate isolation, such as modifying another process's memory or accessing the disk directly. A seperation must be made between safe instructions (instructions that do not violate isolation, such as addl on registers) and privileged instructions (those that would violate isolation, such as hsc, inb, outb). Any process can run safe instructions, but only the kernel can run privileged instructions. Processes must have access to their own set of registers and these registers must be saved when switching processes, a task which falls upon the kernel. To keep track of all the processes running, the kernel has a list of process descriptors; a process descriptor contains information regarding the process's register set, address space, and virtualized I/O resources (such as open files).

How does a process call into the kernel?

addr-space.jpg

The problem with the above example is that the control transfer to the kernel is unprotected; a process can jump to anywhere in the kernel (right into a piece of data that looks like an hsc instruction, for example). We want to be able to limit the locations where processes can enter the kernel to safe locations. The solution is to implement system calls using hardware support for protected control transfer. This allows a user process to enter the kernel at a location specified by the kernel itself. The kernel can then limit its entry points so that processes cannot commit mischief. A common mechanism to implement protected control transfer is an instruction such as int (x86, stands for interrupt) that causes a trap: a software-generated interrupt. The kernel installs a trap handler so that when a user process executes an int instruction, the kernel gets control. This uses the same mechanism as timer and other hardware controlled interrupts.

The ideal way for a process to call into the kernel is to execute the trap instruction, which then starts executing kernel code with kernel privilege. The kernel starts by saving the process's state, allowing for the kernel to safely return to the calling process when it is done. In order to save the process's state, the process's registers are pushed onto the kernel's stack and then the kernel state is loaded.

Client/Service

Client/service is a way of implementing communication between processes. Under this system the only type of communication between processes occurs in specialized links (ex. pipes). This helps to preserve isolation by disallowing arbitrary communication between processes. How should we implement these links? In order to implement the links we need to have an abstraction of sending and receiving messages. Sending a message should be like writing to a file; while receiving a message should be like reading from a file. There is one key difference, however, there is no way to reread a message. While a file is like a book and it is possible to reread any section of the book at any time, a link should be more like a phone in that once a message has been sent and received it is gone forever.

client-service.jpg

Consider these two functions:

int main() {
    int i = 0;
    while (1) {
        printf("%d\n",i);
        i++; 
    }
}

int main() {
    int i = 0;
    while (!feof(stdin)) {
        char buf[...];
        fgets(buf, stdin);
        i++;
    }
    printf("%d\n",i);
}

The first function writes integers to a file, while the second function reads characters from a file. Theoretically the first function could write forever to a file, while the secoond function could read forever if there was an unending file. However, because disk space is limited the first function can only write a finite number of bytes. It would also be impossible to have a file of infinite length from which the second function could read forever. What if we hooked the first function's output up to the second function's input? The functions could conceptually run forever if as soon as a line is written to the file from the first function it is read and discarded by the second process, getting rid of the dependency on disk space.

bounded-buffer.jpg

Consider this example where the two functions are on different computers. If the writer sends a message before the reader is ready, the reader saves the message for later. Now consider the same example if instead of being different computers, there is one computer running different processes. Where should we save the data in this case? We could send data through the kernel and store it in the kernel memory until the reader is ready, however if the reader sends too much data, the kernel curls up and dies. This presents a major problem. It makes sense, therefore, to create a limit on how much data can temporarily be stored in the kernel. A virtual communication link with such a storage limit is known as a bounded buffer.

What happens in the case where the buffer is full and can no longer accept any new data? There are two options:

  1. the kernel could stop the writer so that it doesn't run again until there is room in the buffer; this is known as blocking.
  2. the kernel could send an error message to the writer and the writer could try again; this is known as polling.

So what are the advantages and disadvantages of these two approaches?

  • Blocking doesn't take up CPU time and allows kernel to run other useful processes.
  • Polling allows the process to do something else until the buffer is ready, however if there is nothing else to do, the process just sits there idly and eats up resources

To implement blocking in our kernel loop from earlier

while (1) {
   pick an unblocked process;
   run it;
   for (p = each blocked process that has become ready)
       unblock p;
}

The process descriptor must now also contain a variable to represent its state (blocked or not).

Creating a new process

The first step in creating a new process is to copy the current process (by forking) then setting up the environment of the new process (by using open, close, read) and loading the new code into the new process (by using execvp).

pid_t fork() returns the child's process ID in the parent and zero in the child.

The following code fragment properly illustrates creating a new process:

pid_t c = fork();
if (c == 0) { // in child
     set up new process environment;
     execvp(command, arguments);
} else { // in parent
     parent code;
}

In the kernel, fork() is implemented by allocating a new process descriptor, then copying the old process descriptor into the new one, and setting the return value.

fork.jpg

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