2023 Homework2 (quiz1) ====================== #### contributed by <[shlin41](https://github.com/shlin41)> --- ## Q1:解釋程式碼運作原理 > 註:解釋上述程式碼運作原理,應該涵蓋測試方法、futex 使用方式、Linux 核心開發者針對 POSIX Threads 強化哪些相關 futex 實作機制等等 ### [atomic.h] * 目的:用 MACRO 簡化 <stdatomic> 中function 的名字 * read_fense: thread_fence(state, acquire) -> 用來避免 LL, LS 互換。保護前面的那個 L。 ```graphviz digraph orderAcquire { node [shape=box] { a [label="L"] c [label="L"] } { a1 [label="L"] c1 [label="S"] } { a->a1 [dir="both" fontsize="20" color = "red" headlabel="×" labeldistance="3.5" labelangle="0" minlen="2"] c->c1 [dir="both" fontsize="20" color = "red" headlabel="×" labeldistance="3.5" labelangle="0" minlen="2"] } } ``` * write_fense: thread_fence(state, release) -> 用來避免 SS, LS 互換。保護後面的那個 S。 ```graphviz digraph orderRelease { node [shape=box] { a [label="S"] c [label="L"] } { a1 [label="S"] c1 [label="S"] } { a->a1 [dir="both" fontsize="20" color = "red" headlabel="×" labeldistance="3.5" labelangle="0" minlen="2"] c->c1 [dir="both" fontsize="20" color = "red" headlabel="×" labeldistance="3.5" labelangle="0" minlen="2"] } } ``` * 心得:[Atomic_thread_fence](https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence) * ==memory_order_acquire: 先 atomic_action 再 read_fence== ``` atomic_exchange_explicit(state, true, acquire); 等於 atomic_exchange_explicit(state, true, relaxed); // 先 atomic action atomic_thread_fence(acquire); // 再 read memory barrier ``` * ==memory_order_release: 先 write_fence 再 atomic_action== ``` atomic_exchange_explicit(state, true, release); 等於 atomic_thread_fence(release); // 先 write memory barrier atomic_exchange_explicit(state, true, relaxed); // 再 atomic action ``` * ==memory_order_acq_rel: 先 write_fence 再 atomic_action 再 read_fence== ``` atomic_exchange_explicit(state, true, acq_rel); 等於 thread_fence(release); // 先 write memory barrier atomic_exchange_explicit(state, true, relaxed); // 再 atomic action thread_fence(acquire); // 再 read memory barrier ### [futex.h] * SystemCall 宣告: (six arguments) ``` long futex(uint32_t *uaddr, int futex_op, uint32_t val, const struct timespec *timeout, /* or: uint32_t val2 */ uint32_t *uaddr2, uint32_t val3); ``` * kernel 對應的 function: (7 arguments) ``` long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout, u32 __user *uaddr2, u32 val2, u32 val3) ``` * 對照看 [man futex(2)](https://man7.org/linux/man-pages/man2/futex.2.html): * FUTEX_WAIT * FUTEX_WAKE * FUTEX_REQUEUE * FUTEX_CMP_REQUEUE * FUTEX_CMP_REQUEUE_PI ### [mutex.h] * 實做 mutex_trylock, mutex_lock, mutex_unlock * int state: 搭配 {MUTEX_LOCKED, MUTEX_SLEEPING} 紀錄狀態 * int state: 因為至少需要兩個 bits,所以直接使用 int * MUTEX_SLEEPING: 設計得很怪 * 應該是為了紀錄有沒有人在 queue 裡面等待 * 如果有 (i.e. MUTEX_SLEEPING is set) 則 futex_wake * 但是 state == b01 的機會很少 * 用到 cond_wait 時,每次都設定 state == b11 * 只有在使用 mutex_trylock / mutex_lock 的第一個人,才有機會 state = b01 * ==不設計/處理 MUTEX_SLEEPING,每次都 futex_wake,其實程式也可正常執行== ``` static inline void mutex_unlock(mutex_t *mutex) { int state = exchange(&mutex->state, 0, release); //if (state & MUTEX_SLEEPING) futex_wake(&mutex->state, 1); } ``` * ==mutex 中的 memory fence 擺放的位置搞不懂== [Release-Acquire_ordering](https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering) > Q1: 為什麼 mutex_lock 要在最後面擺一個 read_fence? 為什麼 mutex_unlock 要在最前面擺一個 write_fence? Q2: read_fence 和 write_fence 夾出了一個區間,和滿足 Visible 有何關係? ### [spinlock.h] * 實做 spin_trylock, spin_lock, spin_unlock * bool state: 因為只需要紀錄 locked / unlocked * 因為 spinlock 屬於 busy wait,因此不需要借助 futex 進入 queue 中睡覺 * 利用 spin_hint():生成對應平台的 pause 機器指令 * 和 mutex 有一樣的 memory_fence 設計 * spin_trylock / spin_lock 最後面擺一個 read_fence * spin_unlock 最前面擺一個 write_fence ### [cond.h] * 實做 cond_wait, cond_signal, cond_broadcast * int seq:很有趣的設計,流水號遞增。 * 為了讓 cond_wait 當時的 seq 和之後 cond_signal 當時的 seq 不同 * 如果用 bool seq:會有如下問題 > 假設: step 1: waiter_1, waiter_2 都讀到 seq == 0 進入 queue step 2: server 呼叫 cond_signal(seq = 1) 可以叫醒一個 waiter step 3: server 呼叫 cond_signal(seq = 0) 無法叫醒另一個 waiter * 那 int seq 就沒問題嗎? > Ans: 當然不是 同時 max_uint 個人 wait,server 一個一個叫,一樣會有最後一個人叫不醒的問題 ### [main.c] * node 連接方式: ```graphviz digraph mygraph{ node1 -> node0; node2 -> node1; nodeN -> node2; } ``` * 每個 node 做的事情:(clock 是共享的) * clock_wait(clock, tick):等到 tick 時間到 (即 clock >= tick) * 如果有 parent,則等 parent ready * 交替地 clock_tick(clock) 和 node_signal(self) * 程式停止條件:clock == 2^16^,由 main 去停止 (set clock = -1) * 其他討論: * 為什麼不會卡在 node_wait? > Ans: 因為 node0 不會 wait,導致 node1 一定可以起床。以此類推。 * 為什麼不會卡在 clock_wait? > Ans: 比較難解釋。原則上 clock 上漲飛快。在地一次由 main 呼叫 clock_tick 後,所有 node 被喚醒且都會去跑一次 clock_tick。 --- ## Q2:修改第 1 次作業的測驗 γ >註:修改第 1 次作業的測驗 γ提供的並行版本快速排序法實作,使其得以搭配上述 futex 程式碼運作 修改完的版本:[Source Code](https://github.com/shlin41/GammaModify) * 把 USE_PTHREAD 的部份刪除,只保留 USE_LINUX 的部份 (避免不小心又用到原本的 pthread) * 把 pthread_mutex_XXX / pthread_cond_XXX 的 functions 都改成 mutex_XXX / cond_XXX * function 的 argus 和 return 以 pthread 的 templete 為主:mutex.h, cond.h 搭配修改。主要是補 return 0。 ==注意:mutex_trylock 原本的邏輯剛好相反,改過來。== ## Q3:實作 priority inheritance mutex >註:研讀〈並行程式設計: 建立相容於 POSIX Thread 的實作〉,在上述程式碼的基礎之上,實作 priority inheritance mutex 並確認與 glibc 實作行為相同,應有對應的 PI 測試程式碼 ## Q4:設計 POSIX Threads 風格的 API > 註:比照 skinny-mutex,設計 POSIX Threads 風格的 API,並利用內附的 perf.c (斟酌修改) 確認執行模式符合預期,過程中也該比較 glibc 的 POSIX Threads 效能表現 ## Reference * [man futex(2)](https://man7.org/linux/man-pages/man2/futex.2.html) * [Atomic_thread_fence](https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence)、[Release-Acquire_ordering](https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering)