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 APIis a basal data structure.uthread APIis a library for users to start multi-threaded function in a process.Preemptionis an activatable feacture for the library for extra control. - The data structure choice:
Queuewhich is FIFO. It is good for round-robin thread scheduling logic. - Platform: For
Linuxin user-level. - Relationship among modules:
uthread APIis build on top ofQueue APIwhilePreemptionis 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_createandtest_queue_simpleare pre-giventest_delete_1enqueues two match items and an unmatch item in the queue to test ifqueue_dequeuedeletes the first match item near the head. In similar fashion,test_delete_2tests deletion of the head item andtest_delete_3tests deletion of the tail item.test_iterateenqueues 4 integers and applys ainc_itemfucntion 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_iteratehas 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_1tests error handling when trying toqueue_dequeuean empty queue.test_error_2tests error handling when trying toqueue_deletean 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_queueandthread_count. Then I create a main thread(TID=0) and set it as current_thread. Ifpreemptis 1, I will also callpreempt_startto 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_jointhese 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_initto 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_switchto 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_queueand dequeue a new thread from it. Then I calluthread_ctx_switchto 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_iterateto check ifzombie_queuehas the child thread. If not, parent thread will keep yield until the child finish. I check if the child thread is in theready_queuein 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_handlersignal handler to ask a thread to yield by callinguthread_yieldpreempt_startfirst mountssig_handlertoSIGVTALRMusingsigactionthen sets up a virtual alarm that sendsSIGVTALRMby specifying certaintv_usecusingsetitimer. This virtual alarm counts how long has the thread been using the CPU.preempt_stopfirst stops the timer usingsetitimerthen restoreSIG_DFLforSIGVTALRM. This way it prevents accidental termination of thread.preempt_enablefirst creates a signal setunblock_alarmand adds it to the signal mask usingsigaddsetpairing withSIGVTALRM, make the change forSIG_UNBLOCKsignals withsigprocmask- In similar fashion,
preempt_disablefirst creates a signal setblock_alarmand adds it to the signal mask usingsigaddsetpairing withSIGVTALRM, make the change forSIG_BLOCKsignals withsigprocmask
- The idea is simple, it schedules two thread
thread0andthread1with Preemption on.thread0is going to take control of the CPU with a while-loop forever if Preemption doesn't work.thread1prints a message if it got the CPU.
