# 2019q1 Homework5 (daemon)
contributed by < `flawless0714` >
Kernel 版本: `4.15.0-47-generic`
## 筆記
### TCP backlog
原先擔心 kernel module 中的這項參數會影響測試,理解後得知其是在 `accept()` 成功前 queue 中最大能存放的連線要求,然而我的測試程式是在 `accept()` (以測試程式的角度看是 `connect()`) 成功後才會開始計時,所以這邊除非 queue 太容易塞滿,導致測試程式的部份執行緒沒有被涵蓋到 kecho 並行處理通訊時的情境。
### kthread
工作於 kernel-space 的 thread。
#### kthread_run()
建立新 kthread 並開始執行,回傳值為 `task_struct`。
#### kthread_stop()
通知 kthread 準備關閉(將 `kernel_should_stop` 旗標設為 true),並等待直到 `threadfn` 結束(由此可知 `threadfn` 中需要有一直驗證停止旗標的實做)。其中會把 `kthread->kthread_should_stop` 設為 true。此函數也用於喚醒 kthread。
#### 排程
當 kthread 閒置時需要將其 block,以免發生浪費資源的情況。常見的 block 方法為使用 `schedule()`、`ssleep()`。
##### `schedule()` 睡眠之運作方式
首先使用 `set_current_state()` 將 kthread 的 state 改成 `TASK_INTERRUPTIBLE`,以使得待會可以被移出 run queue (排程器排程時會從 run queue 挑 task),接著呼叫 `schedule()`,告知排程器可以切到其他 task 了,也就是讓出 CPU 資源,至此這個 kthread 即開始睡眠。假如要喚醒這個 kthread,我們可以使用 `wake_up_process()` (帶入參數為目標 kthread 的 `task_struct*`),這個函數會把 kthread 的 state 切回 `TASK_RUNNING` ,並將其它放回 run queue,接著就等待排程器下一次排程了。
### kecho
#### 運作流程
1. 初始化時,進行 server socket 的 `create`, `bind` 與 `listen` 等 socket 設定。
2. 開始 `listen` 後,建立第一個 kthread,這個執行緒為 server daemon,其負責進行 `listen`,每當有新進連線時,它會開新的執行緒(server worker)來 handle 新連線。
3. server worker 會先呼叫 `get_response` 來等待 client 端傳入訊息。
4. 收到來自 client 的訊息後隨即呼叫 `send_request` 以回傳 client 傳入之訊息,以達到 echo 訊息的功能。
5. 重複第四步直到 client 關閉連線(至此 server worker 這個執行緒將被關閉)或 kecho 模組被移除(`rmmod`)。
#### 問題
- server worker (kthread) 是如何在 client 端關閉連線時得知需要把自己關掉?
> 由不斷 evaluate `kthread_should_stop` 回傳的布林值去判斷是否需要關閉。
- 為什麼 detachstate 為 JOINABLE (使用 `pthread_attr_getdetachstate` 確認過) 的 pthread 可以在 `pthread_join` 結束後(檢查過 retval 正常)才結束?
#### 已知問題
- 當輸入資料量大到一定量時,會回傳亂碼,例如 `/` 下的檔案名稱,還有出現過自己的 kernel cmdline...。
> 多次測試後發現就算輸入資料很小也會發生回傳檔名,而且是發生在 `get_response` 的時候,所以可以說是 `telnet` 自己傳過去的,或是 `kmalloc` 配置的記憶體沒有初始化的關係(首次 echo 即發生此問題),待測試。
> > ==看來是 `kmalloc` 配置的記憶體沒初始化造成的==,剛剛測試配置完馬上用 `memset` 初始化記憶體,就沒再遇到這問題了,不然每次 `telnet` 第一次連線就算你直接按 enter 鍵送空字串, `kecho` 也會回傳一串亂碼。不知道這算不算漏洞...,連 cmdline (/proc/cmdline) 都露出來了,這樣可能整個 `/` 下的檔名都可用這招掏出來...。
> > > 想想覺得這應該不算漏洞,因為這個 module 算是沒有經過驗證之類的檢查,而且載入後 kernel 就變成 tainted kernel 了。
- 修復:
如上述,只需於使用 `buf` 前初始化記憶體即可。
`echo_server_worker`:
```cpp
buf = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!buf) {
printk(KERN_ERR MODULE_NAME ": kmalloc error....\n");
return -1;
}
memset(buf, 0, BUF_SIZE);
```
- msg buffer 沒有重置,進而導致每筆新訊息會與前一筆訊息重疊(前提是當前訊息長度小於前一次,否則新訊息會完全覆蓋舊訊息)。
- 修復:
透過在每次 echo 結束後重置 buffer,須注意由於 kthread 在關閉前還會執行一套接收(get/send \_request),而由於我們有做清空的動作,所以進行最後一次 `send_request` 時帶入的 `size` 會是0,這會讓 `send_request` 中的 `kernel_sendmsg` 帶入的 `size` 參數拿到18446744073709551615(以 size_t 來計算 0 - 1),進而造成 `kernel_sendmsg` 執行時發生 kernel panic (null dereference at 000000000000)。話說這是個有趣的故事的開始,遇到 panic 後我想說這個 size 對記憶體位置來說已經是天文數字了,於是就試試小一點的,我直接把 `size` 給定 `BUF_SIZE + 1000`,想看看 panic log 會說我想存取哪段記憶體,答案是 `null dereference at 0000000013E8`,看到這邊還稍有意義(`13E8` 轉為十進制是5096),再接著我又好奇假如放2^48^(x86_64 最大定址位置)會發生啥事,結果還沒測到2^48^-1,只是先測2^48^就差點把我的 `/` 搞爆了,會這麼說是因為我開不了機...,只能用 `ctrl + fx` 進 TTY 而開不了視窗桌面,而且原本的密碼不能登入..,然後進 TTY 後還跳出 kdump 無法初始化的錯誤,並且提示 `/` 已無可用空間..,結果 `df -h` 一看發現 `/` 被用光了(原本還有 4GB 左右),實在不懂這是怎麼造成的,等虛擬機有成功打開 kdump 再來測試看看2^48^-1會發生什麼事,不然怕下次我的實體機沒這麼好運還可以開機...。
`echo_server_worker()`:
```cpp
while (!kthread_should_stop()) {
res = get_request(sock, buf, BUF_SIZE - 1);
if (res <= 0) {
if (res) {
printk(KERN_ERR MODULE_NAME ": get request error = %d\n", res);
}
break;
}
res = send_request(sock, buf, strlen(buf));
if (res < 0) {
printk(KERN_ERR MODULE_NAME ": send request error = %d\n", res);
break;
}
memset(buf, 0, BUF_SIZE);
}
```
`send_request()`:
```cpp
length = kernel_sendmsg(sock, &msg, &vec, 1, (size < BUF_SIZE) ? size : 0);
```
#### 效能分析
測試用之 [client 端程式碼](https://github.com/flawless0714/kecho/blob/master/perf_measure.c)。
- 已知問題
- (已改用 `gettimeofday`)數據出現負號。發生原因為平均數量級差異過大造成溢位,故須捨棄 ns 級的時間量測 API,改用更粗糙的單位。
- (已解決)不該於 worker 中使用 `while(flag);` 去等待其他 thread 的建立(等1000個 thread 的實驗已經等了至少20分鐘了...,最後單次傳輸超過一秒被強制停止。推測發生原因是這支 process 的資源被那些已建立的 thread 拿去卡在 `while`,所以後續建立的 thread 其實沒什麼資源可以用)。
> 修好後不只問題解決,連 concurrent thread 有數百個時 CPU 也不會有太大的負載(原本是全部核心使用率都接近100%)。
- (已解決)若未於 `pthread_join` 後使用 `usleep` 稍待一會再開始下一輪(concurrent thraed 數量相同,此舉之目的為取平均時間)測試,則 thread 用來索引量測時間陣列的變數(`idx`)會發生多次 `idx++` 的問題,進而導致 OOB access of array。目前已經確認(手動使用 `pthread_attr_init` 去初始化 attr,再使用 `pthread_attr_getdetachstate` 去檢驗)過 `pthread_join` 使用的預設 `attr` 中的 `detachstate` 是 `JOINABLE` 的(雖然 manpage 已經有說這是預設了,但我快找不到問題點了QQ,所以還是檢查一下)
> 剛剛測試又發現新情境,在沒有使用 `usleep` 的情況下若從初始數量大一點(e.g. 500)的 concurrent thread 開始測試即不會發生上述的 OOB 問題,問題越來越深了..。
> > 找到問題了,是我太蠢...,我把 `cur_thread_cnt` 的初始值設0,這造成 `pthread_join` 的迴圈在 `cur_thread_cnt` 為 0 (其實是單個 thraed) 時連進都進不了,因為我的迴圈設定是 `for (int x = 0; x < cur_thread_cnt; x++)`。
##### 原始版(未使用 cmwq)

完全不可考...,晚點更新修正版本。(此版本在 thread function 中使用 while 迴圈來等待一個 global 的旗標,此舉耗費非常多資源在單個 thread 上面)
[修正版] (添加 `pthread_cond_wait` 以節省運算資源之浪費)

由圖中可發現通訊時間平均花費了 10ms 左右
##### cmwq 修改版
##### 整合 Kernel Httpd 版
#### 注意事項
- (已排除, )關閉 `telnet` 時建議使用 `close` or `q` command 關閉連線,若用 `ctrl` + `z`,會讓未關閉的連線一睡不起 (suspend),進而造成下一次 `insmod` 時發生位置佔用的問題(address already in use),目前解法為重開機。可用下方 command 來查看哪些 port 正在使用:
```shell
sudo netstat -apn |grep {port (e.g. 12345)}
```
## 參考資料
- [Kernel Korner - Sleeping in the Kernel](https://www.linuxjournal.com/article/8144)
- [Module param](http://b8807053.pixnet.net/blog/post/339227792-linux-driver-modules-param)
- [Networking (Kapi)](https://www.kernel.org/doc/html/latest/networking/kapi.html)
###### tags: `linux2019`