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

Lecture 6 notes

Lecture date: 10/17/2006
Notes by: Jennifer Chen, Tiffany Lay and Oussama Sekkat.

Interrupts

Why Interrupts?

Interrupts provide a way for the kernel to notify an application of a crucial event.

For example, if the power goes out and you have an UPS (uninterruptable power supply) hooked up to your system, the kernel may realize that there is limited power before the entire system must shut down. It is thus vital that all running applications save their states so that when the system starts up again, the applications can resume what they were doing before.

Question: Why doesn't the kernel save everything in volatile memory?
One possible answer: Not enough disk space, and/or want a clean boot next time the computer turns on. (But some operating systems do try to save everything in volatile memory.)

One option would be to hibernate: store the contents of the RAM into the disk.
We need an "interrupt-like" abstraction for processes.
In case the computer needs to shutdown, it would send a message to the process telling it to "save itself". The process should then act differently and decide what to save to the disk. That way, more power is given to the application as suggested by the End-to-End argument

 End-to-end argument - "The application knows best."

This means that instead of having the kernel save states, it will instead interrupt all running applications and have the applications decide what data can be quickly reconstructed and what should get saved to the disk.

Interrupt Implementation

We looked at both polling and at blocking for an interrupt implementation.

Let's look at the polling method:

   fd = open("/dev/power", O_READ)
   c = readc(fd);
   if (c == 0)
      /* power is ok */;
   else if (c == 1)
      /* power is failing */;

This implementation will have a specific file that has write permission given to the kernel only. When the power is fine, the kernel will write a 0 to it. If the power is failing, then the kernel will write a 1. All applications will continuously check for power failure, and act accordingly if they see a 1.

Problems with polling
Robustness If the application doesn't poll often enough, then it may not realize there is a power failure until too late.
Performance If the application polls too often, it is unnecessarily consuming resources.

Let's see if blocking works better:

   fd = open("/dev/power", O_READ)
   while (readc(fd) == 0) //block until power goes out
     /**/;

This works the same way as the polling method except it will block on a separate thread until the power actually goes out.

Problems with blocking
Performance Requires a separate thread for every application solely to check if the power has gone out
Robustness How will one thread inform all the other threads in the same application of the power failure?

There are many problems with implementing an interrupt with polling or blocking, mostly stemming from the fact that the model of a file is designed to fit an isolated virtual computer. So now we turn to a new concept to solve this: signals!

Signals

A signal is a way to interrupt a process asynchronously. More specifically, it is used to tell the process to act differently in certain circumstances. For instance, in the case of a power outage the Kernel can send a signal to the process telling it to behave accordingly.

Once the process receives a signal, it is "immediately" interrupted regardless of its progreess, and it makes a call to the appropriate signal handler. The signal handler is a function defined just to handle the signal. To make sure that the right signal handler is called, we use function pointers. When the signal handler returns, the process resumes its work (except if the signal received was SIGKILL or SIGTERM ...). Here is an example:

   typedef void (*sighandler_t)(int);  // function pointer type:
                                       // fits all functions that take a single int argument
                                       // and return nothing
   void powerout(int s) {
      save state to disk;
   }
 
   // This system call/library function installs a new signal handler, "func".
   sighandler_t signal(int signalnumber, sighandler_t func);
 
   void main() {
     ...
 
     // somewhere in your main function, you should indicate the signal
     // and call the interrupt function
     signal(SIGPWR, powerout);
   }

Note that signal handlers are a very good analogy to trap handlers, which the kernel uses (with hardware support) to implement hardware interrupts and system calls. Signals really are "interrupts for processes".

You can also use interrupts to:

  • Signal a timeout
  • Break out of an infinite loop
  • Kill a thread or process (SIGKILL)

To send a signal to another thread or process, call the kill(pid_t pid, int signumber) system call. The kernel implements some permission checks so that processes cannot alter other processes/threads that it should not have permissions to.
Here are some signals from the Signal Unix Specification for <signals.h>:

SIGABRT - process aborted
SIGALRM - signal raised by alarm
SIGBUS - bus error "access to undefined portion of memory object"(SUS)
SIGCHLD - child process terminated, stopped (*or continued)
SIGCONT - continue if stopped
SIGFPE - floating point exception -- "erroneous arithmetic operation"(SUS)
SIGHUP - hangup
SIGILL - illegal instruction
SIGINT - interrupt
SIGKILL - kill
SIGPIPE - write to pipe with no one reading
SIGSEGV - segmentation violation
SIGSTOP - stop executing temporarily
SIGTERM - termination
SIGTSTP - terminal stop signal
SIGURG - urgent data available on socket
SIGVTALRM - signal raised by timer counting virtual time -- "virtual timer expired"(SUS)
SIGXCPU - CPU time limit exceeded
SIGXFSZ - file size limit exceeded
source:Wikipedia.org

Here are other examples of how to use signal functions to do asynchronous notification. The goal is to make the child's process exit number be the number of SIGSUR1 which is the signal it receives. The following is a first attempt to write the code:

