# 2020q1 Homework4 (kcalc) contributed by < [`KYG-yaya573142`](https://github.com/KYG-yaya573142/kcalc) > > [H07: kcalc](https://hackmd.io/@sysprog/linux2020-kcalc) ## 預期目標 * 學習數值系統:浮點運算和定點數 * 整合 fibdrv 和 quiz4 開發成果 * 學習 [Livepatch](https://www.kernel.org/doc/Documentation/livepatch/livepatch.txt) 運作機制 * 撰寫可適用於使用者和核心層級的程式 * 自動測試機制 ## Linux 核心中使用定點數的案例 我的電腦 linux kernel 版本是 5.3,以下資料都是參照此版本進行說明 ```shell $ uname -r 5.3.0-45-generic ``` ### 定點數簡介 並不是所有硬體都有 [FPU](https://en.wikipedia.org/wiki/Floating-point_unit),而且 kernel 內也不建議進行浮點數計算 (可參閱 [Linux 核心的浮點數運算](https://hackmd.io/@sysprog/linux2020-kcalc#-Linux-%E6%A0%B8%E5%BF%83%E7%9A%84%E6%B5%AE%E9%BB%9E%E6%95%B8%E9%81%8B%E7%AE%97)),因此須改用 [定點數計算 (Fixed-point arithmetic)](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) * 小數點的位置固定 * 以 10 進位為例,精確度到小數點後 3 位,$number_{10} \times 10^3 = 定點數_{10}$ * 以 2 進位為例,精確度到小數點後 4 位,$number_{2} \times 2^4 = 定點數_{2}$ 定點數的運算有以下法則 * 定點數加法與減法 - 可直接進行 * 定點數乘法 - 結果須再**除** $b^n$ (b 為進制,n 為小數位數) * 定點數除法 - 結果須再**乘** $b^n$ (b 為進制,n 為小數位數) ### `fixed_power_int` 在研究 Load Average 的時候發現 [linux/kernel/sched/loadavg.c](https://elixir.bootlin.com/linux/v5.3/source/kernel/sched/loadavg.c#L110) 有一個用來計算定點數次方的函式 `fixed_power_int`,很適合用來當作理解定點數計算的範例 ```cpp /** * fixed_power_int - compute: x^n, in O(log n) time * * @x: base of the power * @frac_bits: fractional bits of @x * @n: power to raise @x to. * * By exploiting the relation between the definition of the natural power * function: x^n := x*x*...*x (x multiplied by itself for n times), and * the binary encoding of numbers used by computers: n := \Sum n_i * 2^i, * (where: n_i \elem {0, 1}, the binary vector representing n), * we find: x^n := x^(\Sum n_i * 2^i) := \Prod x^(n_i * 2^i), which is * of course trivially computable in O(log_2 n), the length of our binary * vector. */ static unsigned long fixed_power_int(unsigned long x, unsigned int frac_bits, unsigned int n) { unsigned long result = 1UL << frac_bits; if (n) { for (;;) { if (n & 1) { result *= x; result += 1UL << (frac_bits - 1); result >>= frac_bits; } n >>= 1; if (!n) break; x *= x; x += 1UL << (frac_bits - 1); x >>= frac_bits; } } return result; } ``` * 以 2 進位來思考次方項 $x^n = x^{\sum n_i \cdot 2^i} ,\ n_i\in{0, 1}$ * 例如 $x^{11} = x^{1 \cdot 2^0} \times x^{1 \cdot 2^1} \times x^{0 \cdot 2^2} \times x^{1 \cdot 2^3}$ * `if (n & 1)` 是否為 true 即對應到 $n_i$ 是否為 1 * `1UL << frac_bits` 代表定點數 1 (2 進位,小數點後有 `frac_bits` 位) * `x += 1UL << (frac_bits - 1)` 會讓 `x` 進行無條件進位 * 因為是進行定點數乘法,需再除 $2^{frac\_bits}$,也就是 `x >>= frac_bits` ### `loadavg_proc_show` 從 [man proc(5)](http://man7.org/linux/man-pages/man5/proc.5.html) 可以知道 `/proc/loadavg` 記錄著 load average 相關的資訊 ```shell $ cat /proc/loadavg ``` 觀察 [/fs/proc/loadavg.c](https://elixir.bootlin.com/linux/v5.3/source/fs/proc/loadavg.c#L33) 會發現 `/proc/loadavg` 是一個虛擬檔案系統,還可以看到實際輸出資料的函式 `loadavg_proc_show` ```cpp static int loadavg_proc_show(struct seq_file *m, void *v) { unsigned long avnrun[3]; get_avenrun(avnrun, FIXED_1/200, 0); seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %ld/%d %d\n", LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]), LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]), LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]), nr_running(), nr_threads, idr_get_cursor(&task_active_pid_ns(current)->idr) - 1); return 0; } ``` 觀察 `seq_printf` 使用的格式 `%lu.%02lu`,然後從 [include/linux/sched/loadavg.h](https://elixir.bootlin.com/linux/v5.3/source/include/linux/sched/loadavg.h#L43) 了解 `LOAD_INT` 和 `LOAD_FRAC` 的實作 ```cpp #define LOAD_INT(x) ((x) >> FSHIFT) #define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100) ``` * `%lu.%02lu` 分別使用 `LOAD_INT` 與 `LOAD_FRAC` 來組成完整的 10 進位數值 * `LOAD_INT` 就是直接取用 interger * `LOAD_FRAC` 會取用 fraction 的部分 * `FIXED_1-1` 可視為 fraction 的 mask * 乘上 100 的目的是保留 10 進位下的小數點後 2 位 * `FIXED_1/200` 可理解為是 10 進位下的定點數 0.005,由於結果會保留小數點後 2 位,加上此數值目的是進行無條進件位 * load average 的資料實際上儲存在 `avnrun` 這個 global array ### Load Average 從 [linux/kernel/sched/loadavg.c](https://elixir.bootlin.com/linux/v5.3/source/kernel/sched/loadavg.c#L331) 可以看到負責更新`avnrun` 的函式 ```cpp /* * calc_load - update the avenrun load estimates 10 ticks after the * CPUs have updated calc_load_tasks. * * Called from the global timer code. */ void calc_global_load(unsigned long ticks) { unsigned long sample_window; long active, delta; sample_window = READ_ONCE(calc_load_update); if (time_before(jiffies, sample_window + 10)) return; /* * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs. */ delta = calc_load_nohz_fold(); if (delta) atomic_long_add(delta, &calc_load_tasks); active = atomic_long_read(&calc_load_tasks); active = active > 0 ? active * FIXED_1 : 0; avenrun[0] = calc_load(avenrun[0], EXP_1, active); avenrun[1] = calc_load(avenrun[1], EXP_5, active); avenrun[2] = calc_load(avenrun[2], EXP_15, active); WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ); /* * In case we went to NO_HZ for multiple LOAD_FREQ intervals * catch up in bulk. */ calc_global_nohz(); } ``` `calc_load` 是實際計算 load average 的函式,在 [/include/linux/sched/loadavg.h](https://elixir.bootlin.com/linux/v5.3/source/include/linux/sched/loadavg.h#L18) 可以看到其函式實作 ```cpp /* * a1 = a0 * e + a * (1 - e) */ static inline unsigned long calc_load(unsigned long load, unsigned long exp, unsigned long active) { unsigned long newload; newload = load * exp + active * (FIXED_1 - exp); if (active >= load) newload += FIXED_1-1; return newload / FIXED_1; } ``` * 根據 [exponential smoothing](https://en.wikipedia.org/wiki/Exponential_smoothing) 來計算 load average * `newload += FIXED_1-1` 目的是讓所有的小數位數都進一位,但目前想不出來和 `fixed_power_int` 中的進位方式的差異是甚麼 * 可以看到 `exp` 是函式參數,也就是說會使用事先算好的對應數值,來避免進行複雜的浮點數計算 ```cpp /* * These are the constant used to fake the fixed-point load-average * counting. Some notes: * - 11 bit fractions expand to 22 bits by the multiplies: this gives * a load-average precision of 10 bits integer + 11 bits fractional * - if you want to count load-averages more often, you need more * precision, or rounding will get you. With 2-second counting freq, * the EXP_n values would be 1981, 2034 and 2043 if still using only * 11 bit fractions. */ extern unsigned long avenrun[]; /* Load averages */ extern void get_avenrun(unsigned long *loads, unsigned long offset, int shift); #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */ ``` * `EXP_1` 、`EXP_5` 和 `EXP_15` 是事先算好並轉換成定點數的數值 * 例如 $1/e^{(5/60)} \times \text{FIXED_1}\approx 1884_{10}$ * 使用 11 個 bits 作為 fraction,剩餘的部分是 interger * 可參閱 [Linux Kernel Load Average 計算分析](http://brytonlee.github.io/blog/2014/05/07/linux-kernel-load-average-calc/) * 註解有提到,如果會頻繁的計算 load average,須提高 fraction 的大小,否則會造成 rounding error 的累積 * 可參閱[你所不知道的 C 語言: 浮點數運算](https://hackmd.io/@sysprog/c-floating-point#Rounding) ## [MathEx](https://github.com/jserv/mathex) ### MathEx 如何解析給定字串 從 `test-simple.c` (也就是 README 中的示範程式碼) 可以得知 `expr_create` 是負責將字串解析為表達式 (expression) 的函式,再觀察可以發現 `expr_create` 中會使用 `expr_next_token` 尋找字串中 token 的位置,接著 `expr_create` 其餘的程式碼會對 token 進行分析 #### `expr_next_token` * 負責回傳 token 的長度 (單位為 char) * 會忽略 `'#'` 整行的內容 * 跳過空白與換行符號 (具體來說是跳過 [isspace](http://man7.org/linux/man-pages/man3/isspace.3p.html) 為真的 char) * 使用 flag 來建立防錯檢查機制,避免 invalid 的輸入,例如 * `EXPR_TOP` 代表可接受運算子 * `EXPR_TOPEN` 代表可接受左括號 * `EXPR_TCLOSE` 代表可接受右括號 #### `expr_create` token 的解析主要是在 `for (;;)` 這個大迴圈中進行,由於程式碼很長,請至 [expression.c](https://github.com/jserv/MathEX/blob/master/expression.c) 閱覽對應的原始碼。另外,`expr_create` 會使用三個 vector 物件 `es` 、 `os` 與 `as`,會在後面進行討論。 * 一開始會先略過空白與換行符號 * 684 行會將常數 token 直接存進 `es` * 687 行會處理當 token 是 operator 的情況 * 如果是一般的 operator 就存進 `os` * 如果是 comma operator 同時最接近 `os` 內容是 `'{'`,代表 operand 是函式的參數,會存進 `as` * 719 行會處理當 token 是變數或函式名稱的情況 * 可以得知 `id` 與 `idn` 是變數或函式的名稱與名稱的長度 * 因此 543 行的 `if (idn > 0)` 是在處理 token 是變數與函式的情況 * 變數會存進 `es` * 使用 `expr_func` 來判斷函式名稱是否有對應的定義,若有則將函式名稱存進 `os` * 實際將函式指標登入至 `es` 的程式碼在 667 行 * 若 token 是函式名稱,會將 parent flag 改為 `EXPR_PAREN_EXPECTED` 來告知接下來遇到的左括號會是函式的左括號而不是一般的括號,並在 574 行改以 `'{'` 存入 `os` 作為標示 * 587 行會開始根據 `es` 、 `os` 與 `as` 的內容,將最終結果整合進 `es` (概念類似展開方程式) * 最後在 750 行將 `es` 作為最後結果回傳 #### `es` 、 `os` 與 `as` 在 `expr_create` 開頭定義了三個 vector 物件 `es` 、 `os` 與 `as` ```cpp vec_expr_t es = vec_init(); vec_str_t os = vec_init(); vec_arg_t as = vec_init(); ``` 從 [expression.h](https://github.com/jserv/MathEX/blob/master/expression.h) 查看相關的定義 ```cpp #define vec_init() { NULL, 0, 0 } #define vec(T) \ struct { \ T *buf; \ int len; \ int cap; \ } typedef vec(struct expr) vec_expr_t; typedef vec(struct expr_string) vec_str_t; typedef vec(struct expr_arg) vec_arg_t; ``` * 定義的 vector 儲存以下資訊 * `buf` 儲存的目標資料的起始位置 * `len` 為此 vector 現有的資料長度 * `cap` 為此 vector 的最大容量 接著觀察相關的定義 ```cpp typedef vec(struct expr) vec_expr_t; struct expr { int type; union { struct { float value; } num; struct { float *value; } var; struct { vec_expr_t args; } op; struct { struct expr_func *f; vec_expr_t args; void *context; } func; } param; }; ``` * `es` 可視為保存解析後的表達式的 vector * `struct expr` 內還會有 `vec_expr_t`,代表可以是樹狀結構 * 根據 `type` 來決定用哪種方式取用 `union` 內的資料 (`type` 定義在 expression.c 的 `enum expr_type` 內) * `num` 單純儲存數值 * `var` 儲存變數 * `op` 儲存 operands,使用的 operator 則是根據 `type` 決定 * `func` 儲存使用者自訂的函式以及對應的 operands * `func.f` 會指向存放有對應函式的 `struct expr_func` 物件,該物件內的 `exprfn_t f` 才是真正的 function pointer * `func.args` 存放使用者函數對應的 operands * `func.context` 目前看不懂用途 ```cpp typedef vec(struct expr_string) vec_str_t; struct expr_string { const char *s; int n; }; ``` * 主要儲存使用者自訂函數名稱的字串與長度、以及兩種括號 `'('` 與 `'{'` ```cpp typedef vec(struct expr_arg) vec_arg_t; struct expr_arg { int oslen; int eslen; vec_expr_t args; }; ``` * 主要儲存函式的參數 * `oslen` 與 `eslen` 是用來定位相對於參照的函式或 operator 的位置 ### `MathEx` 中,`nop` 一類使用者自訂的函數如何實作 為了使用自訂的函數,首先需先定義自訂函數,以自訂函數 `add` 為例 ```cpp static float add(struct expr_func *f, vec_expr_t args, void *c) { ... } ``` 接著定義一個 `expr_func` 資料結構,內有使用者自訂函數的名稱 `"add"` 與函數指標 `add`,並將此資料結構作為參數帶入 `expr_create` ```cpp static struct expr_func user_funcs[] = { { "add", add, 0 }, { NULL, NULL, 0 }, }; struct expr *e = expr_create(s, strlen(s), &vars, user_funcs); ``` 在 `expr_create` 中會分析字串,跟自訂函數有關的部分主要是 * 719 行會判斷 token 可能是函式名稱 * 下一次迴圈,在 556 行使用 `expr_func` 來判斷函式名稱是否有對應的定義 (從 `user_funcs`),若有則將函式名稱存進 `os` * 最後在 667 行處會將函式指標登入至 `es`,至此就可以在表達式中呼叫使用者自訂的函數了 ## livepatch ### 用法簡介 livepatch 模組的整體寫法與 kernel module 幾乎相同,kalc 中其實已經寫好可以運作的範本,以下簡單分析 kalc 使用 livepatch 的方式 * 首先定義欲 patch 的新函式 ```cpp void livepatch_nop_cleanup(struct expr_func *f, void *c) { ... } int livepatch_nop(struct expr_func *f, vec_expr_t args, void *c) { ... } ``` * 建立 [`klp_func`](https://elixir.bootlin.com/linux/latest/source/include/linux/livepatch.h#L27) 資料結構,連結原本的函式名稱及 patch 中定義的函式名稱 ```cpp static struct klp_func funcs[] = { { .old_name = "user_func_nop", .new_func = livepatch_nop, }, { .old_name = "user_func_nop_cleanup", .new_func = livepatch_nop_cleanup, }, {}, }; ``` * 建立 patch 物件 [`klp_object`](https://elixir.bootlin.com/linux/latest/source/include/linux/livepatch.h#L106),包含目標核心模組的名稱與對應的 `klp_func` 物件名稱 ```cpp static struct klp_object objs[] = { { .name = "calc", .funcs = funcs, }, {}, }; ``` * 最後使用 [`klp_patch`](https://elixir.bootlin.com/linux/latest/source/include/linux/livepatch.h#L106) 資料結構來定義此 patch 的名稱與內容 ```cpp static struct klp_patch patch = { .mod = THIS_MODULE, .objs = objs, }; ``` 準備完 patch 的內容後,從 [Livepatch - Loading](https://www.kernel.org/doc/html/latest/livepatch/livepatch.html#loading) 可以知道需要在 `module_init` 時呼叫 `klp_enable_patch()` ```cpp static int livepatch_calc_init(void) { return klp_enable_patch(&patch); } static void livepatch_calc_exit(void) { klp_unregister_patch(&patch); } module_init(livepatch_calc_init); module_exit(livepatch_calc_exit); ``` 使用 kbuild Makefile 編譯後,就可將此 patch 掛載 ```shell $ sudo insmod livepatch-calc.ko ``` 用 `dmesg` 觀察是否掛載成功 ```shell $ dmesg [1144023.202333] livepatch: enabling patch 'livepatch_calc' [1144023.216455] livepatch: 'livepatch_calc': starting patching transition [1144024.750831] livepatch: 'livepatch_calc': patching complete ``` ### 在 livepatch-calc 中使用 MathEX API `livepatch-calc` 的目的是在不卸載核心模組的前提下,修補 kcalc 中錯誤的函式,因此在 `livepatch-calc` 中勢必會使用到 MathEX API。 然而實際使用後,會發現儘管已 `#include "expression.h"`,但使用 MathEX API 仍會導致編譯錯誤 ```shell ERROR: "expr_eval" [/home/kyg/Documents/kcalc/livepatch-calc.ko] undefined! ``` 觀察 Makefile 會發現錯誤的原因是因為 `livepatch-calc` 的相依性少了 `expression.o`,參考 [kbuild Makefile](https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt) 將其加入 ```diff KDIR=/lib/modules/$(shell uname -r)/build obj-m += calc.o obj-m += livepatch-calc.o calc-objs += main.o expression.o +livepatch-calc-y = livepatch-calc.o expression.o ccflags-y := -std=gnu99 -Wno-declaration-after-statement ``` 再次編譯會發現仍然出錯,使用 `make V=1` 查看錯誤訊息 ``` ... make[2]: Circular /home/kyg/Documents/kcalc/livepatch-calc.o <- /home/kyg/Documents/kcalc/livepatch-calc.o dependency dropped. ... ld -m elf_x86_64 -z max-page-size=0x200000 -r -o /home/kyg/Documents/kcalc/calc.o /home/kyg/Documents/kcalc/main.o /home/kyg/Documents/kcalc/expression.o ld -m elf_x86_64 -z max-page-size=0x200000 -r -o /home/kyg/Documents/kcalc/livepatch-calc.o /home/kyg/Documents/kcalc/expression.o ``` 發現如果 module 的名稱和 source file 相同,會導致 Makefile 誤判相依性,因此解決方法就是 module 和 source file 不要使用相同的名稱。由於 `test.sh` 等測試腳本都是使用 `livepatch-calc` 這個名稱,所以我將 `livepatch.c` 更改為 `klp-calc.c`,更改完後即可正常編譯 ### 配合使用 livepatch 將 [fibdrv](https://hackmd.io/BZjwYw1FQ1O0NPY5kIS7VQ) 的成果整合入 kcalc kalc 原本已將 [MathEX](https://github.com/jserv/mathex) 整合,且在 [main.c](https://github.com/KYG-yaya573142/kcalc/blob/master/main.c#L103) 中可以看到使用 MathEX API 的範例 `user_func_nop` 和 `user_func_nop_cleanup`,接下來仿造範例的用法,將 fibdrv 中計算費氏數列的函數以 MathEX 使用者自訂函數的形式進行改寫,並以 livepatch 的方式 patch 成正確的函式 首先在 kcalc 中先定義錯誤的使用者自訂函數 `user_func_fib` ```cpp noinline int user_func_fib(struct expr_func *f, vec_expr_t args, void *c) { (void) c; /* echo what you inputed */ return expr_eval(&vec_nth(&args, 0)); } ``` 記得將此自訂函數使用 MathEX API 註冊,名稱使用 `fib` ```diff static struct expr_func user_funcs[] = { {"nop", user_func_nop, user_func_nop_cleanup, 0}, + {"fib", user_func_fib, 0}, {NULL, NULL, NULL, 0}, }; ``` 簡單測試結果如預期 ```shell $ echo -ne "fib(666)\0" > /dev/calc $ dmesg [1155511.080853] calc: Device successfully closed [1155542.870672] calc: Received 9 -> fib(666) [1155542.870682] calc: Result: 10656 [1155542.870687] calc: Device successfully closed $ fromfixed 10656 666 ``` 接下來要製作 patch,我選擇直接更新本來的 livepatch-calc.c ```diff /* clang-format off */ static struct klp_func funcs[] = { { .old_name = "user_func_nop", .new_func = livepatch_nop, }, { .old_name = "user_func_nop_cleanup", .new_func = livepatch_nop_cleanup, }, + { + .old_name = "user_func_fib", + .new_func = livepatch_fib, + }, {}, }; ``` `fib` 實作的部分直接從 [fibdrv](https://github.com/KYG-yaya573142/fibdrv/blob/master/fibdrv.c#L43) 取用,並根據 [kcalc 使用的定點數格式](#kcalc-使用的定點數格式)進行修改 (MSB 28 bits 為 mantissa) ```cpp noinline int livepatch_fib(struct expr_func *f, vec_expr_t args, void *c) { if (args.len != 1) { pr_err("fib(): too many or no argument\n"); return -1; } int n = expr_eval(&vec_nth(&args, 0)); /* fixed-point */ int frac = GET_FRAC(n); /* frac */ n >>= 4; /* mantissa */ /* fixed-point -> interger */ if (frac > 0) { for (; frac; frac--) n *= 10; } if (frac < 0) { for (; frac; frac++) n /= 10; } if (n < 2) { /* F(0) = 0, F(1) = 1 */ return (n << 4); } long long fib[2]; unsigned int ndigit = 32 - __builtin_clz(n); fib[0] = 0; /* F(k) */ fib[1] = 1; /* F(k+1) */ /* fast doubling algorithm */ for (unsigned int i = 1U << (ndigit - 1); i; i >>= 1) { long long k1 = fib[0] * (fib[1] * 2 - fib[0]); long long k2 = fib[0] * fib[0] + fib[1] * fib[1]; if (n & i) { fib[0] = k2; fib[1] = k1 + k2; } else { fib[0] = k1; fib[1] = k2; } } return FP2INT(fib[0], 0); } ``` * 因為 fib 是 MathEX 的使用者自訂函數,輸入與輸出的定點數格式需符合原本的格式 * 由於直接進行加法會相當複雜 (類似手動處理浮點數運算),我選擇先將定點數轉為整數再進行運算 * `FP2INT` 和 `GET_FRAC` 是 expression.c 中的輔助函數 * kcalc 中定點數的轉換方式後文會進行討論 ## kcalc ### kcalc 使用的定點數格式 kcalc 中的定點數格式如下 ```shell MSB 31 4 3 2 1 0 LSB +----------------------------------+------+----------+ | mantissa | sign | exponent | +----------------------------------+------+----------+ ``` kcalc 中使用定點數的方式和[定點數簡介](#定點數簡介)中說明的方式不同,以類似 floating-point 的方式定義 $\text{value} = \text{mantissa} \times 10^{exponent}$ * 以 10 進位為基礎 * 不同於 [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) 中使用 bias 來調整 exponent 的正負號,kcalc 是將 LSB 第 4 bit 作為 sign bit 使用,因此可以將 exponent 視作長度為 4 bits 的 signed interger 進行思考 * 由於 `NAN_INT` 定義為 `(1 << 4) | ((1 << 4) - 1)`,所以當 exponent 是 -1 時,會將 mantissa 進位並調整 exponent 為 -2 (在 expression.c 中的函式 `FP2INT` 可以看到這種操作) ### 使用 union 參考 [quiz2](https://hackmd.io/OXUSiB-IQiqC17pIK367Yw) 中的 union 用法,嘗試應用在 kcalc ```cpp typedef union { int data; struct { int frac : 4, mantissa : 28; }; } fxp; ``` * 優點是 frac 的部分不需使用 `GET_FRAC` 就可直接取用,以及可以節省一些 bit shift 的操作 * 缺點是需特別注意 [Endianness](https://en.wikipedia.org/wiki/Endianness) 對 bitfield 的影響,因為順序是 implementation-defined (C99 6.7.2.1-10 僅提到 bitfield 會相鄰,但沒規範順序) * 這同時會降低 kcalc 的 portability * 可參考 [CMU 的範例](https://wiki.sei.cmu.edu/confluence/display/c/EXP11-C.+Do+not+make+assumptions+regarding+the+layout+of+structures+with+bit-fields) ### 將 quiz4 使用到的技巧予以應用,提出對空間效率更好的實作 (待補) 從 expression.h 可以看到 MathEX 使用記憶體的方式和 quiz4 中的例子相似,最明顯的不同處為 quiz4 會先使用 stack,超過 capacity 才動態配置記憶體,而 MathEX 則是直接使用動態記憶體 ```cpp /* Simple expandable vector implementation */ static inline int vec_expand(char **buf, int *length, int *cap, int memsz) { if (*length + 1 > *cap) { void *ptr; int n = (*cap == 0) ? 1 : *cap << 1; #ifdef __KERNEL__ ptr = krealloc(*buf, n * memsz, GFP_KERNEL); #else ptr = realloc(*buf, n * memsz); #endif if (!ptr) return -1; /* allocation failed */ *buf = (char *) ptr; *cap = n; } return 0; } ``` 之前 quiz4 記憶體相關的分析尚未完成,待完成後會互相整合 ### MathEx 對於負數的除法結果不正確、乘法和加法的 overflow 沒有做處理 #### 除法 ```diff static int divid(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); if (n1 == 0 && n2 == 0) return NAN_INT; if (n2 == 0) return INF_INT; + if (n1 < 0) { + n1 = -n1; + n2 = -n2; + } while (n1 * 10 < ((1 << 27) - 1)) { --frac1; n1 *= 10; printf("n1: %d, frac: %d\n", n1, frac1); } int n3 = n1 / n2; int frac3 = frac1 - frac2; return FP2INT(n3, frac3); } ``` * 實際進行除法的 `int n3 = n1 / n2` 本來就能處理負數,導致問題的是 `while` 迴圈的判斷沒有考慮到 `n1 < 0` 的情況 * 因此只需要修正當 `n1 < 0` 的情況即可 #### 乘法 乘法時可能有 2 種產生 overflow 的情況 * 同號,可能產生 positive overflow * 異號,可能產生 negative overflow 我能想到偵測 overflow 的方式有 * 將結果除以原本的 `n1`,若無 overflow 應該要等於 `n2` * 但可能因數值的精確度限制而產生誤判 * 先產生 overflow 才偵測的到 * 根據與 `int` 最大 ($T{max}$) 與最小值 ($T{min}$) 的距離判斷 * 使用 gcc 的 [Built-in Functions to Perform Arithmetic with Overflow Checking](https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html) * 但不適合用在 kcalc 中,因為是使用自訂的數值型態 考量 overflow 是 undefined behavior,最好的方式應該是預防 overflow 而不是發生後才偵測,因此最後決定使用第二個方法。首先根據 `n1` 與 `n2` 是否為同號來列出不產生 overflow 的條件 * 同號時 * $n_1 \times n_2 < T_{max}$ * 異號時 * $T_{min} < n_1 \times n_2$ 根據上兩式,將 `n1` 或是 `n2` 移項就可以得到避免 overflow 的判斷式,實作的程式碼如下 ```cpp #define S28_MIN (-(1 << 27)) #define S28_MAX ((1 << 27) - 1) #define FRAC_MIN (-(1 << 3)) #define FRAC_MIN ((1 << 3) - 1) /* (2^27 - 1) ~ -2^27 */ static int mult(int a, int b) { if (!a || !b) { return 0; } int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); if ((n1 > 0 && (n2 > S28_MAX / n1 || n2 < S28_MIN / n1)) || (n2 == -1 && n1 == S28_MIN) || (n1 < 0 && (n1 < S28_MAX / n2 || n1 < S28_MIN / n2))) { /* num overflow */ pr_err("mult: num overflow\n"); return -1; } if ((frac1 > 0 && frac1 > (FRAC_MAX - frac2)) || (frac1 < 0 && frac1 < (FRAC_MIN - frac2))) { /* frac overflow */ pr_err("mult: frac overflow\n"); return -1; } int n3 = n1 * n2; return FP2INT(n3, (frac1 + frac2)); } ``` * 特別注意 $-T{max}$ 是存在的數值,但是 $-T{min}$ 本身就會產生 overflow,因此需特別處理分母可能為 -1 的狀況 * `(n2 == -1 && n1 == S28_MIN)` * 若 `n1` 或 `n2` 其中一者為 0 會直接回傳 0,所以不用考慮分母為 0 的情況 * `S28_MAX` 的名稱是參考 [linux/include/linux/limits.h](https://github.com/torvalds/linux/blob/master/include/linux/limits.h) 中的 `S32_MAX` #### 加法 ```cpp static int plus(int a, int b) { ... if ((n1 > 0 && n2 > (S28_MAX - n1)) || (n1 < 0 && n2 < (S28_MIN - n1))) { /* plus overflow */ pr_err("plus: overflow\n"); return -1; } ... } ``` * 跟乘法中 frac 的處理方式一樣 * 注意移項的問題,例如不能寫成 `n1 > 0 && n1 > (S28_MAX - n2)`,因為當 `n2` 為負數時 `S28_MAX - n2` 會發生 overflow #### 減法 ```cpp static int minus(int a, int b) { ... if ((n1 < 0 && n2 > (S28_MAX + n1)) || (n1 > 0 && n2 < (S28_MIN + n1))) { /* plus overflow */ pr_err("minus: overflow\n"); return -1; } ... } ``` * 跟加法的處理方式一樣 #### 右移 ```diff static int right_shift(int a, int b) { /* FIXME: should use 2-base? */ - return divid(a, mult(2 << 4, b)); + return divid(a, power(2 << 4, b)); } ``` * 應該只是單純的寫錯成 `mult` #### 次方 測試時發現 power 的運算有誤,例如 ```shell $ echo -ne "10**2\0" > /dev/calc $ dmesg [1465823.655749] calc: Received 6 -> 10**2 [1465823.655763] calc: Result: 19 $ fromfix 19 1000 ``` 觀察後發現原本的寫法會讓次方的計數出錯,修改後的程式碼如下 ```diff static int power(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); while (frac2 != 0) { if (frac2 < 0) { n2 /= 10; ++frac2; } else { n2 *= 10; --frac2; } } int on1 = n1; int of1 = frac1; - n1 = 1; - for (int i = 0; i < n2; ++i) { + for (int i = 1; i < n2; ++i) { frac1 += of1; n1 *= on1; } return FP2INT(n1, frac1); } ``` #### 動態調整數值 因為 kcalc 使用定點數方式的關係,可能會產生計算結果是 kcalc 可容納的數值,但是計算過程會出現 kcalc 定點數格式無法容納的數值。 舉例來說,考量計算 $3.14^{4} \approx 97.211$ ```shell $ echo -ne "3.14**4\0" > /dev/calc $ dmesg [ 895.091423] calc: Received 8 -> 3.14**4 [ 895.091435] calc: Result: 919916808 $ fromfix 919916808 .57494800000000000000 ``` 這是因為 power 在計算時,實際上會計算 $314^4$,導致 overflow,因此我想到的方法為當超過可用範圍時,動態的移動數值的小數點位置 (也就是調整 exponent 的值並調整整數的部分),實作如下 ```diff static int power(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); while (frac2 != 0) { if (frac2 < 0) { n2 /= 10; ++frac2; } else { n2 *= 10; --frac2; } } int on1 = n1; int of1 = frac1; for (int i = 1; i < n2; ++i) { + while ((on1 > 0 && (n1 > S28_MAX / on1)) || + (n1 == -1 && on1 == S28_MIN) || + (on1 < 0 && (on1 < S28_MAX / n1 || on1 < S28_MIN / n1))) { + ++frac1; + n1 /= 10; + } frac1 += of1; n1 *= on1; } return FP2INT(n1, frac1); } ``` 測試後,結果如下 ```shell $ echo -ne "3.14**4\0" > /dev/calc $ dmesg [ 1256.659897] calc: Received 8 -> 3.14**4 [ 1256.659908] calc: Result: 1555385194 $ fromfixed 1555385194 97.21157400000000000000 ``` :::warning 目前仍有多項缺點待改善 * 除了 `power` 之外,`mult` 等函式都有一樣的問題 * 這麼做會造成誤差在數值的末端開始累積,例如 $3.14^4 = 97.211712$,但是目前算出來是 $97.211574$ * frac 的部分如果超過定點數可用的範圍,最終會造成小數點位置的誤差,須對 `FP2INT` 以及 `expr_parse_number` 進行修正 ::: ## reference [系統負荷是怎麼計算的?](http://tinylab.org/how-to-calc-load-and-part1/) [Load average explained](https://wiki.nix-pro.com/view/Load_average_explained) [Built-in Functions to Perform Arithmetic with Overflow Checking](https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html) ###### tags: `linux 2020`