Operating Systems, Fall 2002

Exercise 5: Extending our User Level Thread Library

 

Subject: Extending User-Level Threads.

Due Date: 16/02/03

This exercise is not very difficult, but you will do yourself a favor if you start early.

This exercise is optional. You are not required to do it, if you feel that your grade will be sufficiently high even without it.


In this exercise you will extend the basic user thread library developed in Exercise 2. The extension is in the following two dimensions.

As usual, the package is delivered in form of a static library. The public interface of your library is given by uthreads.h. Needless to say, that you are required to study it thoroughly, and that you are not allowed to make any changes to it.

There may be a number of internal functions, and data structures that you might find necessary to implement. These functions are not visible outside the library, and, therefore, you are not restricted in their number, signatures, and content. The same goes for the private data structures of the library.

Please, note the following. When you write a library, you cannot assume that the user will always use the library correctly. The order in which the functions are called matters. You can infer the correct order from carefully reading the comments inside uthreads.h. If you allow, for instance, to use some function from the library before you initialized the library, and your library does not fail, and, moreover, operates correctly - that is a problem. The problem is that you do not follow the required behavior. However, this is a mild problem. If the program fails with SIGSEGV as a result of an unchecked NULL pointer reference, this is a big problem, and you will lost much more points.

The goals of this exercise include:

  1. Understand the issues involved with implementation of synchronization primitives;
  2. Understand preemptive scheduling algorithms;
  3. Understand the implementation issues involved with preemptive scheduling;
  4. Understand the race condidions due to asynchronous signal processing.

Background reading

  1. Carefully read the man pages on setsigmask/getsigmask/kill/sleep/signal system calls.
  2. Read in the book / notes / slides the sections that deal with: CPU Scheduling, threads, Linux schedulingetc.
  3. Carefully read the attached Demo code with rudimentary preemptive scheduling.
  4. Chapter 10 in the "Advanced Programming in the Unix Environment", by R. Stevens that discusses signalling.

 

Assignment


Synchronization Primitives


You are required to implement the following functions to provide the functionality of semaphores in your library.

If a thread cannot complete its semwait() operation, and the operation blocks, the thread should not be among the READY threads. In other words, it should not be a part of scheduling now, because it cannot continue running anyway. The thread will get back to the READY state queue only when this operation can be completed without blocking. When a thread blocks on a semaphore, it enters the BLOCKED state.

You are required to implement the FIFO order of the threads waiting on the same semaphore. This means that when there is a possibility to complete the semwait() operation, the thread that waits the most time on the semaphore will complete semwait(). Thus, the semaphore implementation would be fair.

Needless to say that you should assign the errno variable in your library accordingly, so that uthreads_perror() can be used as before in ex2

Interface Extensions

To make the things more interesting, shall we say, we add the following three functions to the library interface.

The first two operations can be called by any thread on any other thread.

The last operation can be called at any point in time after the library has been initialized. If there are no threads the buffer should contain only '\0'

Scheduling Extensions

You are required to implement a preemptive scheduling algorithm for your library. However, for the sake of simplicity (and for the sake of easy and valid checking) we will use virtual (simulated) time instead of real time. The idea is a follows. Signal SIGUSR1 is defined by us as "clock tick". Your process (i.e., the user process linked with your library) will receive these clock ticks from another program that you write. There is no need to submit this program, because the checkers will have their own. To give you an idea of what this process needs to do consider the following excerpt:

for (;;) {
kill (pid, SIGUSR1); //simulate clock tick
sleep(10);
}

That's about it. Should not be difficult to implement :)

Now, to the scheduling algorithm itself. The virtual time in our system is divided into epochs. At the beginning of each epoch each thread gets the virtual quantum which is the number of the simulated clock ticks. This is maximal virtual time that a thread can use in this epoch. The quantums of different threads may differ as explained later.

Each time there is a clock tick, the library deducts 1 from the remaining quantum of the currently running thread. The currently active thread runs in the given epoch until one of the following occurs:

In the first case, the thread enters BLOCKED state. From this state the thread can move to:

In the second case, when the thread is resumed, it returns to the BLOCKED state to complete its wait() operation. It can move to the READY state only if it completes its last wait() operation. Suspending the already suspended thread has no effect on the internal state of the thread, and the thread remains in the SUSPENDED state. If suspend is called on the already suspended thread, it should return with -1 as specified in the uthreads.h file.

In case of yielding the CPU, the thread remains in the READY state. It will run when scheduler gets to it. If its quantum is greater than zero, this will happen in the current epoch.

If the quantum of a thread became 0, the thread is in the READY state, but the scheduler cannot schedule it. It cannot run until its quantum is refilled. This may happen only when a new epoch starts.

When does an epoch start? When does it end? The first epoch starts when uthreads_start() is called for the first time for the given thread set. At the beginning of epoch i, each thread is given its virtual quantum according to the formula: VQ(i)= V_QUANTUM + VQ(i-1)/2 where V_QUANTUM is the base quantum defined in uthreads.h, and VQ(i-1) is the quantum value that remains from the previous epoch, i-1. For the first epoch the second term is, of course, 0.
An epoch terminates when no READY thread with non-zero quantum exists. Then the new epoch is started, and the virtual quantums are allocated to all threads according to the formula above.

What is the role of the virtual time quantum? It serves as the priority of the thread. The rule is as follows. Every time the scheduler needs to choose which thread to run, it chooses the READY process with the highest virtual quantom.

When a thread terminates, it enters the ZOMBY state, as before. In this state, the thread does not run, but the library retains statistical information (see struct thread_status defined in uthreads.h) about this thread's execution. This informationis as follws. Please, note the new field. These statistics are made available through uthreads_wait() library function. A terminated thread is finally cleared from the library if and only if uthreads_wait() has been performed. Note similarities and differences to UNIX's wait() that are reflected in uthreads.h file.
  1. Design data structure(s) for managing threads.
  2. Implement the library interface as defined in uthreads.h. Note the new things in this file. The library functions are well documented, please, pay attention.
  3. Return Values:

     

    Submission

    Submit tar file named ex5.tar containing:

    1. README file containing your logins as usual.
    2. Source code.
    3. Makefile - your makefile should generate a library file named: libuthreads.a

    Make sure that the tar file can be extracted and that the extracted files do compile.

    Guidelines:

    1. Run everything on local machine (workstation you are using)
    2. Avoid collisions: do not use public machines (i.e. inferno, bagel...)
    3. Design your program carefully before you start writing.
    4. Always check the return value of a system call wherever applicable.
    5. Test you code thoroughly - write test programs - cross test program with other groups.
    6. Use asserts and debug printouts, but make sure to remove all asserts and any debug output from the library before submission. A library function should not have side-effects such as debug output or exit calls.
    7. CONVENTIONAL DEBUGERS ARE NOT SUITABLE FOR THREADS DEBUGGING !

     


    Ex5 errorcodes
    GOOD LUCK !!
    To the course home page