15
Ben-Gurion University of the Negev, Operating Systems 2012 1 OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this assignment you are to implement two types of threads packages on xv6: user level threads (ULT) and kernel level threads (KLT). Each approach has pros & cons; the goal of this assignment is to understand the differences between the 2 approaches. Assignment overview The assignment consists of the following parts: 1. Cooperative User-level threads package 2. Kernel-level threads package Synchronization primitives: mutex, condition variable 3. Synchronization problem- a variant of dining philosophers' problem with a producer-consumer scheme Implementing twice- first time using the user threads package and the second time using the kernel threads package. Task 0: running xv6 Begin by downloading our revision of xv6, from the os122 svn repository: Open a shell, and traverse to the desired working directory. Execute the following command (in a single line): svn checkout http://bgu-os-122-xv6-rev6-1.googlecode.com/svn/trunk assignment2 This will create a new folder called assignment1 which will contain all project files. Build xv6 by calling: make Run xv6 on top of QEMU by calling: make qemu NOTE: CURRENT REVISION IN OUR SVN REPOSITORY IS DIFFERENT THAN THE ONE USED IN ASSIGNMENT 1. MAKE SURE YOU DOWNLOAD THE LATEST REVISION!

OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

  • Upload
    others

  • View
    41

  • Download
    0

Embed Size (px)

Citation preview

Page 1: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

1

OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION

SUBMISSION DATE: 17/05/2012, 22:00

In this assignment you are to implement two types of threads packages on xv6: user level threads (ULT) and kernel level threads (KLT). Each approach has pros & cons; the goal of this assignment is to understand the differences between the 2 approaches.

Assignment overview

The assignment consists of the following parts:

1. Cooperative User-level threads package

2. Kernel-level threads package

Synchronization primitives: mutex, condition variable

3. Synchronization problem- a variant of dining philosophers' problem with a producer-consumer scheme

Implementing twice- first time using the user threads package and the second time using the kernel threads package.

Task 0: running xv6

Begin by downloading our revision of xv6, from the os122 svn repository:

Open a shell, and traverse to the desired working directory.

Execute the following command (in a single line):

svn checkout http://bgu-os-122-xv6-rev6-1.googlecode.com/svn/trunk

assignment2

This will create a new folder called assignment1 which will contain all project files.

Build xv6 by calling: make

Run xv6 on top of QEMU by calling: make qemu

NOTE: CURRENT REVISION IN OUR SVN REPOSITORY IS DIFFERENT THAN THE ONE USED IN ASSIGNMENT 1. MAKE SURE YOU DOWNLOAD

THE LATEST REVISION!

Page 2: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

2

PART 1: COOPERATIVE USER 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 merely by manipulating the stack pointer. ULT are usually cooperative, i.e. threads have to voluntarily give up the CPU by calling the thread_yield function.

Task 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 thread control block (TCB). Such a struct should look something like the following:

typedef struct

{

int tid; // A unique thread ID within the process

void *ss_sp; // Stack pointer (top of stack)

size_t ss_size; // Stack size

int priority; // The priority of the thread 0…9 (0 is highest)

} uthread_t;

You may add any fields you see fit to the above struct. Note that a state field is unnecessary as the state of all threads except the one that is running is always ready. This type and the prototypes of the ULT package API functions are defined for you in uthread.h file, which we added to the current revision in the svn reposotory.

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 macro (defined in uthread.h as well).

The application programming interface (API) of your threads package has to include the following functions: int uthread_create(void (*start_func)(), int priority);

This function receives as arguments a pointer to the thread’s entry function and an initial priority. The function should allocate space for the thread, initialize it but not run it just yet. Once the thread’s 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.

Don’t forget to allocate space for the thread’s stack. A size of 4000 bytes should

suffice.

Consider what happens when the thread’s entry function finishes: does it have an address to return to? Hint: wrap the entry function, so that a thread_exit call will be carried out implicitly after the entry function finishes (see appendix).

Page 3: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

3

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 cooperative ULT includes a unique call stack and a pointer to its top (stack pointer). Since the context switch is voluntary it is guaranteed that no computation is interfered in the middle, therefore there is no need to store the contents of all other registers except esp. 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 (refer to Call stack scheme in the Appendix). 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 1 below.

