# [2022q1](http://wiki.csie.ncku.edu.tw/sysprog/schedule) 第 4 週測驗題 ###### tags: `linux2022` :::info 目的: 檢驗學員對 **[記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory), [指標](https://hackmd.io/@sysprog/c-pointer), [bitwise](https://hackmd.io/@sysprog/c-bitwise), [前置處理器](https://hackmd.io/@sysprog/c-preprocessor)** 的認知 ::: 作答表單: * ==[測驗 1-2](https://docs.google.com/forms/d/e/1FAIpQLSd3TNI33NJ4q2FKLV4Kakk3EFp0C4bMVMIXXH1j4JaQCqjOjg/viewform)== (Linux 核心設計) * ==[測驗 3-4](https://docs.google.com/forms/d/e/1FAIpQLScUiX4g8G3NwVYln-xl9Ah8xx_3mqPMug4SuzBX4ofJi4k4-Q/viewform)== (Linux 核心設計) * ==[測驗 5](https://docs.google.com/forms/d/e/1FAIpQLScekM55C_2I1ie4D5alUNkGHuhnIMIZVEbD8yFbGjB8fjEgcQ/viewform)== (Linux 核心實作) ### 測驗 `1` 延伸[第 3 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz3)的測驗 `7`,已知輸入必為大於 `0` 的數值 (即 `x > 0`),以下程式碼可計算 $\lceil log_2(x) \rceil$ ,也就是 [ceil](https://man7.org/linux/man-pages/man3/ceil.3.html) 和 [log2](https://man7.org/linux/man-pages/man3/log2.3.html) 的組合並轉型為整數: ```cpp 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; } ``` 請補完,使其行為符合預期。作答規範: * `EXP1` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) * `EXP1` 為表示式,限制使用 `^`, `&`, `|`, `<<`, `>`> 這幾個運算子,可能會出現常數 * `EXP1` 不該包含小括號 (即 `(` 和 `)`) * 為了自動批改的便利,變數出現的順序 (可從缺) 從左到右為 `r`, `shift`, `x` * `EXP1` 不可包含 `,` 和 `;` 這樣的分隔符號 (separator) :::success 延伸問題: 1. 解釋上述程式碼運作原理 2. 改進程式碼,使其得以處理 `x = 0` 的狀況,並仍是 branchless 3. 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例 (提示: 在 Linux 核心排程器就有) ::: --- ### 測驗 `2` 複習〈[你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)〉,改寫[第 3 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz3)的測驗 `11` 裡頭的 fls 函式 (fls 意謂 "find last set"),使其得以計算 [Find first set](https://en.wikipedia.org/wiki/Find_first_set): ```cpp #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; } ``` 請補完,使其行為符合預期。作答規範: * `EXP2` 和 `EXP3` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) * `EXP2` 和 `EXP3` 限制使用 ^, &, |, <<=, >>=, +=, -= 這幾個運算子,可能會出現常數 * `EXP2` 和 `EXP3` 不該包含小括號 (即 `(` 和 `)`) * 為了自動批改的便利,變數出現的順序 (可從缺) 從左到右為 `x`, `o`, `t`, `shift`,也就是說,你應該寫 `x ^ t` 而非 `t ^ x` * `EXP2` 和 `EXP3` 不可包含 `,` 和 `;` 這樣的分隔符號 (separator) :::success 延伸問題: 1. 解釋上述程式碼運作原理,並迴避 while, for, goto 等關鍵字以改寫出功能等價的實作 2. 在 Linux 核心原始程式碼的 [include/asm-generic/bitops](https://github.com/torvalds/linux/tree/master/include/asm-generic/bitops) 目錄找出相關程式碼,並探討應用案例 ::: --- ### 測驗 `3` 考慮以下改寫自 Linux 核心的程式碼: ```cpp struct 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; } ``` 請補完,使 `consumer_del` 行為符合註解規範。作答規範: 1. `EXP4` 和 `EXP5` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) 2. `EXP4` 和 `EXP5` 都包含指標操作 (如 `->`) :::success 延伸問題: 1. 解釋上述程式碼運作原理,並探討這樣區分 `foo` 和 `foo_consumer` 的好處 2. 在 Linux 核心原始程式碼相似技巧的程式碼 (例如 [uprobe](https://docs.kernel.org/trace/uprobetracer.html)),並探討應用案例 ::: --- ### 測驗 `4` 以下嘗試用 [lab0](https://hackmd.io/@sysprog/linux2022-lab0) 提及的 [setjmp](https://man7.org/linux/man-pages/man3/setjmp.3.html) 和 [longjmp](https://man7.org/linux/man-pages/man3/setjmp.3.html),用以實作〈[你所不知道的 C 語言: goto 和流程控制篇](https://hackmd.io/@sysprog/c-control-flow)〉闡述的 [coroutine](https://en.wikipedia.org/wiki/Coroutine),參考的程式執行輸出如下: ``` Task 0: n = 3 Task 1: n = 3 Task 0: resume Task 1: resume Task 0: resume Task 0: complete Task 1: resume Task 1: complete ``` 原始程式碼如下: (檔名 `jmp.c`) ```cpp #include <setjmp.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "list.h" struct task { jmp_buf env; struct list_head list; }; static LIST_HEAD(tasklist); static void (**tasks)(void *); static int ntasks; static jmp_buf sched; 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); } 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); } } 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); } /* 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); } #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) int main(void) { void (*registered_task[])(void *) = {task0, task1}; tasks = registered_task; ntasks = ARRAY_SIZE(registered_task); schedule(); return 0; } ``` 請補完,使行為符合註解規範。作答規範: 1. `EXP6`, `EXP7`, `EXP8`, `EXP9` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) 2. `EXP6` 和 `EXP7` 都包含 List API 的使用 3. `EXP8` 和 `EXP9` 應有函式呼叫 :::success 延伸問題: 1. 解釋上述程式碼運作原理,舉出上述 [coroutine](https://en.wikipedia.org/wiki/Coroutine) 實作的限制 (注意 stack) 2. 對比 [concurrent-programs](https://github.com/sysprog21/concurrent-programs) 裡頭的 coroutine 程式碼,如 [tinync](https://github.com/sysprog21/concurrent-programs/blob/master/tinync) 和 [preempt_sched](https://github.com/sysprog21/concurrent-programs/blob/master/preempt_sched),嘗試引入 UNIX Signal 來做到 preemptive scheduling 3. 對比〈[A brief history of the Linux Kernel's process scheduler: The very first scheduler, v0.01](https://dev.to/satorutakeuchi/a-brief-history-of-the-linux-kernel-s-process-scheduler-the-very-first-scheduler-v0-01-9e4)〉,解說早期 Linux 核心 CPU 排程器的行為,並利用上述程式碼來模擬 ::: --- ### 測驗 `5` 〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉已提過若干 Linux 核心的巨集,不難發現這些巨集經歷多次演變。Linux 核心一度有個巨集 `ACCESS_ONCE`,其作用是確實地讀取所指定記憶體位址的內容值,且限這一次,其原始定義如下: ```cpp #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x)) ``` > 注意 `volatile` 關鍵字的使用 `ACCESS_ONCE` 巨集的使用情境,可參照 Linux v4.0 的 `kernel/locking/mutex.c`: ```cpp while (true) { struct task_struct *owner; ... /* * If there's an owner, wait for it to either * release the lock or go to sleep. */ owner = ACCESS_ONCE(lock->owner); if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 然而,如果不使用 `ACCESS_ONCE` 巨集,程式碼如下: ```cpp= while (true) { struct task_struct *owner; ... /* * If there's an owner, wait for it to either * release the lock or go to sleep. */ owner = lock->owner; if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 由於,編譯器偵測到第 8 行的 `owner = lock->owner` 在迴圈中沒有被更改,所以其最佳化機制可能將第 8 行的程式碼搬出 while 迴圈之外,如此不用每次都實際地讀取 `lock->owner`,其程式碼變成: ```cpp struct task_struct *owner = lock->owner; while (true) { ... if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 但問題來了,`lock->owner` 有可能被其它核心執行緒修改,從而造成資料不一致。因此使用 `ACCESS_ONCE` 巨集可以防止編譯器做相關最佳化工作,並確保每次都能到實體記憶體位址讀取。其做法便是將某參數暫時性地轉換成具備 `volatile` 的型態。如此,存取該參數在非引入 `ACESS_ONCE` 巨集之處 (不具 `volatile` 特性),仍可享用編譯器最佳化的好處。 在 [ACCESS_ONCE() and compiler bugs](https://lwn.net/Articles/624126/) 則提及 Linux 核心捨棄上述 `ACCESS_ONCE` 巨集,改為 `READ_ONCE` 巨集。在 Linux 核心原始程式碼曾存在以下註解: > `ACCESS_ONCE` will only work on scalar types. For union types, ACCESS_ONCE on a union member will work as long as the size of the member matches the size of the union and the size is smaller than word size. > > The major use cases of `ACCESS_ONCE` used to be > 1. Mediating communication between process-level code and irq/NMI handlers, all running on the same CPU, and > 2. Ensuring that the compiler does not fold, spindle, or otherwise mutilate accesses that either do not require ordering or that interact with an explicit memory barrier or atomic instruction that provides the required ordering. 以下是可能的實作: ```cpp #include <stdint.h> #include <string.h> #include <stdlib.h> #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; } #define READ_ONCE(x) \ ({ \ union { \ typeof(x) __val; \ DECL0; \ } __u; \ __read_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ``` `READ_ONCE` 巨集會判斷變數的寬度,針對寬度為 1, 2, 4, 8 位元組的變數,將其轉換為對應的純量 (scalar) 型態並增加 volatile 關鍵字,而對於其他資料寬度的類型,改呼叫 `memcpy` 來避免編譯器對該變數進行最佳化。 請補完程式碼,使其運作符合預期。書寫規範: 1. 符合一致的程式碼風格,使用最精簡的方式撰寫 2. `DECL0` 為變數宣告,不該存在 `;` 和 `,` 分隔字元 :::success 延伸問題: 1. 解釋上述程式碼運作原理,並佐以〈[ACCESS_ONCE() and compiler bugs](https://lwn.net/Articles/624126/)〉及〈[你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization)〉進行說明 2. 研讀〈[Why the "volatile" type class should not be used](https://docs.kernel.org/process/volatile-considered-harmful.html)〉和〈[並行程式設計: Atomics 操作](https://hackmd.io/@sysprog/concurrency-atomics)〉,並分析近期 [include/asm-generic/rwonce.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/rwonce.h) 中 `READ_ONCE` / `WRITE_ONCE` 巨集的實作和其演化 (使用 `git log` 觀察) :::