1   int num = 0;
2   void h(int signal) {
3      num++;
4   }
5   int main(int argc, char *argv[]) {
6      int i, status;
7      printf("Pre-fork...\n");
8      fflush(stdout);
9      pid_t p = fork();
10     if (p > 0) {
11        printf("P, C is %d\n", p);
12        kill(p, SIGUSR1);
13        waitpid(p, &status, 0);
14        printf("C status is %d\n", status);
15     } else if (p == 0) {
16        signal(SIGUSR1, h);
17        printf("C %d, P is %d\n", getpid(), getppid());
18        for (i = 0; i < 1000000000; i++)
19           /* do nothing */;
20        exit(num);
21     } else
22        abort();
23  }

We can see that this code presents some issues. In fact, after forking, it is up to the operating system to decide in which order to run the parent and child processes. Consider the case where the parent's code is executed up to line 12 where it calls the kill function before the child has a chance to install the signal handler. In such a case the child's default handler will be called, which is not what we want. So how do we make sure that the right signal handler is called? One way would be to setup the child's signal handler before the fork. Since signal handlers are copied upon forking, the child will have in this case the correct signal handler from the beginning. Of course we should make sure to restore the parent's signal handler. Here is an improved version of the previous code:

1   int num = 0;
2   void h(int signal) {
3      num++;
4   }
5   int main(int argc, char *argv[]) {
6      int i, status;
7      printf("Pre-fork...\n");
8      fflush(stdout);
9      signal(SIGUSR1, h);  // set handler before fork
10     pid_t p = fork();
11     if (p > 0) {
12        printf("P, C is %d\n", p);
13        signal(p, SIG_DFL);  // restore default handler
14        kill(p, SIGUSR1);
15        waitpid(p, &status, 0);
16        printf("C status is %d\n", status);
17     } else if (p == 0) {
18        printf("C %d, P is %d\n", getpid(), getppid());
19        for (i = 0; i < 1000000000; i++)
20           /* do nothing */;
21        exit(num);
22     } else
23        abort();
24  }

the previous two codes were obtained from the scribe notes dated 4/13/05

Process Management

How do signals affect process management?

Pros

  • Manageability
  • Removed previous performance/robustness issues

Cons

  • Poked holes in process isolation

Indivisibility

2 sequences of steps or system calls are indivisible or atomically isolated if their effect (from the point of view of the calling processes) looks like one of them happened completely before the other.

In other words, "do it all before, or do it all after". This is what the OS tries to provide for system calls.

Why?
To limit propagation of effects!

For example,

   int main (int c, char **v) {
     pid_t p = fork();
     printf("Where am I?\n");
     if (p == 0)
       printf("Child\n");
     else if (p > 0)
       printf("Parent\n");
   }

Possible outputs include:

  • WWCP
  • WWPC
  • WCWP
  • WPWC

where W = "Where am I?", P = "Parent", and C = "Child".

Switching the second and third line will give us the effect that we desire.

   int main(int c, char **v) {
       printf("Where am I?\n");      //  <---switch!
       pid_t p = fork();             //  <---switch!
       if (p == 0)
           printf("Child\n");
       else if (p > 0)
           printf("Parent\n");
   }

The possible outputs now become:

  • WPC
  • WCP

However if each character were to be outputted separately, the system call would no longer be indivisible. Imagine getting an output like this!

 Where am I?
 CPhairlednt

Note: There is still one possible scenario where the output could be like the previous outputs, i.e. WPWC and so on. How could that be possible? The answer lies in the implementation of printf. When outputting to the terminal, printf behaves as one would expect and prints out the whole string that was passed to it. However, when writing to a file, printf() is trying to reduce the number of system calls by using buffered IO (introduced in lecture 2). In such a case, the printf() first writes to a buffer before writing to the file. Thus, after the fork, the buffer that contains the "Where am I?" will be copied in the child's buffer. When outputting the result to the screen, there will be 2 "Where am I?"(1 in each process) that will be outputted.
For instance, if we run the following commands :
./something > file
cat file
then the outputs could be WWPC, WWCP, WPWC or WCWP. In order to avoid this, we should first flush the buffer before forking as in the following:

   int main(int c, char **v) {
       printf("Where am I?\n");      
       fflush(stdout); // Flush buffer so that "Where am I?" doesn't get copied in the child
       pid_t p = fork();             
       if (p == 0)
           printf("Child\n");
       else if (p > 0)
           printf("Parent\n");
   }

So the question now is how to organize process running at the same time, because threads can get in each other's way if not properly coordinated.

Synchronization

The idea behind synchronization is to ensure that threads cooperate.

Quick implementation of Bounded Buffer
 Define structure for this buffer
   typedef struct pipebuf {
     char buf[N];		//allocate a buffer than can hold N bytes
     unsigned head;
     unsigned tail;
   } pipebuf_t;
 
   char readc(pipebuf_t *pb) {
       while (pb->tail == pb->head)
           /*  */;
       int c = pb->buf[pb->head % N];
       pb->head++;
       return c;
   }
 
   void writec(pipebuf_t *pb, char c) {
       while (pb->tail - pb->head == N)
           /*  */;
       pb->buf[pb->head % N] = c;
       pb->tail++;
   }

hypothetical-buffer2.jpg

The number of characters in buffer always equals (head - tail).

actual-bounded-buffer.jpg

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