--- tags: 2022 linux kernel --- # 2022q1 Homework5 (quiz5) contributed by < [`Risheng1128`](https://github.com/Risheng1128) > > [作業說明](https://hackmd.io/@sysprog/linux2022-homework5) > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz5) ## 測驗 `1` :::info 延伸問題: - [ ] 解釋上述程式碼運作原理,指出可改進之處並實作 > 是否有必要先將數值轉成字串?用十進位的角度處理運算是否產生額外的計算負擔? - [ ] Linux 核心原始程式碼 [lib/math/prime_numbers.c](https://github.com/torvalds/linux/blob/master/lib/math/prime_numbers.c) 有類似的程式碼,請解釋其作用、裡頭 [RCU](https://hackmd.io/@sysprog/linux-rcu) 的使用情境,及針對執行時間的改進 ::: ## 測驗 `2` :::info 延伸問題: - [ ] 解釋上述程式碼運作原理,指出可改進之處並實作 - [ ] 上述程式碼開啟多個 reader 時,無法通過 [ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html),請指出具體問題並克服 - [ ] 比較 [RCU](https://hackmd.io/@sysprog/linux-rcu) 和 hazard pointer (HP),探討為何 Linux 不採用 HP 而是另外發展 RCU ::: ### 初始化 首先從實作中兩個重要的結構 `domain_t` 及 `hp_t` 開始討論 ```c typedef struct { hp_t *pointers; // hazard pointers hp_t *retired; // retire list void (*deallocator)(void *); } domain_t; typedef struct __hp { uintptr_t ptr; struct __hp *next; } hp_t; ``` 從結構定義可以很明顯的看出兩種結構分別的用意 - `domain_t` 用來建立管理 hazard pointer 和 retire list 的結構,其中 `deallocator` 是用來釋放記憶體的函式指標 - `hp_t` 為一般的 linked list 實作,用來儲存共享資料,並加進 hazard pointer 或 retire list ,`ptr` 則是正在讀取的資料的地址 以上述的結構定義可以得到和題目一樣的架構 ![](https://i.imgur.com/545Imjp.png) 接著討論結構的建立以及釋放,實作為函式 `init` 及 `deinit` ```c // 共享資料 typedef struct { unsigned int v1, v2, v3; } config_t; static config_t *shared_config; static domain_t *config_dom; void init() { // 建立共享資料,且初始 (v1,v2,v3) = (0,0,0) shared_config = create_config(); // Create a new domain on the heap config_dom = domain_new((void *)delete_config); if (!config_dom) err(EXIT_FAILURE, "domain_new"); } void deinit() { delete_config(shared_config); domain_free(config_dom); } ``` 函式 `init` 的部份,一共做了兩件事 - 建立資料 (型態為 `config_t`) ,並且由指標 `shared_config` 指著,以下為函式 `create_config` 的實作 ```c config_t *create_config() { config_t *out = calloc(1, sizeof(config_t)); if (!out) err(EXIT_FAILURE, "calloc"); return out; } ``` - 建立 hazard pointer 的整個架構,並且由指標 `config_dom` 指著,以下為函式 `domain_new` 的實作 ```c domain_t *domain_new(void (*deallocator)(void *)) { domain_t *dom = calloc(1, sizeof(domain_t)); if (!dom) return NULL; dom->deallocator = deallocator; return dom; } ``` 函式 `deinit` 則是將 `shared_config` 和 `config_dom` 分別釋放,以下為函式實作 ```c void delete_config(config_t *conf) { assert(conf); free(conf); } void domain_free(domain_t *dom) { if (!dom) return; if (dom->pointers) list_free(&dom->pointers); // 釋放 hazard pointer if (dom->retired) list_free(&dom->retired); // 釋放 retired list free(dom); } ``` ### Reader Thread 程式主要執行的部份可以分為 `reader thread` 及 `writer thread` ,首先討論 `reader thread` 的部份 ```c= static void *reader_thread(void *arg) { (void) arg; for (int i = 0; i < N_ITERS; ++i) { config_t *safe_config = (config_t *) load(config_dom, (uintptr_t *) &shared_config); if (!safe_config) err(EXIT_FAILURE, "load"); print_config("read config ", safe_config); drop(config_dom, (uintptr_t) safe_config); } return NULL; } ``` 函式 `reader_thread` 最主要的步驟就是先將讀取資料(這邊是 `shared_config`) 加進 hazard pointer (`line 7`) ,成功之後進行資料讀取的動作 (`line 11`) ,最後將該節點從 hazard pointer 移除 (`line 12`) 接著分析資料加進 hazard pointer 的實作部份,即函式 `load` ```c= /* * Load a safe pointer to a shared object. This pointer must be passed to * `drop` once it is no longer needed. Returns 0 (NULL) on error. */ uintptr_t load(domain_t *dom, const uintptr_t *prot_ptr) { const uintptr_t nullptr = 0; while (1) { uintptr_t val = atomic_load(prot_ptr); // 判斷 val 所擁有的資料是否已經在 hazard pointer 裡 hp_t *node = list_insert_or_append(&dom->pointers, val); if (!node) return 0; /* Hazard pointer inserted successfully */ if (atomic_load(prot_ptr) == val) return val; /* * This pointer is being retired by another thread - remove this hazard * pointer and try again. We first try to remove the hazard pointer we * just used. If someone else used it to drop the same pointer, we walk * the list. */ uintptr_t tmp = val; if (!atomic_cas(&node->ptr, &tmp, &nullptr)) list_remove(&dom->pointers, val); } } ``` 首先判斷 `val` 是否已經存在於 hazard pointer 裡 (`line 12`) ,接著判斷 hazard pointer 是否加入成功 (`line 17`) ,如果是就回傳資料的地址 `val` ,如果否就繼續判斷 `val` 是否在 retired list 裡,如果是就從 retired list 移除 (`line 28`) ```c /* Attempt to find an empty node to store value, otherwise append a new node. * Returns the node containing the newly added value. */ hp_t *list_insert_or_append(hp_t **head, uintptr_t ptr) { hp_t *node; bool need_alloc = true; LIST_ITER (head, node) { uintptr_t expected = atomic_load(&node->ptr); /* atomic_cas: 判斷 node->ptr 和 expected 是否相等 * 若相等則回傳 1,反之則回傳 0 * 如果相同表示該資料已經存在於 hazard pointer 裡,因此不需要創新的空間 */ if (expected == 0 && atomic_cas(&node->ptr, &expected, &ptr)) { need_alloc = false; break; } } if (need_alloc) node = list_append(head, ptr); return node; } /* Allocate a new node with specified value and append to list */ static hp_t *list_append(hp_t **head, uintptr_t ptr) { hp_t *new = calloc(1, sizeof(hp_t)); if (!new) return NULL; new->ptr = ptr; hp_t *old = atomic_load(head); // 將節點加在 linked list 的頭 do { new->next = old; } while (!atomic_cas(head, &old, &new)); return new; } ``` 函式 `list_insert_or_append` - 變數 `need_alloc` 判斷是否需要建立新的節點 - 對 hazard pointer 的 linked list 進行走訪 - 如果資料已經存在,則不需要新增節點,直接回傳已經存在的節點即可 - 如果不存在則使用函式 `list_append` 新增節點到 hazard pointer 的 linked list 上- 函式 `list_append` - 將新的節點加在 linked list 頭的位置 最後討論函式 `drop` 的部份 ```c /* * Drop a safe pointer to a shared object. This pointer (`safe_val`) must have * come from `load` */ void drop(domain_t *dom, uintptr_t safe_val) { if (!list_remove(&dom->pointers, safe_val)) __builtin_unreachable(); } ``` 函式 `drop` 主要的功能是將 hazard pointer 裡儲存資料 `safe_val` 的節點移除 ```c /* Remove a node from the list with the specified value */ 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 如果成立,nullptr 複製到 node->ptr,即移除節點並回傳 true if (expected == ptr && atomic_cas(&node->ptr, &expected, &nullptr)) return true; } return false; } ``` 從函式 `list_remove` 可以得知移除節點的方式是利用巨集函式 `atomic_cas` ,當 `node->ptr` 等於 `expected` 時, `nullptr` 會複製給 `node->ptr` 最後總結 reader thread 的執行步驟 :::success 1. 取得目標讀取資料的 safe pointer 2. 印出讀取資料 3. 將資料從 hazard pointer 中移除 ::: ### Writer Thread 以下分析 writer thread 執行的函式 ```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(); print_config("updating config", new_config); swap(config_dom, (uintptr_t *) &shared_config, (uintptr_t) new_config, 0); print_config("updated config ", new_config); } return NULL; } ``` 函式 `writer_thread` 一共執行以下步驟 - 建立新資料的結構 - 將新的資料和舊的資料交換 - 釋放舊的資料 ```c /* Swaps the contents of a shared pointer with a new pointer. The old value will * be deallocated by calling the `deallocator` function for the domain, provided * when `domain_new` was called. If `flags` is 0, this function will wait * until no more references to the old object are held in order to deallocate * it. If flags is `DEFER_DEALLOC`, the old object will only be deallocated * if there are already no references to it; otherwise the cleanup will be done * the next time `cleanup` is called. */ void swap(domain_t *dom, uintptr_t *prot_ptr, uintptr_t new_val, int flags) { // new_val 寫進 prot_ptr,並回傳舊的 prot_ptr 給 old_obj const uintptr_t old_obj = atomic_exchange(prot_ptr, new_val); // 把舊的資料釋放 cleanup_ptr(dom, old_obj, flags); } ``` 函式 `swap` 將新的資料地址 `new_val` 複製到指標 `prot_ptr` 所指到的地址,並且釋放原本的資料 `old_obj` ```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); } } ``` 函式 `cleanup_ptr` 會根據不同的情況釋放空間 - 直接釋放 - 放進 retired list - 等待到 reader thread 讀取完畢再釋放 ```c /* Returns 1 if the list currently contains an node with the specified value */ bool list_contains(hp_t **head, uintptr_t ptr) { hp_t *node; LIST_ITER (head, node) { if (atomic_load(&node->ptr) == ptr) return true; } return false; } ``` 函式 `list_contains` 用來判斷 linked list 裡是否包含儲存資料 `ptr` 的節點 ### 可改進之處