# 2020q1 Homework4 (khttpd)
contributed by < `nickyanggg` >
###### tags: `linux2020`
## [作業要求](https://hackmd.io/@sysprog/linux2020-khttpd#-%E4%BD%9C%E6%A5%AD%E8%A6%81%E6%B1%82)
- [自我檢查清單](https://hackmd.io/@sysprog/linux2020-khttpd#-%E8%87%AA%E6%88%91%E6%AA%A2%E6%9F%A5%E6%B8%85%E5%96%AE)
- 將 [fibdrv 作業](https://hackmd.io/@nickyanggg/fibdrv) 整合進來
## 實驗環境
```shell
$ uname -a
Linux nickyanggg-ubuntu18 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
```
## Kernel module 的參數傳遞
觀察 `main.c`,可發現預設的 port 為 8081,而透過掛載的時候加上 `port=1999` 可將參數傳入,推測是透過 `module_param`,於是就追進去看。
```cpp
#define DEFAULT_PORT 8081
static ushort port = DEFAULT_PORT;
module_param(port, ushort, S_IRUGO);
static int __init khttpd_init(void)
{
int err = open_listen_socket(port, backlog, &listen_socket);
if (err < 0) {
pr_err("can't open listen socket\n");
return err;
}
param.listen_socket = listen_socket;
http_server = kthread_run(http_server_daemon, ¶m, KBUILD_MODNAME);
if (IS_ERR(http_server)) {
pr_err("can't start http server daemon\n");
close_listen_socket(listen_socket);
return PTR_ERR(http_server);
}
return 0;
}
```
參考 [Linux drivers module param](https://b8807053.pixnet.net/blog/post/339227792-linux-driver-modules-param),發現 `module_param` 為 [<linux/moduleparam.h>](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 中所定義的巨集,透過它可以在啟動系統或模組裝載時指定參數值。
- `name`:使用者看到的參數名,也是模組內接受參數的變數。
- `type`:參數的類型。
- `perm`:sysfs 中相應檔案的存取權限。
```cpp
/**
* module_param - typesafe helper for a module/cmdline parameter
* @name: the variable to alter, and exposed parameter name.
* @type: the type of the parameter
* @perm: visibility in sysfs.
*
* @name becomes the module parameter, or (prefixed by KBUILD_MODNAME and a
* ".") the kernel commandline parameter. Note that - is changed to _, so
* the user can use "foo-bar=1" even for variable "foo_bar".
*
* @perm is 0 if the the variable is not to appear in sysfs, or 0444
* for world-readable, 0644 for root-writable, etc. Note that if it
* is writable, you may need to use kernel_param_lock() around
* accesses (esp. charp, which can be kfreed when it changes).
*
* The @type is simply pasted to refer to a param_ops_##type and a
* param_check_##type: for convenience many standard types are provided but
* you can create your own by defining those variables.
*
* Standard types are:
* byte, short, ushort, int, uint, long, ulong
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
*/
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
```
## kHTTPd web 伺服器

> 出處:[CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG)
`khttpd_init` 中的 `open_listen_socket` 基本上做的事就是開一個 socket、bind 到一個 port 上並開始 listen,和圖中的 `open_listenfd` 做的事一樣。接著會透過 kernel thread 去執行 `http_server_daemon`,當中的 `kernel_accept` 相當於圖中的 `accept` 是用來等待 client 端的連線。在處理 client 的 request 時,會另外開一條 thread 去執行 `http_server_worker`,讓新開的 socket 去和 client 連線接收 request 封包。因此原本的 socket 只要負責 listen 即可,一旦有 request 則透過新建的 socket 去處理。
```cpp
int http_server_daemon(void *arg)
{
struct socket *socket;
struct task_struct *worker;
struct http_server_param *param = (struct http_server_param *) arg;
allow_signal(SIGKILL);
allow_signal(SIGTERM);
while (!kthread_should_stop()) {
int err = kernel_accept(param->listen_socket, &socket, 0);
if (err < 0) {
if (signal_pending(current))
break;
pr_err("kernel_accept() error: %d\n", err);
continue;
}
worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
if (IS_ERR(worker)) {
pr_err("can't create more worker process\n");
continue;
}
}
return 0;
}
```
## 整合 [fibdrv 作業](https://hackmd.io/OWwxp6sWRbKnbR-LZO0WOg) 進 kHTTPd
- include [<bignum.h>](https://github.com/nickyanggg/khttpd/blob/master/bignum.h)
- 將原本寫死的 `Hello world` 以及字串長度改寫成 `%d`、`%s`。
- 將 `http_server_response` 回傳成功的部分改寫成呼叫 `fib_url`。
- `fib_url` 會檢查是否為正確格式,並且呼叫 `fib_sequence` (fast doubling) 來計算應當回傳的數字。
- 因為 `bignum` 實作的方式,目前只會去計算 fib(4500) 以內的數。
- `fib_sequence` 的回傳內容會是反向的,所以要將它轉向。
- malloc 的空間要多加 `length * 2` 是因為除了 `%s` (fib 所得到的結果) 的長度之外,還要再加上 `%d` 的長度,那這邊就直接取一個確定大等於它的數。
- 最後 response 完之後要釋放回傳內容所用到的空間。
```cpp
#define HTTP_RESPONSE_200_DUMMY \
"" \
"HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \
"Content-Type: text/plain" CRLF "Content-Length: %d" CRLF \
"Connection: Close" CRLF CRLF "%s" CRLF
#define HTTP_RESPONSE_200_KEEPALIVE_DUMMY \
"" \
"HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \
"Content-Type: text/plain" CRLF "Content-Length: %d" CRLF \
"Connection: Keep-Alive" CRLF CRLF "%s" CRLF
static char *fib_url(char *url, int keep_alive)
{
char *response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY
: HTTP_RESPONSE_200_DUMMY;
char *ret, *token, *tmp = url, result[MAX_DIGIT];
int size, length, i;
strsep(&tmp, "/");
token = strsep(&tmp, "/");
if (!strcmp(token, "fib")) {
token = strsep(&tmp, "/");
long num;
kstrtol(token, 10, &num);
// bignum max digit = 1000, fib(4500) digit = 941
if (num > 4500 || num < 0)
return NULL;
char *rev_fib = fib_sequence(num)->decimal;
length = strlen(rev_fib);
for (i = 0; i < length; i++)
result[i] = rev_fib[length - 1 - i];
result[length] = '\0';
} else
return NULL;
size = strlen(response) + length * 2;
ret = kmalloc(size, GFP_KERNEL);
snprintf(ret, size, response, length, result);
return ret;
}
static int http_server_response(struct http_request *request, int keep_alive)
{
char *response;
pr_info("requested_url = %s\n", request->request_url);
if (request->method != HTTP_GET)
response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501;
else
response = fib_url(request->request_url, keep_alive);
http_server_send(request->socket, response, strlen(response));
kfree(response);
return 0;
}
```
參考[黃鈺盛](https://gist.github.com/wilson6405/c8b86f71ce680efee26850298c9c237c)所提供的驗證程式,並改寫 `test.sh` 來驗證 $fib(1)$ ~ $fib(4500)$。
```=shell
#!/usr/bin/env bash
KHTTPD_MOD=khttpd.ko
if [ "$EUID" -eq 0 ]
then echo "Don't run this script as root"
exit
fi
# load kHTTPd
sudo rmmod -f khttpd 2>/dev/null
sleep 1
sudo insmod $KHTTPD_MOD
# check fibonacci 0 ~ 4500
for i in {1..4500};
do
python scripts/test.py -p 8081 -i $i
done
# epilogue
sudo rmmod khttpd
echo "Complete"
```
執行結果皆正確。
```shell
$ bash scripts/test.sh
Pass
Pass
Pass
...
Pass
Pass
Pass
```