The functions to implement fall naturally into two groups.
One group is all the synchronization functions for mutexes and condition variables. There are 8 such functions in this group.
The other group consists of 13 functions needed to manage threads: initialize the package, create theads, schedule threads, cleanup threads, yield to another thread, etc.
The function comments describe what is required and you should read those comments carefully. The following notes add some details and/or repeat the requirements for the group of 13 functions. I suggest trying to implement and test these first. After that you can do the same for the second group of 8 synchronization functions.
Must be called by user process first (and only once).
Note: This function calls create_first_thr() which will initialize ut_curthr - global pointer to the current executing thread.
This function is called from uthread_init (and shouldn't be called again)
It should call utqueue_init to initialize each run queue in the runq_table.
Set ut_curthr's entries in uthreads:
Check if this thread is detached (ut_curthr->ut_detached == true ?). If so, the reaper should handle its stack cleanup, etc. So call
make_reapable(ut_curthr)
Otherwise, check if some thread called uthread_join. How?
By checking if the ut_curthr->ut_waiter is !NULL.
In that case just wakup the "waiter":
uthread_wake(ut_curthr->ut_waiter)
How do you check that uid exists?
Set errno to ESRCH and return -1, otherwise.
Only one thread can execute uthread_join for a given thread.
Check that ut_waiter is NULL. Otherwise, some other thread already called uthread_join for this thread.
Set errno to EINVAL and return -1 if there is already a waiter.
Check that this thread is NOT detached.
If detached set errno = EINVAL and return -1.
Check that the calling thread is not trying to joint itself. This would cause a form of deadlock. If joining itself, set errno = EDEADLK and return -1.
Ok. If no errors, the target thread's ut_waiter entry to the address of the caller's entry.
If the target thread is not a zombie (i.e., still active), calling thread needs to wait. How? Call
uthread_block();
When the target thread terminates, its uthread_exit() will unblock.
If the target thread has terminated or when it it finally does, now you clean up the target thread by simply setting its ut_detached entry to 1 (ready to be reaped).
If the return_value int pointer is not NULL, assign the ut_exit value of the target thread to *return_value.
Now arrange for the target to be reaped by calling make_reapable with a pointer to the target thread's entry in uthreads. and return 0 (for success)
Good idea to check that ut_curthr is not NULL.
Change its state to UT_WAIT
Block by calling uthread_switch()
Good idea to check that uthr is not NULL (assert ok)
Check the priority is in valid range (assert ok)
IF the thread state is actually UT_WAIT, change it to UT_RUNNABLE and insert it on the correct run queue.
Check uidp != NULL. Using assert is ok here.
Check that the priority is in the range 0 >= prio <= UTH_MAXPRIO
To find a free entry in uthreads, call uthread_alloc() (Check what it checks to see if an entry is free!)
If there is no free entry set errno to EAGAIN and return -1.
Allocate a stack for this thread: call alloc_stack().
If this returns NULL, set errno = EAGAIN and return -1.
Fill in the stack member of selected entry in the uthreads array with the stack you just allocated
Set up this stack so you can uthread_switch can activate this new thread thread.
To do this you can call
uthread_makecontext(...)
passing the address of the context entry, the stack entry, the default stack size (UTH_STACK_SIZE), the func and 2 arguments passed to uthread_create.
Set the state to UT_RUNNABLE
Set the priority by calling
uthread_setprio(id, prio)
where id is the new id for this thread.
Assign this id to *uidp
return 0 for success
Find the first unused entry in the uthreads array. An entry is unused if its state is UT_NO_STATE.
Return the ut_id value of that entry.
If no such free entry is found, return -1.
Check id is valid. Set errno = EINVAL and return -1 if not.
Check prio is in the valid range. Set errno = EINVAL, return -1 if not.
The current priority might be -1 (unused entry) or we might be changing the priority of a thread that has been running.
Check if prio is different than the current priority. If not, it is already on the correct run queue.
Otherwise this is either a new thread or we are changing the priority of an existing thread. If the thread state is runnable:
(Use utqueue_remove and utqueue_enqueue functions)
Finally update the ut_prio entry to the the (new) prio and return 0.
Read the comment for this function carefully.
Call uthread_idle() then look for the highest priority runnable thread. If none exists, the repeat by calling uthread_idle() again and search again. Keep doing this until you find a highest priority runnable thread. Remove it from the run queue.
Now do some checks: Do you have a pointer to a runnable thread whose stack is not NULL. (Can use assert)
Save the current thread, ut_curthr, e.g. in old_thrd. Set ut_curthr to the new thread with the highest priority runnable thread you just selected. Change that new thread's state to UT_ON_CPU.
Finally, call uthread_swapcontext to switch from the old thread to the new one.
The state of the old thread will have been set by the function that called uthread_switch().
Read the comment!
Error: Check if the thread uid exists. errno = ESRCH
Error: Check if the thread is already detached. errno = EINVAL
Otherwise, set ut_detached to true.
If this thread is already a ZOMBIE, call make_reapable with a pointer to its entry in uthreads so that the reaper thread will find it and cleanup.
return 0
Change the current thread's state to UT_RUNNABLE (from UT_ON_CPU)
Insert the current thread on the proper run queue.
Call uthread_switch()
Check uth is not NULL (use assert)
Call free_stack(...) to deallocate this thread's stack. (It was allocated with alloc_stack().)
Reset this thread's entry to be unused as in uthread_init; i.e., all members in the uthread_t struct should be reset, not just the ut_state member.
You can run the ta_test program with a test number command line argument. For example:
ta_test 2
The value test number arguments are 0 - 8:
These functions are required for all tests (except 0):
Then in addition (with some duplication) here are the functions that are tested for each test number.
The Makefile includes the gcc option -Werror. This causes the compilation to treat warnings as errors, causing compilation to fail if there are any warnings.
Since warnings will be generated initially. For example, if functions are defined, but not used, a warning is generated.
You can (temporarily) remove the -Werror in order to build your library and test it before all functions have been implemented. Change comment line in the Makefile:
CFLAGS = -g -Wall -Werror
And add a line with -Werror omitted:
# CFLAGS = -g -Wall -Werror CFLAGS = -g -Wall