# 2020q1 Homework4 (khttpd) contributed by < `StevenChen8759` > ###### tags: `linux2020` > [作業說明 - khttpd](https://hackmd.io/@sysprog/linux2020-khttpd) > [作業繳交區](https://hackmd.io/@sysprog/linux2020-homework4) > [Github](https://github.com/StevenChen8759/khttpd) > [color=lightgreen] # :computer: 功能開發記錄 ## :camera: `khttpd` 運作分析與 `HTTP Response` 訊息修改 * 首先,我們觀察 `khttpd` 回覆要求的執行流程。 * 在模組安裝到核心時,我們透過自訂的 [Make Target](https://github.com/StevenChen8759/khttpd/blob/master/Makefile) 查看模組與網路的狀態 ```shell $ make ... $ make load sudo insmod khttpd.ko port=8081 $ make stat ------------------------------------------------------------------------ lsmod | grep khttpd khttpd 40960 0 ------------------------------------------------------------------------ sudo netstat -antp Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN - tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN 1054/dnsmasq tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 2356/cupsd tcp6 0 0 ::1:631 :::* LISTEN 2356/cupsd ``` * 觀察以上輸出訊息,可發現在插入模組的初始化 Callback 時已開啟 Socket ,[對應的程式碼](https://github.com/StevenChen8759/khttpd/blob/master/main.c)如下: ```cpp=93 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, &param, 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; } ``` * 觀察上述程式,除了開啟對應的 Socket 以外,初始化 Callback 亦同時創建了一個 Kernel Thread,其 callback 為 `http_server_daemon` ,負責 `Socket Accept` 的處理,[對應的程式碼](https://github.com/StevenChen8759/khttpd/blob/master/http_server.c)如下: ```cpp=188 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; } ``` * 觀察上述 `kernel_accept` 的呼叫,在每一次的裝置連結時, `http_server_daemon` 將會再創建一個 Kernel Thread,其 Callback 為 `http_server_worker` ,以處理連結裝置的 Request, 並將相應的 Response 回傳給 Client,[對應程式碼](https://github.com/StevenChen8759/khttpd/blob/master/http_server.c)如下: ```cpp=144 static int http_server_worker(void *arg) { char *buf; struct http_parser parser; struct http_parser_settings setting = { .on_message_begin = http_parser_callback_message_begin, .on_url = http_parser_callback_request_url, .on_header_field = http_parser_callback_header_field, .on_header_value = http_parser_callback_header_value, .on_headers_complete = http_parser_callback_headers_complete, .on_body = http_parser_callback_body, .on_message_complete = http_parser_callback_message_complete}; struct http_request request; struct socket *socket = (struct socket *) arg; allow_signal(SIGKILL); allow_signal(SIGTERM); buf = kmalloc(RECV_BUFFER_SIZE, GFP_KERNEL); if (!buf) { pr_err("can't allocate memory!\n"); return -1; } 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); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } 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; } ``` * 觀察此 Callback 中的 `setting` 變數,此變數是用於控制模組 `http_parser` 在 Request 解析過程中所呼叫的本地 Callback,在 [http_server.c](https://github.com/StevenChen8759/khttpd/blob/master/http_server.c) 中可找到對應各執行階段的 callback。 * 因為我們欲從輸入的網址 (URL) 找出欲回傳的費氏數,故我們觀察以下在 [http_server.c](https://github.com/StevenChen8759/khttpd/blob/master/http_server.c) 中的 callback 函式: ```cpp=99 static int http_parser_callback_request_url(http_parser *parser, const char *p, size_t len) { struct http_request *request = parser->data; strncat(request->request_url, p, len); return 0; } ``` ```cpp=136 static int http_parser_callback_message_complete(http_parser *parser) { struct http_request *request = parser->data; http_server_response(request, http_should_keep_alive(parser)); request->complete = 1; return 0; } ``` * `http_parser` 會自動替我們取得 Request 的 URL,並在訊息接收完成後呼叫對應的 Response 函式,如下所示: ```cpp=76 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 = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY : HTTP_RESPONSE_200_DUMMY; http_server_send(request->socket, response, strlen(response)); return 0; } ``` * `http_server_response` 會根據 HTTP 的 Request 方法 (e.g. GET, POST...等) 回應對應的訊息,四種訊息對應的 Marco 如下所示: ```cpp=12 #define HTTP_RESPONSE_200_DUMMY \ "" \ "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: 12" CRLF \ "Connection: Close" CRLF CRLF "Hello World!" CRLF #define HTTP_RESPONSE_200_KEEPALIVE_DUMMY \ "" \ "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: 12" CRLF \ "Connection: Keep-Alive" CRLF CRLF "Hello World!" CRLF #define HTTP_RESPONSE_501 \ "" \ "HTTP/1.1 501 Not Implemented" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: 21" CRLF \ "Connection: Close" CRLF CRLF "501 Not Implemented" CRLF #define HTTP_RESPONSE_501_KEEPALIVE \ "" \ "HTTP/1.1 501 Not Implemented" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: 21" CRLF \ "Connection: KeepAlive" CRLF CRLF "501 Not Implemented" CRLF ``` * 參考 `HTTP_RESPONSE_200_DUMMY` 與 `HTTP_RESPONSE_200_KEEPALIVE_DUMMY` 兩個 Marco,可見到我們透過 `wget` 連接 `127.0.0.1:2020` 回應的 `Hello World!` 字串,如下所示 (搭配自訂的 [Make Target](https://github.com/StevenChen8759/khttpd/blob/master/Makefile)): ```shell $ make request PORT=2020 wget 127.0.0.1:2020/ --2020-04-03 01:01:47-- http://127.0.0.1:2020/ Connecting to 127.0.0.1:2020... connected. HTTP request sent, awaiting response... 200 OK Length: 32 [text/plain] Saving to: ‘index.html.2’ index.html.2 100%[================================================================>] 32 --.-KB/s in 0s 2020-04-03 01:01:47 (3.64 MB/s) - ‘index.html.2’ saved [32/32] ``` ```shell $ cat index.html.2 Hello World! Fibonacci Number. ``` :::warning 可用 `$ wget -O - http://foo > file` 搭配 redirection 來驗證 Fibonacci 數的計算是否符合預期 :notes: jserv ::: * 在上述的測試中,我們針對回應的字串做出修改,並調整原始 Response 中的字串長度以符合新字串的長度 (12 -> 32)。 * 因此,我們只要==針對字串與字串長度進行動態地修改==,即可正確地以文字的方式回傳 Request 中所指定的 Fibonacci Number。 ## :camera: 整合自訂的大數運算型別 (char-based) * 在先前[自訂的 `bignum` 實現](https://github.com/StevenChen8759/fibdrv/tree/master/bignum)中,我們使用了字元運算搭配鏈結串列,藉以正確地儲存位數極大的費氏數。 > 在此版本的大數型別實作中,測試回傳 F[10000] 時的執行時間不甚理想,甚至在測試 F[30000] 時直接造成系統崩潰,仍有很大的改善空間。(原有實現改以 `Fast Doubling` 方法實作 or 整合 [jserv 老師的 `bignum` 實作](https://github.com/sysprog21/bignum)、記憶體配置改善......等 ) > [name=Steven HH Chen][time=Sat, Apr 5, 2020 3:30 PM][color=red] * 但是先前的實現並未成功地整合到 `fibdrv` 中,其主因在於 <font color=red>程式的系統呼叫函式庫是從 User Space 呼叫 Kernel Space</font>,無法適用在 Kernel Space 的程式呼叫! * 因此,我們必須將==舊有的一些系統呼叫置換成適用於 Kernel Space 的系統呼叫==,如下列的置換對應: | 適用於 User Space 的系統呼叫 | 對應適用於 Kernel Space 的呼叫 | | ---------------------------- | ------------------------------ | | printf() | printk() 或 pr_info() ...等 | | malloc() | kmalloc() | | calloc() | kcalloc() | | free() | kfree() | > 可透過 `條件式編譯` 的技巧,選擇程式在編譯時是使用 `User Space` 或 `Kernel Space` 的系統呼叫,並輸出適合 `User Space` 或 `Kernel Space` 的 Object。 > [color=green] * 除此之外,為方便處理 Response 字串的串接,故在 `bignum` 中新增函式 `bn_tostring_and_free` ,此函式會將大數轉換成字串(字元陣列)回傳,並同時釋放大數所占用的空間。實作如下: ```cpp=206 char *bn_tostring_and_free(bignum_t **bnum) { int i; char *str; bnlist_t *ptr; if (*bnum == NULL) return NULL; str = (char *) kcalloc((*bnum)->cnt_d + 1, sizeof(char), GFP_KERNEL); ptr = (*bnum)->msd; for (i = 0; i < (*bnum)->cnt_d; i++) { str[i] = ptr->value + 0x30; ptr = ptr->next; } bn_free(*bnum); *bnum = NULL; return str; } ``` * 在 [main.c](https://github.com/StevenChen8759/khttpd/blob/master/main.c) 中整合 `bignum`,並呼叫函式測試其功能,實作如下: ```cpp=9 #include "bignum.h" ``` ```cpp=94 static int __init khttpd_init(void) { bignum_t *fibn = bn_fibonacci(60); pr_info("fibnum is : %s", bn_tostring_and_free(&fibn)); // bn_free(fibn); 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, &param, 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; } ``` * 修改 `Makefile` 中編譯核心模組相關的變數,並執行編譯,實作如下: ```diff $ git diff 54194e b8bfe3 diff --git a/Makefile b/Makefile index c478aa4..41e4792 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,14 @@ LDFLAGS_user = -lpthread obj-m += khttpd.o khttpd-objs := \ + bignum.o \ http_parser.o \ http_server.o \ main.o GIT_HOOKS := .git/hooks/applied -all: $(GIT_HOOKS) http_parser.c htstress +all: $(GIT_HOOKS) bignum.c http_parser.c htstress make -C $(KDIR) M=$(PWD) modules $(GIT_HOOKS): ``` ```shell $ make cc -std=gnu99 -Wall -Wextra -Werror -o htstress htstress.c -lpthread make -C /lib/modules/4.15.0-91-generic/build M=/home/stch/linux2020/khttpd modules make[1]: Entering directory '/usr/src/linux-headers-4.15.0-91-generic' CC [M] /home/stch/linux2020/khttpd/bignum.o CC [M] /home/stch/linux2020/khttpd/http_parser.o CC [M] /home/stch/linux2020/khttpd/http_server.o CC [M] /home/stch/linux2020/khttpd/main.o LD [M] /home/stch/linux2020/khttpd/khttpd.o Building modules, stage 2. MODPOST 1 modules CC /home/stch/linux2020/khttpd/khttpd.mod.o LD [M] /home/stch/linux2020/khttpd/khttpd.ko make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-91-generic' ``` * 成功編譯後,將[模組掛載](https://github.com/StevenChen8759/khttpd/blob/master/Makefile)至核心,並觀察 `dmesg` 輸出費氏數列的第 60 項: ```shell $ make load ... $ dmesg ... [66587.574298] khttpd: fibnum is : 1548008755920 ... ``` ## :camera: 建立任意輸入費氏數的 Response (Via URL 輸入) * 首先,我們先統整常用的數值與字串處理函式在 `Kernel Space` 執行的替代: | 適用於 User Space 的系統呼叫 | 對應適用於 Kernel Space 的呼叫 | | ---------------------------- | ------------------------------ | | itoa() | snprintf() | | atoll() | kstrtoll () | | strtok() | strsep() | | strncpy() | 無須替換,或可採用 snprintf() | * 參考 [基於 `strsep()` 之 URL 傳遞參數分割法](https://blog.wu-boy.com/2010/04/cc-c%E8%AA%9E%E8%A8%80%E5%88%87%E5%89%B2%E5%AD%97%E4%B8%B2%E5%87%BD%E5%BC%8F-strsep%EF%BC%8C%E5%88%86%E6%9E%90-url-get-%E5%8F%83%E6%95%B8/) ,在 `http_response` 函式內實作 URL 分割,輸出結果如下: ```shell $ make reload ... $ make request RFILE=fib/10 ... $ dmesg [82143.401840] khttpd: requested_url = /fib/10, len = 7 [82143.401844] khttpd: sep / -> url afetr: 10, ptr return: fib ``` * 透過檢查字串是否符合我們的輸入後,計算指定輸入的費氏數列,並將其打印在 dmesg 上,結果如下: ```shell [82143.401840] khttpd: requested_url = /fib/10, len = 7 [82143.401844] khttpd: sep / -> url afetr: 10, ptr return: fib [82143.401849] khttpd: F[10] = 55 [82143.401849] khttpd: free fib! [82160.456446] khttpd: requested_url = /fib/10000, len = 10 [82160.456451] khttpd: sep / -> url afetr: 10000, ptr return: fib [82185.911054] khttpd: F[10000] = 3364476487643178326662161200510754331030214846068006390656476997468008144216666236815559551363373402558206533268083615937373479048386526826304089246305643188735454436955982749160660209988418393386465273130008883026923567361313511757929743785441375213052050434770160226475831890652789085515436615958298727968298751063120057542878345321551510387081829896979161312785626503319548714021428753269818796204693609787990035096230229102636813149319527563022783762844154036058440257211433496118002309120828704608892396232883546150577658327125254609359112820392528539343462090424524892940390170623388899108584106518317336043747073790855263176432573399371287193758774689747992630583706574283016163740896917842637862421283525811282051637029808933209990570792006436742620238978311147005407499845925036063356093388383192338678305613643535189213327973290813373264265263398976392272340788292817795358057099369104917547080893184105614632233821746563732124822638309210329770164805472624384 ``` * 在建立費氏數計算的機制後,我們著手修改定義的 Macro,並以 `snprintf()` 函式整合計算的費氏數以及 HTTP Response 文字格式,並==嚴格管制回傳字串的記憶體空間配置==,實作如下: ```cpp=11 #define CRLF "\r\n" #define HTTP_RESPONSE_200_DUMMY \ "" \ "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: %zu" 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: %zu" CRLF \ "Connection: Keep-Alive" CRLF CRLF "%s" CRLF ``` ```cpp=79 char *respmsg_edition(char *msg, int keep_alive) { char *rpmsg = NULL; int msgl; size_t rpmsglen; // Calculate space needed for response message return // KEEPALIVE_DUMMY vs DUMMY, excluding 5 characters "%zu" and "%s" rpmsglen = keep_alive ? strlen(HTTP_RESPONSE_200_KEEPALIVE_DUMMY) - 5 : strlen(HTTP_RESPONSE_200_DUMMY) - 5; // Add msg length into needed space rpmsglen += strlen(msg); // Probing the length of strlen(msg) and add into needed space rpmsg = (char *) kcalloc(strlen(msg) + 1, sizeof(char), GFP_KERNEL); if (rpmsg == NULL) { pr_err("Allocate space for response message fail..."); return NULL; } msgl = snprintf(rpmsg, strlen(msg), "%zu", strlen(msg)); if (msgl > 0) { rpmsglen += msgl; } kfree(rpmsg); /* Formal allocation for response needed */ rpmsg = (char *) kcalloc(rpmsglen, sizeof(char), GFP_KERNEL); /* Compose formal HTTP response */ msgl = keep_alive ? snprintf(rpmsg, rpmsglen, HTTP_RESPONSE_200_KEEPALIVE_DUMMY, strlen(msg), msg) : snprintf(rpmsg, rpmsglen, HTTP_RESPONSE_200_DUMMY, strlen(msg), msg); pr_info("HTTP response snprintf result: %d", msgl); return rpmsg; } ``` * 整合輸入判斷、費氏數計算與 HTTP Response 文字組合的 `http_response()` 函式之實現如下: ```cpp=122 static int http_server_response(struct http_request *request, int keep_alive) { char *response, *url = NULL, *ptr_n, *ptr_i, fib_s, *rpmsg = NULL; long long fib_input; int kres; bignum_t *bn_res; /* Allocate string space for url copying */ url = (char *) kcalloc(strlen(request->request_url), sizeof(char), GFP_KERNEL); /* Prevent allocation fail case*/ if (url == NULL) { pr_err("Allocate url space fail!"); goto rsp; // Response allocation error directly } /* Copying URL */ strncpy(url, request->request_url + 1, strlen(request->request_url)); ptr_n = url; pr_info("requested_url = %s, len = %zu\n", request->request_url, strlen(request->request_url)); /* Seperate instruction pattern and requested number pattern */ /* Note: ptr_i for instruction pattern, ptr_n for number pattern */ ptr_i = strsep(&ptr_n, "/"); pr_info("sep / -> number: %s, instruction: %s\n", ptr_n, ptr_i); /* Check if the instruction pattern is matched... */ if (strncmp(ptr_i, "fib", 4) == 0) { /* Transfer input number to type long long (fit bn_fibonacci(long long)) */ kres = kstrtoll(ptr_n, 10, &fib_input); /* Calculate fibonacci number while return success */ if (kres == 0) { /* CPU bound task, disable preemption for better performance */ preempt_disable(); /* Calculate fibonacci number */ bn_res = bn_fibonacci(fib_input); /* Enable preemption */ preempt_enable(); } else { pr_err("Input to long long fail, fail code: %d", kres); } } else { pr_err("Pattern fib is NOT matched!"); } //----------------------------------------Allo---------------------------------------- // Integrate response message to formal HTTP response! rsp: if (url == NULL) { rpmsg = (char *) kcalloc(sizeof("Allocate url space fail!\n"), sizeof(char), GFP_KERNEL); strncat(rpmsg, "Allocate url space fail!\n", sizeof("Allocate url space fail!\n")); } else { // rpmsg = (char *) kcalloc(sizeof("Calculate fibonacci number!"), // sizeof(char), GFP_KERNEL); // strncat(rpmsg, "Calculate fibonacci number!\n", sizeof("Calculate // fibonacci number!\n")); rpmsg = bn_tostring_and_free(&bn_res); } // pr_info("%s, %zu", rpmsg, strlen(rpmsg)); if (request->method != HTTP_GET) response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; else { response = respmsg_edition(rpmsg, keep_alive); /*if (response != NULL) kfree(response); response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY : HTTP_RESPONSE_200_DUMMY;*/ } ``` :::danger `if (strncmp(ptr_i, "fib", 3) == 0 && strlen(ptr_i) == 3) {` 這樣的敘述,會造成可能存取 `ptr_i` 開頭的記憶體空間兩次,是很沒效率的做法,請你思考改進方案。我們可做到無論如何,只要存取記憶體內容一次。 :notes: jserv ::: >解決方案:將 `strncmp(ptr_i, "fib", 3) == 0 && strlen(ptr_i) == 3` 表達式,改以 `strncmp(ptr_i, "fib", 4) == 0 ` 實作,因 ASCII 字串編碼是以 `\0` 作為結尾,因此將 `strncmp()` 的比較字元長度改為 4 ,即可免除判斷 `ptr_i` 之長度,且因常數字串 `fib` 在記憶體實際占用的空間為 4 Byte (包含 `\0`),因此不會有 `緩衝區溢位 (Buffer overflow)` 的問題產生,一兼兩顧。 >[name=Steven HH Chen][time=Thu, Apr 9, 2020 4:04 PM][color=Yellow] :::warning Good. 多想兩分鐘,你可以寫出精簡正確又安全的程式碼 :notes: jserv ::: * 透過 wget 進行實測: ```shell $ make request RFILE=fib/55 ... $ cat 55 139583862445 $ make request RFILE=fibo/ ... $ cat index.html Instruction pattern is NOT matched! $ make request RFILE=fib/xxx ... $ cat xxx Input to long long fail, fail code: -22 ``` # :triangular_ruler: 模組改善與效能評測 ## :wrench: 檢測 `khttpd` 造成系統停擺之原因 * 首先,我們觀察基於 `Dynamic Programming` 方法計算費氏數的函式 `bn_fibonacci()` 在 Runtime 的記憶體配置行為: ```cpp=38 bignum_t *bn_fibonacci(long long n) { /* Declare dynamic array for big number */ bignum_t **bn_fib; bignum_t *ptr_retn; long long i; /* Return NULL while input is not non-negative value */ if (n == 0) return NULL; /* Allocate n bignum_t* pointer spaces for fibonacci number storage */ bn_fib = kcalloc(n + 1, sizeof(bignum_t *), GFP_KERNEL); // Allocate pointer spaces... /* Return NULL while failed allocation */ if (bn_fib == NULL) return NULL; /* Allocate bignum_t spaces for initial value F[0] and F[1] */ bn_fib[0] = bn_create(); // Allocate F[0] bn_fib[1] = bn_create(); // Allocate F[1] /* Return NULL while failed allocation*/ if (bn_fib[0] == NULL || bn_fib[1] == NULL) { if (bn_fib[0] != NULL) bn_free(bn_fib[0]); if (bn_fib[1] != NULL) bn_free(bn_fib[1]); kfree(bn_fib); return NULL; } /* Fibonacci number initial value assignment */ ((bn_fib[0])->lsd)->value = 0; // F[0] = 0 ((bn_fib[1])->lsd)->value = 1; // F[1] = 1 /* Calculate Fibonacci Number (Start from 2) */ for (i = 2; i <= n; i++) { bn_fib[i] = bn_add_with_malloc(bn_fib[i - 1], bn_fib[i - 2]); if (i == n) ptr_retn = bn_fib[i]; // Return nth sequence } // free bignum storage space for (i = n - 1;; i--) { bn_free(bn_fib[i]); if (i == 0) break; } // free pointer to pointer space for bignum kfree(bn_fib); return ptr_retn; } ``` :::danger 避免說「造成 Kernel 可用的記憶體空間枯竭」這樣不精準的話語,因為: 1. 「核心可用的記憶體空間」是對應於虛擬記憶體的配置,你不能單就 [kmalloc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kmalloc.html) 失敗就說「沒有可用的記憶體空間」,相反地,你需要解讀問題的根源; 2. 需要量化分析 :notes: jserv ::: * 在 Client 要求的費氏數在數萬項等級 (e.g. 30000) 時, `Dynamic Programming` 的方法會配置 Kernel Space 的記憶體空間,<s>造成 Kernel 可用的記憶體空間枯竭</s>,致使系統崩潰。 * 但觀察費氏數列的計算過程,F[2] 計算完成後,F[0] 項即永遠不再使用,此使我們即應釋放該空間,如此一來記憶體的耗用即可大幅減少,配置如下: ```diff diff --git a/bignum.c b/bignum.c index 926bdd8..992f40d 100644 --- a/bignum.c +++ b/bignum.c @@ -86,15 +86,13 @@ bignum_t *bn_fibonacci(long long n) for (i = 2; i <= n; i++) { bn_fib[i] = bn_add_with_malloc(bn_fib[i - 1], bn_fib[i - 2]); + bn_free(bn_fib[i - 2]); + if (i == n) ptr_retn = bn_fib[i]; // Return nth sequence } - for (i = n - 1;; i--) { - bn_free(bn_fib[i]); - if (i == 0) - break; - } + bn_free(bn_fib[n - 1]); kfree(bn_fib); ``` ## :straight_ruler: `khttpd` 之效能評測機制與設計 * 首先,我們將已完成的程式碼透過 Git 操作移植至雙系統 ubuntu 上進行測試,並參考 `fibdrv` 中的[排除測試干擾因素](https://hackmd.io/@sysprog/linux2020-fibdrv#%E6%8E%92%E9%99%A4%E5%B9%B2%E6%93%BE%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E7%9A%84%E5%9B%A0%E7%B4%A0)排除測試干擾因素。 ## :hammer: 改善 `bignum` 之計算效能與比較 * 將原有的 `Dynamic Programming` 方法修改成 `Fast Doubling`。 實作敬請參閱最新的 [bignum.c](https://github.com/StevenChen8759/khttpd/blob/master/bignum.c) 與 [bignum.h](https://github.com/StevenChen8759/khttpd/blob/master/bignum.h)。 :::danger 上述的 `bignum` 仍然基於字元操作進行數值計算,仍有很大的效能改進空間。 :arrow_forward: Steven HH Chen ::: ## :wrench: 驗證回應費氏數的正確性 * 我們自 [Facebook 的討論區]()引入[費氏數驗證的 Python Script](https://gist.github.com/wilson6405/c8b86f71ce680efee26850298c9c237c)並加以修改,利用套件的網路爬蟲功能產生正確的費氏數比較檔案 `fib_crawler.txt`。 * 在此同時,我們撰寫 Shell Script 操作網路通訊套件 `wget`,並將 `khttpd` 模組回應的費氏數列數值儲存在 `fibnum.txt` 檔案內,同時利用 `diff` 加以驗證。結果如下: ```shell $ diff -s fibnum.txt scripts/fibnum_crawler.txt Files fibnum.txt and scripts/fibnum_crawler.txt are identical ``` > 目前共驗證 10000 項,實際查詢[提供費氏數列值的網站](http://www.protocol5.com/Fibonacci),可發現在要求超過數百萬項的值時,該網站並無建立對應的資料,因此我們可針對要求的費氏數項次做出一些限制,儘管我們的大數計算效能能夠持續改善,但在計算時間與空間隨著要求的費氏數項次的上升而暴增時 (其增長率約為 $O(2^n)$ ),我們也必須做出一些要求輸入值上的限制,才能在 Request 處理能力上取得平衡。 > [name=Steven HH Chen][time=Sat, Apr 11, 2020 6:03 PM][color=yellow] ## :hammer: Socket 通訊處理機制之改善與探討 * 瀏覽器與 wget 之通訊行為上的不同... ## :wrench: khttpd 安全性探討與改善 # :bulb: 問題與討論 ## :arrow_forward: `khttpd` 之初始化參數傳遞方法 * 參考 [`The Linux Kernel Module Programming Guide`](http://tldp.org/LDP/lkmpg/2.4/html/x354.htm) 之說明,核心模組的參數會先透過程式內部的 `MODULE_PARAM()` 宣告參數的名稱與參數型別,參考我們的 `khttpd` 實現如下:(in `main.c`) ```cpp=14 static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` * 因此,我們在載入模組時,輸入以下命令,即可決定模組所開啟的 socket port,參閱 [Makefile](https://github.com/StevenChen8759/khttpd/blob/master/Makefile) 內的 Target `load` : ```=32 PORT := 8081 load: all sudo insmod khttpd.ko port=$(PORT) $(MAKE) stat ``` ```make=42 stat: @echo "------------------------------------------------------------------------" -lsmod | grep khttpd @echo "------------------------------------------------------------------------" sudo netstat -antp ``` * 實際執行結果如下: ```shell $make load PORT=2020 make -C /lib/modules/4.15.0-96-generic/build M=/home/stch/linux2020/khttpd modules make[1]: Entering directory '/usr/src/linux-headers-4.15.0-96-generic' Building modules, stage 2. MODPOST 1 modules make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-96-generic' sudo insmod khttpd.ko port=2020 make stat make[1]: Entering directory '/home/stch/linux2020/khttpd' ------------------------------------------------------------------------ lsmod | grep khttpd khttpd 45056 0 ------------------------------------------------------------------------ sudo netstat -antp Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:2020 0.0.0.0:* LISTEN - tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN 1090/dnsmasq tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 926/cupsd tcp 1 0 127.0.0.1:8081 127.0.0.1:50676 CLOSE_WAIT - tcp6 0 0 ::1:631 :::* LISTEN 926/cupsd make[1]: Leaving directory '/home/stch/linux2020/khttpd' ``` * Ref: https://lwn.net/Articles/22197/ ## :arrow_forward: `epoll()` 系統呼叫分析 *