--- tags: linux kernel --- # 2022q1 Homework6 (ktcp) contributed by < `zoanana990` > > [作業要求](https://hackmd.io/@sysprog/linux2022-ktcp) > 程式碼:[ktcp](https://github.com/zoanana990/kecho), [khttpd](https://github.com/zoanana990/khttpd) ## :checkered_flag: 自我檢查清單 - kecho - [x] 給定的 `kecho` 已使用 CMWQ,請陳述其優勢和用法 - [x] 核心文件 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) 提到 "The original create_`*`workqueue() functions are deprecated and scheduled for removal",請參閱 Linux 核心的 git log (不要用 Google 搜尋!),揣摩 Linux 核心開發者的考量 - [x] 解釋 `user-echo-server` 運作原理,特別是 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用 - [x] 是否理解 `bench` 原理,能否比較 `kecho` 和 `user-echo-server` 表現?佐以製圖 - [ ] 解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼? - khttpd - [x] 參照 [fibdrv 作業說明](https://hackmd.io/@sysprog/linux2020-fibdrv) 裡頭的「Linux 核心模組掛載機制」一節,解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢? - [x] 參照 [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 [kecho](https://github.com/sysprog21/kecho),目標是修正 `kecho` 的執行時期的缺失,提升效能和穩健度 (robustness) * [ ] 若使用者層級的程式頻繁傳遞過長的字串給 `kecho` 核心模組,會發生什麼事? * [x] 參照 [kecho pull request #1](https://github.com/sysprog21/kecho/pull/1),嘗試比較 kthread 為基礎的實作和 CMWQ,指出兩者效能的落差並解釋 * [ ] 如果使用者層級的程式建立與 `kecho` 核心模組的連線後,就長期等待,會導致什麼問題? * [ ] 研讀 [Linux Applications Performance: Introduction](https://unixism.net/2019/04/linux-applications-performance-introduction/),嘗試將上述實作列入考量,比較多種 TCP 伺服器實作手法的效能表現 * 在 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) --- ## 文獻回顧 ### `kecho` #### 給定的 `kecho` 已使用 [CMWQ](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html),請陳述其優勢和用法 * 簡介 * 有很多情況需要用到執行非同步行程時而工作佇列 (workqueue) 的 API 就是最常用到這個機制的情況 * 當需要非同步行程,一個工作項目描述該函式執行是放在一個列隊中。一個獨立的執行緒作為非同步執行上下文,稱為 workqueue,而執行緒稱為 worker * 當工作佇列上面有工作項時,就會依序執行 work item,列隊上沒有東西的時候則會空閒,當一個新的工作項目進入列隊後會再繼續執行。 * 為什麼需要 CMWQ? * 最初的工作佇列中,多執行緒 (multithread) 的工作佇列是每個 CPU 就有一個工作執行緒,而單執行緒的工作佇列為系統範圍內統一使用一個工作佇列。每個多執行緒的工作佇列需要保持和 CPU 數量相同的 worker。 * 多執行緒的工作佇列使用了許多資源,但是發揮出並行的效果卻不盡人意。 Work Item 必須競爭那些非常有限的上下文,這會引發許多問題,這也對導致用戶必須為了並行和資源之間進行不必要的權橫,這也引發了並行管理工作佇列 (CMWQ) 的開發,主要關注以下幾點目標: * 和原始的工作佇列保持相容性 * 使用每一個 CPU 統一個工作池 (Worker Pools) 根據不同需求提供彈性的並行級別,減少資源浪費 * 自動調節工作池與並行級別,使 API 用戶不需要擔心這些細節 * CMWQ 的設計 * 為了簡化非同步執行引入了工作項 (work item) 的概念,工作項的結構很簡單,它包含一個指向非同步執行函式的指標。每當子系統或是驅動程式想要非同步執行函式時他必須設置一個指向函式的工作項,並將該工作項排入工作佇列中。 * 工作池由工作執行緒管理,其主要執行工作佇列的功能 * CMWQ 設計區分了子系統與驅動程式的工作項與後端機制 * 兩個工作池,一個用在正常的工作項目,另外一個用在高優先級的工作項目 * 子系統和驅動程式會根據他們合適的工作佇列的 API 函式來創建和排列工作項,他們可以通過不同的 flags 來判斷工作項的執行方式 * 當工作項進入工作佇列後,根據列隊參數和工作佇列屬性工作池,並附加工作池的共享工作佇列中。 * 對於任何工作池的實現, CMWQ 嘗試將並行保持在最低但是足夠的水平,最大限度節省資源 * 當 worker-pool ==?==,worker-pool 都會收到通知,並跟踪當前可運行的工作人員的數量。通常,工作項不會佔用 CPU 並消耗很多周期,只要 CPU 上有一個或多個可運行的 worker,worker-pool 就不會開始執行新的工作,但是,當最後一個運行的 worker 進入睡眠狀態時,它會立即調度一個新的 worker,以便 CPU 不會當有待處理的工作項時,不要閒著。 * 除了 kthread 的記憶體空間之外,保持空閒的 worker 不會花費其他成本,因此 cmwq 在釋放它們的系統資源之前,會保留空閒的 worker 一段時間。 * 總整 CMWQ 的優勢 * 相較於傳統的工作佇列需要考慮 CPU 調用的問題,CMWQ 使用 worker-pool 全部幫開發者處理好,可以用最少的資源做最多的事 * 相容於原本的程式開發介面 #### 核心文件 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) 提到 "The original create_`*`workqueue() functions are deprecated and scheduled for removal",請參閱 Linux 核心的 git log (不要用 Google 搜尋!),揣摩 Linux 核心開發者的考量 * 參考 [oscarshiang/kecho](https://hackmd.io/@oscarshiang/linux_kecho) * 題目中提到 "The original create_*workqueue() functions are deprecated and scheduled for removal" 原始的 create_*workqueue() 函式已經棄用,請說明原因,這裡主要先關注幾個 [`commit`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/log/include/linux/workqueue.h) * 輸入命令 ```shell $ git log --pretty=format:"%h - %an, %ar : %s" ./include/linux/workqueue.h ``` * 輸出結果示例,下面會以這裡列出的 commit 進行舉例: ```shell fb0e7beb5c1b - Tejun Heo, 12 years ago : workqueue: implement cpu intensive workqueue 649027d73a63 - Tejun Heo, 12 years ago : workqueue: implement high priority workqueue e22bee782b3b - Tejun Heo, 12 years ago : workqueue: implement concurrency managed dynamic worker pool c790bce04818 - Tejun Heo, 12 years ago : workqueue: kill RT workqueue 0d557dc97f4b - Heiko Carstens, 14 years ago : workqueue: introduce create_rt_workqueue ``` * 首先從 [oscarshiang](https://hackmd.io/@oscarshiang/linux_kecho) 提到這個 [commit](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=d320c03830b17af64e4547075003b1eeb274bc6c) 寫到將 `create` 改成 `alloc` 是因為目前的 workqueue 趨於複雜,若是改寫函式要將整塊相關函式都進行改寫非常不便如 [workqueue: introduce create_rt_workqueue](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=0d557dc97f4bb501f086a03d0f00b99a7855d794) 中提到的創造 `create_rt_workqueue` 。尷尬的是,在 [workqueue: kill RT workqueue](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=c790bce0481857412c964c5e9d46d56e41c4b051) 中馬上把 `create_rt_workqueue` 刪除,下面可以看到兩個 commit 改寫的程式碼: 引入 [workqueue: introduce create_rt_workqueue](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=0d557dc97f4bb501f086a03d0f00b99a7855d794) 時做的改動: ```diff ... -#define __create_workqueue(name, singlethread, freezeable) \ - __create_workqueue_key((name), (singlethread), (freezeable), NULL, NULL) +#define __create_workqueue(name, singlethread, freezeable, rt) \ + __create_workqueue_key((name), (singlethread), (freezeable), (rt), \ + NULL, NULL) -#define create_workqueue(name) __create_workqueue((name), 0, 0) -#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1) -#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0) +#define create_workqueue(name) __create_workqueue((name), 0, 0, 0) +#define create_rt_workqueue(name) __create_workqueue((name), 0, 0, 1) +#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1, 0) +#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0, 0) ``` 刪除 [workqueue: introduce create_rt_workqueue](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=0d557dc97f4bb501f086a03d0f00b99a7855d794) 時做的改動: ```diff ... -#define __create_workqueue(name, singlethread, freezeable) \ - __create_workqueue_key((name), (singlethread), (freezeable), NULL, NULL) +#define __create_workqueue(name, singlethread, freezeable, rt) \ + __create_workqueue_key((name), (singlethread), (freezeable), (rt), \ + NULL, NULL) -#define create_workqueue(name) __create_workqueue((name), 0, 0, 0) -#define create_rt_workqueue(name) __create_workqueue((name), 0, 0, 1) -#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1, 0) -#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0, 0) +#define create_workqueue(name) __create_workqueue((name), 0, 0) +#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1) +#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0) ``` 可以發現如果要引入一個 `create_rt_workqueue`,儘管只是多加一個變數,整段程式碼都要重寫,這是一個非常不明制的手法,最後要拿掉 `create_rt_workqueue` 時,需要把整段刪除,最後甚至改回引入前的程式碼。 上面的現象證實引入新的概念對於程式碼改棟宇維護成本過高,這也就是為什麼要引入 `alloc_workqueue()` 這個概念,對於函式改寫有許多幫助,以下面的 commit 進行舉例: 新增 [`high priority workqueue`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=649027d73a6309ac34dc2886362e662bd73456dc) ```diff @@ -231,6 +231,7 @@ enum { WQ_SINGLE_CPU = 1 << 1, /* only single cpu at a time */ WQ_NON_REENTRANT = 1 << 2, /* guarantee non-reentrance */ WQ_RESCUER = 1 << 3, /* has an rescue worker */ + WQ_HIGHPRI = 1 << 4, /* high priority */ WQ_MAX_ACTIVE = 512, /* I like 512, better ideas? */ WQ_DFL_ACTIVE = WQ_MAX_ACTIVE / 2, ``` 新增 [`cpu intensive workqueue`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=fb0e7beb5c1b6fb4da786ba709d7138373d5fb22) ```diff @@ -232,6 +232,7 @@ enum { WQ_NON_REENTRANT = 1 << 2, /* guarantee non-reentrance */ WQ_RESCUER = 1 << 3, /* has an rescue worker */ WQ_HIGHPRI = 1 << 4, /* high priority */ + WQ_CPU_INTENSIVE = 1 << 5, /* cpu instensive workqueue */ WQ_MAX_ACTIVE = 512, /* I like 512, better ideas? */ WQ_DFL_ACTIVE = WQ_MAX_ACTIVE / 2, ``` [`remove WQ_SINGLE_CPU and use WQ_UNBOUND instead`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=c7fc77f78f16d138ca997ce096a62f46e2e9420a) ```diff enum { WQ_NON_REENTRANT = 1 << 0, /* guarantee non-reentrance */ - WQ_SINGLE_CPU = 1 << 1, /* only single cpu at a time */ + WQ_UNBOUND = 1 << 1, /* not bound to any cpu */ WQ_FREEZEABLE = 1 << 2, /* freeze during suspend */ WQ_RESCUER = 1 << 3, /* has an rescue worker */ WQ_HIGHPRI = 1 << 4, /* high priority */ WQ_CPU_INTENSIVE = 1 << 5, /* cpu instensive workqueue */ - WQ_UNBOUND = 1 << 6, /* not bound to any cpu */ WQ_MAX_ACTIVE = 512, /* I like 512, better ideas? */ WQ_MAX_UNBOUND_PER_CPU = 4, /* 4 * #cpus for unbound wq */ ``` 可以看到引入工作佇列的 flag 之後,對於維護程式碼的成本大幅度的降低,對於新增、修改僅需要改動 workqueue flag 這個 enum 就好,這也是為什麼將 `create_*workqueue()` 移除的原因 #### 解釋 `user-echo-server` 運作原理,特別是 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用 根據 CS:APP 第十一章中說明伺服器的運作方式: ![](https://i.imgur.com/ouqFEx2.png) > 圖片來源: CS:APP 第十一章 圖中可以看到連線分為 client 與 Server 兩端。 Server 端會利用 socket 創造一個監聽器,經過 bind, listen 兩個函式做好接收連線的準備,若是直接進行 accept 連接客戶端的話,會有效率不夠的問題,因為一次盡能對一個客戶連線。 因此在 CS:APP 第十二章有說明可以使用 `select` 函式使多個 clent 進行連接,但是由於 `select` 效率不彰,因此 `kecho` 使用 `epoll` 進行實做,使用這種多執行緒的方式就可以達到下圖的效果: ![](https://i.imgur.com/Q0QE842.png) > 圖片來源: CS:APP 第十二章 下面進行程式碼原理的追蹤說明: 建立一個監聽器,利用 man page 及 CS:APP 可知,socket 是回傳一個檔案描述子 (file descriptor),如果產生失敗則回傳 `-1` ```c int main() { ... int listener; if ((listener = socket(PF_INET, SOCK_STREAM, 0)) < 0) server_err("Fail to create socket", &list); printf("Main listener (fd=%d) was created.\n", listener); ... } ``` 由 [Linux 核心設計:事件驅動伺服器之原理和實例](https://hackmd.io/@zoanana990/r1AFIziNq)中提到 non-blocking I/O ,`setnonblock` 是將監聽器的描述子設定為 non-blocking ,程式碼如下: ```c static int setnonblock(int fd) { int fdflags; if ((fdflags = fcntl(fd, F_GETFL, 0)) == -1) return -1; fdflags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, fdflags) == -1) return -1; return 0; } int main(){ ... if (setnonblock(listener) == -1) server_err("Fail to set nonblocking", &list); ... } ``` 宣告 `epoll_event` ,程式碼如下: ```c struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ } __EPOLL_PACKED; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; int main(){ ... 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); ... } ``` 這段程式碼的幾個重點如下,以下資料來源為 Linux Programmer's Manual,這裡順帶介紹其他在這個專案中用到的 epoll 函式: * `epoll`: `epoll` 是類似於 `poll` 的 API,目的在於監視大量的檔案描述子是否可以進行 I/O, `epoll API` 的核心概念是 `epoll instance`,這是 linux 核心內部的資料結構,以 user space 的角度來看,它被視為兩個串列: * interest list:監測檔案描述子的集合 * ready list:為 I/O 準備好的檔案描述子的集合,其為 interest list 的子集合 常見函式: * `epoll_create`:創建 `epoll instance` ,如果不需要用到時,使用 `close` 關閉。回傳值為一個檔案描述子 (file descriptor)。過去需要提供 `epoll instance` 的大小以利於 linux 核心分配空間,現在都是動態分配,但是為了向前兼容,仍然需要填入一個大於 0 數字。 * `epoll_ctl`:對 `interest_list` 中的`epoll_instacce` 加入、移除或修改。取決於不同操作 op 對於目標檔案描述子 fd * 函式形式: ```c int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` * `EPOLL_CTL_ADD`: 將 fd 添加到 interest list ,並根據 event 連接到 fd 的內部文件。 * `EPOLLIN`: epoll 讀取模式 * `EPOLLET`: epoll 邊緣觸發模式,預設是 level trigger 模式 * `epoll_wait`: * 函式形式 ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` * `*event`:同 `epoll_ctl` * `maxevents`:事件數量,須大於 0 * `timeout`:指定 `epoll_wait` 阻塞的豪秒數,時間的量測方法是根據 `CLOCK_MONOTONIC` 的方式,直到: * 檔案描述子傳到一個事件 * 時間失效 * 中斷 * 如果 `timeout = -1` ,則無限期的阻塞;如果 `timeout = -1` 則立刻回傳 * 回傳值:成功時,epoll_wait() 回傳準備好給 I/O 的檔案描述子的數量,如果超時,回傳 0。發生錯誤時,epoll_wait() 回傳 -1。 接下來就近如連線階段,通常會使用一個 while 迴圈進行訊息間的傳遞: ```c int main(){ ... while (1) { struct sockaddr_in client_addr; int epoll_events_count; if ((epoll_events_count = epoll_wait(epoll_fd, events, EPOLL_SIZE, EPOLL_RUN_TIMEOUT)) < 0) server_err("Fail to wait epoll", &list); printf("epoll event count: %d\n", epoll_events_count); clock_t start_time = clock(); for (int i = 0; i < epoll_events_count; i++) { /* EPOLLIN event for listener (new client connection) */ if (events[i].data.fd == listener) { int client; while ( (client = accept(listener, (struct sockaddr *) &client_addr, &socklen)) > 0) { printf("Connection from %s:%d, socket assigned: %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client); setnonblock(client); ev.data.fd = client; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client, &ev) < 0) server_err("Fail to control epoll", &list); push_back_client(&list, client, inet_ntoa(client_addr.sin_addr)); printf( "Add new client (fd=%d) and size of client_list is " "%d\n", client, size_list(list)); } if (errno != EWOULDBLOCK) server_err("Fail to accept", &list); } else { /* EPOLLIN event for others (new incoming message from client) */ if (handle_message_from_client(events[i].data.fd, &list) < 0) server_err("Handle message from client", &list); } } printf("Statistics: %d event(s) handled at: %.6f second(s)\n", epoll_events_count, (double) (clock() - start_time) / CLOCKS_PER_SEC); } close(listener); close(epoll_fd); exit(0); } ``` 創造一個 client_addr,用 epoll_wait 找到準備好 I/O 的 epoll_instance 的數量,如果事件的描述符為 listener 則會建立新連線,並且把 client 也設為 nonblocking I/O 。連線完成後,會將客戶端的 epoll_instance 加入 instance_list 中,並把已經連線好的 client 加入 `client_list_t` 這個鏈結串列中。 如果事件描述符不是連線,則會進入 `handle_message_from_client` 的函式中。 ```c static int handle_message_from_client(int client, client_list_t **list) { int len; char buf[BUF_SIZE]; memset(buf, 0, BUF_SIZE); if ((len = recv(client, buf, BUF_SIZE, 0)) < 0) server_err("Fail to receive", list); if (len == 0) { if (close(client) < 0) server_err("Fail to close", list); *list = delete_client(list, client); printf("After fd=%d is closed, current numbers clients = %d\n", client, size_list(*list)); } else { printf("Client #%d :> %s", client, buf); if (send(client, buf, BUF_SIZE, 0) < 0) server_err("Fail to send", list); } return len; } ``` 在 `handle_message_from_client` 函式中,這裡主要看兩個函式: * `recv`:讀取 `client` 的描述符寫入 `buf` 中。回傳 0,代表連線被關閉;回傳 -1,代表錯誤產生;正常來說回傳寫入 buf 的數目 * `send`:將 `buf` 的資料傳向 `fd` 這個描述符, `len` 為 byte 作為單位。 send 回傳實際傳送資料的長度 ,若錯誤則回傳 -1 #### 是否理解 bench 原理,能否比較 kecho 和 user-echo-server 表現?佐以製圖 首先說明 `bench.c` 的原理,這裡我先觀察 bench 的函式 ```c static void bench(void) { for (int i = 0; i < BENCH_COUNT; i++) { ready = false; create_worker(MAX_THREAD); ... } } ``` 首先看到 `create_worker` 函式,在 bench.c 中的註解有提到預設開啟的執行緒是 1000,這裡可以根據註解的提示看到我使用的電腦最多可以開啟的執行緒: ```shell $ sudo sysctl net.core.somaxconn net.core.somaxconn = 4096 ``` create_worker 的目的是創造 thread_qty 個執行緒。創造方式則是使用 pthread_create ```c static void create_worker(int thread_qty) { for (int i = 0; i < thread_qty; i++) { if (pthread_create(&pt[i], NULL, bench_worker, NULL)) { puts("thread creation failed"); exit(-1); } } } ``` 查看 linux programmer's manual 中的 pthread 相關函式 * `pthread_create` * `pthread_cond_wait` * `pthread_mutex_lock`, `pthread_mutex_unlock` * `pthread_cond_broadcast` * `pthread_join` * `pthread_exit` #### 比較 kecho 和 user-echo-server 的效能 1. user-echo-server 的效能 * 用兩個終端機,一個輸入 `./user-echo-server`,另外一個輸入`./bench` ![](https://i.imgur.com/emI3wQz.png) 2. kecho 的效能 * 用兩個終端機,一個輸入 `sudo insmod kecho.ko`,另外一個輸入`./bench` ![](https://i.imgur.com/sbfn56I.png) ![](https://i.imgur.com/SKJT6vu.png) #### 解釋 drop-tcp-socket 核心模組運作原理。TIME-WAIT sockets 又是什麼 *## TODO ##* 參考:[eecheng/kecho](https://hackmd.io/@eecheng/Sy9XsgjOI#%E8%A7%A3%E9%87%8B-drop-tcp-socket-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E9%81%8B%E4%BD%9C%E5%8E%9F%E7%90%86%E3%80%82TIME-WAIT-sockets-%E5%8F%88%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F) #### TIME_WAIT 依據 [TIME_WAIT and its design implications for protocols and scalable client server systems](http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html): ```shell $ netstat -n Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 127.0.0.1:45138 127.0.0.1:12345 TIME_WAIT tcp 0 0 127.0.0.1:40494 127.0.0.1:12345 TIME_WAIT tcp 0 0 127.0.0.1:44190 127.0.0.1:12345 TIME_WAIT tcp 0 0 127.0.0.1:47138 127.0.0.1:12345 TIME_WAIT tcp 0 0 127.0.0.1:39680 127.0.0.1:12345 TIME_WAIT tcp 0 0 127.0.0.1:42390 127.0.0.1:12345 TIME_WAIT ``` #### drop-tcp-socket 核心模組運作原理 參考 [The Linux Kernel Module Programming Guide Chapter 7]() --- ### `khttpd` #### 參照「[Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module)」,解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢? 由 [`khttpd`](https://github.com/zoanana990/khttpd) 原始碼: ```c #define DEFAULT_PORT 8081 #define DEFAULT_BACKLOG 100 static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` 由上面的程式碼可知,端口是由 `module_param` 這個函式進入的,而因為上面的巨集預設為 8081,因此若不特別指定,都是使用 8081 作為輸入端口 這裡需要觀察 `module_param` 的原始碼,[`moduleparam.h`](https://elixir.bootlin.com/linux/v4.18/source/include/linux/moduleparam.h),這裡我們將 `module_param` 展開: ```c #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define module_param_named(name, value, type, perm) \ param_check_##type(name, &(value)); \ module_param_cb(name, &param_ops_##type, &value, perm); \ __MODULE_PARM_TYPE(name, #type) #define module_param_cb(name, ops, arg, perm) \ __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0) #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \ /* Default value instead of permissions? */ \ static const char __param_str_##name[] = prefix #name; \ static struct kernel_param __moduleparam_const __param_##name \ __used \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ = { __param_str_##name, THIS_MODULE, ops, \ VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } } #define __MODULE_PARM_TYPE(name, _type) \ __MODULE_INFO(parmtype, name##type, #name ":" _type) #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(tag) "=" info ``` 可以看到上面的巨集分為三個部份: * `param_check_##type(name, &(value))` * `module_param_cb(name, &param_ops_##type, &value, perm)` * `__MODULE_PARM_TYPE(name, #type)` #### 複習[前置處理器](https://hackmd.io/@sysprog/c-preprocessor)和[連結器和執行檔資訊](https://hackmd.io/@sysprog/c-linker-loader) * `#`: Stringification Operator, e.g.: `#abc` &rarr; `"abc"` * `##`: Concatenation, e.g.: `a##b` &rarr; `ab` * 使用 `objdump` 查看記憶體存放的情況 ```shell objdump -s kecho.ko ``` * 產出: ``` Contents of section .modinfo: 0000 7061726d 74797065 3d62656e 63683a62 parmtype=bench:b 0010 6f6f6c00 7061726d 74797065 3d626163 ool.parmtype=bac 0020 6b6c6f67 3a757368 6f727400 7061726d klog:ushort.parm 0030 74797065 3d706f72 743a7573 686f7274 type=port:ushort 0040 00766572 73696f6e 3d302e31 00646573 .version=0.1.des 0050 63726970 74696f6e 3d466173 74206563 cription=Fast ec 0060 686f2073 65727665 7220696e 206b6572 ho server in ker 0070 6e656c00 61757468 6f723d4e 6174696f nel.author=Natio 0080 6e616c20 4368656e 67204b75 6e672055 nal Cheng Kung U 0090 6e697665 72736974 792c2054 61697761 niversity, Taiwa 00a0 6e006c69 63656e73 653d4475 616c204d n.license=Dual M 00b0 49542f47 504c0073 72637665 7273696f IT/GPL.srcversio 00c0 6e3d4236 41303343 42323530 44393031 n=B6A03CB250D901 00d0 32303936 44394544 43006465 70656e64 2096D9EDC.depend 00e0 733d0072 6574706f 6c696e65 3d59006e s=.retpoline=Y.n 00f0 616d653d 6b656368 6f007665 726d6167 ame=kecho.vermag 0100 69633d35 2e31332e 302d3430 2d67656e ic=5.13.0-40-gen 0110 65726963 20534d50 206d6f64 5f756e6c eric SMP mod_unl 0120 6f616420 6d6f6476 65727369 6f6e7320 oad modversions 0130 00 . ``` * 在 `modinfo` 這個區段中可以看到有模組的名稱、授權條款類型、版本等資訊 #### `param_check_##type(name, &(value))` 由 [`moduleparam.h`](https://elixir.bootlin.com/linux/v4.18/source/include/linux/moduleparam.h) 的註解說明可以透過 `param_check_##type` 定義自己所需類型的變數 > param_check_##type: for convenience many standard types are provided but you can create your own by defining those variables. #### 巨集`module_param_cb(name, &param_ops_##type, &value, perm)` 由於巨集中多次使用 [`__attribute__`](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes),因此先對巨集中提到的 [`__attribute__`](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes) 進行基本的了解: * `unused` * 原文表示,它可能不會被用到,希望 GCC 不要對這個沒有用到的函式跳警告 > This attribute, attached to a function, means that the function is meant to be possibly unused. GCC does not produce a warning for this function. * `__section__` * 通常,編譯器將它生成的物件放在 data 和 bss 等部分中。但是,有時我們需要將資料放在其他地方,最好是可以讓我們指定存放地點,這類的巨集在撰寫核心模組時非常容易遇到。 > Normally, the compiler places the objects it generates in sections like data and bss. Sometimes, however, you need additional sections, or you need certain particular variables to appear in special sections, for example to map to special hardware. The section attribute specifies that a variable (or function) lives in a particular section. * 上述的巨集中,`__section__ ("__param")`,資料會在連結時期將資料放到指令區段後產生 elf 檔案,也正是如此,可以指定輸入時使用的 `port` ,其產生的資料如下: ```shell Relocation section '.rela__param' at offset 0xc2780 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000700000001 R_X86_64_64 0000000000000000 .rodata + 1248 000000000008 005700000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000010 005500000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000020 000f00000001 R_X86_64_64 0000000000000000 .data + 4 000000000028 000700000001 R_X86_64_64 0000000000000000 .rodata + 1250 000000000030 005700000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000038 005500000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000048 000f00000001 R_X86_64_64 0000000000000000 .data + 6 ``` * `aligned`: 該屬性規定變數或結構體成員的最小的對齊格式,以位元組為單位。 > This attribute specifies a minimum alignment for the variable or structure field, measured in bytes. 此時可以看到 `__module_param_call` 的目的就是為了對 `struct kernel_param` 進行初始化,並且可以自行命名。由上面的巨集,可以看到 `struct kernel_param` 扮演著重要的角色,原始碼如下: ```c struct kernel_param { const char *name; struct module *mod; const struct kernel_param_ops *ops; const u16 perm; s8 level; u8 flags; union { void *arg; const struct kparam_string *str; const struct kparam_array *arr; }; }; ``` 將結構與巨集展開如下所示: ```c module_param(port, ushort, S_IRUGO); ``` 將上面的巨集展開得 ```c module_param_named(port, port, ushort, S_IRUGO); ``` 這裡有一個比較特別的點是 `S_IRUGO` 這個讀取權限,由[官方文件](https://docs.huihoo.com/doxygen/linux/kernel/3.7/include_2linux_2stat_8h.html#acdc8b67c51df098a07f0b7705f7115fd)可以知道 `S_IRUGO=S_IRUSR | S_IRGRP | S_IROTH` ,由 CS:APP 第十章可以得知這三個遮罩的意思: | Mask | Description | | -------- | -------- | | S_IRUSR | User (owner) can read this file | | S_IRGRP | Members of the owner’s group can read this file | | S_IROTH | Others (anyone) can read this file | 由上表知, `S_IRUGO` 是唯讀的意思。 繼續展開上面的巨集: ```c param_check_ushort(port, &(port)); module_param_cb(port, &param_check_ushort, &(port), S_IRUGO); __MODULE_PARM_TYPE(port, "ushort") ``` 這邊有兩個巨集需要進行展開,這裡先展開 `module_param_cb`: ```c __module_param_call(MODULE_PARAM_PREFIX, port, &param_check_ushort, &(port), S_IRUGO, -1, 0) ``` 將巨集進行展開得: ```c static const char __param_str_port[] = MODULE_PARAM_PREFIX "port"; \ static struct kernel_param __moduleparam_const __param_port \ __used \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ = { __param_str_port, THIS_MODULE, &param_check_ushort, \ VERIFY_OCTAL_PERMISSIONS(S_IRUGO), -1, 0, { arg=&(port) } } ``` 針對上面的巨集,這裡進行查詢: 1. `__moduleparam_const` 這裡可以看到如果它不是 `const` 就是空白,原始碼如下: ```c #if defined(CONFIG_ALPHA) || defined(CONFIG_IA64) || defined(CONFIG_PPC64) #define __moduleparam_const #else #define __moduleparam_const const #endif ``` 2. `MODULE_PARAM_PREFIX` ,根據原始碼的定義,如果 `MODULE` 存在, `MODULE_PARAM_PREFIX` 就是空的 ```c #ifdef MODULE #define MODULE_PARAM_PREFIX /* empty */ #else #define MODULE_PARAM_PREFIX KBUILD_MODNAME "." #endif ``` 將巨集展開後的結構如下所示: ```c struct kernel_param { const char *name = "port"; struct module *mod = THIS_MODULE; const struct kernel_param_ops *ops = &param_check_ushort; const u16 perm = VERIFY_OCTAL_PERMISSIONS(S_IRUGO); s8 level = -1; u8 flags = 0; void *arg = &(port); }; ``` 可以看到這個巨集主要是對 `kernel_param` 這個結構體進行參數傳遞,僅憑此巨集無法充分回答這個問題,因此下面從另外一個角度進行切入。 由 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)中提到,可以利用 [strace](https://linux.die.net/man/1/strace),進行 insmod 的追蹤,這裡進行 `insmod` 分別有指定 `port` 值與沒有,結果如下所示: ```shell $ sudo strace insmod khttpd.ko ... finit_module(3, "", 0) = -1 EEXIST (File exists) ... $sudo strace insmod khttpd.ko port=1999 ... finit_module(3, "port=1999", 0) = -1 EEXIST (File exists) ... ``` * 這邊因為卸載核心模組,隨即掛載,因此這邊可以看到其返回值為 `-1` 上面可以看到,輸出結果都有 `finit_module(3, "", 0)`。也就是說,有沒有指定值,命行列的 `port` 都會將值傳給 `finit_module` 中,這裡我們可以參考 [man: finit_module](https://man7.org/linux/man-pages/man2/init_module.2.html)及 [`module.h`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c) 程式碼: ```c SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { struct load_info info = { }; void *buf = NULL; int len; int err; err = may_init_module(); if (err) return err; pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags); if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS |MODULE_INIT_IGNORE_VERMAGIC |MODULE_INIT_COMPRESSED_FILE)) return -EINVAL; len = kernel_read_file_from_fd(fd, 0, &buf, INT_MAX, NULL, READING_MODULE); if (len < 0) return len; if (flags & MODULE_INIT_COMPRESSED_FILE) { err = module_decompress(&info, buf, len); vfree(buf); /* compressed data is no longer needed */ if (err) return err; } else { info.hdr = buf; info.len = len; } return load_module(&info, uargs, flags); } ``` 上面是相關系統呼叫 (System Call) 的描述,這裡針對幾個地方進行追蹤: * `load_info`:這個結構真的有夠大,[原始碼在這](https://elixir.bootlin.com/linux/latest/source/kernel/module-internal.h#L11),主要就是將模組的資訊進行讀取 * `may_init_module` 函式定義如下 ```c static int may_init_module(void) { if (!capable(CAP_SYS_MODULE) || modules_disabled) return -EPERM; return 0; } ``` * `capable`:被定義在 [capability.h](https://elixir.bootlin.com/linux/latest/source/include/linux/capability.h#L210) 中,僅是用來回傳布林值 * `CAP_SYS_MODULE`:被定義在 [capability.h](https://elixir.bootlin.com/linux/latest/source/include/linux/capability.h#L210) 中,註解中提示到僅是用來掛載或卸載核心模組,且修改時不能有任何的限制 > Insert and remove kernel modules - modify kernel without limit * `modules_disabled`:被定義在 [module.c](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L293) 中,作為模組是否被抑制的布林值 * `EPERM`:這是錯誤碼,被定義在 [error-base.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/errno-base.h#L5) 原始程式碼如下: ```c #define EPERM 1 /* Operation not permitted */ ``` 也就是這個動作沒有權限 * `kernel_read_file_from_fd`:根據檔案描述子讀取檔案,[原始碼 (kernel_read_file.c) ](https://elixir.bootlin.com/linux/latest/source/fs/kernel_read_file.c#L174) * `module_decompress`:[原始碼在這](https://elixir.bootlin.com/linux/latest/source/kernel/module_decompress.c#L204),目的是將讀取進來的模組進行解壓縮 * `load_module`:定義在 [module.c](https://elixir.bootlin.com/linux/latest/source/kernel/module.c) ,這個函式比較複雜,這裡分段進行探討,其宣告如下: ```c static int load_module(struct load_info *info, const char __user *uargs, int flags) ``` * `info` 是紀錄模組中二進位的訊息 * `uargs` 是 user space 傳入的字串 * 嘗試解讀 `struct module` * 記錄模組的二進位訊息 * 嘗試解讀 `module_sig_check` * 定義在 [`kernel/module.c`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L2880) * 這裡主要是檢查模組有沒有簽名,沒有的話會報錯 * 嘗試解讀 `elf_validity_check` * 定義在 [`kernel/module.c`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L2968) * 這個函式是針對二進位的檔案、錯誤的 arch 及奇怪的 elf 檔案進行檢查,當然對最基本的區段偏移量、大小及索引都有驗證的作用 > sanity checks against invalid binaries, wrong arch, weird elf version. Also do basic validity checks against section offsets and sizes, the section name string table, and the indices used for it (sh_name). * 嘗試解讀 `setup_load_info` * 定義在 [`kernel/module.c`](https://elixir.bootlin.com/linux/v5.13/source/kernel/module.c#L3154) * 主要用途:初始化模組,讀取各個 section 將其載入模組中 * 這個函式主要是將不同 section 的資訊讀取到 `struct load_info *info` 中,下面舉幾個例子說明 * 將 `section header` 的起始地址紀錄到 `info->sechdrs` * 這裡可以用 `readelf` 指令進行說明,輸入命令: ```shell $ readelf -S khttpd.ko ``` * 輸出 ``` ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1367296 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 46 Section header string table index: 45 ``` * 這裡可以關注一行 ``` Start of section headers: 1367296 (bytes into file) ``` * 就是這一行被讀進去 * 這裡還有另外一個例子,從 section table 中找到 `.modinfo` 將他的地址紀錄到 `info->index.info` 中。 * 這裡我使用另外一個指令查看 ``` readelf -S khttpd.ko ``` * 輸出 ``` Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [19] .modinfo PROGBITS 0000000000000000 000070d4 0000000000000119 0000000000000000 A 0 0 1 ``` * 這樣可以找到 `.modinfo` 的區段並且將他的位址與偏移量記錄下來,這可以用另外一個指令 * 嘗試解讀 `blacklisted` * 定義在 [`kernel/module.c`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L3218) * 這裡是檢查模組的名稱,在黑名單中的模組不會被讀取 * 嘗試解讀 `rewrite_section_headers` * 定義在 [`kernel/module.c`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L2880) * 將所有 section 中 `sh_addr` 和地址載入記憶體中,然而 section 中 `__version` 與 `.modinfo` 不會載入記憶體,其細節在 `setup_load_info` 的函式中 * `uargs` 是 `port` 傳入引數的地方 ```c mod->args = strndup_user(uargs, ~0UL >> 1); if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; } ``` * `strndup_user` 就是將字串從 user_space 複製到 kernel 中,這也是為什麼資料可以傳進來 * `parse_args`,類似於 python 的 parser,可以將命令行的引述傳入 ```c /* 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); ``` * `parse_args` 定義於 [linux/kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c)中 * 其中, `parse_args` 還有使用到 `parse_ones` 的函式 * `parse_args` 會取得命令行的所有引數,並將這些引述使用 `parse_one` 一個一個進行處理,丟入 `struct module` 中,達到傳值得效果 * 這裡看到 [`struct module`](https://elixir.bootlin.com/linux/latest/source/include/linux/module.h#L365) 這個結構,前面只有介紹這個結構是用來紀錄二進位的資訊,這裡會在進行兩點的介紹 * 第一個是 `enum module_state`,這個列舉裡面包含四個型態,註解如下 ```c enum module_state { MODULE_STATE_LIVE, // 模組的正常狀態 MODULE_STATE_COMING, // 模組讀取完成,正在初始化 MODULE_STATE_GOING, // 模組正在被移除 MODULE_STATE_UNFORMED, // 模組正在被讀取 }; ``` * 第二個是前面提到的 `args` ,這裡就是終端機傳遞引數的位置 前面還有一個重點, `THIS_MODULE` 的使用,其實 `THIS_MODULE` 就是一個全域變數 ```c #ifdef MODULE extern struct module __this_module; #define THIS_MODULE (&__this_module) #else #define THIS_MODULE ((struct module *)0) #endif ``` 此時可以看到 `finit_module` 在文件中寫到他是一個考慮到檔案描述子 (File Descriptor) 的 `init_module` ```c int init_module(void *module_image, unsigned long len, const char *param_values); int finit_module(int fd, const char *param_values,int flags); ``` #### 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分? 由 CSAPP 第十一章所提到的 web 伺服器流程如下: ![](https://i.imgur.com/ouqFEx2.png) > 圖片來源: CS:APP 第十一章 由上圖可知, CS:APP 第十一章的 server 並沒有使用多執行緒,且是運行在 user-space 的。 接下來看 khttpd 的程式碼。在 [The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/) 的第二章提到模組是由 `__init` 的函式開始,在 `__exit` 的函式結束。 這裡先從 `static int __init khttpd_init(void)` 函式開始探究。在 `khttpd_init` 函式中,分為兩個部份與一個意外處理: * 函式: * `open_listen_socket` * `kthread_run` * 意外處理: * `close_listen_socket` `open_listen_socket` 中進行了幾件事: * `socket` 的創建與設定 * `kernel_bind` * `kernel_listen` [`kthread_run`](https://elixir.bootlin.com/linux/latest/source/include/linux/kthread.h#L51) 是創造一個執行 `http_server_daemon` 函式的執行緒,其定義如下: ```c #define kthread_create(threadfn, data, namefmt, arg...) \ kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg) ... #define kthread_run(threadfn, data, namefmt, ...) \ ({ \ struct task_struct *__k \ = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \ if (!IS_ERR(__k)) \ wake_up_process(__k); \ __k; \ }) ``` 上面可以看到 kthread_run 其實是用來呼叫 `kthread_create_on_node` 的函式,根據其註解可以得知 [`kthread_create_on_node`](https://elixir.bootlin.com/linux/latest/source/kernel/kthread.c) 可以創造並命名一個核心執行緒,註解中也有提到停止執行緒需要使用 `kthread_stop` 或是 `kthread_should_stop` 。理解完 `kthread_run` 的作用後,接下來對於 kthread_run 創造的執行緒所需要執行的 `http_server_daemon` 進行探討: ```c int http_server_daemon(void *arg) { struct socket *socket; struct task_struct *worker; struct http_server_param *param = (struct http_server_param *) arg; ``` 由上面的原始碼得知, `args` 會放 `socket` 型態的引數,這裡有一個比較特別的點: ```c allow_signal(SIGKILL); allow_signal(SIGTERM); ``` 這裡是允許使用 `SIGKILL` 與 `SIGTERM` 這兩種訊號,其中 * `SIGKILL`:無條件停止執行緒,這個訊號無法被忽略也無法被處置(例如:中途停止),因此這個訊號非常危險 * `SIGTERM`:停止執行緒,相較於 `SIGKILL` 這個訊號比較「有禮貌」,它可以被中途停止、忽略 參考: CS:APP 第八章 ```c 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; } ``` 這裡來說明程式碼中的函式: * [`kthread_should_stop`](https://elixir.bootlin.com/linux/latest/source/kernel/kthread.c#L156):根據 [`kthread_stop`](https://elixir.bootlin.com/linux/latest/source/kernel/kthread.c#L683) 與其本身的註解可以得知, `kthread_stop` 無法停止 `threadfn`,但是透過 `kthread_should_stop` 即可實現。總之這是一個查看木閒的執行緒是否需要停止的函式。 * `kernel_accept`:類似於 user-space 的 `accept` 其中,`http_server_daemon` 對於已經成功連接的 `socket` 會創建新的執行緒進行,這裡我們來探討 `http_server_worker` 的作用為何 ```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; ``` 函式的一開始對於 http_parser 進行設定,這裡我們來了解它的設定,然而,這些函式的引數是 `http_parser` ,這裡需要先對於這個結構進行解析: ```c struct http_parser { /** PRIVATE **/ ... /** PUBLIC **/ void *data; /* A pointer to get hook to the "connection" or "socket" object */ }; ``` 由原始碼可以看到, `http_parser` 這個結構空有一個空型態的指標進行 socket 的儲存,而下面幾個函式也只有應用到這個結構成員,因此我們對其他的結構成員暫時不進行討論。 然而,下面的函式還有用到另外一個結構 `http_request`,這裡我們來看一下原始碼: ```c struct http_request { struct socket *socket; enum http_method method; char request_url[128]; int complete; }; ``` 上面有一個列舉空間 `enum http_method` 這個被定義在 `http_parse.h` 中,其主要是定義 `http` 連接的動作。 `http_parser_callback_message_begin`:初始化 `http_request` `http_parser_callback_request_url`:`request` 中的 `request_url` 與 `p` 進行串連,這裡可能會有安全問題,因為 `request_url` 僅有 128 位元的空間,若是 len 超過 128 或是 `request_url` 本身的長度與 `len` 的和大於 128 會有不可預期的錯誤,這裡要特別注意。 `http_parser_callback_message_complete`:這裡將 `request` 傳送到 server 中,傳送後將 `request->complete` 設定為 1 代表要求以完成,這裡我們發現傳送的過程是由 `http_server_response` 進行,因此這裡研究一下 `http_server_response` 這個函式: ```c 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_RESPONSE_200` 與 `HTTP_RESPONSE_501` ,根據 [mozilla](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status) 提供給開發者的文件中提到這幾個狀態, HTTP 的回應狀態總共有五大類: 1. 資訊回應 (Informational responses, 100–199) 2. 成功回應 (Successful responses, 200–299) 3. 重定向 (Redirects, 300–399) 4. 用戶端錯誤 (Client errors, 400–499) 5. 伺服器端錯誤 (Server errors, 500–599) 根據上面的敘述可以看到 `HTTP_RESPONSE_200` 代表伺服器成功回應… * 200:請求成功。成功的意義依照 HTTP 方法而定: * GET:資源成功獲取並於訊息主體中發送。 * HEAD:entity 標頭已於訊息主體中。 * POST:已傳送訊息主體中的 resource describing the result of the action。 * TRACE:伺服器已接收到訊息主體內含的請求訊息。 * 501:無法執行。服務器不支援這種請求格式,以本專案來說,服務器僅支持 HTTP_GET 。 > The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are GET and HEAD. 連線成功後使用 `http_server_send` 函式進行資料的傳送,由於資料傳送是傳送到 linux 核心中,因此 `http_server_send` 使用 `kvec` 這個結構進行傳輸,這裡看部份的程式碼: ```c static int http_server_send(struct socket *sock, const char *buf, size_t size) { ... int done = 0; while (done < size) { struct kvec iov = { .iov_base = (void *) ((char *) buf + done), .iov_len = size - done, }; int length = kernel_sendmsg(sock, &msg, &iov, 1, iov.iov_len); if (length < 0) { pr_err("write error: %d\n", length); break; } done += length; } return done; } ``` 根據 [`kvec`](https://elixir.bootlin.com/linux/v4.0/source/include/linux/uio.h#L17) 其定義如下: ```c struct kvec { void *iov_base; /* and that should *never* hold a userland pointer */ size_t iov_len; }; ``` 會使用 `kvec` 結構主要是需要符合 `kernel_sendmsg` 的定義。在 while 迴圈中,會先將尚未發出的字串放入 `kvec` ,`kernel_sendmsg` 會回傳已經傳出去的字串長度,當所有字串都發送出去後,迴圈停止。 到目前為止,我們已經了解了 `http_server_worker` 的基礎設定,接下來我們繼續探討 `http_server_worker` 這個函式。 ```c #define RECV_BUFFER_SIZE 4096 static int http_server_worker(void *arg) { char *buf; ... 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; } ``` 接續前面,這裡開創一個字串空間給接收到的字串,空間大小被定義為 4096,這裡會發生第二個危險,若是字串大於 4096 的話會發生字串無法順利接收的情況。 `http_server_recv` 接收的方式其實也是使用 `kernel_recvmsg` 進行接收,而 `kernel_recvmsg` 則是回傳接收字串的長度,當然接收的結構也是使用 `kvec` 。 最後任務結束時, socket 及 buf 都會被釋放。 總結 khttpd 的原理,這裡以圖像的方式進行呈現: *## TODO: 作圖與總結 ##* ```graphviz digraph khttpd { } ``` #### htstress.c 用到 epoll 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何? 首先從 `main` 函式開始看起, `main` 函式中分為兩個部份: * 訊號處理、解析函式與連線 * epoll 系統呼叫 #### 訊號處理、解析函式與連線 在 `main` 函式中,常常看到有許多類似下面程式碼: ```c int main(int argc, char *argv[]){ ... sighandler_t ret = signal(SIGINT, signal_exit); if (ret == SIG_ERR) { perror("signal(SIGINT, handler)"); exit(0); } ... } ``` 在 CS:APP 第八章第五節中提到幾種 linux 核心接收到的訊號,如下表所示,由鍵盤引發的中斷給出的訊號為 `SIGINT`,中止行程的訊號為 `SIGTERM`,這裡可以看 CS:APP 第八章的表格,表格如下。上面的程式碼則是查看接收到什麼需要做出的應對事件。 ![](https://i.imgur.com/T30K4Zd.png) 接下來說明 `khttpd` 裡面接收終端機參數的處理: ```c int main(int argc, char *argv[]){ ... if (argc == 1) print_usage(); ... } ``` 由[你所不知道的 C 語言: 執行階段程式庫 (CRT)](/dUD91WrNQgugW3pM9fqcDg?view#int-mainint-argc-char-argv-%E8%83%8C%E5%BE%8C%E7%9A%84%E5%AD%B8%E5%95%8F)中提到執行時期接收到的參數,文章中提到一個[連結](https://blog.csdn.net/z_ryan/article/details/80985101),文章中提到若是僅有一個引數,則 `argc = 1` 且 `argv[0] = ./...`。以 `htstress.c` 為例,若僅輸入下面的命令: ```shell $ ./htstress ``` 則 `argc = 1` 且 `argv[0] = ./htstress`。此時,正好觸發程式碼中的判斷式,顯示出如何使用 `./htstress` 的資訊,如下: ```shell Usage: htstress [options] [http://]hostname[:port]/path Options: -n, --number total number of requests (0 for inifinite, Ctrl-C to abort) -c, --concurrency number of concurrent connections -t, --threads number of threads (set this to the number of CPU cores) -u, --udaddr path to unix domain socket -h, --host host to use for http request -d, --debug debug HTTP response --help display this message ``` 接下來就是解析如何使用這些變數,如 `-n`, `-c` 等等。根據 Linux Programmer's Manual 得知 `getopt_long` 的函式結構與描述: ```c int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex); ``` `getopt` 和 `getopt_long` 都可以解析終端機的引數 (arguments) ,差別在於 `getopt` 僅能解析以 `-` 形式開頭的參數,例如:`-v`, `-h`;相反的,`getopt_long` 可以解析以 `--` 開頭的參數,例如:`--help` 等等。參數開頭應該使用 `-` 或是 `--` 在下面會進行說明,兩個函數會依序回傳終端機的引數。 這裡先從函式的引數 (Argument) 開始著手,其中: * `argc` 代表傳數的引數個數 * `argv` 代表傳數的引數字串。舉例來說: ```shell ./htstress http://localhost:8081/ ``` 則我們可以得到: * `argv[0] = ./htstress` * `argv[1] = http://localhost:8081/` * `optstring` 根據 Linux Programmer's Manual 得知,`optstring` 是裝著引數的種類,中間用 `:` 分開。 ```c static const char short_options[] = "n:c:t:u:h:d46"; ``` * `longopts` ```c struct option { const char *name; int has_arg; int *flag; int val; }; ``` 在這個結構中, `name` 代表的是引數名稱,例如:`help`。而 `has_arg` 則是查看這個引數是否有附帶參數,0 為沒有,1 為必須有附帶參數,2 為選擇性。`*flags` 則是指定引數回傳結果,若是 `NULL` 則是回傳到 `val` 以 `htstress.c` 為例, ```c static const struct option long_options[] = { {"number", 1, NULL, 'n'}, {"concurrency", 1, NULL, 'c'}, {"threads", 0, NULL, 't'}, {"udaddr", 1, NULL, 'u'}, {"host", 1, NULL, 'h'}, {"debug", 0, NULL, 'd'}, {"help", 0, NULL, '%'}, {NULL, 0, NULL, 0}}; ``` * `longindex` 這個參數這裡暫時不討論,通常是使用 `NULL` 在 `htstress.c` 中,這裡使用 `while` 迴圈與 `switch`,將引數一個一個解析。 在終端機引數的最後放入想要連接的網址,然而,如何透過輸入網址進行連線呢?這裡我們繼續追蹤程式碼。 這裡我們先看怎麼進行連線的,這裡是使用 `getaddrinfo` 進行連線,而函式定義如下: ```c int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); ``` `getaddrinfo` 將主機名稱、主機地址與連接埠 (port) 傳換成 `struct socket` 的形式。若是傳換成功則回傳 0,否則回傳錯誤碼。其中, `node` 為網域名稱,或是 IP 地址、 `service` 是連接埠的編號。轉換的結果放入 `res` 的鍊結串列中。`htstress.c` 的程式碼如下: ```c int j = getaddrinfo(node, port, &hints, &result); ``` 接下來利用兩個例子說明連接時 `node` 與 `port` 分別為何: 案例一、連接 http://localhost:8081/ 。此時 `node = localhost` 而 `port = 8081` 案例二、連接 http://www.google.com/ 。此時 `node = www.google.com` 而 `port = http` 呼叫 `getaddrinfo` 成功後,會開始走訪 `struct addrinfo` 鍊結串列,如下圖,嘗試進行 `bind` 和 `connect`,若是調用成功,則會建立起連結。 ![](https://i.imgur.com/Zggzvj5.png) > 圖片來源: CS:APP 第十一章 程式碼如下: ```c int main(int argc, char *argv[]){ ... for (rp = result; rp; rp = rp->ai_next) { int testfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (testfd == -1) continue; if (connect(testfd, rp->ai_addr, rp->ai_addrlen) == 0) { close(testfd); break; } close(testfd); } ... } ``` 在這個部份的最後, `htstress.c` 使用 `pthread_create` 創造多執行緒,開始進行測試: ```c int main(int argc, char *argv[]){ ... /* run test */ for (int n = 0; n < num_threads - 1; ++n) pthread_create(&useless_thread, 0, &worker, 0); worker(0); ... } ``` 在 for 迴圈中創造多執行緒進行測試,而最後的 `worker(0)` 則是使用主執行緒進行測試,這樣可以避免浪費任何一個運算資源,增加程式效率。 上面的 num_threads 預設值是 1,而 num_threads 可以透過終端機進行修改,終端機參數為 `-t`。 #### epoll 系統呼叫 前面提到使用 `pthread_create` 時,每個執行緒都要執行 worker 這個函式,連主執行緒也不例外,這裡讓我們查看 `worker` 函式的作用。 ```c static int concurrency = 1; static void *worker(void *arg) { ... int efd = epoll_create(concurrency); if (efd == -1) { perror("epoll"); exit(1); } for (int n = 0; n < concurrency; ++n) init_conn(efd, ecs + n); ... } ``` 一開始先利用 epoll_create 創造 epoll_instance,由 linux 核心 2.6 之後,輸入的值沒有意義, `epoll_create` 會根據需求動態創建。 接下來是使用 for 迴圈對每個 epoll instance 進行 `init_conn`,因此這裡進行 `init_conn` 函式的探究。在 `init_conn` 函式中,主要執行以下步驟,函式詳情請參照[Linux 核心設計:事件驅動伺服器之原理和實例](https://hackmd.io/@zoanana990/r1AFIziNq#%E9%AB%98%E6%95%88-Web-%E4%BC%BA%E6%9C%8D%E5%99%A8%E9%96%8B%E7%99%BC)及解釋 user-echo-server 運作原理,特別是 epoll 系統呼叫的使用一節中所提到的函式: 1. 將 `ec->fd` 與 `sss` 進行連線,並且將 `ec->fd` 設定為 Non blocking I/O 2. 將事件為設定寫入狀態 (EPOLLOUT) 3. 利用 `epoll_ctl` 將 `ec->fd` 加入到 `efd` 中。 接下來進入無窮迴圈 `for(;;)`: ```c nevts = epoll_wait(efd, evts, sizeof(evts) / sizeof(evts[0]), -1); ``` 由解釋 user-echo-server 運作原理,特別是 epoll 系統呼叫的使用一節中可知 `epoll_wait` 是回傳 `ready_list` 中的個數,其中 `ready_list` 是進行 I/O 的鍊結串列。當 `epoll_wait` 執行結束後會根據四個不同的事件狀態執行對應的程式碼: 1. EPOLLERR:發生錯誤 2. EPOLLHUP:被中止 3. EPOLLOUT:寫入 4. EPOLLIN:讀取 其中, 1 和 2 都是錯誤的時候進行的處理,因此這裡我們著重 3 和 4。每個預設的 epoll_event 都是 EPOLLOUT。因此每個事件處理都是先進行訊息發送,當訊息發送結束後會將 epoll_event 改為 EPOLLIN,request + 1。當訊息發送結束後會進行訊息接收,一樣的是,訊息接收成功後也會將 request + 1 ,並且會開始檢查所有 request 是否執行結束,若執行完則呼叫 end_time 函式,結束計時。 ---- ## 開發紀錄 ### `kecho` #### 若使用者層級的程式頻繁傳遞過長的字串給 kecho 核心模組,會發生什麼事? 這裡參考 bench.c 撰寫 test.py 進行大小為 5000 的隨機字串以 1000 個執行緒進行發送,測試其發送的字串與接收的字串是否相同。結果是==有時候==會有漏字或是缺字的情況,測試程式碼[在這](https://github.com/zoanana990/kecho/blob/master/test.py)。 使用 `dmesg` 查看 kecho 的訊息可以看到當有漏字的情況時,出現以下的錯誤: ```shell get request error = -104 ``` 由 [linux error number](https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/errno.h#L87) 得知出現這個號碼的主因是連線出現問題 ```c #define ECONNRESET 104 /* Connection reset by peer */ ``` 因此這進行 kecho_mod.c, echo_server.c 與 echo_server.h 三個檔案的探討,解讀為什麼是「有時候」會有這種現象,並且進行解決方法的開發。 首先查看 kecho 的運作原理。在 `kecho_init_module` 已經使用 CMWQ 進行 kecho 的運作。 ```c static int kecho_init_module(void) { int error = open_listen(&listen_sock); if (error < 0) { printk(KERN_ERR MODULE_NAME ": listen socket open error\n"); return error; } param.listen_sock = listen_sock; kecho_wq = alloc_workqueue(MODULE_NAME, bench ? 0 : WQ_UNBOUND, 0); echo_server = kthread_run(echo_server_daemon, &param, MODULE_NAME); if (IS_ERR(echo_server)) { printk(KERN_ERR MODULE_NAME ": cannot start server daemon\n"); close_listen(listen_sock); } return 0; } ``` #### 參照 kecho pull request #1,嘗試比較 kthread 為基礎的實作和 CMWQ,指出兩者效能的落差並解釋 由 [kecho pull request #1](https://github.com/sysprog21/kecho/pull/1) 提到,kthread 和 CMWQ 的實作效率差了近十倍,但是上面也提到,kecho 已經使用 CMWQ 進行實做,因此這裡可以參考 khttpd 的實做,將 kecho 改為 kthread 的版本。 程式碼改動:[commit](https://github.com/sysprog21/kecho/commit/a26761394ad04275daeee8265a2301fbba7484f8) 效能比較: * kthread-based kecho ![](https://i.imgur.com/q0uGRHb.png) * CMWQ based kecho ![](https://i.imgur.com/sbfn56I.png) ### `khttpd` #### 指出 kHTTPd 實作的缺失 (特別是安全疑慮) 並予以改正 在「參照 CS:APP 第 11 章,給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?」一節中提到 khttpd 總共有兩個安全上的問題: 1. `http_parser_callback_request_url` 在傳入字串時並沒有長度的檢查 2. 進行 `http_server_recv` 時可能因為 `buf` 的大小不足而有資訊接受不完全的危險 (***--TODO--***) 對於第一種情況,需要在傳入字串之前進行檢查,字串長度是否超過 127 (第 128 位為結束字元),程式改動部份如 [Fix request function security problem](https://github.com/sysprog21/khttpd/commit/478973eedb7b877f7fb3cbb0aa6154610f98c741) ,這裡老師有提起一個問題,將程式碼的 127 改掉,減少維護上的成本,更新的程式碼如 [Avoid hard code 127](https://github.com/sysprog21/khttpd/commit/97c596c1544a64c5c500dd0e44fe4e5d601e44ab) #### 引入 Concurrency Managed Workqueue (cmwq),改寫 kHTTPd,分析效能表現和提出改進方案,可參考 kecho 修改前: ```shell requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 2.609 requests/sec: 38326.247 ``` 修改後: ```shell requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 1.491 requests/sec: 67065.303 ``` 程式碼已經提交成 [commit](https://github.com/sysprog21/khttpd/compare/master...zoanana990:master) #### Directory Listing 的實做 參考[陳日昇同學的作業](https://hackmd.io/@Risheng/linux2022-ktcp/https%3A%2F%2Fhackmd.io%2F%40Risheng%2Flinux2022-khttpd)。 欲進行 Directory Listing 的實做需要考慮幾件事: - [x] 將資訊傳入網頁的語法 - [x] 讀取目錄、檔案 - [x] 考慮副檔名,根據不同副檔名開啟檔案,須考慮 MIME 的實做 - [ ] 加入 WWWROOT 第一個是 HTTP 連線的方式,這裡參考 `HTTP_RESPONSE_200_DUMMY` 的傳送格式: ```c #define HTTP_RESPONSE_200_DUMMY \ "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: 120" CRLF \ "Connection: Close" CRLF CRLF "Hello World!" CRLF ``` 在上面這段程式碼內容格式是 `text/plain` ,而這種內容的傳送會根據後面的撰寫的內容長度傳送。然而 `text/plain` 對於超連結並不好傳送,因此這裡我使用 `text/html` 的方式進行。 這裡就需要看到 HTML 的格式規範, 這裡比較特別的是 `Content-Type` 的格式不同,這裡按照不同的檔案格式,這裡詳細格式如 [MIME](http://www.iana.org/assignments/media-types/media-types.xhtml) ,其中 `Content-Length` 是要發送文字的長度。 第二個是讀取目錄及檔案,參考[這篇文章](https://stackoverflow.com/questions/29458157/how-to-get-a-file-list-from-a-directory-inside-the-linux-kernel),雖然內容年代有點久遠,但是仍有參考之處: - 參考 [`linux/fs.h`](https://github.com/torvalds/linux/blob/master/include/linux/fs.h),查看裡面的結構 - 由上面的文章可知,走訪整個目錄的檔案需要用到 `struct dir_context`, `iterate_dir` 及 `filp_open` 等函式 - 參考 [Overview of the Linux Virtual File System](https://www.kernel.org/doc/html/latest/filesystems/vfs.html) 及 [Linux 核心設計: 檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system) 理解虛擬檔案系統的概念。 ::: warning ### Question 在 [Linux 核心設計: 檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system) 中提到下面這張圖,`file object` 需要找到對應的 `dentry object` 再找到 `inode`,而 `inode` 才是檔案主體。然而,根據 [`linux/fs.h`](https://github.com/torvalds/linux/blob/master/include/linux/fs.h) 原始碼中的第 925 行 `struct file` 的宣告,我發現 `file object` 內並沒有 `dentry object`,此外 `inode` 被放在 `struct file` 中。這是不是代表其實並不需要 `dentry_object`? ![](https://i.imgur.com/b195Apa.png) ```c struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; ... } ``` ::: 閱讀完上面的文獻並實做後暫時的成果如下 ![](https://i.imgur.com/svNGr0T.png) 然而目前遇到連線不穩定的問題。 使用命令 dmesg 後發現,錯誤代碼為 104,查詢錯誤代碼後, ```shell [82147.668253] khttpd: khttpd: pwd = / [82147.670601] khttpd: recv error: -104 ``` 會出現連線不穩的錯誤主要是傳輸資料的格式錯誤,傳送字串長度須為 16 進位,非 10 進位,當傳送長度改為 16 進位後即可。 ![](https://i.imgur.com/DvaXoGM.png) 接下來是點開連結後無法進入新的位置的問題,這裡觀察連線到根目錄的網址: ``` http://localhost:8081/ ``` 點擊任何一個目錄的網址: ``` http://lib32/ ``` 可以發現因為網址的變化導致連線中斷,這裡需要修改連線進入目錄的問題。 將原本的使用的 memset 換成字串串連即可。 ![](https://i.imgur.com/71XlmB7.png) 目前已經可以走訪每個目錄,現在需要根據副檔名打開各種檔案 #### 加入 MIME 打開副檔名 參考 [MIME-TYPE](https://mimetype.io/all-types/) 將檔名打入 mime.h 中 雜湊表結構: ```c typedef struct mime { char *file_extension; char *http_type; mime_t *next; } mime_t; ``` 目前雜湊表已經實做完成,輸入附檔名會輸出對應的 HTTP 打開方式 但是時裝到 kernel 時遇到一些問題: ``` ld: /home/khienh/linux_kernel/khttpd/mime.o: in function `strdup': /home/khienh/linux_kernel/khttpd/mime.h:13: multiple definition of `strdup'; /home/khienh/linux_kernel/khttpd/http_server.o:/home/khienh/linux_kernel/khttpd/mime.h:13: first defined here ld: /home/khienh/linux_kernel/khttpd/mime.o:/home/khienh/linux_kernel/khttpd/mime.h:80: multiple definition of `MIME'; /home/khienh/linux_kernel/khttpd/http_server.o:/home/khienh/linux_kernel/khttpd/mime.h:80: first defined here ld: /home/khienh/linux_kernel/khttpd/main.o: in function `strdup': /home/khienh/linux_kernel/khttpd/mime.h:13: multiple definition of `strdup'; /home/khienh/linux_kernel/khttpd/http_server.o:/home/khienh/linux_kernel/khttpd/mime.h:13: first defined here ld: /home/khienh/linux_kernel/khttpd/main.o:/home/khienh/linux_kernel/khttpd/mime.h:80: multiple definition of `MIME'; /home/khienh/linux_kernel/khttpd/http_server.o:/home/khienh/linux_kernel/khttpd/mime.h:80: first defined here ``` 主要程式的改動在 [commit](https://github.com/sysprog21/khttpd/commit/c0c44b74af30d5df68510b208b703ca369a0c43e) 將重複定義的地方刪除,這裡出現另外問題,這主要是路徑上的問題: ***TODO*** 這裡有收到老師的訊息,接下來使用 kernel 的 hashtable API 改寫。 #### 實作 HTTP 1.1 keep-alive 比較 HTTP 1.0 與 HTTP 1.1 的差別: HTTP 1.0 的主要缺點是,每個 TCP 只能發送一個請求。當資料發送結束,則連線關閉,如果需要請求其他資源,則需要在進行一次連線。 然而, TCP 的新建連線成本很高,需要客戶端 (Client) 與伺服器 (Server) 進行三次握手,這會大幅度的降低資料吞吐量。 為了解決這個問題,HTTP 1.1 中引入了 `keep-alive` 的連線方式,在一段時間內不要關閉連線,可以讓連線重複使用,如果發現一段時間沒有使用時,則主動關閉連線。不過通常是客戶端在最後一個請求時,可以發送 `Connection: close` ,這種方式就是持久連線 (Persistent Connection)。 先前與老師討論的過程中,有提到參考 [sehttpd](https://github.com/zoanana990/sehttpd.git) 的 HTTP 1.1 實做方法。 這裡簡述了一下 sehttpd 的流程: mainloop.c 是將 #### Priority Queue 與 Timer 實做 :::warning 應說明程式碼改寫的動機,而非急著張貼程式碼。 :notes: jserv ::: 這裡主要是將 sehttpd 中的 timer.h 及 timer.c 進行改寫: ## 參考文獻 - [eecheng87/sehttpd](https://hackmd.io/@eecheng/BJpGSJWq8) - [二元堆積 (Binary Heap)、最小堆積 (Min Heap)、最大堆積 (Max Heap) (附 python 實作) ](https://shubo.io/binary-heap/)