In our implementation context switch only changes the return location from the uthread_yield call, thus causing the process to execute the code of another thread.

Modifying only the esp works only if the frame of uthread_yield is empty, and in

such case esp=ebp. If there are local variables defined on the stack- there’s a need to store for each thread its ebp as well.

Tip: backing-up and restoring esp and performing stack related operations require inlining assembly code into your C code. The required commands are provided for you as macros in uthread.h and appear in the appendix as well).

Tip: the cases when the next thread to run is a newly created thread and when

the next thread is a thread which already ran (and called uthread_yield) needs to be dealt with differently.

void uthread_exit(); Terminates the calling thread and transfers control to some other thread (similar to yield). Don’t 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).

Stack of B

old ebp

local vars of yield

ret to fooB

Top of stack

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).

Top of stack, current esp

Stack of A

old ebp

local vars of yield

ret to fooA

Page 4: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

4

int uthread_start_all();

This function is called by the main thread after it has created one or more user threads. It is similar to yield; it picks the first available thread and starts it. If successful, this function never returns, and any code beyond it will never be executed (much like execvp). Since the main thread was not created using uthread_create, and hence has no entry in the threads table, its context will never be restored. Any subsequent calls (after threads were already started) to uthread_start_all should not succeed. In such a case, this function should return -1 to indicate an error.

int uthread_setpr(int priority); int uthread_getpr();

The first function sets the priority of the calling thread to the specified argument, and returns the previous priority on success, and -1 on failure. The second function returns the current priority of the calling thread.

uthread_t uthread_self();

Returns the thread control block associated with the calling thread.

Scheduling policies

Your threads package should support a round-robin scheduling policy where threads are scheduled in a cyclic manner, and a priority based scheduling policy where threads are scheduled according to their priorities. In case of several threads having the same priority, they should be scheduled in a round-robin manner.

The scheduling policy should be defined using the macros SCHED_RR or SCHED_PB for round–robin or priority based, respectively. The default policy, if none of the macros is defined, is round robin. In case both of the macros are defined, you may choose either one of the policies.

Task 2: Sanity test

To test your implementation, write a (very) simple test program named uthread_sanity which receives 2 command line integer arguments n and k, where n is the number of threads and k is number of iterations. The program should create n user threads. The task each thread should perform is to iterate k times, printing “thread <id> iteration <#iteration>”, and calling uthread_yield after each iteration.

The goal of this simple sanity test is to test the core functionality of your ULT package, that is, context switching, scheduling and proper termination.

Page 5: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

5

PART 2: KERNEL THREADS

Kernel level threads (KLT) are implemented and managed by the kernel. Our implementation of KLT will be based on the implementation of processes in xv6. One key characteristic of threads is that all threads of the same process share the same virtual memory space. As such, a simple way to create threads in xv6 is to use code much similar to that of the fork system call, which creates another process. To achieve a thread-like behavior we will have the newly created "process" share the same virtual memory space of the "parent process". By doing so, both "processes" share the same virtual memory space, and so may essentially be considered as two threads. You will need to mark the new "process" internally so the kernel can tell whether it is a process or one of our newly created "threads". Each thread should keep the PID of the process within which it is running.

In our implementation KLTs are represented by the struct proc, in a similar way to how processes are represented in the kernel (see proc.h file). However, threads differ from processes by having certain fields of the struct proc shared among threads of the same process.

We added a file named kthread.h which contains some macros which will be used in the following tasks, as well as the prototypes of the KLT package API functions.

Read this whole part thoroughly before you start implementing. There is a usage example in the appendix.

Task 1: Kernel threads

In this part of the assignment you will add system calls that will allow applications to create kernel threads. Implement the following new system calls: int kthread_create( void*(*start_func)(), void* stack, uint stack_size ); Calling kthread_create will create a new thread within the context of the calling process. The newly created thread state will be runnable. The caller of kthread_create must allocate a stack for the new thread to use, which size is defined by the MAX_STACK_SIZE macro in proc.h. start_func is a pointer to the entry function, which the thread will start executing. Upon success, the identifier of the newly created thread is returned. In case of an error, a non-positive value is returned. int kthread_id(); Upon success, this function returns the caller thread's id. In case of error, a non-positive error identifier is returned. For this assignment we will settle on the simple solution where a thread's ID is identical to its underlying process ID assigned to it by the customized fork call which created it. Different threads within a process should have different IDs. void kthread_exit(); This function terminates the execution of the calling thread. If called by a thread (even the main thread) while other threads exist within the same process, it shouldn’t terminate the whole process.

