# 2019q1 Homework2 (fibdrv)
contributed by < `zodf0055980` >
###### tags: `Linux 核心設計`
## Reviewed by `bauuuu1021`
* 建議一次 commit 著重一個主要修改,例如 [06f574f](https://github.com/zodf0055980/fibdrv/commit/06f574f2da2069845d6bdc838d0d9a3ec7fc6342) 可以拆成兩次,分別修改 client.c 及 showtime.gp;或在 commit message 中更清楚提及 ==以 gnuplot 展示時間變化==
* 共筆中有許多實驗都有製圖,但在 GitHub 中只有看到其中一個圖的 script。建議將其他的 script 也放到 GitHub 中,並在 Makefile 中加入對應操作,方便他人重現實驗或提出改進
# 自我檢查清單
- [ ] 檔案 `fibdrv.c` 裡頭的 `MODULE_LICENSE`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION` 等巨集做了什麼事,可以讓核心知曉呢? `insmod` 這命令背後,對應 Linux 核心內部有什麼操作呢?請舉出相關 Linux 核心原始碼並解讀
在 [linux/module.h](https://github.com/torvalds/linux/blob/master/include/linux/module.h)中可以看到:
```clike
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)
#define MODULE_VERSION(_version) MODULE_INFO(version, _version)
```
都可以發現這些都對應到 MODULE_INFO(),如果在同一個檔案下搜尋能發現 MODULE_INFO() 對應到 `__MODULE_INFO()`
```clike
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
```
再到 [linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h)可以找到
```clike
#define __MODULE_INFO(tag, name, info) \
static const char __UNIQUE_ID(name)[] \
__used __attribute__((section(".modinfo"), unused, aligned(1))) \
= __stringify(tag) "=" info
```
再依序把上面給拆解,發現在 [linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 有這段
```clike
#include <linux/kernel.h>
```
再到 linux/kernel.h 中發現有
```clike
#include <linux/compiler.h>
```
因此至 [compiler.h](https://github.com/torvalds/linux/blob/bb617b9b4519b0cef939c9c8e9c41470749f0d51/include/linux/compiler.h) 中可以發現
```clike
# define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __LINE__)
```
而在 [linux/compiler_types.h](https://github.com/torvalds/linux/blob/9105b8aa50c182371533fc97db64fc8f26f051b3/include/linux/compiler_types.h) 中可以發現
```clike
#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)
```
而在 macro 中 [##](https://www.cprogramming.com/tutorial/cpreprocessor.html)為 Pasting Tokens,可把兩參數或文字做串連。
因此 `__UNIQUE_ID(prefix)` 可視為 `__UNIQUE_ID_ + prefix + __LINE__` ,去產生一個獨立名稱的變數。
而 [__attribute__](https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html)可以發現 `__attribute__((section(".modinfo"), unused, aligned(1)))`的意涵為把變數放到 .modinfo 中,且變數可能不會備使用,使 GCC 不會發出警告,並對變數最小的對齊為1 byte。
而 `__stringify` 定義在 [linux/stringify.h](https://github.com/torvalds/linux/blob/6f0d349d922ba44e4348a17a78ea51b7135965b1/tools/include/linux/stringify.h)中
```clike
#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
```
因此會以 static const char 的特殊名稱的變數儲存 "license = Dual MIT/GPL" 、 "author = National Cheng Kung University, Taiwan" 、"description = Fibonacci engine driver" 、 "version = 0.1",儲存在 .modinfo 中。
- [ ] 當我們透過 insmod 去載入一個核心模組時,為何 module_init 所設定的函式得以執行呢?Linux 核心做了什麼事呢?
在 inux/module.h 中可以找到
```clike
#define module_init(x) __initcall(x);
```
再到 [linux/init.h](https://github.com/torvalds/linux/blob/f346b0becb1bc62e45495f9cdbae3eef35d0b635/include/linux/init.h) 可以找到
```clike
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
```
```clike
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif
```
因此如果傳入的變數為 X ,會在 會建立 type 為 initcall_t 的變數`__initcall_X6 __used`,並儲存設定檔在 .initcall6.init 中。
而為什麼是 6 這個數字,可以在 init.h 中找到
```clike
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
```
而其中後方的數字為其優先權。 在 init/main.c中可以看到
```clike
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
```
在 kernel 啟動過程中,會呼叫 do_initcalls 呼叫我們透過 xxx_initcall() 所註冊的函式。所以我們用 module_init() 註冊函式的會依其優先權依序備執行。而由 for 迴圈可知0的優先權最高,7 的優先權最小。
- [ ] 試著執行 $ readelf -a fibdrv.ko, 觀察裡頭的資訊和原始程式碼及 modinfo 的關聯,搭配上述提問,解釋像 fibdrv.ko 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心
ELF 和其他檔案資料是由一個一個 bit 組成的,然而因為它內部依循著一定的結構,[如下圖](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format):
![](https://i.imgur.com/orySNce.png)
所以我們能用 readelf 這個工具去解讀 ELF 檔案,而由於原本電腦預設的語系,所以顯示出來有些資訊是中文,因此可以改成下方的指令使中文資訊換成英文。
```
$ LC_ALL=C readelf -a fibdrv.ko
```
readelf 去解讀 elf 檔的最前 16 位元,也就是 magic numbe,去確定此檔案是 elf 檔和知道有關這檔案的資訊。而有關 magic numbe 和 elf 檔案的資訊可以在 [linux/elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h) 查詢的到。
```clike
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
```
可以看到 magic number 分別代表不同的資訊,像是檔案的儲存方式是 2's complement, little endian 等等,而 readelf 可以去解讀並顯示資訊內容。
而由 magic numbe 可知 Elf64_Off e_shoff 的值,因此能利用 e_shoff 找到 Section Headers 的位置, readelf 去讀取顯示 elf Section 的資訊。
而由於 MODULE_VERSION 等巨集是對 .modinfo 做寫入,因此能發現有出現 [12] .modinfo 的資訊。而我們可以利用另一個工具 objdump 去顯示 Section 的資訊。
```
$ sudo objdump --section=.modinfo -s fibdrv.ko
```
```
Contents of section .modinfo:
0000 76657273 696f6e3d 302e3100 64657363 version=0.1.desc
0010 72697074 696f6e3d 4669626f 6e616363 ription=Fibonacc
0020 6920656e 67696e65 20647269 76657200 i engine driver.
0030 61757468 6f723d4e 6174696f 6e616c20 author=National
0040 4368656e 67204b75 6e672055 6e697665 Cheng Kung Unive
0050 72736974 792c2054 61697761 6e006c69 rsity, Taiwan.li
0060 63656e73 653d4475 616c204d 49542f47 cense=Dual MIT/G
0070 504c0000 00000000 73726376 65727369 PL......srcversi
0080 6f6e3d32 34444335 46423745 37363038 on=24DC5FB7E7608
0090 41463136 42304343 31460000 00000000 AF16B0CC1F......
00a0 64657065 6e64733d 00726574 706f6c69 depends=.retpoli
00b0 6e653d59 006e616d 653d6669 62647276 ne=Y.name=fibdrv
00c0 00766572 6d616769 633d342e 31352e30 .vermagic=4.15.0
00d0 2d34352d 67656e65 72696320 534d5020 -45-generic SMP
00e0 6d6f645f 756e6c6f 61642000 mod_unload .
```
- [ ] 這個 fibdrv 名稱取自 Fibonacci driver 的簡稱,儘管在這裡顯然是為了展示和教學用途而存在,但針對若干關鍵的應用場景,特別去撰寫 Linux 核心模組,仍有其意義,請找出 Linux 核心的案例並解讀。
文章一開始有提到 entropy,原本以為指的是以前修熱力學所提到的測量能量增減所用的熵,沒想到其實有[資訊熵](https://zh.wikipedia.org/wiki/%E7%86%B5_(%E4%BF%A1%E6%81%AF%E8%AE%BA))。指的是是接收的每條消息中包含的資訊的平均量。
在文章中有提到,CPU 執行時間可能因為要填滿 pipeline、處理 branch 等等而會產生[時基誤差](https://zh.wikipedia.org/wiki/%E6%8A%96%E5%8A%A8)使的執行時間難以預估。而為了測量時基誤差,透過 [crypto/jitterentropy.c](https://github.com/torvalds/linux/blob/master/crypto/jitterentropy.c) 中的 jent_measure_jitter() 去產生隨機的 bit來解決。
- [ ] 查閱 ktime 相關的 API,並找出使用案例
- [ ] clock_gettime 和 High Resolution TImers (HRT) 的關聯為何?
參考這篇[文章](https://elinux.org/High_Resolution_Timers)提到,clock_gettime 為 HRT Posix Timers API,並透過下面的 timespec 的結構去儲存時間單位。
```clike
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
```
而 time.c 提供多種時鐘,最主要的是 CLOCK_REALTIME and CLOCK_MONOTONIC。CLOCK_REALTIME 使用的是系統本身的時間,因此在執行時可透過改變系統時鐘測量時間受到影響。而 CLOCK_MONOTONIC 是指從過去到現在經過的絕對時間,時間會穩定遞增,不是系統時間影響。
仿效此篇[文章](http://www.guyrutenberg.com/2007/09/22/profiling-code-using-clock_gettime/)做測試:
```clike
#include <time.h>
#include <stdio.h>
static int diff_in_ns(struct timespec t1, struct timespec t2)
{
struct timespec diff;
if (t2.tv_nsec - t1.tv_nsec < 0) {
diff.tv_sec = t2.tv_sec - t1.tv_sec - 1;
diff.tv_nsec = t2.tv_nsec - t1.tv_nsec + 1000000000;
} else {
diff.tv_sec = t2.tv_sec - t1.tv_sec;
diff.tv_nsec = t2.tv_nsec - t1.tv_nsec;
}
return (diff.tv_sec * 1000000000 + diff.tv_nsec);
}
int main()
{
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
sleep(2);
clock_gettime(CLOCK_MONOTONIC, &end);
printf("%d\n",diff_in_ns(start, end));
return 0;
}
```
測量程序暫停兩秒前後相差多少 ns,執行後得到的結果為 2000182543。這種測量前後時間相差多少 ns 的方法也可以用在這次作業去測量 userspace 執行的時間。
- [ ] fibdrv 如何透過 Linux Virtual File System 介面,讓計算出來的 Fibonacci 數列得以讓 userspace (使用者層級) 程式 (本例就是 client.c 程式) 得以存取呢?
VFS是一個檔案系統虛擬層,再往下是實體的檔案系統。VFS主要是使上層的程式用單一的方式去和底層檔案系統溝通。
在 [linux/fs.h](https://github.com/torvalds/linux/blob/master/include/linux/fs.h) 中有看到 file_operations 的 結構。(結構眾多,只列出此次用到1的)
```clike
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 (*release) (struct inode *, struct file *);
} __randomize_layout;
```
在 fibdrv.c 中利用下面宣告去定義相關操作。
```clike
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,
};
```
並利用下面兩個呼叫去註冊。
```clike
cdev_init(fib_cdev, &fib_fops);
rc = cdev_add(fib_cdev, fib_dev, 1);
```
並利用下面兩個去創建 class 以及設備文件作為接口。
```clike
fib_class = class_create(THIS_MODULE, DEV_FIBONACCI_NAME);
device_create(fib_class, NULL, fib_dev, NULL, DEV_FIBONACCI_NAME)
```
- [ ] 注意到 fibdrv.c 存在著 DEFINE_MUTEX, mutex_trylock, mutex_init, mutex_unlock, mutex_destroy 等字樣,什麼場景中會需要呢?
在多執行序的程式如果共用相同的記憶體並同時對它做操作,可能因為在不同 CPU 同時執行或是因為 CPU 排班先後執行而導致執行錯誤,這裡簡單寫個 pthread 程式去測試。
```clike
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter = 0;
void* child() {
for(int i = 0;i < 5;++i) {
counter = counter + 1;
printf("Counter = %d\n", counter);
sleep(1);
}
pthread_exit(NULL);
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, child, NULL);
pthread_create(&t2, NULL, child, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("End = %d", counter);
return 0;
}
```
執行結果:
```
yuan@yuan-X555LF:~$ gcc test.c -lpthread
yuan@yuan-X555LF:~$ ./a.out
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9
End = 9
```
可以見到因為 counter 是全域變數,因此透過 pthread 建立得執行序都能對 counter 做操作,應此可能會造成不可預期的結果。
如果加上 mutex ,如果執行序再做切換時程序停留在臨界區段,而有其他程式要執行臨界區段時,便會產生 busywaiting。因此把上述程式加上 mutex,便能正確執行。
```clike
pthread_mutex_t mutex0 = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void* child() {
for(int i = 0;i < 5;++i) {
pthread_mutex_lock(&mutex0);
counter = counter + 1;
printf("Counter = %d\n", counter);
pthread_mutex_unlock(&mutex0);
sleep(1);
}
pthread_exit(NULL);
}
```
- [ ] 許多現代處理器提供了 clz / ctz 一類的指令,你知道如何透過演算法的調整,去加速 費氏數列 運算嗎?
參考這篇[文章](https://www.slideshare.net/erickitten/fibonacci-fast-doubling),對於 Fast doubling 的判斷由bit是0還是1去做判斷。
首先,先利用 clz 去求位數,並由位數由高到低去做判斷。
如果是 1 則先求 f(2n) 和 f(2n+1),並再依此求 f(2n+2)
如果是 0 則求 f(2n) 和 f(2n+1)即可。
假如要求Fib(10),10的二進位表示式為1010。
| bit | start | 1 | 0 | 1 | 0 | return |
| ------ | ------ | ------ | ------ | ------ | ------| ------|
| f(n) | f(0) | f(2*0+1) | f(1*2) | f(2*2+1) | f(5*2) | f(10)|
| a | 0 | 1 | 1 | 5 | 55 | 55 |
| b | 1 | 1 | 2 | 8 | 89 | |
因此能透過clz去得知判斷次數,減少不必要的判斷。
# 改善效率
用 ktime 去取的時間,並把時間回傳給 userspace。回想到大三上 OS 課程寫的 [mailbox](https://github.com/zodf0055980/OSclass_hw2_mailbox/blob/master/module/mailbox.c) 作業,利用 char *buf 去把要回傳的東西寫入 buf。
```clike
sprintf(buf,"%s",stmkbuf[stmheadpdf->number].path);
```
因此想套用此方法去取得回傳的時間
```clike
char tmp[128];
memset(tmp, 0, 128);
sprintf(tmp, "%lld\n", runtime);
strncpy(buf,tmp,128);
```
但發現程式執行到一半會結束執行,並且觀察程式的輸出檔 out ,發現程式會執行到 write 中途就結束了。
```
Writing to /dev/fibonacci, returned the sequence 1
Writing to
```
因此參考其他同學的[共筆](https://hackmd.io/s/SJ34e1-PV)發現有相同的問題,改用 copy to user,便能解決。
![](https://i.imgur.com/hjWKGuW.png)
由上圖可以發現除了一開始因為要設定初值要花時間較多外,時間大致以 O(n) 上升。
## Fast doubling
透過下面兩個式子,便可以求出 O(logn)的演算法
$\begin{split}
F(2k) &= F(k)[2F(k+1) - F(k)] \\
F(2k+1) &= F(k+1)^2+F(k)^2
\end{split}$
```clike
if (k == 0)
return 0;
long long fn = 1;
long long fn1 = 1;
long long f2n = 1;
long long f2n1 = 0;
long long i = 1;
while (i < k) {
if ((i << 1) <= k) {
f2n1 = fn1 * fn1 + fn * fn;
f2n = fn * (2 * fn1 - fn);
fn = f2n;
fn1 = f2n1;
i = i << 1;
} else {
fn = f2n;
f2n = f2n1;
f2n1 = fn + f2n1;
i++;
}
}
return f2n;
```
求出的時間如下:
![](https://i.imgur.com/YWB5krQ.png)
並去比較 Iterative 和 Fast Doubling 的執行時間
![](https://i.imgur.com/C6dwj8U.png)
發現當數字越大, Fast Doubling 的執行時間越短,但由於我們只有測量到 FIB(100),因此看不太出來 logn 的趨勢,但明顯 Fast Doubling 是比較快的。
## CLZ加速
參考[重新理解數值](https://hackmd.io/s/SkKZBXZT#)中 byte-shift version 的 clz 並嘗試改寫成 64 bit 的版本。
```clike
int n = 0;
unsigned long long clz = k;
if (clz <= 0x00000000FFFFFFFF) {
n += 32;
clz <<= 32;
}
if (clz <= 0x0000FFFFFFFFFFFF) {
n += 16;
clz <<= 16;
}
if (clz <= 0x00FFFFFFFFFFFFFF) {
n += 8;
clz <<= 8;
}
if (clz <= 0x0FFFFFFFFFFFFFFF) {
n += 4;
clz <<= 4;
}
if (clz <= 0x3FFFFFFFFFFFFFFF) {
n += 2;
clz <<= 2;
}
if (clz <= 0x7FFFFFFFFFFFFFFF) {
n += 1;
clz <<= 1;
}
k <<= n;
n = 64 - n;
```
最後求出除了 clz 外的 bit 數作為判斷次數。並把 k 作往左位移使 MSB 為 1 使方便作判斷。
```clike
unsigned long long fn = 0;
unsigned long long fn1 = 1;
unsigned long long f2n = 0;
unsigned long long f2n1 = 0;
int i;
for (i = 0; i < n; i++) {
f2n1 = fn1 * fn1 + fn * fn;
f2n = fn * (2 * fn1 - fn);
if (k & 0x8000000000000000) {
fn = f2n1;
fn1 = f2n + f2n1;
} else {
fn = f2n;
fn1 = f2n1;
}
k <<= 1;
}
return fn;
```
利用 k & 0x8000000000000000 去判斷 MSB 的位元,如果是 1 則先求 f(2n+1) 和 f(2n+2),如果是 0 則求 f(2n) 和 f(2n+1)。
![](https://i.imgur.com/aauPmOn.png)
由上圖可見透過 clz 可以有效加速 Fast doubling。而為何 Fast doubling 執行時間有較大的變化,使之執行時間產生鋸齒狀,我認為和 bit 有關。例如: 19 的二進為表示式為 10011,除了五次利用公式求出 f(2n) 外,還需要3次加法。而 24 的二進位表示是為 11000 卻只需要2次加法而已。這種因為 1 bit 個數差距所導致額外的加法我想是造成執行時間起伏的原因。
# Reference
[你所不知道的C語言:遞迴呼叫篇](https://hackmd.io/nWwDmm64SyqmlhGpgNbbag?view)