---
title: 2023 年 Linux 核心設計/實作課程作業 —— fibdrv
image: https://i.imgur.com/KXUuZLV.png
description: 引導學員開發 Linux 核心模組,預期知悉核心模組如何掛載進入 Linux 核心、Virtual File System (VFS) 概況、character device driver 撰寫,和準備對應的測試及效能分析工具
tags: linux2023
---
# L04: fibdrv
> 主講人: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2023 年系統軟體課程](https://www.facebook.com/groups/system.software2023/)
:mega: 返回「[Linux 核心設計/實作](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表
## :package: 撰寫 Linux 核心模組
請自行參閱以下教材:
* 《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》
* [Introduction to Linux kernel driver programming](https://events.linuxfoundation.org/wp-content/uploads/2017/12/Introduction-to-Linux-Kernel-Driver-Programming-Michael-Opdenacker-Bootlin-.pdf)
* [Part 1: Introduction](http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/)
* [Part 2: A Character Device](http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/)
## :dart: `fibdrv`: 可輸出 Fibonacci 數列的 Linux 核心模組
### 前期準備
:::info
1. 開發環境預期和 ==[lab0](https://hackmd.io/@sysprog/linux2021-lab0)== 相似,如果你剛好重新安裝 Ubuntu Linux,請依據指示將必要的開發套件裝好;
2. ==自本次作業==,我們就有分析應用程式和 Linux 核心效能的需求,==不該在虛擬機器環境== 裡頭測試 (但 [container](https://linuxcontainers.org/) 或 [Linux KVM](https://www.linux-kvm.org/page/Main_Page) 可接受),否則會有效能偏差的問題 $\to$ 及早在實體機器上安裝好 GNU/Linux;
:::
* 自從 Linux 核心 4.4 版以來,Ubuntu Linux 預設開啟 `EFI_SECURE_BOOT_SIG_ENFORCE`,這使得核心模組需要適度的簽章才可掛載進入 Linux 核心,為了後續測試的便利,我們需要將 UEFI Secure Boot 的功能**關閉**,請見 [Why do I get “Required key not available” when install 3rd party kernel modules or after a kernel upgrade?](https://askubuntu.com/questions/762254/why-do-i-get-required-key-not-available-when-install-3rd-party-kernel-modules)
* 檢查 Linux 核心版本
```shell
$ uname -r
```
預期是大於等於 `5.4.0` 的版本,例如 `5.4.0-66-generic`。若在你的開發環境中,核心版本低於 `5.4` 的話,需要更新 Linux 核心,請自行參照相關文件
* 安裝 `linux-headers` 套件 (注意寫法裡頭有 `s`),以 [Ubuntu Linux 20.04 LTS](https://packages.ubuntu.com/focal/linux-headers-generic) 為例:
```shell
$ sudo apt install linux-headers-`uname -r`
```
* 確認 `linux-headers` 套件已正確安裝於開發環境
```shell
$ dpkg -L linux-headers-5.4.0-66-generic | grep "/lib/modules"
```
預期得到以下輸出:
```
/lib/modules/5.4.0-66-generic/build
```
* 檢驗目前的使用者身份
```shell
$ whoami
```
預期為「不是 root 的使用者名稱」,例如 `jserv` (或者你安裝 Ubuntu Linux 指定的登入帳號名稱)。由於測試過程需要用到 sudo,請一併查驗:
```shell
$ sudo whoami
```
預期輸出是 `root`
:notes: 在下列操作中,請==避免用 root 帳號輸入命令==,而該善用 `sudo`
:::danger
之後的實驗中,我們會重建 root file system,若濫用 root 權限,很可能就把 GNU/Linux 開發環境不小心破壞 (當然,你還是可重新安裝),現在開始養成好習慣
:::
* 安裝後續會用得到的工具
```shell
$ sudo apt install util-linux strace gnuplot-nox
```
* 取得原始程式碼
```shell
$ git clone https://github.com/sysprog21/fibdrv
$ cd fibdrv
```
* 編譯並測試
```shell
$ make check
```
預期會看到綠色的 ` Passed [-]` 字樣,隨後是
```
f(93) fail
input: 7540113804746346429
expected: 12200160415121876738
```
這符合預期,因為給定的 `fibdrv` 存在==缺陷==。
:::info
就因世界不完美,才有我們工程師存在的空間。
:::
* 觀察產生的 `fibdrv.ko` 核心模組
```shell
$ modinfo fibdrv.ko
```
預期可得以下輸出:
```
description: Fibonacci engine driver
author: National Cheng Kung University, Taiwan
license: Dual MIT/GPL
name: fibdrv
vermagic: 5.4.0-45-generic SMP mod_unload
```
* 觀察 `fibdrv.ko` 核心模組在 Linux 核心掛載後的行為(要先透過 `insmod` 將模組載入核心後才會有下面的裝置檔案 `/dev/fibonacci`)
模組載入核心:
```shell
$ sudo insmod fibdrv.ko
```
```shell
$ ls -l /dev/fibonacci
$ cat /sys/class/fibonacci/fibonacci/dev
```
新建立的裝置檔案 `/dev/fibonacci`,注意到 `236` 這個數字,在你的電腦也許會有出入。試著對照 [fibdrv.c](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c),找尋彼此的關聯。
```shell
$ cat /sys/module/fibdrv/version
```
預期輸出是 `0.1`,這和 [fibdrv.c](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c) 透過 `MODULE_VERSION` 所指定的版本號碼相同。
```shell
$ lsmod | grep fibdrv
$ cat /sys/module/fibdrv/refcnt
```
這兩道命令的輸出都是 `0`,意味著目前的 [reference counting](https://en.wikipedia.org/wiki/Reference_counting)。
## 計算 F~93~ (包含) 之後的 Fibonacci 數 (功能受限)
因 F~93~ 之後的運算會發生 overflow,導致無法正確地計算結果。可以使用底下方法計算 big number:
* 使用 [GCC `__int128`](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html) 型態,或者自行定義的結構:
```cpp
struct BigN {
unsigned long long lower, upper;
};
```
* 使用數字字串做運算,並運用 [Small/Short String Optimization](http://wdv4758h.github.io/notes/cpp/string-optimization.html)。[實作程式碼](https://github.com/AdrianHuang/fibdrv/tree/big_number)。
底下為數字字串加法實作,細節如下:
* 確保 `a` 字串長度大於 `b` 字串
* 將 `a` 與 `b` 字串反轉
* 逐字對數字字元做加法運算
* 將得出字串反轉,即得出最終結果
```cpp
static void string_number_add(xs *a, xs *b, xs *out)
{
char *data_a, *data_b;
size_t size_a, size_b;
int i, carry = 0;
int sum;
/*
* Make sure the string length of 'a' is always greater than
* the one of 'b'.
*/
if (xs_size(a) < xs_size(b))
__swap((void *) &a, (void *) &b, sizeof(void *));
data_a = xs_data(a);
data_b = xs_data(b);
size_a = xs_size(a);
size_b = xs_size(b);
reverse_str(data_a, size_a);
reverse_str(data_b, size_b);
char buf[size_a + 2];
for (i = 0; i < size_b; i++) {
sum = (data_a[i] - '0') + (data_b[i] - '0') + carry;
buf[i] = '0' + sum % 10;
carry = sum / 10;
}
for (i = size_b; i < size_a; i++) {
sum = (data_a[i] - '0') + carry;
buf[i] = '0' + sum % 10;
carry = sum / 10;
}
if (carry)
buf[i++] = '0' + carry;
buf[i] = 0;
reverse_str(buf, i);
/* Restore the original string */
reverse_str(data_a, size_a);
reverse_str(data_b, size_b);
if (out)
*out = *xs_tmp(buf);
}
```
如此一來,計算到 F~500~ [(The first 500 Fibonacci numbers )](https://home.hiwaay.net/~jalison/Fib500.html) 也是正確的,結果如下:
```shell
$ sudo ./client
...
Reading from /dev/fibonacci at offset 499, returned the sequence 86168291600238450732788312165664788095941068326060883324529903470149056115823592713458328176574447204501.
Reading from /dev/fibonacci at offset 500, returned the sequence 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125.
...
```
## 減少 copy to user 傳送的資料量
### 計算 leading zeros
以 $fib(100) = 354224848179261915075=$
$100110011001111011011011101101010011111000101100101001011111111000011_2$ 為例,先對 $fib(100)$ 取2為底的對數
$\log_2(354224848179261915075) = 68.26322731561805$
無條件進位後是69,要表達到 $2^{69}-1$ 總共需要 69 個 bit,每個 int 是 32 bit,共需要 3 個 int。
leading zeros 數量為 96-69 = 27個
可以用 bitwise 操作最高位的整數來計算 leading zeros。
```c
int clz (int x){
if (x == 0) return 32;
int n = 0;
if ((x & 0xFFFF0000) == 0) {n += 16; x <<= 16;}
if ((x & 0xFF000000) == 0) {n += 8; x <<= 8;}
if ((x & 0xF0000000) == 0) {n += 4; x <<= 4;}
if ((x & 0xC0000000) == 0) {n += 2; x <<= 2;}
if ((x & 0x80000000) == 0) n += 1;
return n;
}
```
這個程式碼以二分搜尋法找出 leading zeros,做法如下:
* 檢查最高 16 bit 是否全為零,全為零的話將 `x` 的首16個零移除(`x` 左移16 bit)
* 檢查
以 `fib(100)` 為例,最高位整數儲存的是 `0x00000013`
* 0x00000013 & 0xFFFF0000 == 0 , n = 16 , x = 0x00130000
* 0x00130000 & 0xFF000000 == 0 , n = 24 , x = 0x13000000
* 0x13000000 & 0xF0000000 != 0
* 0x13000000 & 0xC0000000 == 0 , n = 26 , x = 0x4C000000
* 0x4C000000 & 0x80000000 == 0 , n = 27
與之前的計算一致。
### 計算以 2 為底數的對數
以 32 減去 `clz(x)` 可獲得 x 所使用到的 bit 數量。
令 $n = 32 - clz(x)$
使用這些 bit 可表達的範圍為 $2^{n-1}$ ~ $2^n -1$,可得
$n = 32 - clz(x) = \lceil log_2x \rceil$
且
$n - 1 = 31 - clz(x) = \lfloor log_2x \rfloor$。
`fibdrv` 計算完後,會藉由 `copy_to_user()` 將結果傳回使用者空間。計算leading zeros,並將 `int32` 細分為 4 個位元組,呼叫`copy_to_user()` 時不傳送全為 0 的位元組。
```c
static int my_copy_to_user(const bn *bignum, char __user *buf)
{
int i = MAX_LENGTH_BN - 1;
for (; bignum->num[i] == 0; i--)
;
int lzbyte = __builtin_clz(bignum->num[i]) >> 3;
size_t size = sizeof(int32_t) * (i + 1) - lzbyte;
copy_to_user(buf, bignum->num, size);
return size;
}
```
使用 `gcc` 內建函式 `__builtin_clz` 來計算 leading zero bit 數量,其中 ` >> 3` 除以 8 並捨棄餘數換算成 leading zero byte。
針對 little-endian 架構,非零的位元組會被存在較低的記憶體位址。以 `fib(100)` 為例,需要 3 個整數,最高位整數是 `0x00000013`,倘若我們只要前 2 個整數以及第 3 個整數的 `0x13` 由 `copy_to_user()` 傳送。

將 `copy_to_user()` 的 size 指定為 2 個整數再加 1 個位元組,就不用傳送 leading zero byte。

將複製的 byte 數量作為 `read` 的回傳值傳回 user。
```c
sz = my_copy_to_user(a, buf);
...
return sz;
```
在 `client.c` 中將 `bn` 初始化後以 `memcpy` 複製資料即可。
```c
sz = read(fd, buf, 1);
bn a;
bn_init(&a);
memcpy(a.num, buf, sz);
int j = MAX_LENGTH_BN;
for (; a.num[j] == 0; j--)
;
a.length = j + 1;
...
```
### 預先計算儲存 $F(n)$ 所需的空間
倘若一開始就知道該配置多少空間給 Fibonacci 運算,就能避免空間浪費,或在計算過程中重複呼叫 `krealloc`。
首先,我們可用 [Binet 公式](https://en.wikipedia.org/wiki/Fibonacci_number)計算任意 Fibonacci 數列中第 $n$ 個數
$$Fn= \frac{(\phi^n)-(1-\phi)^n}{\sqrt5}$$ $$\phi = \frac{1+\sqrt5}{2} (golden \ ratio)$$
上式的近似值:
$$Fn= \frac{\phi^n}{\sqrt5} \ (since\ (1-\phi)^n\ is\ very\ small\ for\ large\ n)$$
知道近似值後,我們可使以 10 為底的對數計算其位數。具體如下:
$$
Digits = \log_{10}(\frac{\phi^n}{\sqrt5}) \\
Digits = n\log_{10}\phi - (\frac{\log_{10}5}{2}) \\
$$
當 $n$ 為足夠大的正整數時,
$$
F(n)\approx0.4472135955\times1.61803398875^n
$$
將近似值取 2 為底的對數,可得到需要使用的位元數
$$
log_2(0.4472135955\times1.61803398875^n)\approx-1.160964 + n\times0.694242
$$
再除以 32 可得到需要使用的 `uint32` 數量
$$
(-1.160964 + n\times0.694242)\div32 = -0.036280125 + n\times0.0216950625
$$
Linux 核心無法使用 (正確說法是,不能直接使用) 浮點數運算,因此將算式乘以 $10^{10}$,亦即:
$$\lfloor\frac{-362801250 + n\times216950625}{10^{10}}\rfloor+1
$$
此算式的結果就是該事先配置的 `uint32` 數量。
參考的 C 程式:
```c
#define BUFFSIZE (8 * sizeof(int) * LENGTH / 3 + 2)
#define LOGPHI (0.20898764025)
#define LOGSQRT5 (0.34948500216)
int main()
{
int offset = 100; /* TODO: try test something bigger than the limit */
int fd = open(FIB_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
exit(1);
}
float digits;
int size;
for (int i = 0; i <= offset; i++) {
// calculate how many digits are needed for fib(i)
// digits needed for fib(n) = n*LOG(Phi) - (LOG √5)
digits = i * LOGPHI - LOGSQRT5;
float buf_size = floor(digits / 9);
size = (int) buf_size;
char *buf = malloc(sizeof(char) * (BUFFSIZE * size));
lseek(fd, i, SEEK_SET);
long long time1 = read(fd, buf, 0);
long long time2 = read(fd, buf, 1);
printf("%d %lld %s\n", i, time1, buf);
free(buf);
}
```
## `fibdrv` 核心模組內部
觀察使用者層級 (user-level) 的程式如何與 fibdrv 互動:
```cpp
fd = open(FIB_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
exit(1);
}
for (i = 0; i <= offset; i++) {
sz = write(fd, write_buf, strlen(write_buf));
printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz);
}
for (i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, 1);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%lld.\n",
i, sz);
}
```
`fibdrv` 設計為一個 [character device](https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html),可理解是個能夠循序存取檔案,透過定義相關的函數,可利用存取檔案的系統呼叫以存取 (即 open, read, write, mmap 等等)。因此,使用者層級 (user-level 或 userspace) 的程式可透過 `read` 系統呼叫來得到輸出。
接著來看如何實作:
```cpp
/* calculate the fibonacci number at given offset */
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_sequence(*offset);
}
const struct file_operations fib_fops = {
.owner = THIS_MODULE,
.read = fib_read,
.write = fib_write,
.open = fib_open,
.release = fib_release,
.llseek = fib_device_lseek,
};
```
不難發現是透過 `fib_fops` 中的自行定義的 read 來實作讀取操作,而 `fib_read` 最終會回傳 `fib_sequence(*offset)`,因此就是透過讓使用者指定不同的 offest 作為 Fibonacci 數列的 $x$ 然後透過 read 的回傳值輸出 $fib(x)$ 給使用者。
在 [LP64](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 資料模式中,`long long` 僅寬 64 位元,因此若要表示更大的數,需要用兩個以上的變數表示一個數,在這種情況下作加法運算時,若使用 "+" operator ,會發生 overflow,可考慮實作一個更多位元的全加器。
$fib(92)$ 的計算結果用 16 進位表示是 `0x1 11F3 8AD0 840B F6BF`,超過 64 位元整數所夠表示的最大數值,因此考量到要正確輸出 $fib(100)$ 或數列更後面的數值,就必須使用到特製的結構來處理回傳值。以下是參考實作
```cpp
struct BigN {
unsigned long long lower, upper;
};
```
使用 struct BigN 來將一個數字分成兩部份:
* 高位的 64 bits 保存 upper 中;
* 低位的 64 bit 則是保存在 lower 中;
進行大數加法時,則需要注意 lower 是否需要進位到 upper:
```cpp
static inline void addBigN(struct BigN *output, struct BigN x, struct BigN y)
{
output->upper = x.upper + y.upper;
if (y.lower > ~x.lower)
output->upper++;
output->lower = x.lower + y.lower;
}
```
因為 `x.lower + ~x.lower = ~0`,移項後 `~x.lower = ~0 - x.lower`,亦即 `~x.lower` 表示 `x.lower` 跟最大值(`~0`) 的距離。所以若 `y.lower` 比 `x.lower` 距離最大值的距離還大,就表示相加後會 overflow,需要進位,這時執行 `output->upper++`。
### [Linux Virtual File System](https://www.win.tue.nl/~aeb/linux/lk/lk-8.html) 介面
透過 [Linux Virtual File System](https://www.win.tue.nl/~aeb/linux/lk/lk-8.html) 介面,本核心模組可將計算出來的 Fibonacci 數讓 `client.c` 程式得以存取。
VFS 提供一統各式檔案系統的共用介面,方便使用者操作不同的裝置或檔案時,可用一致的方式存取。Linux 的裝置驅動程式大致分為:
* Character Device Driver;
* Block Device Driver;
* Network Device Driver;

在使用裝置前需要對其定義一些 file operation,並將其註冊到 kernel 中。
依據 [/include/linux/fs.h](https://elixir.bootlin.com/linux/v4.15/source/include/linux/fs.h#L1692) 中的定義
```cpp
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
int (*open) (struct inode *, struct file *);
...
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
...
} __randomize_layout;
```
此手法可參見 [你所不知道的 C 語言:物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop)。
以本例來說,宣告一個 file_operations 的資料型態並設定一些對應到 VFS 操作的函式 (fib_read, fib_write 等等)。當在使用者模式程式中呼叫到 read 系統呼叫時,藉由 VFS 將其對應到 fib_read。
```cpp
const struct file_operations fib_fops = {
.owner = THIS_MODULE,
.read = fib_read,
.write = fib_write,
.open = fib_open,
.release = fib_release,
.llseek = fib_device_lseek,
};
```
對照 [Linux 核心設計: 檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system)。
## :calling: Linux 核心的浮點數運算
Robert Love 在 [Linux Kernel Development](https://www.amazon.com/Linux-Kernel-Development-Robert-Love/dp/0672329468/) 一書論及浮點運算:
> No (Easy) Use of Floating Point
> When using floating-point instructions kernel normally catches a trap and then initiates the transition from integer to floating point mode. Unlike user-space, the kernel does not have the luxury of seamless support for floating point because it cannot easily trap itself. Using a floating point inside the kernel requires manually saving and restoring the floating point registers. Except in the rare cases, no floating-point operations are in the kernel.
* 解說: [Use of floating point in the Linux kernel](https://stackoverflow.com/questions/13886338/use-of-floating-point-in-the-linux-kernel)
Rusty Russell 在 [Unreliable Guide To Hacking The Linux Kernel](https://docs.kernel.org/kernel-hacking/hacking.html) 則說:
> The FPU context is not saved; even in user context the FPU state probably won't correspond with the current process: you would mess with some user process' FPU state. If you really want to do this, you would have to explicitly save/restore the full FPU state (and avoid context switches). It is generally a bad idea; use fixed point arithmetic first.
#### Lazy FP state restore
[CVE-2018-3665](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-3665) 存在於 Intel Core 系列微處理器中,因為 speculative execution(推測執行)技術中的一些缺陷加上特定作業系統中對 FPU(Floating point unit)進行 context switch 所產生的漏洞,允許一個本地端的程式洩漏其他程序的 FPU 暫存器內容。
[Lazy FP state leak](https://en.wikipedia.org/wiki/Lazy_FP_state_restore) 的原理是透過 FPU 的 lazy state switching 機制達成。因為 FPU 和 SIMD 暫存器不一定會在每個任務持續被使用到,因此作業系統排程器可將不需要使用到 FPU 的任務,標註為不可使用 FPU,而不更改裡面的內容。
然而,在現今的[亂序執行](https://en.wikipedia.org/wiki/Out-of-order_execution) CPU 中,lazy state switching 裡會設定的 "FPU not available" 可能沒辦法馬上被偵測到,導致我們在 process B,但仍然可以存取到 process A 的 FPU 暫存器內容。
基於上述原因,儘管我們在 Linux 核心模式中仍可使用浮點運算,但這不僅會拖慢執行速度,還需要手動儲存/取回相關的 FPU 暫存器,因此核心文件不建議在核心中使用到浮點運算。
### 浮點運算時間差異比較
[afcidk](https://github.com/afcidk) 透過開發簡單的 Linux 核心模組,來測試在單純的浮點數運算及整數運算花費的時間差異。
> 相關的程式碼可見 [floating.c](https://gist.github.com/afcidk/7178ef368d98ee4b49c7a1acc3704303)

可見到核心模式中的浮點數運算,時間成本較整數運算高。