Vous êtes sur la page 1sur 10

OPERATING SYSTEMS, ASSIGNMENT 2

SIGNALS, USER THREADS and SYNCHRONIZATION

Task 1: The Signals framework


In order to support a mechanism (preemptive) for user space threads that will
enable to switch control from an executing thread to the threads scheduler, we will
use the signals mechanism.
As you have seen in class, signals are a simple form of inter process communication
(currently not implemented in xv6). In this part of the assignment, you will add the
framework that will enable the passing of signals from one process to another. This
implementation will cover the basic features needed for a signals framework, and
despite its resemblance to the Linux framework it is far from being complete.
1.1. Updating the process data structure:
The first step towards meeting this goal is to extend the proc struct located at
proc.h. The struct should contain a field called pending that represents all currently
unhandled (pending) signals. For example, when this field's value is 0x3 (the hex
representation of the binary word 000011) then the process will eventually receive
two signals whose identifier are 2 and 1. For simplicity, you may assume that your
implementation will never have to support more than NUMSIG signals (you can find
the appropriate definition at signals.h #define NUMSIG 32)

Note that this representation means that multiple instances of a signal with
the same type (having the same identifier) do not stack (are treated as a
single signal).

Each signal must also be associated with some action. To support this, add an array
of NUMSIG entries where every entry is a pointer to a function. We require that
every signal will have a default handler. This handler must only print the following
message: A signal was accepted by process %pid%, where %pid% is the pid of the
process.

1.2. Registering signal handlers:


A process wishing to register a custom handler for a specific signal will use the
following system call which you should add to xv6:
int signal(int signum, sighandler_t handler)
This system call will register a new handler for a given a signal number (signum). If
successful, 0 is returned otherwise a -1 value is returned. The type sighandler_t
should be defined as:
typedef void (*sighandler_t)(void);
1.3. Sending a signal:
So far, we have allowed a process to prepare itself for an incoming signal. Next we
will add the ability to send a signal to a process. Add the following system call:
int sigsend(int pid, int signum)
Although kill is the standard name for the system call in charge of sending a signal,
it is already used in xv6 for terminating processes.
Given a process id (pid) and a signal number (signum), the sigsend system call will
send the desired signal to the process pid. Upon successful completion, 0 will be
returned. A -1 value will be returned in case of a failure.
1.4. Getting the process to handle the signal:
Finally, you are to implement a mechanism which will make sure that a process
receiving a signal actually executes the relevant signal handler.
In your extension of xv6, a signal should be handled (if at all) whenever control is
passed from the kernel to the process. That is, before restoring the process context,
the kernel first checks for pending signals to that process and if needed it updates
the instruction pointer (and stack pointer) by calling register_handler. This way,
when the process receives its time slice, the code for the signal handler is executed.

void register_handler(sighandler_t sighandler)


{
char* addr = uva2ka(proc->pgdir, (char*)proc->tf->esp);
if ((proc->tf->esp & 0xFFF) == 0)
panic("esp_offset == 0");
/* open a new frame */
*(int*)(addr + ((proc->tf->esp - 4) & 0xFFF)) = proc->tf->eip;
proc->tf->esp -= 4;
/* update eip */
proc->tf->eip = (uint)sighandler;
}

The register_handler function is not native to xv6 and was added by the OS team to
support the present assignment (you can find it in proc.c). The function locates the
stack of the current process and opens a new frame. It must also update the old
instruction pointer so that when the new code (sighandler) is completed the process
normally resumes its execution.

Notice that our implementation differs from real world signal handling
schemes. In a real OS the kernel must not use the user stack (try to think
why) and should return to kernel mode after executing the handler code in
order to restore the process context.

1.5. Inheriting handlers to a child process:


Modify the fork system call so that a child process will have the same registered
signal handlers as its parent.

1.6. Implement the alarm system call


void alarm(int ticks);
The alarm system call shall cause the kernel to generate a SIGALRM (see signals.h)
signal for the process after a given number of ticks (specified by the system call
argument).

Note: the scheduler may prevent the process from handling the signal as
soon as it is generated

If the argument ticks is 0, a pending alarm request, if any, is canceled.


Alarm requests are not stacked; only one SIGALRM generation can be scheduled in
this manner. If the SIGALRM signal has not yet been generated, the call shall result in
rescheduling the time at which the SIGALRM signal is generated.

You must use function prototypes and signals definitions from the signal.h
file.

Task 2: User level threads


User level threads (ULT) avoid using the kernel (and are transparent to it), and
manage their own tables and their own scheduling algorithm (thus allowing more
efficient, problem-specific scheduling). Context switching between threads within
the same process is achieved by manipulating the stack pointer and registers. ULTs
are usually cooperative, i.e. threads have to voluntarily give up the CPU by calling the
thread_yield function. In this task you will implement preemptive (non-cooperative)
user space threads.

2.1. User level threads package


In order to manage threads within a process, a struct is required to represent a
thread (much like a PCB for a process). We will refer to this as the thread control
block (TCB). Such a struct should look something like the following:
struct uthread {
int
tid;
uint
esp;
uint
ebp;
void*
stack;
uthread_state state;
};

/* thread's id */
/* current stack pointer register */
/* current base pointer register */
/* the thread's stack */
/* thread's state: running, runnable, waiting */

You may add any fields you see fit to the above struct. This type and the prototypes
of the ULT package API functions are defined for you in the uthread.h file, which we
added to the current revision in the svn repository.
The threads framework must maintain a table of the threads that exist within the
process. You can define a static array for this purpose, whose size will be defined by
the MAX_UTHREADS=64 macro (defined in uthread.h as well).
The application programming interface (API) of your threads package has to include
the following functions:

2.2. int uthread_init();


This function must be called by the process (that wants to use the uthread library).
This function performs the following:

Initializes the thread table


Creates the main thread
Registers the SIGALRM handler to the uthread_yield function
Executes the alarm system call with UTHREAD_QUANTA=5

After performing the initialization steps you are free to use the uthread library.

2.3. int uthread_create(void (*start_func)(void *), void*arg);


This function receives as arguments a pointer to the threads entry function and an
argument for it. The function should allocate space for the thread, initialize it but not
run it just yet. Once the threads fields are all initialized, the thread is inserted into
the threads table. The function returns the identifier of the newly created thread or 1 in case of a failure.

Dont forget to allocate space for the threads stack. A size of a single page
(4096 bytes) should suffice.
Consider what happens when the threads entry function finishes: does it
have an address to return to? Hint: you can
o Wrap the entry function, so that a thread_exit call will be carried out
implicitly after the entry function finishes
o On thread creation, push the thread_exit as the return address.

2.4. void uthread_yield();


This function picks up the next thread from the threads table, according to the
scheduling policy (explained later), and restores its context. The context of a ULT
includes a unique call stack, a pointer to its top (stack pointer) and a set of other
registers.
The context restore is merely switching the active stack of the process by changing
the value of esp. To clarify this, consider a scenario where thread A calls
uthread_yield and the next thread to run is thread B. Assuming thread B already ran
before and called uthread_yield in the past (to allow thread A or another thread to
run), its stack should contain the values defining the frame to which it should return
when continued. After the stacks are switched, and the execution continues, thread
B continues to execute the code of uthread_yield, and when it reaches the closing
bracket it returns to the point (ret address on its stack) where it made the call to
uthread_yield and continues to run. See figure below.

Figure 1: depicts the stacks of threads A and B, executing functions fooA and fooB
respectively. Thread A calls uthread_yield. The value of the esp register should be
changed to point to the top of stack of thread B, so uthread_yield will return to the
next instruction in fooB (after the call to uthread_yield).

Tip: backing-up and restoring esp and performing stack related operations
requires inlining assembly code into your C code.

2.5. void uthread_exit();


Terminates the calling thread and transfers control to some other thread (similar to
yield). Dont forget to clear the terminated thread from the threads table, and free
any resources used by it (the stack, in our case). When the last thread in the process
calls uthread_exit the process should terminate (i.e. exit should be called).
2.6. int uthred_self();
Returns the thread id associated with the calling thread.

2.7. int uthred_join(int tid);


The uthread_join function waits for the thread specified by tid to terminate. If that
thread has already terminated, then uthread_join returns immediately.

Scheduling policy:
Your threads package should support a round-robin scheduling policy where threads
are scheduled in a cyclic manner.

For more information about basic assembly operations (and conventions) you
can refer to: http://www.cs.virginia.edu/~evans/cs216/guides/x86.html

Task 3: Binary (fair) Semaphores


In this part of the assignment you will add functionality that allows users to create
and use binary semaphores. Before you start your implementation, you can examine
the implementation of spinlocks in the XV6's kernel (for example the wait uses a
spinlock). Spinlocks are used in XV6 in order to synchronize kernel cores while they
(the spinlocks) do not change the status of processes. Your task is to implement
binary semaphores as a user service. The semantics of binary semaphores were
described in class and include the two operations described below.
Add the following functions to your threads package:
3.1. void binary_semaphore_init(struct binary_semaphore* semaphore, int
value);
This function initializes a binary semaphore with the given initial value.

