# 2023q1 Homework3 (fibdrv)
contributed by < [zeddyuu](https://github.com/zeddyuu) >
## 實驗環境
```shell
$ neofetch --stdout
zhenyu@zhenyu-HP-Laptop-15s-du2xxx
OS: Kubuntu 22.04.1 LTS x86_64
Host: HP Laptop 15s-du2xxx
Kernel: 5.15.0-43-generic
Uptime: 53 mins
Packages: 1993 (dpkg), 7 (snap)
Shell: bash 5.1.16
Resolution: 1920x1080
DE: Plasma 5.24.4
WM: KWin
Theme: [Plasma], Breeze [GTK2/3]
Icons: [Plasma], breeze-dark [GTK2/3]
Terminal: konsole
CPU: Intel i5-1035G1 (8) @ 3.600GHz
GPU: NVIDIA GeForce MX330
GPU: Intel Iris Plus Graphics G1
Memory: 2089MiB / 11754MiB
```
```shell
$ gcc -v
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04)
```
## 自我檢查清單
- [x] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗;
- [x] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
- [x] 複習 C 語言 [數值系統](https://hackmd.io/@sysprog/c-numerics) 和 [bitwise operation](https://hackmd.io/@sysprog/c-bitwise),思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
- [ ] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑
- [x] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 $\to$ 搭配閱讀〈[並行和多執行緒程式設計](https://hackmd.io/@sysprog/concurrency)〉
## 開發紀錄
剛開始 `make check` 後得到錯誤訊息
`insmod: ERROR: could not insert module fibdrv.ko: Operation not permitted`
將 secure boot 關閉並確認
```shell
$ sudo dmesg|grep -i secure
[ 0.000000] secureboot: Secure boot disabled
[ 0.019923] secureboot: Secure boot disabled
[ 1.143830] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing: 61482aa2830d0ab2ad5af10b7250da9033ddcef0'
[ 1.143842] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (2017): 242ade75ac4a15e50d50c84b0d45ff3eae707a03'
[ 1.143854] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (ESM 2018): 365188c1d374d6b07c3c8f240f8ef722433d6a8b'
[ 1.143867] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (2019): c0746fd6c5da3ae827864651ad66ae47fe24b3e8'
[ 1.143878] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (2021 v1): a8d54bbb3825cfb94fa13c9f8a594a195c107b8d'
[ 1.143889] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (2021 v2): 4cf046892d6fd3c9a5b03f98d845f90851dc6a8c'
[ 1.143900] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (2021 v3): 100437bb6de6e469b581e61cd66bce3ef4ed53af'
[ 1.143912] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (Ubuntu Core 2019): c1d57b8f6b743f23ee41f4f7ee292f06eecadfb9'
[ 1.330599] sdhci: Secure Digital Host Controller Interface driver
[ 24.730196] Bluetooth: hci0: Secure boot is enabled
```
再次編譯執行如預期結果
```shell
Passed [-]
f(93) fail
input: 7540113804746346429
expected: 12200160415121876738
```
### 使用 Fast Doubling 改寫
根據 [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a) 中所提到的 Fast Doubling 方法進行改寫,可以更快的方式計算。
$$
\begin{split}
F(2k) &= F(k)[2F(k+1) - F(k)] \\
F(2k+1) &= F(k+1)^2+F(k)^2
\end{split}
$$
```c
static long long fib_sequence(long long k)
{
/* FIXME: C99 variable-length array (VLA) is not allowed in Linux kernel. */
if (k < 2)
return k;
long long a = 0, b = 1;
int len_zero = __builtin_clzll(k), counter = 64 - len_zero;
k <<= len_zero;
for (; counter; k <<= 1, counter--) {
long long t1 = a * (2 * b - a);
long long t2 = b * b + a * a;
a = t1, b = t2;
if (k & 1ULL << 63) {
t1 = a + b;
a = b;
b = t1;
}
}
return a;
}
```
用 `builtin_clzll` 先算出 leading zero 的個數,再位移到第一個出現的 1 開始計算,迴圈執行次數也能由此推論出來,原理就是透過位元左移達到乘 2 的效果,並且用 1 在最高位其餘為 0 的 mask 確認 MSB 是否為 1 來判斷是否要將 Fibnacci number 往後計算一位。
若不使用硬體指令 `clz` 的話則必須使用一個從 64 開始的迴圈,使用硬體指令後可以省略不必要的計算,直接從最高位的 1 開始計算
![](https://i.imgur.com/IeINLKi.png)
上圖為有使用 clz 以及 沒有使用 clz 的執行時間差距,其中執行時間以 kernel time 為準。
```shell
+Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
+Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
+Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
+Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429.
+Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
+Reading from /dev/fibonacci at offset 95, returned the sequence 7540113804746346429.
```
另外將乘以 2 部份改為用 bitwise 完成,觀察是否能減少乘法運算的成本
```c
long long t1 = a * ((b << 1) - a);
```
![](https://i.imgur.com/fcbINE6.png)
可以看到 bitwise 對於減少乘法運算的成本是有效的
目前可以正確計算到 $F_{92}$ 的值, $F_{93}$ 以後運算會 overflow。
### 字串運算
利用字串做運算,實作字串加法可以突破 $F_{93}$
```c
static void add_string(char *x, char *y, char *next) {
int x_size = strlen(x), y_size = strlen(y);
int i, carry = 0;
int sum;
if (x_size >= y_size) {
for(i = 0; i < y_size; i++) {
sum = (x[i] - '0') + (y[i] - '0') + carry;
next[i] = '0' + sum % 10;
carry = sum / 10;
}
for(i = y_size; i < x_size; i++) {
sum = (x[i] - '0') + carry;
next[i] = '0' + sum % 10;
carry = sum / 10;
}
} else {
for(i = 0; i < x_size; i++) {
sum = (x[i] - '0') + (y[i] - '0') + carry;
next[i] = '0' + sum % 10;
carry = sum / 10;
}
for(i = x_size; i < y_size; i++) {
sum = (y[i] - '0') + carry;
next[i] = '0' + sum % 10;
carry = sum / 10;
}
}
if (carry) {
next[i] = '1';
}
next[++i] = '\0';
}
```
此為字串加法的函式,根據[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a)的演算法實作,由 low index 累加到 high index 每個步驟都會計算進位看是否在下一位累加 1。
```c
void reverse_string(char *str, size_t size){
for (int i = 0; i < size - i; i++){
str[i] = str[i] ^ str[size - i];
str[size -i] = str[i] ^ str[size - i];
str[i] = str[i] ^ str[size - i];
}
}
```
此為反轉字串的函式,頭尾做交換直到迴圈結束即可完成,交換一般都需要一個暫存的變數,但根據[你所不知道的 C 語言:數值系統](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator)中用 XOR 操作做變數交換(在[你所不知道的C語言:遞迴呼叫篇](https://hackmd.io/@sysprog/c-recursion#%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90%EF%BC%9A%E5%AD%97%E4%B8%B2%E5%8F%8D%E8%BD%89),也有提及到字串反轉的演算法,但使用限定使用遞迴操作),可以節省額外暫存變數的記憶體空間,原理是用 a XOR a = 0 , b XOR 0 = b 來完成。
```c
struct strbuf
{
char fib[128];
};
static long long fib_sequence_string(long long k, char *buf) {
struct strbuf *res = kmalloc(sizeof(struct strbuf) * (k + 1), GFP_KERNEL);
strncpy(res[0].fib, "0", 2);
strncpy(res[1].fib, "1", 2);
for (int i = 2; i <= k; i++) {
add_string(res[i - 1].fib, res[i - 2].fib, res[i].fib);
}
size_t size = strlen(res[k].fib);
reverse_string(res[k].fib, size - 1);
__copy_to_user(buf, res[k].fib, size + 1);
return size;
}
```
先為每一個 Fibonacci number 定義一個結構,最多可以用 128 長度的字串代表數字,可以解決 64 位元運算 overflow 的問題。
[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b)中是每次都將字串反轉後做加法運算,但可以做完計算後在最後呼叫一次反轉就好,省略函式呼叫,且在[作業說明](https://hackmd.io/@sysprog/linux2022-fibdrv#%E9%97%9C%E6%96%BC-clientc)中提到 kernel space 以及 user space 進行溝通的方法必須透過 `copy_to_user()` 將kernel space 記憶體內容複製到 user space 記憶體之中,因為 user space 無法直接存取 kernel space,參考 [Why do you have to use copy_to_user()/copy_from_user() to access user space from the kernel?](https://stackoverflow.com/questions/12666493/why-do-you-have-to-use-copy-to-user-copy-from-user-to-access-user-space-from) 以及一些學過的 OS。
```shell=
$(MAKE) unload
$(MAKE) load
sudo ./client > out
$(MAKE) unload
@diff -u out scripts/expected.txt && $(call pass)
@scripts/verify.py
```
最後 Makefile 中第 5 行有將 out 跟 expected.txt 比較,要將內容改成正確的值比較過後才會 pass。
### 時間測量和效能分析
引入 `ktime_t` 來計算 kernel space 執行時間,在 [The high-resolution timer API ](https://lwn.net/Articles/167897/) 中有提到此資料結構會根據不同的架構上有所變化,像是 64 位元系統上是以 ns 為單位的整數值,在 32 位元系統上則是一個 two-field 結構,一段 32 位元資料存秒數,一段以 ns 為單位儲存。
```c
static long long fib_time_proxy(long long k, char *buf)
{
kt = ktime_get();
long long res = fib_sequence_string(k, buf);
kt = ktime_sub(ktime_get(), kt);
return res;
}
```
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_time_proxy(*offset, buf);
}
```
根據作業說明透過 `fib_time_proxy` 來計算 kernel space 中執行運算的時間。
```c
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
return ktime_to_ns(kt);
}
```
因為 write 操作暫時沒有作用,可以用來輸出上一次的執行時間給 user space 的 `client.c`。
```c
static inline long nanotime()
{
struct timespec t;
clock_gettime(0, &t);
return t.tv_nsec;
}
```
```c
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
```
在 `client.c` 中定義 `nanotime()` 可以透過 `clock_gettime()` 取得當前時間,其中時間是用 `timespec` 這個結構體來儲存,其結構是定義在 `time.h` 標頭檔中,可以存秒為單位也可以存 ns 為單位, `clock_gettime()` 可以得到當前時間,第一個參數需給定時間的類型,這裡選用 `CLOCK_REALTIME` 代表系統的實際時間(但我這邊系統找不到定義,所以根據他的巨集給定 0)。
```c
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
long long start = nanotime();
sz = read(fd, buf, sizeof(buf));
long long user_time = nanotime() - start;
long long kernel_time = write(fd, write_buf, sizeof(write_buf));
printf("Writing to " FIB_DEV ", returned the kernel time %ld\n", kernel_time);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, buf);
fprintf(data, "%d %lld %lld %lld\n", i, kernel_time, user_time, user_time - kernel_time);
}
```
為了要使用[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c)中提到的 [python script](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c) 來生成圖表比較執行時間,透過寫檔的方式將資料寫入檔案再透過 script 讀出資料進行統計,此 script 使用標準化的方式先降低資料的離散程度。
![](https://i.imgur.com/vWjK8ZH.png)
![](https://i.imgur.com/JvE2h3e.png)
這裡可以複習 process 透過 system call 請求 kernel 服務的[流程](https://hackmd.io/@combo-tw/Linux-%E8%AE%80%E6%9B%B8%E6%9C%83/%2F%40combo-tw%2FBJPoAcqQS#%E5%91%BC%E5%8F%AB%E6%B5%81%E7%A8%8B)(圖都來自恐龍本),process 提出 system call 請求後會經由軟體中斷進入 kernel mode ,並查詢 system call table 找到對應的中斷服務常式,完成後發出中斷通知 OS 完成,這個系統呼叫的過程在 process 的壽命中是由 `running` 到 `ready` ,之後還要由 CPU scheduler 排程給 CPU 執行,所以有一些額外的時間要考慮進去,因此在計算時間上必須要細分出幾個細項。
可以將時間分為 `user time`、`real time`、`kernel time`,其中 `real time` 是完整程式執行的時間,且包含了其他 process 以及此 process blocked(e.g. wait for I/O) 的時間,所以我們關注的只有`kernel time` 以及 `user time` ,前者是在 kernel space 執行的 CPU time 總和,後者是在 user space 執行的 CPU time 總和,在程式中只計算 `read` 這個系統呼叫的此兩項標準來做評估。
![](https://i.imgur.com/7dEqap0.png)
以實作的字串運算來計算到 $F_{100}$ 作為時間測量標準,其中將 `user time` 減去 `kernel time` 得到的就是在 user space 以及 kernel space 之間額外傳遞的時間。
由圖表可以看到在計算量較小時,執行時間上呈現劇烈的波動,在後面計算量變大時波動幅度會較為區緩。
### 限定 CPU 給特定程式使用
根據[參考網址](https://askubuntu.com/questions/165075/how-to-get-isolcpus-kernel-parameter-working-with-precise-12-04-amd64)以及[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c#%E9%99%90%E5%AE%9A-CPU-%E7%B5%A6%E7%89%B9%E5%AE%9A%E7%9A%84%E7%A8%8B%E5%BC%8F%E4%BD%BF%E7%94%A8)在 `/etc/default/grub` 中的 `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"` 後方加入 `isolcpus=1` 之後 update-grub,重開機後就可以孤立出 CPU Core 1。
在 `/sys/devices/system/cpu/isolated` 中也可以確認是否正確啟用。
用 `top` 指令確認
```shell
top - 09:51:27 up 34 min, 5 users, load average: 0.72, 1.29, 1.08
Tasks: 353 total, 2 running, 351 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.4 us, 1.1 sy, 0.0 ni, 98.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 1.0 us, 0.7 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.7 us, 0.3 sy, 0.0 ni, 99.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 1.3 us, 0.7 sy, 0.0 ni, 98.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 1.0 us, 0.0 sy, 0.0 ni, 99.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 0.3 us, 1.0 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 1.7 us, 0.3 sy, 0.0 ni, 98.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 11752.6 total, 6750.9 free, 2302.8 used, 2698.9 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 8556.2 avail Mem
```
可以看到 Core 1 的確被孤立出來,沒有行程使用,但 cpu 有時候會突然有反應,並不是完全閒置,推測是為了處理一些中斷請求所造成的
透過觀察 `/proc/interrupts` 可以觀察 IRQ 的統計情況,會顯示每個 CPU 處理中斷的次數,發現確實是因為如此。
```shell
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
1: 2981 0 0 0 0 448 0 0 IR-IO-APIC 1-edge i8042
8: 0 0 0 0 0 0 0 0 IR-IO-APIC 8-edge rtc0
9: 10 11 14 0 0 0 0 0 IR-IO-APIC 9-fasteoi acpi
14: 2 0 0 0 0 0 0 0 IR-IO-APIC 14-fasteoi INT3455:00
16: 1319778 0 0 327770 0 0 0 0 IR-IO-APIC 16-fasteoi i801_smbus, idma64.0, i2c_designware.0, mmc1
19: 0 0 0 0 0 0 0 0 IR-IO-APIC 19-fasteoi mmc0
....
```
### 以特定 CPU 執行程式
```shell
sudo taskset -c 1 ./client
```
使用 `taskset` 將行程固定在 CPU Core 1 執行,再跑一次 driver.py 查看結果
![](https://i.imgur.com/sOW2sSk.png)
變動沒有很明顯,繼續試其他項目。
### 排除干擾效能分析的因素
* 抑制 address space layout randomization (ASLR)
```shell
$ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```
ASLR 能讓每次程式以及動態函式庫的載入位置都不相同,以防止能填入固定的位置到堆疊中的返回位置中,若每次都是固定的返回位置,攻擊者能透過其堆疊中的返回位址來替換為另一條指令的位址,並跳轉到記憶體的特定位置。
在 Linux 系統中,ASLR 可能會影響到系統效能,尤其是在 x86 架構上,因為要讓程式在運行時兼容 ASLR ,在編譯的時候要指定一個叫做 PIE (PositionIndependent Executable) 的選項,根據論文 [Too much PIE is bad for performance](https://hexhive.epfl.ch/publications/files/12TRpie.pdf) 會帶來一些效能上的影響。
參考 [Differences Between ASLR on Windows and Linux](https://insights.sei.cmu.edu/blog/differences-between-aslr-on-windows-and-linux/) 以及 [ASLR Wiki](https://zh.wikipedia.org/zh-tw/%E4%BD%8D%E5%9D%80%E7%A9%BA%E9%96%93%E9%85%8D%E7%BD%AE%E9%9A%A8%E6%A9%9F%E8%BC%89%E5%85%A5)
* 針對 Intel 處理器,關閉 turbo mode
```shell
$ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
* 設定 scaling_governor 為 performance。準備以下 shell script,檔名為 `performance.sh`:
```shell
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
echo performance > ${i}
done
```
之後再用 `$ sudo sh performance.sh` 執行
![](https://i.imgur.com/yUnQaqe.png)
可以看到執行時間降低了很多,經過交叉測試,是因為前面我的 CPUFREQ 設定皆為 powersave 限制了 CPU 時脈速度達到省電的功能,所以時間才會差很多,但波動幅度仍舊很大。
### SMP IRQ affinity
閱讀了 [SMP IRQ affinity](https://www.kernel.org/doc/html/next/core-api/irq/irq-affinity.html),其實跟 processor affinity 很像,只是對象換成了 IRQ (Interrupt Request) ,而 SMP 則是在描述多處理器的架構,可以理解為系統存在多個相同的 CPU ,共同使用相同的主記憶體以及資源,彼此在系統中擁有相同的地位。
故可以藉由調整 IRQ affinity 來避免我們想要指定的 cpu 去處理中斷請求,再將行程放到此 cpu 核心上看是否能解決這個波動的問題。
![](https://i.imgur.com/BmxRRyt.png)
波動幅度仍然存在,但可以看到突然升起的波峰部分似乎變少了,推測是有一些 IRQ Affinity 是沒辦法更動的,所以依舊存在 CPU 去處理 IRQ 的情況。
## 大數運算
### 大數運算中如何加速運算
在 [bignum/mul.c](https://github.com/sysprog21/bignum/blob/master/mul.c) 中使用到 `Karatsuba` 演算法去減少乘法的運算成本。
### 大數運算中如何減少記憶體操作
## 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。
```c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define FIB_DEV "/dev/fibonacci"
void run()
{
long long sz;
char buf[128];
char write_buf[] = "testing writing";
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);
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, sizeof(buf));
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, buf);
}
close(fd);
}
int main()
{
pthread_t pt[2];
for (int i = 0; i < 2; i++) {
pthread_create(&pt[i], NULL, run, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(&pt[i], NULL);
}
return 0;
}
```
新增一個 thread.c 來測試 mutex 的功能,而 thread 執行的函式為 run ,因為只是要測試功能,故為原來 client.c 的簡化版本。
```shell
sudo ./thread
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Reading from /dev/fibonacci at offset 3, returned the sequence 2.
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 4, returned the sequence 3.
Reading from /dev/fibonacci at offset 5, returned the sequence 5.
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Reading from /dev/fibonacci at offset 6, returned the sequence 8.
Reading from /dev/fibonacci at offset 7, returned the sequence 13.
Reading from /dev/fibonacci at offset 8, returned the sequence 21.
```
可以看到因為 mutex 先被其中一個 thread 取得,則另外一個 thread 無法開啟 /dev/fibonacci
```c
static int fib_open(struct inode *inode, struct file *file)
{
if (!mutex_trylock(&fib_mutex)) {
printk(KERN_ALERT "fibdrv is in use");
return -EBUSY;
}
return 0;
}
```
參考 [mutex_trylock](https://archive.kernel.org/oldlinux/htmldocs/kernel-locking/API-mutex-trylock.html) 文件,此 API 的功能是一個 thread 會嘗試取得 mutex,且此動作是 atomically 代表要不一氣呵成地完成,要不就是失敗,不會有中間狀態,且 mutex 只能被持有 (acquire) 的 thread 釋放,保證共享資源不會發生有兩個以上的 thread 同時使用造成 [race condition](https://en.wikipedia.org/wiki/Race_condition)。
```shell
Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738.
Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167.
Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905.
Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072.
Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977.
Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049.
Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
```
將 fibdrv 中 mutex 相關的程式碼註解掉,可以發現兩個 thread 都可以正常執行,但這是發生在兩個 thread 都是做讀取的情況,若是有寫入檔案相關可能會發生 race condition,導致讀取發生資料錯誤(這裡的 fib 數值錯誤是因為使用 fast doubling 版本,正確數值只能顯示到 $F_{92}$,沒有使用大數運算或是字串運算)。
:::warning
做實驗確認,並觀察實際行為,避免籠統地說「可能會發發生 race condition」,我們要知曉「行為」並根治問題。
另外,使用 (mutex) lock 會影響 Fibonacci 數運算效率,你該提出兼顧並行處理的正確和運算效率的方案。
:notes: jserv
>收到
:::
設計一個實驗去驗證當有寫入檔案時會不會發生 race condition,將文字檔案當成是共享資源,實驗在有無 mutex 下是否會正確執行
將 `fib_write` 的功能改為寫檔
```c
/* write operation is skipped */
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
struct file *fptr;
loff_t pos;
fptr = filp_open("/home/usr/fibdrv/fib_sequence.txt", O_RDWR | O_APPEND, 0644);
if (IS_ERR(fptr)) {
return -1;
}
long long res = fib_sequence(*offset);
char formatted_str[80];
int str_size = snprintf(NULL, 0, "offset %lld = %lld\n", *offset, res);
sprintf(formatted_str, "offset %lld = %lld\n", *offset, res);
pos = fptr->f_pos;
kernel_write(fptr, formatted_str, str_size, &pos);
filp_close(fptr, NULL);
return 1;
}
```
為了在 kernel space 開檔案,使用了 `filp_open`,其回傳值和參數跟 user space 的 `fopen` 一樣,都是回傳 `FILE *`,參數則是檔名和開檔的模式以及創建檔案時的權限,開檔的權限對應了 User, Group, Others 並且用三個位元表示 Read, Write, Execute,像我這邊設定成 User 可以進行讀寫就是 110,轉十進位後就是 6,之後處理開檔失敗則回傳 -1。
接著就是字串處理的部份,先預先宣告一個長度一定不會超過計算結果長度的字元陣列,防止 buffer overflow,取得 `fib_sequence` 的計算結果後使用 `snprintf` 計算出格式化字串的長度,再使用 `sprintf` 寫入字元陣列,正在尋找有沒有更省空間的方法。
```c
struct file *fptr;
loff_t pos;
fptr = filp_open("/home/usr/fibdrv/fib_sequence.txt", O_RDWR | O_APPEND,
0644);
if (IS_ERR(fptr)) {
return -1;
}
long long res = fib_sequence(*offset);
char *formatted_str;
formatted_str = kasprintf(GFP_KERNEL, "offset %lld = %lld\n", *offset, res);
if (formatted_str == NULL) {
return -1;
}
printk("%s", formatted_str);
pos = fptr->f_pos;
kernel_write(fptr, formatted_str, strlen(formatted_str), &pos);
filp_close(fptr, NULL);
kfree(formatted_ptr);
return 1;
```
更省空間的方法,使用 `kasprintf` 去動態輸出格式化字串,如此一來就不需要事先宣告比較大的字元陣列,節省空間,在過程中也使用 `printk` 搭配 `dmesg` 去觀察輸出的字串是否正確
```shell
sudo dmesg | tail
[15389.339349] offset 83 = 99194853094755497
[15389.339426] offset 84 = 160500643816367088
[15389.339460] offset 85 = 259695496911122585
[15389.339491] offset 86 = 420196140727489673
[15389.339516] offset 87 = 679891637638612258
[15389.339551] offset 88 = 1100087778366101931
[15389.339577] offset 89 = 1779979416004714189
[15389.339628] offset 90 = 2880067194370816120
[15389.339655] offset 91 = 4660046610375530309
[15389.339678] offset 92 = 7540113804746346429
```
最後是使用 `kernel_write` 寫入檔案,一開始是想要使用 `vfs_write` 來進行,但看 [Unknown symbol vfs_write (err -2) in kernel module in kernel 4.20](https://stackoverflow.com/questions/53917041/unknown-symbol-vfs-write-err-2-in-kernel-module-in-kernel-4-20) 討論後,發現 Linux v4.14 以後已經不會 export `vfs_write` 給 module 使用,改用 `kernel_write` 來完成寫檔,寫入的長度為預先計算出的字串本身長度,最後再用 `filp_close` 關檔。
```c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define FIB_DEV "/dev/fibonacci"
void run()
{
long long sz;
char buf[128];
char write_buf[] = "testing writing";
int offset = 92; /* TODO: try test something bigger than the limit */
int fd = open(FIB_DEV, O_RDWR);
while (fd < 0) {
perror("Failed to open character device");
//exit(1);
fd = open(FIB_DEV, O_RDWR);
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = write(fd, buf, sizeof(buf));
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%lld.\n",
i, sz);
}
close(fd);
}
int main()
{
pthread_t pt[2];
for (int i = 0; i < 2; i++) {
pthread_create(&pt[i], NULL, run, NULL);
}
for (int i = 0; i < 2; i++) {
printf("thread %d is running\n", i);
pthread_join(&pt[i], NULL);
printf("thread %d is end\n", i);
}
return 0;
}
```
client 端使用兩個 thread 執行 fib_write 寫入檔案,跟讀取差不多,只是換成 write 操作,且將 fib_open 的動作改為 while,使得不會因為另外一個 thread 嘗試取得 mutex 而導致所有的 thread 因為 `exit(1)` 而停止,如此一來所有的 thread 都會全部執行,雖然會因為 while 迴圈不斷檢查以及嘗試取得 mutex 而在時間效能上有所影響,但可以保證寫入的順序是正確的。
* **在有 mutex 的控制下**
終端訊息
```shell
sudo ./thread
thread 0 is running
Reading from /dev/fibonacci at offset 0, returned the sequence 1.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
Reading from /dev/fibonacci at offset 3, returned the sequence 1.
Failed to open character device: Device or resource busy
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 4, returned the sequence 1.
Failed to open character device: Device or resource busy
.
.
.
Reading from /dev/fibonacci at offset 89, returned the sequence 1.
Reading from /dev/fibonacci at offset 90, returned the sequence 1.
Reading from /dev/fibonacci at offset 91, returned the sequence 1.
Reading from /dev/fibonacci at offset 92, returned the sequence 1.
```
檔案內容
```
offset 0 = 0
offset 1 = 1
offset 2 = 1
offset 3 = 2
offset 4 = 3
offset 5 = 5
offset 6 = 8
offset 7 = 13
offset 8 = 21
offset 9 = 34
offset 10 = 55
offset 11 = 89
.
.
.
offset 85 = 259695496911122585
offset 86 = 420196140727489673
offset 87 = 679891637638612258
offset 88 = 1100087778366101931
offset 89 = 1779979416004714189
offset 90 = 2880067194370816120
offset 91 = 4660046610375530309
offset 92 = 7540113804746346429
offset 0 = 0
offset 1 = 1
offset 2 = 1
offset 3 = 2
offset 4 = 3
offset 5 = 5
offset 6 = 8
offset 7 = 13
offset 8 = 21
.
.
.
offset 84 = 160500643816367088
offset 85 = 259695496911122585
offset 86 = 420196140727489673
offset 87 = 679891637638612258
offset 88 = 1100087778366101931
offset 89 = 1779979416004714189
offset 90 = 2880067194370816120
offset 91 = 4660046610375530309
offset 92 = 7540113804746346429
```
* **在沒有 mutex 的控制下**
終端訊息
```shell
sudo ./thread
thread 0 is running
Reading from /dev/fibonacci at offset 0, returned the sequence 1.
Reading from /dev/fibonacci at offset 0, returned the sequence 1.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 2, returned the sequence 1.
.
.
.
Reading from /dev/fibonacci at offset 85, returned the sequence 1.
Reading from /dev/fibonacci at offset 92, returned the sequence 1.
Reading from /dev/fibonacci at offset 86, returned the sequence 1.
Reading from /dev/fibonacci at offset 87, returned the sequence 1.
Reading from /dev/fibonacci at offset 88, returned the sequence 1.
Reading from /dev/fibonacci at offset 89, returned the sequence 1.
Reading from /dev/fibonacci at offset 90, returned the sequence 1.
Reading from /dev/fibonacci at offset 91, returned the sequence 1.
Reading from /dev/fibonacci at offset 92, returned the sequence 1.
```
檔案內容
```shell
offset 0 = 0
offset 0 = 0
offset 1 = 1
offset 2 = 1
offset 1 = 1
offset 2 = 1
offset 3 = 2
.
.
.
offset 87 = 679891637638612258
offset 88 = 1100087778366101931
offset 89 = 1779979416004714189
offset 90 = 2880067194370816120
offset 91 = 4660046610375530309
offset 92 = 7540113804746346429
```
可以看到在有 mutex 的控制下,寫檔可以按照取得 mutex 的順序寫入,在沒有 mutex 的控制下,寫檔過程發生 race condition,對共享資源的寫入順序成為亂序,在這個實驗中學到一些在 kernel module 處理字串以及處理檔案的方式。