Page 6: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

6

int kthread_join(int thread_id);

This function suspends execution of the calling thread until the target thread, indicated by the argument thread_id, terminates, unless the target thread has already terminated. If successful, the function returns zero. Otherwise, -1 should be returned to indicate an error.

Task 2: Synchronization primitives

In this task we will implement two synchronization primitives: mutex and condition variable.

Before implementing this task you should do the following:

1. Read about mutexes and condition variables in POSIX. Our implementation will imitate the behavior and API of the pthread_mutex and pthread_cond. A nice tutorial is provided by Lawrence Livermore National Laboratory.

2. Examine the implementation of spinlocks in xv6's kernel (for example the scheduler uses a spinlock). Spinlocks are used in xv6 in order to synchronize kernel code. Your task is to implement the required synchronization primitives as a kernel service to applications, via system calls. Locking and releasing can be based on the implementation of spinlocks to synchronize access to the data structures which you will create to support mutexes and condition variables.

Task 2.1: mutex

Freely quoting from the pthreads tutorial by Lawrence Livermore National Laboratory:

“Mutex is an abbreviation for "mutual exclusion". Mutex variables are one of the

primary means of implementing thread synchronization and for protecting shared data when multiple writes occur. A mutex variable acts like a "lock" protecting access to a shared data resource. The basic concept of a mutex, as used in pthreads, is that only one thread can lock (or own) a mutex variable at any given time. Thus, even if several threads try to lock a mutex only one thread will be successful, and all other threads will get blocked until the mutex becomes available. Mutex differs from a binary semaphore in its unlock semantics- only the thread which locked the mutex is allowed to unlock it,

unlike binary semaphore on which any thread can perform up.”

Examine pthreads' implementation, and define the type kthread_mutex_t inside the xv6 kernel to represent a mutex object. You can use a static array to hold the mutex objects. The size of the array should be defined by the macro MAX_MUTEXES.

The API for mutex functions is as follows:

int kthread_mutex_alloc();

Allocates a mutex object and initializes it; the initial state should be unlocked. The function should return the ID of the initialized mutex, or -1 upon failure.

int kthread_mutex_dealloc( int mutex_id );

De-allocates a mutex object which is no longer needed.

Page 7: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

7

int kthread_mutex_lock( int mutex_id );

This function is used by a thread to lock the mutex specified by the argument mutex_id.

If the mutex is already locked by another thread, this call will block the calling thread (change the thread state to BLOCKED) until the mutex is unlocked.

int kthread_mutex_unlock( int mutex_id );

This function unlocks the mutex specified by the argument mutex_id if called by the owning thread, and if there are any blocked threads, the longest waiting thread will acquire the mutex. This is called a fair mutex. An error value will be returned if the mutex was already unlocked, or the mutex is owned by another thread.

Implementation notes:

In all the above functions, the value 0 should be returned upon success, and a negative value upon failure.

No busy waiting should be used whatsoever, except for synchronizing access to the mutex array using short-time waiting in spinlock.

Task 2.2: condition variable

A condition variable is a synchronization mechanism that allows threads to suspend execution and relinquish the processor until some condition is satisfied. The basic operations on condition variables (CV) are:

Signal a thread that is waiting on the CV.

Wait on the CV, suspending the thread's execution until another thread signals the CV.

A mutex must always be associated with a condition variable, to avoid a race condition where a thread prepares to wait on a condition variable and another thread signals the condition variable just before the first thread actually starts waiting on it.

Define the type kthread_cond_t as you see fit inside the xv6 kernel. You can use a static array to hold the CV objects. The size of the array should be defined by the macro MAX_CONDS.

The API for condition variable functions is as follows:

int kthread_cond_alloc();

Allocates a new condition variable and initializes it. The function should return the ID of the initialized CV, or -1 upon failure.

