CS372: Solutions for Homework 5

Problem 1

Consider a uniprocessor kernel that user programs can trap into using system calls. The kernel receives and handles interrupt requests from I/O devices. Would there be any need for critical sections within that kernel?

Yes. Assume a user program enters the kernel through a trap. While running the operating system code, the machine receives an interrupt. Now, the interrupt handler may modify global data structure that the kernel code was trying to modify. Therefore, while there is only one thread that runs inside the kernel at any given time, the kernel may not be re-entrant if access to global data structures is not protected through the use of appropriate mutexes.

Problem 2

System Calls vs. Procedure Calls: How much more expensive is a system call than a procedure call? Write a simple test program to compare the cost of a simple procedure call to a simple system call ("getpid()" is a good candidate on UNIX; see the man page.) (Note: be careful to prevent the optimizing compiler from "optimizing out" your procedure calls. Do not compile with optimization on.)

Hint: You should use system calls such as gethrtime() or gettimeofday() for time measurements. Design your code such that the measurement overhead is negligible. Also, be aware that timer values in some systems have limited resolution (e.g., millisecond resolution).

Solution : A system call is expected to be significantly more expensive than a procedure call (provided that both perform very little actual computation). A system call involves the following actions, which do not occur during a simple procedure call, and thus entails a high overhead:

For your experiment, you should measure the total time for a large number of system/function calls, and then find the average time per call in order to overcome the course resolution of your timing functions. For example, here's a sample code for measuring the time taken for a simple system call and a simple function call:

#include <sys/time.h>
#include <unistd.h>
#include <assert.h>

int foo(){
  return(10);
}

long nanosec(struct timeval t){ /* Calculate nanoseconds in a timeval structure */
  return((t.tv_sec*1000000+t.tv_usec)*1000);
}

main(){
  int i,j,res;
  long N_iterations=1000000; /* A million iterations */
  float avgTimeSysCall, avgTimeFuncCall;
  struct timeval t1, t2;

  /* Find average time for System call */
  res=gettimeofday(&t1,NULL); assert(res==0);
  for (i=0;i<N_iterations; i++){
    j=getpid();
  }
  res=gettimeofday(&t2,NULL);   assert(res==0);
  avgTimeSysCall = (nanosec(t2) - nanosec(t1))/(N_iterations*1.0);

  /* Find average time for Function call */
  res=gettimeofday(&t1,NULL);  assert(res==0);
  for (i=0;i<N_iterations; i++){
    j=foo();
  }
  res=gettimeofday(&t2,NULL);   assert(res==0);
  avgTimeFuncCall = (nanosec(t2) - nanosec(t1))/(N_iterations*1.0);
 

  printf("Average time for System call getpid : %f\n",avgTimeSysCall);
  printf("Average time for Function call : %f\n",avgTimeFuncCall);
}

Sample output on a linux machine :

> gcc -O0 testtime.c -o testtime
> ./testtime
Average time for System call getpid : 394.778015
Average time for Function call : 15.080000
 

Problem 3

When an operating system receives a system call from a program, a switch to the operating system code occurs with the help of the hardware. In such a switch, the hardware sets the mode of operation to supervisor mode, calls the operating system trap handler at a location specified by the operating system, and allows the operating system to return back to user mode after it finishes its trap handling. Now, consider the stack on which the operating system must run when it receives the system call. Should this be a different stack from the one that the application uses, or could it use the same stack as the application program? Assume that the application program is blocked while the system call runs.

Most compilers set the stack pointer to be at the beginning of the stack area when a function is called. Thus, automatic
variables are found underneath the location to which the stack pointer is pointing to (except for the HP PA-RISC, in which
the stack grows upward). Thus,

  1. When an interrupt occurs, the kernel has no idea as to how many automatic variables the user program has allocated on the stack Therefore, it cannot just set the stack pointer past the current call frame of the use program, because it does not know where are the call frame limits. As such, the kernel must use a different stack to execute the interrupt handling routine.
  2. On a multiprocessor, executing on the user stack allows another active user thread (on a different processor) to modify the stack and forcing the kernel to fail (maliciously, or unintentionally).
  3. If the current stack is allocated on the user program's heap (such as in user-level thread packages), then the kernel code can cause a stack overflow and start destroying data in the user's address space.