--- tags: Linux --- # 2022q1 Homework3 (fibdrv) contributed by < `LJP-TW` > # 實驗環境 虛擬機器的組態: ```shell $ uname -r 5.11.0-34-generic $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 45 bits physical, 48 bits virtual CPU(s): 2 On-line CPU(s) list: 0,1 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 2 NUMA node(s): 1 Vendor ID: AuthenticAMD CPU family: 23 Model: 96 Model name: AMD Ryzen 7 PRO 4750U with Radeon Graphics Stepping: 1 CPU MHz: 1696.813 BogoMIPS: 3393.62 ``` TODO: 於主機進行效能測試。 # 自我檢查清單 TODO # 修正 fibdrv 為了計算 F~93~ 後的 Fibonacci 數列,需要一套大數運算,在閱讀了作業敘述中的連結 ([Small/Short String Optimization](http://wdv4758h.github.io/notes/cpp/string-optimization.html)。[實作程式碼](https://github.com/AdrianHuang/fibdrv/tree/big_number)。) 後,嘗試自己的實作。 連結的程式碼中,`xs` 實作了大數的結構、運算方式。我將其拆成兩個部分來實作,一個部分是 `sso`,另一部份為 `bignum`,將優化字串的部分,和用優化字串來實作大數的部分兩者區別開來。 目前 `bignum` 還不支持負數運算。 ## sso 其結構定義如下 ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/sso.h#L8)): ```c #define SSO_CAPACITY 0x17 typedef union { char data[0x18]; struct { uint8_t filler[SSO_CAPACITY]; union { uint8_t flags; struct { uint8_t is_ptr : 1, reserved : 2, left : 5; // SSO_CAPACITY - size }; }; }; struct { char *ptr; size_t size; size_t capacity; }; } sso_s; ``` * 當 `is_ptr` 為 0 時,表示為 sso mode,使用 `filler` 來存放字串,在此字串佔滿空間後,`left` 也會為 0,使得最後 1 byte 為 NULL byte,作為 `filler` 的 terminated byte。 * 當 `is_ptr` 為 1 時,則表示字串實際存放位址是從記憶體管理機制分配來的空間,由 `ptr` 指著,大小以及容量分別由 `size` 和 `capacity` 所存放。 * `capacity` 的最高 byte 為 flags,因此在存取 `capacity` 時須通過以下函式 ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/sso.c#L16)): ```c void sso_set_capacity(sso_s *sso, size_t capacity) { if (sso->is_ptr) { // Preserve flags setting sso->capacity = (sso->capacity & (0xff00000000000000)) | capacity; } } size_t sso_get_capacity(sso_s *sso) { if (sso->is_ptr) { return sso->capacity & (0x00ffffffffffffff); // Exclude flag field } else { return SSO_CAPACITY; } } ``` ## bignum 其結構定義如下 ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.h#L10)): ```c typedef struct { sso_s sso; // Must be first member of this struct /* * mode: * 0: Calculatable * 1: Outputable */ int mode; } bignum; ``` `sso_s` 結構為 `bignum` 結構的第一個 member,就能在呼叫以 `sso_s *` 作為參數的函式時,能方便地直接傳遞 `bignum *`,例如以下程式碼片段 ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L132)): ```c size_t bn_size(bignum *bn) { return sso_get_size((sso_s *) bn); } ``` 為了實作 fast Fibonacci algorithm,需要的函式有以下: * `bn_init`: 初始化 `bignum` 為指定數字。 * `bn_release`: 釋放 `bignum` 所使用的資源。 * `bn_add`: 實作 `c = a + b`。 * `bn_sub`: 實作 `c = a - b`。 * `bn_mul`: 實作 `c = a * b`。 * `bn_assign`: 實作 `a = b`。 * `bn_str`: 取得 `bignum` 的大數字串。 * `bn_size`: 取得 `bignum` 大數字串的長度。 ### bn_init [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L110): ```c void bn_init(bignum *bn, int num) { int size = 0; do { bn->sso.filler[size++] = num % 10 + '0'; num /= 10; } while (num); bn->sso.filler[size] = 0; bn->mode = BN_MOD_CALCULATABLE; sso_clear_flag((sso_s *) bn); sso_set_size((sso_s *) bn, size); } ``` 將 `num` 轉為字串形式存至 `bignum`。 由於 `int` 的範圍最大僅為 2147483647,為 10 個字元,因此一定能夠使用 sso mode 來存放。 `sso_clear_flag` 同時也將 `is_ptr` 清除為 0,不用額外設置 `is_ptr`。 ### bn_release [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L127): ```c void bn_release(bignum *bn) { sso_release((sso_s *) bn); } ``` 現階段的實作只需要呼叫 `sso_release` 即可,其會根據 `bn` 是否為 sso mode,來決定是否釋放 `ptr`。 ### bn_add [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L221): ```c void bn_add(bignum *op1, bignum *op2, bignum *result) { char *str_op1, *str_op2, *str_result; size_t sz_op1, sz_op2, cap_left, i; int carry; bn_mode_to_cal(op1); bn_mode_to_cal(op2); bn_mode_to_cal(result); if (bn_size(op1) < bn_size(op2)) { // Swap pointers bignum *tmp = op1; op1 = op2; op2 = tmp; } str_op1 = sso_get_data((sso_s *) op1); str_op2 = sso_get_data((sso_s *) op2); str_result = sso_get_data((sso_s *) result); sz_op1 = bn_size(op1); sz_op2 = bn_size(op2); cap_left = sso_get_capacity((sso_s *) result); carry = 0; for (i = 0; i < sz_op2; ++i) { int num; if (!cap_left) { if (!bn_extend_capacity(result, i, &cap_left, &str_result)) { return; } } num = str_op1[i] - '0' + str_op2[i] - '0' + carry; str_result[i] = num % 10 + '0'; carry = num / 10; cap_left -= 1; } for (; i < sz_op1; ++i) { int num; if (!cap_left) { if (!bn_extend_capacity(result, i, &cap_left, &str_result)) { return; } } num = str_op1[i] - '0' + carry; str_result[i] = num % 10 + '0'; carry = num / 10; cap_left -= 1; } if (carry) { if (cap_left == (size_t) 0) { if (!bn_extend_capacity(result, i, &cap_left, &str_result)) { return; } } str_result[i++] = '1'; cap_left -= 1; } str_result[i] = 0; sso_set_size((sso_s *) result, i); } ``` 先用 `bn_mode_to_cal` 將要被運算的 `bignum` 都轉為 calculatable mode,參考以下函式 ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L23)),其根據目前是要計算或是輸出,來進行 `bn_reverse` 將大數字串倒置: ```c static void bn_mode_to_cal(bignum *bn) { if (bn->mode == BN_MOD_OUTPUTABLE) { bn_reverse(bn); bn->mode = BN_MOD_CALCULATABLE; } } static void bn_mode_to_out(bignum *bn) { if (bn->mode == BN_MOD_CALCULATABLE) { bn_reverse(bn); bn->mode = BN_MOD_OUTPUTABLE; } } ``` 接著的演算法步驟如下: 1. 確保 `op1` 為數字較多的那方。 2. 變數 i 迴圈從 0 到 `op2` size - 1,將 `op1`、`op2` 第 i 個數字和 carry 相加,存到 `result` 第 i 個數字。設置 carry,必要時擴展 `result` 容量。 3. 變數 i 迴圈繼續到 `op1` size - 1,將 `op1` 第 i 個數字和 carry 相加,存到 `result` 第 i 個數字。設置 carry,必要時擴展 `result` 容量。 4. 若 carry 為 1,則將 `result` 的第 i 個數字設為 1,必要時擴展 `result` 容量。 5. 為 `result` 加上 NULL byte。 6. 為 `result` 設置 size。 由於設置 `result` 的第 i 個字時,已經不會再用到 `op1`、`op2` 的第 i 個數字,因此此函式可以直接支援類似 `bn_add(ptr_same_bn, ptr_same_bn, ptr_same_bn)` 這種 `bignum` 同時為 dst 和 src 的操作。 ### bn_sub [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L293): ```c void bn_sub(bignum *op1, bignum *op2, bignum *result) { char *str_op1, *str_op2, *str_result; size_t sz_op1, sz_op2, cap_left, i; int borrow; if (bn_cmp(op1, op2) < 0) { // TODO: Support negative big number return; } if (!bn_cmp(op1, op2)) { bn_release(result); bn_init(result, 0); return; } bn_mode_to_cal(op1); bn_mode_to_cal(op2); bn_mode_to_cal(result); str_op1 = sso_get_data((sso_s *) op1); str_op2 = sso_get_data((sso_s *) op2); str_result = sso_get_data((sso_s *) result); sz_op1 = bn_size(op1); sz_op2 = bn_size(op2); cap_left = sso_get_capacity((sso_s *) result); borrow = 0; for (i = 0; i < sz_op2; ++i) { int num; if (!cap_left) { if (!bn_extend_capacity(result, i, &cap_left, &str_result)) { return; } } num = str_op1[i] - str_op2[i] - borrow; if (num < 0) { num += 10; borrow = 1; } else { borrow = 0; } str_result[i] = num + '0'; cap_left -= 1; } for (; i < sz_op1; ++i) { int num; if (!cap_left) { if (!bn_extend_capacity(result, i, &cap_left, &str_result)) { return; } } num = str_op1[i] - '0' - borrow; if (num < 0) { num += 10; borrow = 1; } else { borrow = 0; } str_result[i] = num + '0'; cap_left -= 1; } if (str_result[i - 1] == '0') { i -= 1; } str_result[i] = 0; sso_set_size((sso_s *) result, i); } ``` 演算法步驟如下: 1. 確保 `op1` 為數字較大的那方。 2. 變數 i 迴圈從 0 到 `op2` size - 1,將 `op1`、`op2` 第 i 個數字相減,並減去 borrow,存到 `result` 第 i 個數字。設置 borrow,必要時擴展 `result` 容量。 3. 變數 i 迴圈繼續到 `op1` size - 1,將 `op1` 第 i 個數字和 borrow 相減,存到 `result` 第 i 個數字。設置 borrow,必要時擴展 `result` 容量。 4. 若 result 的第 i - 1 個字為 0,表示其剛剛被借位減去,將 i -= 1,使其為 NULL byte index。 5. 為 `result` 加上 NULL byte。 6. 為 `result` 設置 size。 ### bn_mul [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L144): ```c void bn_mul(bignum *op1, bignum *op2, bignum *result) { bignum tmp; char *str_op1, *str_op2, *str_result; size_t sz_op1, sz_op2, cap, i, j, inited; bn_mode_to_cal(op1); bn_mode_to_cal(op2); bn_mode_to_cal(result); bn_init(&tmp, 0); str_op1 = sso_get_data((sso_s *) op1); str_op2 = sso_get_data((sso_s *) op2); str_result = sso_get_data((sso_s *) &tmp); sz_op1 = bn_size(op1); sz_op2 = bn_size(op2); cap = sso_get_capacity((sso_s *) &tmp); inited = 0; for (i = 0; i < sz_op2; ++i) { int num2; num2 = str_op2[i] - '0'; for (j = 0; j < sz_op1; ++j) { int num; num = (str_op1[j] - '0') * num2; if (i + j >= cap) { if (!bn_extend_capacity2(&tmp, &cap, &str_result)) { return; } } if (i + j == 0 || i + j > inited) { inited = i + j; } else { num += str_result[i + j]; } str_result[i + j] = num % 10; if (i + j + 1 >= cap) { if (!bn_extend_capacity2(&tmp, &cap, &str_result)) { return; } } if (i + j + 1 > inited) { str_result[i + j + 1] = num / 10; inited = i + j + 1; } else { str_result[i + j + 1] += num / 10; } } } for (i = 0; i <= inited; ++i) { str_result[i] += '0'; } if (str_result[inited] != '0') { inited += 1; } str_result[inited] = 0; sso_set_size((sso_s *) &tmp, inited); bn_assign(result, &tmp); bn_release(&tmp); } ``` 演算法步驟如下: 1. `inited = 0`,其代表目前 `tmp` 的數字字串中,已經初始化過的最大 index。 2. `i` 迴圈從 0 走到 `op2` size - 1: 1. 取得 `str_op2[i]` 放到 num2 2. `j` 迴圈從 0 走到 `op1` size - 1: 1. 將 `str_op1[j]` 與 num2 相乘,存到 num 2. 若 `i + j` 為 0 或是 `i + j > inited`,表示目前將要把計算結果存放到未被初始化過的 `str_tmp[i + j]`,更新 `inited = i + j`;否則表示 `str_tmp[i + j]` 已經存放著稍早的計算結果,將 `num += str_tmp[i + j]`。必要時擴展 `tmp` 容量。 3. `str_tmp[i + j] = num % 10`。 4. 若 `i + j + 1 > inited`,表示目前將要把進位結果存放到未被初始化過的 `str_tmp[i + j + 1]`,更新 `inited = i + j + 1`,並設置 `str_tmp[i + j + 1] = num / 10`;否則表示 `str_tmp[i + j + 1]` 已經存放著稍早的計算結果,直接將進位結果加進去 `str_tmp[i + j + 1] += num / 10`。必要時擴展 `tmp` 容量。 3. 將 `tmp` 的內容全部加 `'0'` 使其從數字變成數字字串。 4. 判斷最後一個字是否為 `'0'`,調整 NULL byte index。 5. 為 `tmp` 加上 NULL byte。 6. 為 `tmp` 設置 size。 7. 將 `tmp` 內容指派給 `result`。 由於設置 `tmp` 的第 i 個字時,還是有可能再用到 `op1`、`op2` 的第 i 個數字,因此此函式無法直接支援類似 `bn_add(ptr_same_bn, ptr_same_bn, ptr_same_bn)` 這種 `bignum` 同時為 dst 和 src 的操作。需要在函式內部新建立一個 `tmp` 作為暫時的 dst,操作結束後再將 `tmp` 指派給真正的 dst,才能支援類似操作。 ### bn_assign [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L377): ```c void bn_assign(bignum *to, bignum *from) { bn_mode_to_cal(to); bn_mode_to_cal(from); sso_assign((sso_s *) to, (sso_s *) from); } ``` 將兩者調整為同一種運算模式後,再呼叫 `sso_assign`。 ### bn_str [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L137): ```c char *bn_str(bignum *bn) { bn_mode_to_out(bn); return sso_get_data((sso_s *) bn); } ``` 將模式調整為輸出模式後,再呼叫 `sso_get_data`。 ### bn_size [程式碼](https://github.com/LJP-TW/fibdrv/blob/master/bignum.c#L132): ```c size_t bn_size(bignum *bn) { return sso_get_size((sso_s *) bn); } ``` 呼叫 `sso_get_size` 取得 `sso_s` 結構 size。 ## fibdrv ### fib_sequence 實作 fast Fibonacci algorithm ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/fibdrv.c#L29)): ```c static void fib_sequence(long long k, bignum *result) { bignum fk0; // F(k) bignum fk1; // F(k+1) bignum fk20; // F(2k) bignum fk21; // F(2k+1) bignum tmp1, tmp2; if (k < 2) { bn_release(result); bn_init(result, k); return; } bn_init(&fk0, 1); bn_init(&fk1, 1); bn_init(&fk20, 0); bn_init(&fk21, 0); bn_init(&tmp1, 0); bn_init(&tmp2, 0); long long mask = 1 << (fls(k) - 2); while (mask) { bn_add(&fk1, &fk1, &tmp1); bn_sub(&tmp1, &fk0, &tmp1); bn_mul(&fk0, &tmp1, &fk20); // fk20 = fk0 * (2 * fk1 - fk0) bn_mul(&fk0, &fk0, &tmp1); bn_mul(&fk1, &fk1, &tmp2); bn_add(&tmp1, &tmp2, &fk21); // fk21 = fk0 * fk0 + fk1 * fk1 bn_assign(&fk0, &fk20); // fk0 = fk20 bn_assign(&fk1, &fk21); // fk1 = fk21 if (k & mask) { bn_add(&fk0, &fk1, &fk20); // fk20 = fk0 + fk1 bn_assign(&fk0, &fk1); // fk0 = fk1 bn_assign(&fk1, &fk20); // fk1 = fk20 } mask >>= 1; } bn_assign(result, &fk0); // return fk0 bn_release(&fk0); bn_release(&fk1); bn_release(&fk20); bn_release(&fk21); bn_release(&tmp1); bn_release(&tmp2); } ``` 跳過 F~0~,從 F~1~ 開始運算。 ### fib_read 調整 `fib_read` ([程式碼](https://github.com/LJP-TW/fibdrv/blob/master/fibdrv.c#L99)): ```c /* calculate the fibonacci number at given offset */ static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { bignum result; int n; bn_init(&result, 0); fib_sequence(*offset, &result); n = bn_size(&result); if (n + 1 <= size) { if (copy_to_user(buf, bn_str(&result), n + 1)) return -EFAULT; } bn_release(&result); return n; } ``` `n + 1` 連同 NULL byte 複製到 user space。 若使用者 buffer size 不夠,則不進行複製,只回傳 Fibonacci number 字串長度告知使用者。 ## client buffer 改成用足夠的空間來接收字串: ```c #define BUF_SIZE 0x200 int main() { char buf[BUF_SIZE]; ... } ``` 改變輸出方式,從原本的以 `read` 回傳值得到結果,改成從 `buf` 得到結果: ```c int main() { ... sz = read(fd, buf, BUF_SIZE); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%s.\n", i, buf); ... } ``` ## Makefile ``` TARGET_MODULE := fibdrv_bn obj-m += $(TARGET_MODULE).o $(TARGET_MODULE)-objs := fibdrv.o bignum.o sso.o ``` `TARGET_MODULE` 需和 `fibdrv` 不同名稱;`$(TARGET_MODULE)-objs` 指定所需的 object files。 原先的 `make check` 使用到 `scripts/expected.txt`,其只是舊實作的結果輸出,將其刪去: ``` check: all $(MAKE) unload $(MAKE) load sudo ./client > out $(MAKE) unload @scripts/verify.py ```