int kthread_cond_dealloc( int cond_id );

De-allocates the cond object, indicated by the argument cond_id, which is no longer needed.

int kthread_cond_wait( int cond_id, int mutex_id );

Blocks the calling thread until the specified condition variable is signaled. This routine should be called while mutex is locked by the calling thread, and it will atomically

Page 8: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

8

release the mutex and put the thread to sleep. After a signal is received, the thread is awakened when the mutex is locked for use by the thread. The thread is then responsible for unlocking the mutex when the thread is finished with it.

int kthread_cond_signal( int cond_id );

This routine is used to signal (wake up) another thread that is waiting on the condition variable. It should be called after the relevant mutex is locked, and it is the code's responsibility to unlock the mutex afterwards (so that the signaled thread will be able to complete its pthread_cond_wait call).

Implementation note:

In all the above functions, the value 0 should be returned upon success, and a negative value upon failure.

NOTICE THAT PROPER LOCKING AND UNLOCKING OF THE ASSOCIATED MUTEX VARIABLE IS ESSENTIAL WHEN USING THE

ABOVE ROUTINES.

PART 3: SYNCHRONIZATION PROBLEM

In this part you will implement a simulation for the Dinning Philosophers problem combined with the Producer Consumer problem. This simulation will test your ULT/KLT implementation from previous tasks.

Your application needs to get its parameters from a configuration file (See below). All printing should go to a file named 'ass2_log.txt'.

Problem description

We model a restaurant. There is a table with k seats around it. A group of n hungry students arrive at the restaurant (represented as triangles in figure 2) and k of them take seats around the table. If n>k the remaining students will have to wait until any seats will become available. Each Student has its unique id (stud_id), starting from 0 in consecutive manner. Our menu consists of three kinds of food: Salad, Pasta and Steak, assigned values 0, 1 and 2 respectively.

Each student must acquire all three kinds of food, but in a different order as follows: First, each student acquires food type with (stud_id % 3) value assigned to it. Second, the student acquires food type with ( (stud_id+1) % 3 ) value assigned to it. At this point, the student must execute a long eating process (explained later) on the 2 dishes acquired.

When the long eating process is done, the third and last dish ( (stud_id + 2) % 3) is to be acquired, which leads to a short eating process (explained later). When the student has finished eating the last dish he leaves the table (the thread representing the student exits).

The number of seats around the table, as well as the initial number of students will be given in a configuration file (see next part) and additional students might join the dining table during the simulation. A waiting student may join the table only when a seat

Page 9: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

9

become available, i.e. another student has finished dinning and left the table. The simulation ends after all students have eaten all three dishes.

Tip: The operation of adding new students to the table can be imagined as a host that welcomes new students and leads them to their seat. The host must verify first that there are any available seats. Otherwise, he should keep the students waiting. When a seat has become available, the host notifies a waiting student and puts him in his seat.

Each food type has its own buffer that is maintained and refilled by a designated waiter (represented as diamonds in figure 2). This (eager) waiter refills his buffer whenever it is not full. Only one item of food type can be added in a single operation of the waiter.

The buffers must be synchronized, which means only one student can use a specific buffer at a given time. Also, a waiter can refill his buffer only in case it is not occupied by another student. Buffers sizes will be given in a configuration file as well, and will remain constant during the simulation. All buffers are full when the simulations start.

Problem scheme

Problem modeling

To model the problem, a thread should be created for each dinning student, each waiter and the host. Arriving students should be assigned a thread only after being seated by the host. If k marks the number of seats around the table, then k+4 threads should be created.

Salad (0)

Steak (2)

Pasta (1)

1

2

3

.

.

00

.

nn

Figure 2: a scheme of the hungry students’ problem.

m 2 1

Hungry students arriving after

simulation has started

Eating student

Arrived student

Waiter

Page 10: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

11

Eating process

The following pseudo code should be applied by a thread after it successfully acquired the first two kinds of food (long eating process) and after acquiring the last one (short

eating process). Note the parameter iter_num is different for each type of eating.

For i:1 to iter_num For j:1 to 1000

t = 1 For k:1 to 20 t *= k;

Long eating process: iter_num = 100,000

Short eating process: iter_num = 1000

Simulation Printing

