--- tags: linux kernel --- # 2022q1 Homework6 (ktcp) contributed by < `huang-me` > # kecho ## 給定的 kecho 已使用 CMWQ,請陳述其優勢和用法 - 優勢: 1. CMWQ 利用的 thread pool 的機制,大幅減少了創建 thread 的 overhead。 2. 使用 unbounded thread 機制使得 thread 可以切換到 idle 的 CPU 上,增加系統資源的使用率。 3. 引入 scheduling 機制資源不會被需要長時間的 thread 佔用。 - 用法: 1. alloc_workqueue : 在初始化模組時用來建立一個 workqueue 2. destroy_workqueue : 用來釋放 workqueue 3. queue_work : 將任務放入 workqueue 中排程 4. INIT_WORK : 用以初始化任務 ## `user-echo-server` 運作原理 1. 先建立一個 socket 的 file descriptor,之後透過它來操作 socket。 ```c listener = socket(PF_INET, SOCK_STREAM, 0); ``` 2. 將剛建立的 socket 與 localhost 綁定 ```c bind(listener, (struct sockaddr*) &addr, sizeof(addr)); ``` 3. 再來我們讓 socket 開始監聽,並將 socket 的 file descriptor 用 epoll_ctl 加入 epoll 監控的列表之中,以對使用者透過 socket 建立連線時可以進行處理。 ```c if (listen(listener, 128) < 0) server_err("Fail to listen", &list); int epoll_fd; if ((epoll_fd = epoll_create(EPOLL_SIZE)) < 0) server_err("Fail to create epoll", &list); static struct epoll_event ev = {.events = EPOLLIN | EPOLLET}; ev.data.fd = listener; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listener, &ev) < 0) server_err("Fail to control epoll", &list); printf("Listener (fd=%d) was added to epoll.\n", epoll_fd); ``` ## kecho、user-echo-server 效能比較 - 因為 `bench` 所記錄的時間是從 client 送出資料到收到 server 端資料回傳的時間,而且因為 user-echo-server 並不是一個並行的 server,所以在 epoll 同時收到多個 client 的事件時,會依據 file descriptor 位於 epoll_ev 陣列的位置依序執行,這樣的作法有可能導致處理速度被拖慢,進而造成測量時間大幅增加。 ![](https://i.imgur.com/1sz6vii.png) - kecho 考慮到 concurrency 議題,使用 CMWQ 將任務分配給空閒的執行緒執行,並且因為在 kernel 執行所以,所以執行的效率比 `user-echo-server` 好許多。 ![](https://i.imgur.com/L5XegoC.png) - 因為 I/O 執行需要很多時間,所以我嘗試將 `user-echo-server` 的所有 `print` 操作刪除之後再做一次 `bench`,可以發現執行的速度確實有很大幅度的提升。 ![](https://i.imgur.com/nMnlxK2.png) - 同樣,也把 `kecho` 的所有 `print` 操作刪除,並且重新執行 `bench` ,可以看出 `kecho` 在無需 `print` 的情況下同時接受多連線時受到的影響較小。 ![](https://i.imgur.com/Z8VrXS0.png) ## drop-tcp-socket 原理 ### TIME_WAIT socket ![](https://i.imgur.com/Z9KPjkb.png) - tcp socket 在結束連線時會進行一系列的溝通,以確保雙方都關閉 socket 以免佔用。而 server 端的最後一個狀態即為 TIME_WAIT 並且會停留在這個狀態 2MLS (2 x Maximum Segment Lifetime),以避免資料遺失。 - 如果有很多 TIME_WAIT 狀態的 socket 佔用著 port 會使得新建立的連線難以找到一個可以使用的 port,但一下狀況使得我們還是需要 TIME_WAIT 機制 1. 若有 A、B 兩次連線都連線到相同的 port,且沒有任何 TIME_WAIT 機制,若是要傳送給 A 的資料在 A 關閉連線且 B ==馬上==連上此 port,則 B 將誤以為是要傳送給自己的訊息。 2. 為了確保 TCP full-duplex 終止連線的可靠性。假設 client 終止連線的最後一個 ACK (由 server 傳來)在傳輸過程丟了,則 client 會再傳一次 FIN,若此時沒有 TIME-WAIT 的機制,client 將會收到傳送錯誤的訊息 RST,因為 server 已經關閉了。 # kHTTPd ## khttp 運作原理 ### `khttp_init` 運作原理 - kernel module 在被 insert 到 kernel 後,執行以下 function,其內容主要做以下事情 1. 先建立新的 socket 的 file descriptor,並且將其與 `localhost:port` bind 在一起。 2. 建立一個 thread 執行 `http_server_daemon` ```c 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; } ``` ### `http_server_daemon` 運作原理 - 執行一個無窮迴圈,在接受到新的資料時,將接收到的資料傳遞給新的 socket 並且建立一個新的 worker 處理此 socket 的資料。 :::warning 因為 `http_server_daemon` 是一個無窮迴圈,因此我們需要利用 kthread 的 `kthread_should_stop` 判斷是否應該結束此 daemon function,並且執行 `kthread_stop` 來終止執行。 ::: ```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); 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; } ``` ### `http_server_worker` 運作原理 - 執行一個無限迴圈,接收此 socket 的所有資料並且藉由 `http_parser` 進行相對應的反應。 :::success 其中 `http_parser_settings` 設定了各時期應該執行的相對應 function。 ::: ```c 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; } ```