Notice that spinlocks are not the same as fair binary semaphores, for
example starvation can happen with spinlocks but not with semaphores due
to the queue of waiting threads that the semaphore maintains.

3.2. void binary_semaphore_down(struct binary_semaphore* semaphore);


3.3. void binary_semaphore_up(struct binary_semaphore* semaphore);
The functions binary_semaphore_down and binary_semaphore_up work as learned
in class.

Task 4: A Synchronization problem


In this task you will implement a solution to the Firing Squad Synchronization
Problem (FSSP) using the synchronization primitives from task 3.
The FSSP problem description (as described by R.M Balzer):
Consider a finite one dimensional array of finite state machines, all of which are alike
except the ones at each end. The machines are called soldiers and one of the end
machines is called a general. The machines are synchronous, and the state of each
machine at time t + 1 depends on the states of itself and of its two neighbors at time
t. The problem is to specify the states and transitions of the soldiers in such a way
that the general can cause them to go into one particular terminal state (i.e., they
fire their guns) all at exactly the same time. At the beginning (i.e., t = 0) all the
soldiers are assumed to be in a single state, the quiescent state. When the general
undergoes the transition into the state labeled "fire when ready", it does not take
any initiative afterwards and the rest is up to the soldiers. The signal can propagate
down the line no faster than one soldier per unit of time and their problem is how to
get all coordinated and in rhythm. The tricky part of the problem is that the same
kind of soldier with a fixed number, k, of states is required to be able to do this
regardless of the length, n, of the firing squad. In particular, the soldier with k states
should work correctly, even when n is much larger than k. Two of the soldiers, the
general and the soldier farthest from the general, are allowed to be slightly different
from the other soldiers in being able to act without having soldiers on both sides of
them. A convenient way of indicating a solution of this problem is to use a piece of
graph paper with the horizontal coordinate representing the spatial position and the
vertical coordinate representing the time. Within the (i,j) square of the graph paper a
symbol may be written indicating the state of the i'th soldier at time j. Visual
examination of the pattern of the propagation of these symbols can indicate what
kind of signaling must take place between the soldiers.
The figure below shows one solution to the FSSP using 15 states and 3n units of time.
Each state is represented by a different color. Time increases from top to bottom.