The following printings must be added to your simulation when different events occur: A student acquires any kind of food: "Student<stud_id> acquired

<food_type_num>". A student starts its long/short eating process: "Student<stud_id> started

<long/short> eating process". A waiter fills his corresponding buffer: "Waiter<food_type_num> increased his

buffer to <num_of_items>/<buffer_size>". A new student joins the table: "Student<stud_id> joined the table". A student leaving the table: "Student<stud_id> leaved the table". Student tries to acquire any kind of food from an empty buffer and blocks on it:

"Student<stud_id> waits for <food_type>".

Configuration File

You need to add a configuration file to xv6 and name it 'ass2_conf.txt' (make the necessary changes in the Makefile). Your application must be able to read properties from the configuration file and start a simulation according to it. The format of the configuration file isn’t strict; the following is just a suggestion.

The configuration file should contain the following properties:

Students_Initial = value1 // Number of hungry students when simulation starts.

Students_Joining = value2 // Number of students joining after simulation started.

Num_of_seats = value 3 // number of total seats around the dining table.

Salad_Buffer_Size = value4 // Size of the salad's bounded buffer.

Pasta_Buffer_Size = value5 // Size of the pasta's bounded buffer.

Steak_Buffer_Size = value6 // Size of the steak's bounded buffer.

Page 11: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

11

Task 1: Adding performance metrics

In this task we will add to each created process (kernel thread) the capability of maintaining some statistics of its run. The following technique deals with threads but can be easily applied to processes as well. When we write process we also refer to a kernel thread.

The first step would be to extend the proc structure (or thread structure you defined in early tasks). Add the following fields: ctime, etime and rtime. These will respectively represent the creation time, end time and running time of the process. Upon the creation of a new process, the kernel will update the process’ creation time. The run time may be incremented by updating the current process (kernel thread) whenever a clock tick occurs.

The second step would be to keep track of total time processes wasted waiting and total time they spent running. These two statistics will allow us to calculate average wait/run time per process (kernel thread). In addition, the total elapsed time of the simulation should be recorded as well. These global variables should be updated every time a thread finishes running.

Tip: to maintain these statistics, changes have to be made in some of the functions in proc.c file.

Since all this information is retained by the kernel, it is necessary to make it accessible to the user. Therefore, you will add the following system call:

int retrieve_process_statistics( int* totalElapsedTime, int* totalRunTime, int* totalWaitTime); The three parameters should be set by the function, and respectively stand for the total elapsed time of the calling process/kthread, total time the process/kthread actually ran, and total time the process/kthread spent waiting. Given these statistics the caller is able to calculate the average run/wait time of a process/kthread. These statistics should be maintained per kernel thread; when called by a kernel thread in a multi-threaded application, the function should return the statistics of the calling thread. When called by a process with a single thread- should return the statistics of the process. Task 2: Simulation with ULT

Construct the appropriate environment for running the simulation described at the beginning of part 3. You will be using the user-level-threads library you implemented in part 1, in the way you find most suitable, i.e. it up to you decide when and where threads should yield. Remember that the threads must cooperate in order to complete the simulation. Make sure you use the retrieve_process_statistics system call you implemented in the previous task.

Tip: the priorities mechanism which you implemented in ULT might come in handy in the simulation.

When using ULT we can't monitor the exact statistics of threads. In order to calculate average statistics per thread, we will take total run_time and total_waiting_time of the process and divide it by the number of created threads.

Page 12: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

12

When simulation ends, print the results of the simulation in the following format:

Simulation: User-Level-Threads

Total run time is: <simulation total time>

Average run time per user-level-thread is: <average thread's run time>

Average wait time per user-level thread is: <average thread's wait time>

Task 3: Simulation with KLT

Construct the appropriate environment for running the simulation described at the beginning of part 3. You will be using the kernel-level-threads you implemented in part 2. Make sure you use the retrieve_process_statistics system call you implemented in the previous task. When simulation ends, print the results of the simulation in the following format:

Simulation: Kernel-Level-Threads

Total run time is: <simulation total time>

Average run time per kernel-level-thread is: <average thread's run time>

Average wait time per kernel-level thread is: <average thread's wait time>

