# 2020q1 Homework4 (khttpd)
contributed by < `Yu-Wei-Chang` >
> [2020q1 作業 khttpd](https://hackmd.io/@sysprog/linux2020-khttpd)
>
## 實驗環境
```shell
$ uname -a
Linux ywc-ThinkPad-X220 5.3.0-46-generic #38~18.04.1-Ubuntu SMP Tue Mar 31 04:17:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
```
## `insmod` 如何將命令行的參數傳遞到核心?
### 在核心合組中增加參數
* 參考 [LINUX KERNEL DEVELOPMENT – KERNEL MODULE PARAMETERS](https://devarea.com/linux-kernel-development-kernel-module-parameters/#.Xvw6K3UzZ8c),核心模組可以藉由宣告全域變數以及 `module_param()` 巨集的使用,使載入核心模組時順便指定參數的數值,使用方式如:
```shell
$ sudo insmod khttpd.ko port=8082
```
* 在 `khttpd` 的原始碼 `main.c` 中可以看到全域變數 `port` 的預設值是 8081,然後透過 `module_param()` 巨集指定它為核心模組的參數,型態為 `ushort`,權限為 read-only,讓變數 `port` 可以在載入核心時被指定其數值。
```cpp
static ushort port = DEFAULT_PORT;
module_param(port, ushort, S_IRUGO);
```
* 我們可以在 `sysfs` 下找到 `khttpd` 的核心參數,它們以檔案的形式表示。查看其數值可以發現是我們在載入模組時所指定的 8082,然後查看其檔案權限是 read-only 沒錯。
```shell
/sys/module/khttpd/parameters$ ls -l
總計 0
-r--r--r-- 1 root root 4096 7月 1 17:28 backlog
-r--r--r-- 1 root root 4096 7月 1 17:28 port
/sys/module/khttpd/parameters$ cat port
8082
```
* 巨集 `module_param()` 可以指定參數的型態,==但無法指定其數值範圍,需要檢查參數的合法輸入值的話就必須使用巨集 `module_param_cb()` 以及搭配 `struct kernel_param_ops` 來自定義核心模組參數的讀寫函式。==
```cpp
/**
* module_param_cb - general callback for a module/cmdline parameter
* @name: a valid C identifier which is the parameter name.
* @ops: the set & get operations for this parameter.
* @arg: args for @ops
* @perm: visibility in sysfs.
*
* The ops can have NULL set or get functions.
*/
#define module_param_cb(name, ops, arg, perm) \
__module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)
...
struct kernel_param_ops {
/* How the ops should behave */
unsigned int flags;
/* Returns 0, or -errno. arg is in kp->arg. */
int (*set)(const char *val, const struct kernel_param *kp);
/* Returns length written or -errno. Buffer is 4k (ie. be short!) */
int (*get)(char *buffer, const struct kernel_param *kp);
/* Optional function to free kp->arg when module unloaded. */
void (*free)(void *arg);
};
```
### 核心模組參數如何傳遞到核心?
* 透過 `strace` 可以看到載入核心模組時,呼叫函式 `finit_module()` 時第二個參數即是我們指定參數的字串。
```shell
$ sudo strace insmod khttpd.ko port=8082
stat("/home/ywc/training_data/linux_kernel/Week6_homework/khttpd/khttpd.ko", {st_mode=S_IFREG|0644, st_size=54432, ...}) = 0
openat(AT_FDCWD, "/home/ywc/training_data/linux_kernel/Week6_homework/khttpd/khttpd.ko", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=54432, ...}) = 0
mmap(NULL, 54432, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4222b0f000
finit_module(3, "port=8082", 0) = 0
```
* 由 [Linux-核心模組掛載機制](https://hackmd.io/@sysprog/linux2020-fibdrv#-Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E6%8E%9B%E8%BC%89%E6%A9%9F%E5%88%B6) 得知核心模組的載入流程中,函式 `finit_module()` 會接著呼叫函式 `load_module()`,其中可以看到它會呼叫函式 `strndup_user()`,推測是把從命令行輸入的參數數值從 user space 搬到 kernel space 來。 (==WIP...==)
```cpp
/* Allocate and load the module: note that size of section 0 is always
zero, and we rely on this for optional sections. */
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
...
/* Now copy in args */
mod->args = strndup_user(uargs, ~0UL >> 1);
if (IS_ERR(mod->args)) {
err = PTR_ERR(mod->args);
goto free_arch_cleanup;
}
...
```
* 接著會呼叫函式 `parse_args()`,推測是處理剛剛搬到核心的模組參數。 (==WIP...==)
```cpp
/* Module is ready to execute: parsing args may do that. */
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
-32768, 32767, mod,
unknown_module_param_cb);
```
### 和 user space 的 socket API `sys/socket.h` 比較
* 建立一個新的 socket endpoint : user space socket API 建立的 socket 是以 file descriptor 的形式由 `socket()` 回傳;kernel socket API 建立的 socket 則是要宣告 `struct socket *` 作為呼叫 `sock_create()` 的第四個參數傳入。
* 綁定 socket 和 address : user space socket API `bind()` 在綁定 socket 是傳入 socket descriptor;kernel socket API `kernel_bind()` 則是傳入 `struct socket *`。
* 將 socket 設定程 passive listening socket : 差異同上。
* 接受 client 的連線 :
* 其中一個差異同上,user space 用 socket descriptor;kernel 都用 `struct socket *`
* user space socket API `accept()` 在接受連線後,函式回傳一個新的 socket descriptor 用來和 client 交換資料;kernel socket API `kernel_accept()` 則是回傳 error code,socket 是自己宣告,然後當成參數傳入。
* user space socket API `accept()` 呼叫時會傳入 `struct sockaddr` 參數,當有人連線後,可以從 `struct sockaddr` 得知連線人的 IP address 以及 port number;kernel socket API 則不是,如何在 kernel 得知連線人的資訊 ==待查==。
### kHttpd 的流程和原理
* 載入核心模組後會建立 kernel thread (`http_server_daemon`),然後在指定的 port (預設是 8081) 上等待 client 的連線。
* 函式 `kernel_accept()` 在接受 client 的連線後會拿到另一個 struct socket,Web server 會使用這個 socket 來接收 HTTP Request。和 user space 的 socket API `sys/socket.h` 類似,聽連線是在一個 socket 聽,連線成功後會換一個 socket 來收送資料,port 也和之前聽連線的不同。
* 有人連線進來時,又會建立另一個 kernel thread (`http_server_worker`),透過函式 `kernel_recvmsg()` 讀取 HTTP Request 的內容,然後透過函式 `http_parser_execute()` 來分析出 `URI`,然後呼叫 on_message_complete 的 callback 函式,再透過函式 `http_server_response()` 把 HTTP Response 回傳給 client。最終呼叫函式 `kernel_sock_shutdown()` 把 socket 關閉。
* HTTP Request 的內容:參閱 [CS:APP 第 11 章](https://hackmd.io/@sysprog/CSAPP-ch11?type=view) 的說明,HTTP Request 是由 `method`、`URI`、`version` 以及 `HTTP header` 組成,此例中 `GET` 是 method,`URI` 就是 URL 中 domain name 後面的部份 (此例因為沒輸入任何 URI 所以是 `/`),`HTTP/1.1` 是 version,剩下的部份都是 http header。
```shell
GET / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed
...
```
### 將 [fibdrv](https://hackmd.io/@sysprog/linux2020-fibdrv) 整合進 kHttpd
#### 分析 HTTP Request 的 URI path
* 預期在 khttpd 回送 response 時 (`http_server_response()`) 來分析 URI path 是否符合 `/fib/<number>` 的格式,並且做費氏數列的運算。
* 分析的方式是用 `strstr()` 在 `request_url` 中找 `/fib/` 字串,接著再靠 `kstrtoll()` 將剩下的字串轉成數字,如此我們便可以得知使用者預期要計算的 Fibonacci sequence。
```cpp
static int http_server_response(struct http_request *request, int keep_alive)
{
char *response;
char *match = strstr(request->request_url, FIB_URL_PATH);
pr_info("requested_url = %s\n", request->request_url);
if (match) {
long long fib_seq_idx;
match += strlen(FIB_URL_PATH);
if (kstrtoll(match, 10, &fib_seq_idx) == 0) {
/* Here we got the Fibonacci sequence which user want to calculate. */
}
}
```
#### 整合 fibdrv
* 之前的 [fibdrv 作業](https://hackmd.io/@Yu-Wei-Chang/SkYVm4xtL)是採用 `BigN` 結構來表示大數,所以除了費氏數列的運算函數之外,還要把 `BigN` 的加、乘、減法運算函式也一併移植,因為用 fast-doubling 方式計算費氏級數時會用到。
* 然後因為之前的作業是先把 `BigN` 結構往 user space 傳,然後交由應用程式去將 `BigN` 轉成字串,所以這個轉換字串的函式也要移植到 `kHttpd` 中。
* 詳細內容見 [commit](https://github.com/Yu-Wei-Chang/khttpd/commit/44e7e2a48dd7ce8055bd96281b0fa76c58b6900f)
### 將費氏數列包進 HTTP Response 訊息中
* 目前預期的 HTTP Response 內容
* 收到 `GET` method:
* 如果 URL path 形式為 `/fib/<number>`,則將字串 `Fibonacci(<number>) = <Fibonacci sequence>` 包進 response 之中。
* 否則行為依舊,回覆內容為 `HTTP_RESPONSE_200_DUMMY`。
* 收到其他 method:行為依舊,回覆 `501 Not Implemented`。
* 宣告 local charater array 來存放 HTTP Response 的內容,搭配 `snprintf()` 函式將數字轉成字串,最後透過 `http_server_send()` 將訊息送出去。
* 詳細內容見 [commit](https://github.com/Yu-Wei-Chang/khttpd/commit/0c82a88d8dceff941c05cde27b7fb7fdccd6d2f4)
### 引入 [CMWQ](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) 改寫 kHttpd
* 原本的 kHttpd 在有人連線後,是使用 `kthread_run()` 來建立新的 kthread 來處理 HTTP 請求,預期引入 cmwq 機制,建立新的 work item 來處理 HTTP 請求。
* 參考 [kecho](https://github.com/sysprog21/kecho) 的實作方式來引入 cmwq 機制。實作如 [commit](https://github.com/Yu-Wei-Chang/khttpd/commit/7f3cb760a4768441322d732e9c6d2ca8df8d9ab9)
* 效能比較:引入 cmwq 執行效率增加。
* 原本的作法,建立 `kthread` 來處理每個 HTTP 請求:
```shell
$ ./htstress -n 100000 -c 100 -t 4 http://localhost:8081/fib/100
0 requests
...
90000 requests
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 10.605
requests/sec: 9429.879
```
* 改善後的作法,建立 `work item`,透過 `workqueue` 來處理每個 HTTP 請求:
```shell
$ ./htstress -n 100000 -c 100 -t 4 http://localhost:8081/fib/100
0 requests
...
90000 requests
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 7.860
requests/sec: 12723.386
```
* 由參考資料 [Linux-workqueue講解](https://iter01.com/427752.html) ==發現 workqueue 實際上內部也是將 kernel thread 的用法封裝起來==,但比起自己使用 kernel thread 來處理 HTTP 請求,==為什用 cmwq 的效率會比較好,其實不是非常清楚==。以下自己推測一些原因:
* 建立/刪除 kernel thread 是會花費資源的。每次有 HTTP 請求進來,就開 kthread 去處理,處理完又釋放掉很浪費資源。
* 核心會替每個處理器建立兩個 kthread,一個正常優先權,一個高優先權;除此之外還有不屬於任何處理器的 ubound worker-pool 也有建立 kthread。(u 開頭的是不屬於處理器的 worker-pool,其他的是處理器專屬的 worker-pool,名字有 `H` 的待表示高優先權)
```shell
$ ps -ef | grep "kworker"
root 5 2 0 09:39 ? 00:00:00 [kworker/0:0-eve]
root 6 2 0 09:39 ? 00:00:00 [kworker/0:0H-kb]
root 13 2 0 09:39 ? 00:00:00 [kworker/0:1-eve]
root 20 2 0 09:39 ? 00:00:00 [kworker/1:0H-kb]
root 32 2 0 09:39 ? 00:00:00 [kworker/3:0H-kb]
root 149 2 0 09:39 ? 00:00:00 [kworker/u17:0-r]
...
```
* 呼叫 `alloc_workqueue()` 透過指定 flags 來決定要用哪個 worker-pool,workqueue 會替使用者有效利用核心預先建立好的 kthread 來處理HTTP 請求,免於自己建立/刪除而花費過多的資源。
###### tags: `Linux核心課程筆記 - Homework`