---
tags: Linux Kernel
---
# khttpd
contributed by < `kevinshieh0225` >
> [作業需求](https://hackmd.io/@sysprog/linux2022-ktcp)
> [GitHub: khttpd](https://github.com/kevinshieh0225/khttpd)
## :penguin: 自我檢查清單與作業需求
- [x] 參照 [fibdrv 作業說明](https://hackmd.io/@sysprog/linux2020-fibdrv) 裡頭的「Linux 核心模組掛載機制」一節,解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢?
- [ ] 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?
- [x] `htstress.c` 用到 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?
* 在 GitHub 上 fork [khttpd](https://github.com/sysprog21/khttpd),目標是提供檔案存取功能和修正 `khttpd` 的執行時期缺失。過程中應一併完成以下:
- [x] 指出 kHTTPd 實作的缺失 (特別是安全疑慮) 並予以改正
- [x] 引入 [Concurrency Managed Workqueue](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) (cmwq),改寫 kHTTPd,分析效能表現和提出改進方案,可參考 [kecho](https://github.com/sysprog21/kecho)
- [ ] 實作 [HTTP 1.1 keep-alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection),並提供基本的 [directory listing](https://cwiki.apache.org/confluence/display/httpd/DirectoryListings) 功能
- 可由 Linux 核心模組的參數指定 `WWWROOT`,例如 [httpd](https://github.com/sysprog21/concurrent-programs/tree/master/httpd)
## 程式碼理解
### htstress.c
### http_parser
專案中的 http_parser 源自於 [nodejs](https://github.com/nodejs/http-parser) 的開源專案:一個用 C 語言撰寫的 http 資料解析器。這份解析器不需要額外的系統呼叫或記憶體配置成本,能夠在輕量的成本處理資料串流。
> Features:
>
>- No dependencies
>- Handles persistent streams (keep-alive).
>- Decodes chunked encoding.
>- Upgrade support
>- Defends against buffer overflow attacks.
>
>The parser extracts the following >- information from HTTP messages:
>
>- Header fields and values
>- Content-Length
>- Request method
>- Response status code
>- Transfer-Encoding
>- HTTP version
>- Request URL
>- Message body
### http_server
#### http_server_daemon
網站伺服器的後台程式碼,接收 client http connect message request。`http_server_daemon` 監聽 client socket request,接收到後建立新的執行緒,讓新的執行緒執行 `http_server_worker` 的函式來回應個別 client socket 的 request。
```c
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);
...
worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
...
}
}
```
:::warning
在 [kecho pull request #1](https://hackmd.io/@Masamaloka/linux2022-kecho#%E6%AF%94%E8%BC%83-kthread-based-%E5%92%8C-CMWQ-based-kecho_mod) 有探討關於使用 kthread-based 和 CMWQ-based 的實作差異,這將是我們可以動手改進與比較的環節。
:::
#### http_server_worker
1. 初始化接收 TCP 封包的 `parser`、parser 的函式庫 `setting` (記錄處理訊息用的函式指標), 自定義結構含有 socket, url 等等資訊的 `request`。
2. `http_parser_init` API 初始化 `parser`
3. 在核心執行緒尚未結束前,`http_server_recv`函式 接收來自 client 的封包,並透過 `http_parser_execute` API 處理這些封包需求。
```c
static int http_server_worker(void *arg)
{
char *buf;
struct http_parser parser;
struct http_parser_settings setting = {
/* settings member reference to function pointers */
};
struct http_request request;
struct socket *socket = (struct socket *) arg;
allow_signal(SIGKILL);
allow_signal(SIGTERM);
buf = kmalloc(RECV_BUFFER_SIZE, GFP_KERNEL);
...
request.socket = socket;
http_parser_init(&parser, HTTP_REQUEST);
parser.data = &request;
while (!kthread_should_stop()) {
int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1);
...
http_parser_execute(&parser, &setting, buf, ret);
if (request.complete && !http_should_keep_alive(&parser))
break;
}
kernel_sock_shutdown(socket, SHUT_RDWR);
sock_release(socket);
kfree(buf);
return 0;
}
```
### main
在 `khttpd_init` 函式利用 `open_listen_socket` 設定 server socket 的屬性,並建立核心執行緒執行 `http_server_daemon`。
## 傳遞核心模組參數
Linux kernel 提供了一個簡單的框架,允許驅動程式聲明參數,使用者在系統啟動或模組掛載時為參數指定相應值,在驅動程式裡,參數的用法如同全域變數。
使用引入標頭檔 [`<linux/moduleparam.h>`](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 以使用巨集 `module_param(name, type, perm)`。`name` 是參數、`type` 是參數型態、`perm` 指定了在sysfs中相應文件的存取權限。
```c
/**
* 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 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, hexint, 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)
/**
* module_param_named - typesafe helper for a renamed module/cmdline parameter
* @name: a valid C identifier which is the parameter name.
* @value: the actual lvalue to alter.
* @type: the type of the parameter
* @perm: visibility in sysfs.
*
* Usually it's a good idea to have variable names and user-exposed names the
* same, but that's harder if the variable must be non-static or is inside a
* structure. This allows exposure under a different name.
*/
#define module_param_named(name, value, type, perm) \
param_check_##type(name, &(value)); \
module_param_cb(name, ¶m_ops_##type, &value, perm); \
__MODULE_PARM_TYPE(name, #type)
/**
* 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)
```
`param_check_##type` 來確認型態是否正確;`module_param_cb` 以取得 cmdline 傳入的數值來更改參數存值;`__MODULE_PARM_TYPE` 用以將資訊記錄於 [`__MODULE_INFO`](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)
這裡可以特別注意到的是,傳入的 `name` 會分別傳入傳入 `name` 和 `value` 的參數中,`name` 維持參數名稱,而 `value` 會取其變數位址,以設定其存值。
在 `main.c` 當中的使用情境如下:
```c
static ushort port = DEFAULT_PORT;
module_param(port, ushort, S_IRUGO);
static ushort backlog = DEFAULT_BACKLOG;
module_param(backlog, ushort, S_IRUGO);
```
在掛載模組時插入參數,可參考 [The kernel’s command-line parameters](https://www.kernel.org/doc/html/v4.12/admin-guide/kernel-parameters.html) 規則。
## kHTTPd 與 [CS:APP 第 11 章](https://hackmd.io/@sysprog/CSAPP-ch11?type=view) 比較
在 CS:APP 11.5 章撰寫了 userspace http server 的範例。
## `htstress.c` 與 `epoll` 使用
> [kecho 的 user-echo-server epoll I/O](https://hackmd.io/@Masamaloka/linux2022-kecho#epoll-IO-monitor)
> 參考 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#2020q1-Homework4-khttpd)
在 `test.sh` 可以看到 htstress 執行的指令:
```
# run HTTP benchmarking
./htstress -n 100000 -c 1 -t 4 http://localhost:8081/
```
利用 `getopt_long(argc, argv)` 處理命令行參數。在這個命令意思是 max_requests=100000,每個 worker 開 1 個 `epoll` 並行的接收 I/O,建立 4 個執行序 worker。
```c
int next_option;
do {
next_option =
getopt_long(argc, argv, short_options, long_options, NULL);
switch (next_option) {
case 'n':
max_requests = strtoull(optarg, 0, 10);
break;
case 'c':
concurrency = atoi(optarg);
break;
case 't':
num_threads = atoi(optarg);
break;
...
}
} while (next_option != -1);
```
`main` 在接收命令行參數、確認 http 連線沒問題、準備 request buffer ,隨後建立 pthread 開始執行 `worker`。
```c
static void *worker(void *arg)
{
...
// 1
int efd = epoll_create(concurrency);
...
// 2
for (int n = 0; n < concurrency; ++n)
init_conn(efd, ecs + n);
//3
for (;;) {
do {
nevts = epoll_wait(efd, evts, sizeof(evts) / sizeof(evts[0]), -1);
} while (!exit_i && nevts < 0 && errno == EINTR);
...
}
...
}
// 2
static void init_conn(int efd, struct econn *ec)
{
ec->fd = socket(sss.ss_family, SOCK_STREAM, 0);
...
do {
ret = connect(ec->fd, (struct sockaddr *) &sss, sssln);
} while (ret && errno == EAGAIN);
...
struct epoll_event evt = {
.events = EPOLLOUT, .data.ptr = ec,
};
if (epoll_ctl(efd, EPOLL_CTL_ADD, ec->fd, &evt)) {
perror("epoll_ctl");
exit(1);
}
}
```
1. [`epoll_create`](https://man7.org/linux/man-pages/man2/epoll_create.2.html) 產生 epoll 專用的 file descriptor,裏面所帶參數須大於 0,自從 Linux 2.6.8 後,裏面參數已不代表任何意義。
2. 在 `init_conn` 建立 client socket ,隨後向目標 http server 發起連線 [connect](https://man7.org/linux/man-pages/man2/connect.2.html),並透過 [`epoll_ctl`](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) 將 client socket 加入到 epoll 中的 interest list,注意:這裡的 `epoll_event` 設定為 `EPOLLOUT`,即是 fd 準備寫入。
```
EPOLLIN
The associated file is available for read(2) operations.
EPOLLOUT
The associated file is available for write(2) operations.
```
3. [`epoll_wait`](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) 讓 interest list 當中的 fd 等待(block)直到接收到喚醒的 I/O,讓 fd 進到 ready list 以回應事件。`epoll_wait` 回傳共有多少 fd 已經準備執行。
4. 執行 ready list 上的 fd,首先檢查 `evts[n].events` 排除例外,正常執行中應只有 `EPOLLOUT` 和 `EPOLLIN` 的情形,分別代表準備輸出與接收。
```c
for (int n = 0; n < nevts; ++n) {
// 4 exception
...
// 5
if (evts[n].events & EPOLLOUT) {
ret = send(ec->fd, outbuf + ec->offs, outbufsize - ec->offs, 0);
if (ret > 0) {
ec->offs += ret;
...
/* write done? schedule read */
if (ec->offs == outbufsize) {
evts[n].events = EPOLLIN;
evts[n].data.ptr = ec;
ec->offs = 0;
}
}
// 6
} else if (evts[n].events & EPOLLIN) {
for (;;) {
ret = recv(ec->fd, inbuf, sizeof(inbuf), 0);
...
if (ret <= 0)
break;
...
ec->offs += ret;
}
if (!ret) {
close(ec->fd);
// record the GOOD/BAD REQUEST
init_conn(efd, ec);
}
}
}
```
5. 如果 `evts[n].events` 是 `EPOLLOUT` ,向 httpserver [send](https://man7.org/linux/man-pages/man2/send.2.html) requeset 訊息,如果完成了則轉換為 `EPOLLIN` 準備接收 response。
6. 如果 `evts[n].events` 是 `EPOLLIN` ,向 httpserver [recv](https://man7.org/linux/man-pages/man2/recv.2.html) response 訊息,使用迴圈分成多次接收,直到回傳小於零,記錄本次回傳成功 / 失敗,關閉這個 socket ,並重新執行 `init_conn`。
在此我們歸納 I/O 的模型與 epoll 帶來的好處:
> [高效 Web 伺服器開發 #I/O 事件模型](https://hackmd.io/@sysprog/fast-web-server#IO-%E4%BA%8B%E4%BB%B6%E6%A8%A1%E5%9E%8B)
Blocking I/O 在等待 I/O 資料準備就緒的過程,使用者層級的行程會被 blocked。為了提高 cpu 使用的效率,可以使用 I/O multiplexing 的機制,在單一行程下一次監聽多個 fd。在 `htstress.c` 中,我們可以一次建立多個 socket fd 放入到 epoll list 中,一旦某個 fd 準備好,就通知 process 來進行讀寫操作, Linux 提供了 3 種這樣的機制,分別為 poll, select 和 epoll。
epoll vs. select / poll
- epoll 在 epoll_wait() 時候可以隨時新增、修改或刪除 file descriptors
- epoll 只會返回準備好的 file descriptor , select / poll 必須要逐一確認 file descriptor
- epoll 的可移植性較差,並不是所有系統都支援。
:::warning
參見 [v.s., vs, vs., v.s 哪個才對!](https://www.storm.mg/lifestyle/516454),`vs.` 是正確,但 `v.s.` 卻是不同的意思。
:notes: jserv
:::
## 如何正確的停止 `kthread`
> 參考 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#2020q1-Homework4-khttpd), [`AndybnA`](https://hackmd.io/@AndybnA/khttpd#khttpd-%E7%9A%84%E5%AF%A6%E4%BD%9C%E5%95%8F%E9%A1%8C%E8%88%87%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95), [`bakudr18`](https://hackmd.io/@bakudr18/SkFA8Favu#Kernel-thread-%E6%9C%AA%E6%AD%A3%E7%A2%BA%E8%A2%AB%E9%87%8B%E6%94%BE), [`KYWeng`](https://hackmd.io/@KYWeng/H1OBDQdKL#%E5%A6%82%E4%BD%95%E6%AD%A3%E7%A2%BA%E7%9A%84%E5%81%9C%E6%AD%A2-kthread)
### `khttpd` 如何終止 `http_server`
在 `khttpd` 的實作中,`khttpd_init` 建立核心執行緒執行 `http_server_daemond`。`http_server_daemon` 接收到 client 連線時,建立新的核心執行序執行 `http_server_worker` 以處理客戶需求。
如果我們今天要卸載 khttpd 時,要如何向 kthread 通知訊息並安全的終止呢?在 khttpd 利用兩個技巧:一個是使用 [`kthread_stop` ](https://docs.huihoo.com/linux/kernel/2.6.26/kernel-api/re79.html),一個利用 [`signal`](https://man7.org/linux/man-pages/man7/signal.7.html) 來傳達終止訊息。
#### `kthread_stop`
[`kthread_create`](https://docs.huihoo.com/linux/kernel/2.6.26/kernel-api/re77.html) 建立一個 kthread ,透過 `wake_up_process` 來喚醒執行 threadfn(如果建立後要立即喚醒執行可直接使用 [`kthread_run`](https://www.unix.com/man-page/suse/9/kthread_run))。文件中提到有兩種終止方式:
1. 如果不會有其他地方會執行 `kthread_stop` 來終止這個子執行緒 ,子執行緒可以直接使用 `do_exit` 關閉自己。
2. 主執行緒透過 `kthread_stop` 關閉子執行緒:將子執行緒的 `kthread_should_stop` 設為 1 ,此時喚醒子執行緒讓他確認 flag 的改變,待子執行緒被釋放。
需要注意:執行 `kthread_stop` 需要確保呼叫的執行緒是存在的,如果執行緒自行呼叫 `do_exit` 會發生錯誤,故而這兩種方式在使用上是互斥的。
以 khttp 建立核心執行緒 `http_server` 為例:
```c
static int __init khttpd_init(void)
{
http_server = kthread_run(http_server_daemon, ¶m, KBUILD_MODNAME);
}
static void __exit khttpd_exit(void)
{
kthread_stop(http_server);
}
int http_server_daemon(void *arg)
{
while (!kthread_should_stop()) {
...
}
return 0;
}
```
#### `signal`
在 [kernel thread and signal handling](https://www.linuxquestions.org/questions/linux-general-1/kernel-thread-and-signal-handling-372857/), [Thread Signal Handling](https://www.ibm.com/docs/en/aix/7.2?topic=threads-thread-signal-handling) 提到核心中處理訊號是有風險的,故在預設 kernel 環境下接收到的 signal 會忽略,包含 `SIGKILL`。於是透過 [`allow_signal`](https://github.com/torvalds/linux/blob/cb690f5238d71f543f4ce874aa59237cf53a877c/include/linux/signal.h#L299) 令建立之 kthread 得以關注並處理指定的訊號。透過 signal 來關閉執行緒的方式如下:
1. 設定 `allow_signal` 允許接收 terminate 類型的訊號。
2. 向執行緒 `send_sig` 發出終結訊號
3. 執行緒在被喚醒後透過 [`signal_pending`](https://man7.org/linux/man-pages/man3/signal_pending.3.html) 確認收到的訊號,並終止執行。
以 `khttpd` 向 `http_server` 發送終結訊號為例:
```c
static void __exit khttpd_exit(void)
{
send_sig(SIGTERM, http_server, 1);
kthread_stop(http_server);
}
int http_server_daemon(void *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;
}
}
return 0;
}
```
這裡可以見到 `kthread_stop` 搭配 `signal` 的使用,為何需要這麼做呢?從 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#%E9%87%8B%E6%94%BE-kernel-thread) 的記錄發現如果我們不使用 signal ,就算使用 `kthread_stop` 來改變 `kthread_should_stop` 的狀態,`http_server_daemon` 會因為仍在等待 `kernel_accept` 而進入 non-block waiting ,在無法脫離該狀態並 return 的狀態下,整段進程便卡住無法繼續。利用 `send_sig` 可讓 `http_server_daemon` 從 `kernel_accept` 中喚醒,並順利執行關閉的行為。
### `khttpd` 的缺失
在卸載 khttpd 時我們確實的釋放了 `http_server`,然而卻無法一併釋放 `http_server_worker`。錯誤此時會發生在使用者正在連線時(瀏覽網頁 http://localhost:8081/ )卸載了核心模組,這使的 `http_server_worker` 並未一同釋放,讓我們依然可以看到並存取網頁上的內容。
目前我想到的解決方法是在 `http_server_daemon` 記錄與追蹤建立的 `http_server_worker` ,當接收到關閉的訊息時,先行釋放追蹤的 `http_server_worker` 再進行 return。
## 以 cmwq 改寫
參考 `kecho` 的 cmwq 實作改寫 `khttpd`,除了透過 cmwq 協助安排執行資源,提昇並行的效益、減少額外建立 kthread 產生的成本。另外搭配 `kecho` 中管理 cmwq work_struct `echo_service` ,在卸載模組時使用 `free_work` 來釋放正在執行中的 work item ,解決原本 khttpd 實作上的缺陷。
[cmwq 實作程式碼](https://github.com/kevinshieh0225/khttpd/tree/bee714c157a9bad1151cf06a78b925ce8af78c5d)
原先 kthread 版本:
```
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 2.796
requests/sec: 35771.379
```
cmwq 版本:
```
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 1.693
requests/sec: 59055.618
```