A multithreading library for Linux
Video demonstration available at: https://claud.pro/multithreading/
- The overall architecture of the User-level thread library consist of three main modules (
Queue API
,uthread API
, andPreemption
) which are technically separatable from each other but also interoperable.Queue API
is a basal data structure.uthread API
is a library for users to start multi-threaded function in a process.Preemption
is an activatable feacture for the library for extra control. - The data structure choice:
Queue
which is FIFO. It is good for round-robin thread scheduling logic. - Platform: For
Linux
in user-level. - Relationship among modules:
uthread API
is build on top ofQueue API
whilePreemption
is a necessary added feature on top ofuthread API
.
The Queue is implement by Doubly Linked List
with FIFO
rule. I choose this structure because it provides an efficient way to add or remove nodes. Doubly Linked List
also allow us delete a node by simple linking the node before it and the node after it. FIFO
is excatly what I need for thread scheduling.
- There are 12 unit tests.
test_create
andtest_queue_simple
are pre-giventest_delete_1
enqueues two match items and an unmatch item in the queue to test ifqueue_dequeue
deletes the first match item near the head. In similar fashion,test_delete_2
tests deletion of the head item andtest_delete_3
tests deletion of the tail item.test_iterate
enqueues 4 integers and applys ainc_item
fucntion that increases int item by 1 or delete item if it has a value of 3 to each of the item. Assert head value and queue length to test iftest_iterate
has the right behaviour.test_en_dequeue_1
,test_en_dequeue_2
,test_en_dequeue_3
,test_en_dequeue_4
: more enqueue/dequeue actions to cove edge cases.test_error_1
tests error handling when trying toqueue_dequeue
an empty queue.test_error_2
tests error handling when trying toqueue_delete
an empty queue.
I designed a sturcture TCB
to store info for a thread, including context
,TID
,state
, a stack for storing context and return value.
I have following global varibles
- ready_queue: stores active threads
- zombie_queue: stores zombie threads
- thread_count: count how many thread I have now and provide unique TID
- current_thread: a pointer to TCB which is currently running
- uthread_start
In this function, I initialize global variables for the API, includingready_queue
,zombie_queue
andthread_count
. Then I create a main thread(TID=0) and set it as current_thread. Ifpreempt
is 1, I will also callpreempt_start
to start using preempt. - uthread_stop
This is the final function I should call to stop running uthread API. I check if there is anything left inready_queue
. If so, Iuthread_join
these threads and let them finish. Then I free everything I allocated, including global variables and anything left in the queues to prevent memory leak. - uthread_create
This function create a new thread. I allocate a TCB and a stack for it and put it at the end of ourready_queue
. Then I calluthread_ctx_init
to initialize it. I want the whole process can be done safely, so I temporarily disable preempt at the beginning and enable it after the new thread was put in queue. - uthread_exit
This function deal with a finished thread. I stores return value in its TCB and put this finished thread into thezombie_queue
. Then I calluthread_ctx_switch
to run next avaliable thread. Also, I disable preempt here. - uthread_yield
This function allows a thread yield and let next thread run. I put the current_thread to the end ofready_queue
and dequeue a new thread from it. Then I calluthread_ctx_switch
to run the new thread. Here, I also disable preempt to protect the whole process. - uthread_join
This function needs the parent thread to wait its child. So I use a infinite loop to make the parent "wait". In the loop, I check if the child thread is finished. I callqueue_iterate
to check ifzombie_queue
has the child thread. If not, parent thread will keep yield until the child finish. I check if the child thread is in theready_queue
in the beginning to prevent join something doesn't exist and waiting forever.
I basically implement 2 types of testing.
- Let a thread create a lot of child threads
- Let the child thread keep create child threads
Then I mix the 2 type to implement stressful test on our API.
Also, I use valgrind to check memory leak.
sig_handler
signal handler to ask a thread to yield by callinguthread_yield
preempt_start
first mountssig_handler
toSIGVTALRM
usingsigaction
then sets up a virtual alarm that sendsSIGVTALRM
by specifying certaintv_usec
usingsetitimer
. This virtual alarm counts how long has the thread been using the CPU.preempt_stop
first stops the timer usingsetitimer
then restoreSIG_DFL
forSIGVTALRM
. This way it prevents accidental termination of thread.preempt_enable
first creates a signal setunblock_alarm
and adds it to the signal mask usingsigaddset
pairing withSIGVTALRM
, make the change forSIG_UNBLOCK
signals withsigprocmask
- In similar fashion,
preempt_disable
first creates a signal setblock_alarm
and adds it to the signal mask usingsigaddset
pairing withSIGVTALRM
, make the change forSIG_BLOCK
signals withsigprocmask
- The idea is simple, it schedules two thread
thread0
andthread1
with Preemption on.thread0
is going to take control of the CPU with a while-loop forever if Preemption doesn't work.thread1
prints a message if it got the CPU.