Write a user space program that:

Receives the number of soldiers by an argument.


Each soldier is represented by a thread.
Each state is represented by a number.
The process's main thread is in charge of synchronizing the threads steps (In
the problem description the system of machines is synchronous).

At each time, t, several actions need to be done in a synchronized matter:

All soldiers change their state according to their transitions function.


All soldiers write the number corresponding to their new state in a shared
array.
The main thread prints the array (every iteration).

You have complete freedom to choose the algorithm you choose to implement in
order to solve the FSSP. Consider using the algorithm proposed by John
McCarthy and Marvin Minsky.

Submission guidelines
Assignment due date: 27/04/2014
Make sure that your Makefile is properly updated and that your code compiles with
no warnings whatsoever. We strongly recommend documenting your code changes
with remarks these are often handy when discussing your code with the graders.
Due to our constrained resources, assignments are only allowed in pairs. Please
note this important point and try to match up with a partner as soon as possible.
Submissions are only allowed through the submission system. To avoid submitting a
large number of xv6 builds you are required to submit a patch (i.e. a file which
patches the original xv6 and applies all your changes). You may use the following
instructions to guide you through the process:
Back-up your work before proceeding!
Before creating the patch review the change list and make sure it contains all the
changes that you applied and nothing more. Modified files are automatically
detected by svn but new files should be added explicitly with the svn add
command:
> svn add <filename>
In case you need to revert to a previous version:
> svn revert <filename>
At this point you may examine the differences (the patch):
> svn diff
Alternatively, if you have a diff utility such as kompare:
> svn diff | kompare o
Once you are ready to create a patch simply make sure the output is redirected to
the patch file:
> svn diff > ID1_ID2.patch
Tip: Although the graders will only apply your latest patch file, the submission
system supports multiple uploads. Use this feature often and make sure you upload
patches of your current work even if you havent completed the assignment.
Finally, you should note that the graders are instructed to examine your code on lab
computers only(!) - Test your code on lab computers prior to submission.

Enjoy!

Vous aimerez peut-être aussi