Tip: When multiple threads are using shared resources (e.g. buffers) extra care should be taken to synchronization. Use the synchronization primitives you implemented in part 2 for the simulation to run correctly.

Task 4: Simulation with KLT on multiple CPUs

The current configuration in the Makefile defines that xv6 runs on a single CPU. Repeat the simulation from task 3, but this time change xv6's configuration to work with 2 CPUs instead of 1.

Note that changes should be made to the Makefile to support multi-core.

You may have noticed that QEMU uses another piece of software called KVM. KVM (Kernel based virtual machine) is a virtualization infrastructure for the Linux kernel. KVM supports native virtualization on processors with hardware virtualization extensions [Wikipedia]. For the KVM to exploit multiple CPUs on a multi-core computer it’s necessary to set the CPU virtualization flag in the BIOS (most modern CPUs support it). A list of CS labs which have these settings enabled will be published on the course site.

If you work on your own computer you’ll have to apply the mentioned settings in in your computer's BIOS. If you are running Linux on a virtual machine it might be a problem (nested virtualization) and we didn’t test it.

Note that except for the multi-core support (and some other features which we don’t use for now) QEMU and KVM are functioning without the BIOS modifications. You can develop on any computer which has QEMU installed, and then test on the labs computers which support hardware virtualization.

Page 13: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

13

Be ready to explain the different results you got in tasks 2-4. The grader may ask you questions regarding the different experiments you conducted and more about related issues.

Submission guidelines

Assignment due date: 17/05/2012, 22:00

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 an svn patch (i.e. a file which patches the original xv6 and applies all your changes).

Tip: although 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 haven’t completed the assignment.

Finally, you should note that graders are instructed to examine your code on lab computers only (!) - Test your code on lab computers prior to submission.

GOOD LUCK!

Page 14: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

14

APPENDIX

Call stack scheme

A call stack which is maintained by the C compiler, and stores information about the

active subroutines of a program.

Wrap function

Upon creation, the stack of a ULT thread should contain a frame of a wrap function, which should look something like:

void wrap_function(void (*entry)())

{

entry();

uthread_exit();

}

Assembly macros

You might find the following macros useful in order to manage user-level threads.

// Saves the value of esp to var #define STORE_ESP(var) asm("movl %%esp, %0;" : "=r" ( var )) // Loads the contents of var into esp #define LOAD_ESP(var) asm("movl %0, %%esp;" : : "r" ( var )) // Calls the function func #define CALL(addr) asm("call *%0;" : : "r" ( addr )) // Pushes the contents of var to the stack #define PUSH(var) asm("movl %0, %%edi; push %%edi;" : : "r" ( var ))

ret addr

old ebp

local vars

ret addr

function args

old ebp

local vars

function args High addr

Low addr

Stac

k g

row

s d

ow

nw

ard

esp

ebp

inactive frame (calling)

Active frame (callee)

FIGURE 2: A GENERAL SCHEME OF A CALL STACK

Page 15: OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6 ...os122/wiki.files... · OPERATING SYSTEMS, ASSIGNMENT 2 THREADS IN XV6, SYNCHRONIZATION SUBMISSION DATE: 17/05/2012, 22:00 In this

Ben-Gurion University of the Negev, Operating Systems 2012

15

Synchronization primitives usage example

To demonstrate a proper usage of mutexes and condition variables in our implementation, consider the following scenario, where thread A runs first, initializes mutex1 and cond1 and waits on cond1 until signaled. Thread B is started after thread A has entered the waiting, and signals it.

The expected output is:

Waiting for signal to go

Sending go signal

We're allowed to go now

Global variables

int mutex1;

int cond1;

int go = 0;

Thread A’s code

mutex1 = kthread_mutex_alloc();

cond1 = kthread_cond_alloc();

kthread_mutex_lock(mutex1);

printf("Waiting for signal to go\n");

while(!go) {

// Thread is blocked until signaled,

// the mutex is released while waiting

kthread_cond_wait(cond1, mutex1);

}

printf("We're allowed to go now!\n");

kthread_mutex_unlock(mutex1);

Thread B’s code

printf("Sending go signal\n");

kthread_mutex_lock(mutex1);

go = 1;

kthread_cond_signal(cond1);

kthread_mutex_unlock(mutex1);