--- tags: linux kernel, linux2022 --- # 2022q1 Homework4 (quiz4) > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz4) ## 測驗 `1` ### 程式碼運作原理 #### 功能 輸入大於 `0` 的無號整數,計算 $\lceil log_2(x) \rceil$ #### 實做 ```c int ceil_log2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (EXP1) + 1; } ``` 同第三周測驗 `7` 的作法,最後在回傳值 `+1` 處理 `ceil`。 最開頭 `x--` 目的是處理當 `x` 為 2 的冪時,取 `ceil` 不應 `+1`。 其公式如下: $$ \lceil log_2(x) \rceil = log_2(x-1)+1,~ x \neq 1 $$ :::danger :warning: 此算法在 `x = 1` 時會輸出 `1` ,為錯誤答案。 ::: 根據第三周測驗 `7` 的作法,`EXP1` 必須包含尚未完成的 `r | shift` 以及剩下最後 2 bits 的判斷與處理,而最後 2 bits 的判斷可以用 `x >> 1` 來實做,因此 * `EXP1` = `r | shift | x << 1` :::success TODO 2. 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless 3. 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例 (提示: 在 Linux 核心排程器就有) ::: --- ## 測驗 `2` ### 程式碼運作原理 #### ffs (find first set) 找到從低位開始的第一個 `1`,並回傳其索引值。 #### 實做 ```c #define BITS_PER_BYTE 8 #define BITS_PER_LONG (sizeof(unsigned long) * BITS_PER_BYTE) #include <stddef.h> static inline size_t ffs(unsigned long x) { if (x == 0) return 0; size_t o = 1; unsigned long t = ~0UL; size_t shift = BITS_PER_LONG; shift >>= 1; t >>= shift; while (shift) { if ((EXP2) == 0) { x >>= shift; EXP3; } shift >>= 1; t >>= shift; } return o; } ``` 其作法也是利用 binary search 找到 `1` 的位置。 首先,`shift` 為 `BITS_PER_LONG` 除以 2,接著將 `t` 右移 `shift` 位,使 `t` 成為高位半邊全為 `0` 的 `unsigned long`。 然後是 `while` 迴圈的部份,目的是要找到 `1` 位於高位還是低位,所以利用 `x` 與 `t` 作 AND 運算,為 `0` 代表在低位沒有 `1` 出現,將 `x` 右移 `shift` 位,並計算 `o += shift` 紀錄位置。 因此: * `EXP2` = `x & t` * `EXP3` = `o += shift` :::success TODO 1. 迴避 while, for, goto 等關鍵字以改寫出功能等價的實作 2. 在 Linux 核心原始程式碼的 include/asm-generic/bitops 目錄找出相關程式碼,並探討應用案例 ::: --- ## 測驗 `3` ### 程式碼運作原理 #### 功能 從結構體 `struct foo` 中刪除特定的 `struct foo_consumer`。 #### 改寫自 Linux 核心的實做 ```c truct foo_consumer { int (*handler)(struct foo_consumer *self, void *); struct foo_consumer *next; }; struct foo { struct foo_consumer *consumers; unsigned long flags; }; #include <stdbool.h> /* * For foo @foo, delete the consumer @fc. * Return true if the @fc is deleted sfccessfully * or return false. */ static bool consumer_del(struct foo *foo, struct foo_consumer *fc) { struct foo_consumer **con; bool ret = false; for (con = &foo->consumers; *con; EXP4) { if (*con == fc) { *con = EXP5; ret = true; break; } } return ret; } ``` 用一個 pointer to pointer `con` 走訪 `foo->consumers` 的所有 `foo_consumers`,因此每次 `con` 應該都要址向下一個 `foo_consumers` 位置,所以: * `EXP4` = `con = &(*con)->next` 當找到目標 `fc` 後,`con` 要指向 `fc` 下一個 `foo_consumers`,所以: * `EXP5` = `fc->next`。 :::success TODO 1. 探討這樣區分 foo 和 foo_consumer 的好處 2. 在 Linux 核心原始程式碼相似技巧的程式碼 (例如 uprobe),並探討應用案例 ::: --- ## 測驗 `4` ### 程式碼運作原理 #### setjmp 和 longjmp #### co-coutine 實做 ```c int main(void) { void (*registered_task[])(void *) = {task0, task1}; tasks = registered_task; ntasks = ARRAY_SIZE(registered_task); schedule(); return 0; } ``` 首先,宣告一個 `*registered_task[]` 陣列,包含 `task0` 與 `task1`。 `tasks` 是一個指標的指標,指向 `registered_task`。 `ntasks` 是負責紀錄 `registered_task` 中 `task` 的數量。 接著,執行 `schedule()` ```c struct task { jmp_buf env; struct list_head list; }; ``` `task` 的結構包含以個 list 以及一個 `jmp_buf`。 ```c void schedule(void) { static int i; srand(0xCAFEBABE ^ (uintptr_t) &schedule); /* Thanks to ASLR */ setjmp(sched); while (ntasks-- > 0) { int n = rand() % 5; tasks[i++](&n); printf("Never reached\n"); } task_join(&tasklist); } ``` 先 `setjump` 紀錄當下的 `jump_buf` 使後續初始化還可以跳回 `schedule()`。 while loop 的地方負責隨機呼叫 `tasks` 中的 function pointer 並進行初始化,接著`tasklist` 會紀錄每一個 task,然後呼叫 `task_join()` 執行 `tasklist` 中的 task。 ```c /* A task yields control n times */ void task0(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 0: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); EXP8; } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 0: resume\n"); } printf("Task 0: complete\n"); longjmp(sched, 1); } void task1(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 1: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); EXP9; } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 1: resume\n"); } printf("Task 1: complete\n"); longjmp(sched, 1); } ``` `task0` 和 `task1` 的內容結構式是相同的,會先進行 `setjump`,然後把 `jump_buf` 加入 `tasklist` 中,初始化完成後再跳回 `schedule`,所以: * `EXP8` = `longjmp(sched, 1)` 後續藉由 `task_join` 切回來時,由於 `longjmp` 的參數設為 `1`,所以 `setjmp` 的回傳值就會都是 `1`,因此會接續執行後面的 for loop。 在 for loop 中,會先 `setjmp`,並執行 `task_switch` 切換到下一個 task,當再一次跳回來時,會印出 resume。直到執行 `arg` 次,跳出迴圈,會印出 complete。最後再 `longjmp` 到 `schedule`。 ```c static void task_switch(struct list_head *tasklist) { jmp_buf env; if (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); EXP6; memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } static void task_join(struct list_head *tasklist) { jmp_buf env; while (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); EXP7; memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } ``` `task_switch` 與 `task_join` 的內容是一樣的,只是 `task_join` 是在 `schedule` 被呼叫;而 `task_switch` 是在 `task0`、`task1` 被呼叫。 這兩個涵式的目的是取出 `tasklist` 的第一個 entry 的 `jump_buf`,並跳到其對應的位置執行。 1. 首先判斷 `tasklist` 是否為空 2. 不為空的話,先取出第一個 entry,並將其從 list 刪除 3. 然後複製該 entry 的 `jump_buf` 4. 釋放該 entry,接著 `longjmp` 到 `env` 對應的位置 因此: * `EXP6` = `list_del(&t->list)` * `EXP7` = `list_del(&t->list)` ```c static void task_add(struct list_head *tasklist, jmp_buf env) { struct task *t = malloc(sizeof(*t)); memcpy(t->env, env, sizeof(jmp_buf)); INIT_LIST_HEAD(&t->list); list_add_tail(&t->list, tasklist); } ``` `task_add` 負責將 `env` 加入到 `tasklist` 的尾端。 :::success TODO 1. 舉出上述 coroutine 實作的限制 (注意 stack) 2. 對比 concurrent-programs 裡頭的 coroutine 程式碼,如 tinync 和 preempt_sched,嘗試引入 UNIX Signal 來做到 preemptive scheduling 3. 對比〈A brief history of the Linux Kernel’s process scheduler: The very first scheduler, v0.01〉,解說早期 Linux 核心 CPU 排程器的行為,並利用上述程式碼來模擬 ::: --- ## 測驗 `5` ### 程式碼運作原理 #### READ_ONCE > 巨集會判斷變數的寬度,針對寬度為 1, 2, 4, 8 位元組的變數,將其轉換為對應的純量 (scalar) 型態並增加 volatile 關鍵字,而對於其他資料寬度的類型,改呼叫 memcpy 來避免編譯器對該變數進行最佳化。 #### 實作 ```c #define __READ_ONCE_SIZE \ ({ \ switch (size) { \ case 1: \ *(uint8_t *) res = *(volatile uint8_t *) p; \ break; \ case 2: \ *(uint16_t *) res = *(volatile uint16_t *) p; \ break; \ case 4: \ *(uint32_t *) res = *(volatile uint32_t *) p; \ break; \ case 8: \ *(uint64_t *) res = *(volatile uint64_t *) p; \ break; \ default: \ memcpy((void *) res, (const void *) p, size); \ } \ }) static inline void __read_once_size(const volatile void *p, void *res, int size) { __READ_ONCE_SIZE; } ``` 巨集 `__READ_ONCE_SIZE` 利用 switch 配別不同 `size`,並根據 `size` 轉換對應的純量型態並加上 `volatile` 關鍵字。 ```c #define READ_ONCE(x) \ ({ \ union { \ typeof(x) __val; \ DECL0; \ } __u; \ __read_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ``` 巨集 `READ_ONCE` 宣告一個 uion `__u`,而 uion 所佔的記憶體大小是根據佔據的最大空間成員的大小來定,因此 `__c` 必須為佔據空間最小的型態,所以: * `DECL0` = `char __c[1]`。 :::success TODO 1. 佐以〈ACCESS_ONCE() and compiler bugs〉及〈你所不知道的 C 語言:編譯器和最佳化原理篇〉進行說明 2. 研讀〈Why the “volatile” type class should not be used〉和〈並行程式設計: Atomics 操作〉,並分析近期 include/asm-generic/rwonce.h 中 READ_ONCE / WRITE_ONCE 巨集的實作和其演化 (使用 git log 觀察) :::