# 2022q1 Homework5 (quiz5) contributed by < `Korin777` > ## 測驗二 ### 程式研讀 #### 程式運作 * reader 會透過 `load 函式` 來讀 `shared_config` 並透過 `list_insert_or_append 函式` 將正在使用的 config 插入 `config_dom->pointer`(即為 hazard pointer) , reader 印出 config 後在透過 `drop 函式` 將 config 從 hazard pointer 移除 * writer 透過 `swap 函式` 更新 `shared_config` , 並將舊的 config 透過 `cleanup_ptr 函式` 釋放 * 只有一個 writer 時 , updating config 到 updated config 中間 , read config 會印出兩種不同的 config , 一個是舊的一個新的 config , 等到 updated config 後 read config 所印出的 config 才會一致 ##### load ```c uintptr_t load(domain_t *dom, const uintptr_t *prot_ptr) { const uintptr_t nullptr = 0; while (1) { // reader 讀 shared_config 並將它插入 config_dom->pointers(hazard pointer) uintptr_t val = atomic_load(prot_ptr); hp_t *node = list_insert_or_append(&dom->pointers, val); if (!node) return 0; /* 如果 prot_ptr != val 表示 writer 已寫入新的 config , * 如果 reader 讀到舊的 val 且尚未插入 hazard pointer , * writer 就去寫入新的 value 並釋放舊的 val 就會發生錯誤 , * 因此要避免上述狀況 */ if (atomic_load(prot_ptr) == val) return val; // 重讀 shared_config 前先把剛插入的 hazard pointer 移除 uintptr_t tmp = val; if (!atomic_cas(&node->ptr, &tmp, &nullptr)) list_remove(&dom->pointers, val); } } ``` ##### list_remove ```c bool list_remove(hp_t **head, uintptr_t ptr) { hp_t *node; const uintptr_t nullptr = 0; LIST_ITER (head, node) { uintptr_t expected = atomic_load(&node->ptr); if (expected == ptr && atomic_cas(&node->ptr, &expected, &nullptr)) return true; } return false; } ``` * `list_remove` 不會將 node 從 link list 移除 , 而是將該 node 變為空的 node 讓其他人可以使用 ##### drop ```c void drop(domain_t *dom, uintptr_t safe_val) { if (!list_remove(&dom->pointers, safe_val)) __builtin_unreachable(); } ``` * `__builtin_unreachable` 能讓 compiler 改變 branch 的順序(e.g. if 先往外面執行) 及消除 dead code (e.g. if 內的 `ret`) , 這裡的 `list_remove 函式` 也不該 return false (因為 hazard pointer 的移出不該失敗) , 若走到 `__builtin_unreachable()` 也能讓我們得知在 `drop 函式` 發生錯誤了 ##### swap ```c void swap(domain_t *dom, uintptr_t *prot_ptr, uintptr_t new_val, int flags) { // 更新新的 shared_config const uintptr_t old_obj = atomic_exchange(prot_ptr, new_val); cleanup_ptr(dom, old_obj, flags); } ``` ##### cleanup_ptr ```c static void cleanup_ptr(domain_t *dom, uintptr_t ptr, int flags) { if (!list_contains(&dom->pointers, ptr)) { /* deallocate straight away */ dom->deallocator((void *) ptr); } else if (flags & DEFER_DEALLOC) { /* Defer deallocation for later */ list_insert_or_append(&dom->retired, ptr); } else { /* Spin until all readers are done, then deallocate */ while (list_contains(&dom->pointers, ptr)) ; dom->deallocator((void *) ptr); } } ``` * 若 `flag` 為 0 則 writer 一直等到 hazard pointer 沒此 config 在釋放它 * 若 `flag` 為 1 則 writer 將此 config 插入 `config_dom->retired`(即 retired list , 之後在透過 `cleanup 函式`來釋放) , 因此 writer 不須等待 reader 繼續更新 config ### 改進 #### 發現 雖然程式有實做 retire list , 但 `writer_thread` 傳入 `cleanup_ptr 函式` 的 flags 為 0 , 因此不會將要移除的 config 插入 retire list 而會用 busy waiting 的方式來釋放舊的 config #### 實做 ```c static void *writer_thread(void *arg) { (void) arg; hp_t *local_retired = NULL, *tmp; for (int i = 0; i < N_ITERS / 2; ++i) { cleanup(config_dom,1, (uintptr_t) &local_retired); config_t *new_config = create_config(); new_config->v1 = rand(); new_config->v2 = rand(); new_config->v3 = rand(); // 將 new_config 插入 hazard pointer list_insert_or_append(&config_dom->pointers, (uintptr_t) new_config); print_config("updating config", new_config); swap(config_dom, (uintptr_t *) &shared_config, (uintptr_t) new_config, 1, (uintptr_t) &local_retired); print_config("updated config ", new_config); // 將 new_config 移出 hazard pointer drop(config_dom, (uintptr_t) new_config); } while(local_retired) cleanup(config_dom,1, (uintptr_t) &local_retired); return NULL; } ``` * 原本的 retired list 是 global 的 , 所有 writer 都能存取到 , 但這裡我將 retired list 改為 local 的 。 每個 writer 需要將自己替換掉的 `shared_config` 加入自己的 `local_retired` 並負責釋放此 config * writer 結束前必須確保自己負責的 config 都已經釋放了 * retired list 改為 local 的好處有讓對 retired list 操作不再需要用 atomic operation ; 而每個 config 被唯一一個 writer 保存 , 因此 traverse retired list 的路徑也可以減少 ```c void swap(domain_t *dom, uintptr_t *prot_ptr, uintptr_t new_val, int flags, uintptr_t local_retire) { // 更新新的 shared_config const uintptr_t old_obj = atomic_exchange(prot_ptr, new_val); cleanup_ptr(dom, old_obj, flags, local_retire); } static void cleanup_ptr(domain_t *dom, uintptr_t ptr, int flags, uintptr_t local_retire) { if (!list_contains(&dom->pointers, ptr)) { /* deallocate straight away */ dom->deallocator((void *) ptr); } else if (flags & DEFER_DEALLOC) { /* Defer deallocation for later */ independent_list_append(local_retire, ptr); } else { /* Spin until all readers are done, then deallocate */ while (list_contains(&dom->pointers, ptr)) ; dom->deallocator((void *) ptr); } } void cleanup(domain_t *dom, int flags, uintptr_t local_retired) { hp_t *node; for(node = *(hp_t **)local_retired; node;) { uintptr_t ptr = node->ptr; hp_t *retire_node, *tmp; tmp = node; node = node->next; if (!ptr) continue; if (!list_contains(&dom->pointers, ptr)) if(independent_list_remove(local_retired, ptr)) dom->deallocator((void *) ptr); } } ``` * 傳入 writer 的 local retired list 及更換操作 retired list 的函式 `independent_list_remove`、`independent_list_append` ```c #define INDEPENDNET_LIST_ITER(head, node) \ for (node = *head; node; node = node->next) static hp_t *independent_list_append(hp_t **head, uintptr_t ptr) { hp_t *new = calloc(1, sizeof(hp_t)); if (!new) return NULL; new->ptr = ptr; new->next = *head; *head = new; return new; } bool independent_list_remove(hp_t **head, uintptr_t ptr) { hp_t *node, *prev = NULL; const uintptr_t nullptr = 0; INDEPENDNET_LIST_ITER (head, node) { if (node->ptr == ptr) { if(prev) prev->next = node->next; else *head = node->next; free(node); return true; } prev = node; } return false; } ``` * 因為 retired list 改為 local 的 , 在插入及移除可以不用 atomic operation #### 比較 retired list 與 busy waiting N_ITERS 固定為 10000 ##### writer 固定為 4 個 , 增加 reader 數量 ![](https://i.imgur.com/jHIWxta.png) * 使用 busy wating 和 retired list 的執行時間差異不大 ##### reader 固定為 4 個 , 增加 writer 數量 ![](https://i.imgur.com/nkhvAqN.png) * 當 writer 為 8 個時 busy waiting 的時間大幅增加 (註:有時當 writer 為 8 個時 busy waiting 的時間還是可以跟 retired list 差不多) * 執行程式並透過 perf 來觀察程式熱點 * 使用 retired list 時 , 在各函式所佔的百分比大致相差不多 , 佔比較多的有 `list_insert_or_append`、`list_remove`、`syscall_exit_to_user_mode`、`__random` , 其中 `random` 函式似乎會使用到 futex * 使用 busy waiting 時 , 可以發現在 `list_contains` 佔了 90% 以上 , 而 `cleanup_ptr` 則佔了 5% 左右 。 會卡在這裡的原因就是要等 reader 及其他 writer drop 掉插入 hardzrad pointer 的 config , 因此 writer 一直透過 `list_contains` 來查看 hardzrad pointer * retired list 和 busy wating 還有一個不同的地方是 , retired list 的 print config 和 updating/updated config 的交錯會比較頻繁 , 因為 writer 不用等 reader ### 開啟多個 writer 無法通過 ThreadSanitizer ```c static void *writer_thread(void *arg) { (void) arg; for (int i = 0; i < N_ITERS / 2; ++i) { config_t *new_config = create_config(); new_config->v1 = rand(); new_config->v2 = rand(); new_config->v3 = rand(); // 將 new_config 插入 hazard pointer list_insert_or_append(&config_dom->pointers, (uintptr_t) new_config); print_config("updating config", new_config); swap(config_dom, (uintptr_t *) &shared_config, (uintptr_t) new_config, 0); print_config("updated config ", new_config); // 將 new_config 移出 hazard pointer drop(config_dom, (uintptr_t) new_config); } return NULL; } ``` #### 問題 * 原本只有一個 writer , `new_config` 並不會被動到 , 而當開啟多個 writer `new_config` 就有可能受到其他 writer 的操作 * e.g. 1. writer1 在 `print_config("updated config ", new_config)` 前停下換 writer2 執行 2. writer2 經過 `swap 函式` 換掉 `shared_config` 並取得 `old_obj`(即 writer1 的 `new_config`) 在透過 `cleanup_ptr 函式` 查看是否能釋放它 , 這時要是剛好沒有 reader 在使用它 , writer2 就能將它釋放 , writer2 停下換 writer1 執行 3. writer1 此時要印出 `new_config` 就會發生問題 #### 解決方法 * 在進入 `swap 函式` 前 , 先將 `new_config` 插入 hazard pointer 讓其他 writer 必須等待而無法將它釋放 , 等 writer 不用使用到 `new_config` 後在透過 `drop 函式` 將它從 hazard pointer 移除 , 這時其他 writer 便能將它釋放 ### 比較 RCU 和 hazard pointer (HP) #### RCU * reader * 透過 `rcu_read_lock`、`rcu_read_unlock` 所建立的 critical section 來保護資料 * `rcu_read_lock` : 通知 reclaimer 自己進入 crtical section * rcu_read_unlock : 通知 reclaimer 自己離開 crtical section * updater * removal * 透過 spinlock 確保同時只有一個 writer 在更新資料 * writer 先複製一份舊的資料並更新它 , 接著用此替換掉舊的資料 * reclamation * 透過 `synchronize_rcu`、`call_rcu` 來釋放資料 * `synchronize_rcu` : block updater 直到 pre-existing RCU read-side critical sections 都完成 * `call_rcu` : updater 不會被 block , pre-existing RCU read-side 都完成才會執行註冊的 callback function #### RCU 和 HP 的差異 1. 在 reader 方面 RCU 不會用到 lock 及 atomic operation ; HP 則需要在使用到 shared data 及 hazard pointer 時使用 atomic operation , 若拿到的 shared data 為舊的就要重新讀取。 2. 在 writer 方面 , RCU 透過 spinlock 來確保同時只有一個 writer 在更新 shared data ; HP 則是透過 atomic operation 3. RCU 透過 crical section 來保護 reader 正在使用的 shared data ; HP 則透過 hazard pointer 來保護 [題目連結](https://hackmd.io/@sysprog/linux2022-quiz5) :::warning 對照研讀 [hp_list](https://github.com/sysprog21/concurrent-programs/tree/master/hp_list) 程式碼,這是測驗題後續改進的實作 :notes: jserv ::: ### 研讀 [hp_list](https://github.com/sysprog21/concurrent-programs/tree/master/hp_list) #### 程式運作 * thread 間共享一個 key 值遞增且唯一的 link list * 每個 thread 有自己的 hazard pointer 及 retired list * insert thread 負責新增節點 ; delete thread 負責移除節點(logically delete) 並嘗試將它 physically delete 。 兩者皆會透過 `__list_find 函式` 嘗試將經過的 logically delete 節點 physically delete * logically delete: 將欲移除 node 的 next 所指向的地址的最後一個 bit 改為 1 (因記憶體對齊的緣故 , 原本記憶體位置的最後一個 bit 為 0) * physically delete: 將 node 從 link list 上移除(尚未釋放) 並加到 retired list * [並行程式設計: Lock-Free Programming](https://hackmd.io/@sysprog/concurrency/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fconcurrency-lockfree#Concurrent-Linked-List) 提到 concurrent linked list 會發生的問題 ```c // 先 insert 且做到一半: node_10_next = atomic_load(&(node_10->next)) node_20->next = node_10_next // 換 delete: head_next = atomic_load(&(H->next)) CAS(&(H->next), head_next, head->next->next) // 回到 insert: CAS(&(node_10->next), node_10_next, node_20) // error ``` ![](https://i.imgur.com/N2V17Uw.png) 原本的 delete 過程中並不會更動到 node_10->next , 所以 insert 如期執行而產生錯誤的結果 ; 而 logically delete 就是透過改變 node_10->next 來避免上述問題 #### __list_find ```c static bool __list_find(list_t *list, list_key_t *key, atomic_uintptr_t **par_prev, list_node_t **par_curr, list_node_t **par_next) { atomic_uintptr_t *prev = NULL; list_node_t *curr = NULL, *next = NULL; try_again: prev = &list->head; curr = (list_node_t *) ATOMIC_LOAD(prev); (void) list_hp_protect_ptr(list->hp, HP_CURR, (uintptr_t) curr); // 確保沒有其他人新增 node 在curr 前或插入 hp 前 curr 被其他人釋放 if (ATOMIC_LOAD(prev) != get_unmarked(curr)) { TRACE(retry); goto try_again; } while (true) { if (is_marked(curr)) TRACE(contains); next = (list_node_t *) ATOMIC_LOAD(&get_unmarked_node(curr)->next); (void) list_hp_protect_ptr(list->hp, HP_NEXT, get_unmarked(next)); // 確保沒有其他人新增 node 在 next 前或插入 hp 前 next 被其他人釋放 if (ATOMIC_LOAD(&get_unmarked_node(curr)->next) != (uintptr_t) next) { TRACE(retry); goto try_again; } // 確保沒有其他人新增 node 在 curr 正前面 if (ATOMIC_LOAD(prev) != get_unmarked(curr)) { TRACE(retry); goto try_again; } // curr 沒被 logically delete if (get_unmarked_node(next) == next) { if (!(get_unmarked_node(curr)->key < *key)) { *par_curr = curr; *par_prev = prev; *par_next = next; return (get_unmarked_node(curr)->key == *key); } prev = &get_unmarked_node(curr)->next; (void) list_hp_protect_release(list->hp, HP_PREV, get_unmarked(curr)); } else { // 查看是否能 physically delete , 將 curr 加到 retired list uintptr_t tmp = get_unmarked(curr); // 確保沒人新增 node 在 curr 正前面及 curr 還沒被 physically delete if (!CAS(prev, &tmp, get_unmarked(next))) { TRACE(retry); goto try_again; } list_hp_retire(list->hp, get_unmarked(curr)); } curr = next; (void) list_hp_protect_release(list->hp, HP_CURR, get_unmarked(next)); TRACE(traversal); } } ``` * par_curr: 持有的 key 大於等於所要尋找的 key 的 node * par_prev: node->next 為 par_curr 的 pointer 地址 * par_next: par_curr->next 的值 * return value: 若 key 存在回傳 true , 反之回傳 false * 會將 par_curr 及其前後一個 node 插入 hp , 這 3 個 node 就是 thread 正在使用的 data , 用完後透過 list_hp_clear 從 hazard pointer 移除 #### list_insert ```c bool list_insert(list_t *list, list_key_t key) { // curr 為新增 node 的 next , *prev 為 list_node_t *curr = NULL, *next = NULL; atomic_uintptr_t *prev = NULL; list_node_t *node = list_node_new(key); while (true) { // key 已經存在了 if (__list_find(list, &key, &prev, &curr, &next)) { list_node_destroy(node); list_hp_clear(list->hp); return false; } ATOMIC_STORE_EXPLICIT(&node->next, (uintptr_t) curr, memory_order_relaxed); uintptr_t tmp = get_unmarked(curr); /* *prev 應該與 get_unmarked(curr) 相等 , 不是的話就 * 表示有其他 thread 新增 node 在 curr 正前方或前一個 * node 已經 physically delete , 則必須重來 */ if (CAS(prev, &tmp, (uintptr_t) node)) { list_hp_clear(list->hp); return true; } TRACE(ins); } } ``` #### list_delete ```c bool list_delete(list_t *list, list_key_t key) { list_node_t *curr, *next; atomic_uintptr_t *prev; while (true) { // key 不存在 if (!__list_find(list, &key, &prev, &curr, &next)) { list_hp_clear(list->hp); return false; } uintptr_t tmp = get_unmarked(next); /* 將 curr logically delete , curr->next 應該與 get_unmarked(next) * 相等 , 不是的話就表示有其他 thread 新增 node 在 curr 正後方或 curr 已經被 logically delete , 則必須重來 */ if (!CAS(&curr->next, &tmp, get_marked(next))) { TRACE(del); continue; } // 若沒人新增 node 在 curr 正前面且 curr 還沒被 physically delete , 將 curr physically delete tmp = get_unmarked(curr); if (CAS(prev, &tmp, get_unmarked(next))) { list_hp_clear(list->hp); list_hp_retire(list->hp, get_unmarked(curr)); } else { list_hp_clear(list->hp); } return true; } } ``` :::warning TODO: 討論上述實作是否能避免 [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem)? :notes: jserv ::: ### 討論 ABA 問題 * 根據 [ABA Problem](https://en.wikipedia.org/wiki/ABA_problem) 所舉的例子 * 假設 link list 一開始為 (head,0) -> (0x4,1) -> (0x8,2) -> (0xc,3) -> (tail,UINTPTR_MAX) * (address,data) thread1 先執行 ```c // list_delete 1 prev = &head->next; curr = 0x4; next = 0x8; tmp = 0x8; ``` thrad1 在執行 `CAS(&curr->next, &tmp, get_marked(next))` 前被 thread2 中斷 ```c // list_delete 1 { prev = &head->next; curr = 0x4; next = 0x8; tmp = 0x8; // logically delete , marked 0x4->next CAS(&curr->next, &tmp, get_marked(next)) // physically delete , head -> 0x8 -> 0xc -> tail CAS(prev, &tmp, get_unmarked(next)) } // list_delete 2 { prev = &head->next; curr = 0x8; next = 0xc; tmp = 0xc; // logically delete , marked 0x8->next CAS(&curr->next, &tmp, get_marked(next)) // physically delete , head -> 0xc -> tail CAS(prev, &tmp, get_unmarked(next)) } // list_insert 1 , 假設 new_node 地址剛好為 0x4 { prev = &head->next; curr = 0xc; ATOMIC_STORE_EXPLICIT // new_node->next = 0xc // head->next = 0x4 , head -> 0x4 -> 0xc -> tail CAS(prev, &tmp, (uintptr_t) new_node) } ``` 回到 thread1 執行 ```c /* prev = &head->next; * curr = 0x4; * next = 0x8; * tmp = 0x8; * head -> 0x4 -> 0xc -> tail */ CAS(&curr->next, &tmp, get_marked(next)) tmp = curr; // 可能會產生 ABA 問題 CAS(prev, &tmp, get_unmarked(next)) ``` * 此時的 curr 為 thread2 插入的 , curr->next 為 0xc , 所以第一個 CAS 不會將 curr logically delete , 在這個狀況下會重新執行 list_delete * 如果 thread2 不只新增了 0x4 還新增了 0x8 , 則 link list 為 `head -> 0x4 -> 0x8 -> 0xc -> tail` , 這時第一個 CAS 就可以將 curr logically delete 並執行第二個 CAS , 而第二個 CAS 也可以將 curr physically delete , 就會產生先執行的 delete 去移除後面 insert 近來的 node 的情況 。 不過實際上 thread1 會將 `head,0x4,0x8` 插入 harzard pointer 來延緩 physically delete , 因為記憶體還沒被釋放 , 所以 thread2 所新增的節點記憶體位置應該不會在 0x4 及 0x8 , 因此上述的情況應該也不會發生。 * 就上述情況 , 此實做能避免 wekipedia 所舉出的 ABA 問題 參考資料 : [What optimizations does __builtin_unreachable facilitate?](https://stackoverflow.com/questions/54764535/what-optimizations-does-builtin-unreachable-facilitate) [What is RCU? -- "Read, Copy, Update"](https://www.kernel.org/doc/Documentation/RCU/whatisRCU.txt)