--- tags: Linux Kernel --- # 2022q1 Homework6 (ktcp) contributed by < [steven1lung](https://github.com/steven1lung) > ## :checkered_flag: 自我檢查清單 - [x] 參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module),解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢? - [ ] 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分? - [ ] `htstress.c` 用到 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何? - [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 核心開發者的考量 - [ ] 解釋 `user-echo-server` 運作原理,特別是 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用 - [ ] 是否理解 `bench` 原理,能否比較 `kecho` 和 `user-echo-server` 表現?佐以製圖 - [ ] 解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼? ## `$ sudo insmod khttpd.ko port=1999` 要將參數傳到核心可以使用 `module_param(name, type, perm)` ,這個巨集定義在 `linux/moduleparam.>` 中,`module_param()` 吃的參數依序為:變數名稱、變數型態、權限。前兩個都好懂,但是最後一個 `@perm` 是要做什麼呢,如果只是要讀取,為什麼讀取也要分權限? [文件](https://tldp.org/LDP/lkmpg/2.6/html/x323.html)提到,`@perm` 參數是要設定對應 sysfs 裡檔案的權限。Sysfs 是 linux 提供的一種虛擬檔案系統,這個系統不僅可以將裝置和驅動的資料傳送到 user space,也可以用來做對裝置和驅動的設定。虛擬檔案系統主要的功能就是讓上層的軟體可以用簡單的方法去和底層不同檔案系統溝通。這裡的 `@perm` 就是吃常見的 `rwx` 變數。 在 `module_param()` 巨集中,我們甚至可以使用陣列,但使用的方式就要透過 `module_param_array()` 或是 `module_param_string()`。 在 `khttpd/main.c` 中,我們看到使用的方式如下: ```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()`,例如說我們執行以下命令 `sudo insmod khttpd.ko` 但不定義 `port=1999`,就會讓程式裡 `port` 的值被設為 `DEFAULT_PORT` 也就是 `8081`。 ## CMWQ ### CMWQ 介紹 CMWQ 全名為 Concurrency Managed Workqueue,workqueue 的功用是為了要在多個非同步的執行緒下進行任務分派,當 workqueue 裡還有任務,workqueue 就會將任務指派給執行緒。 傳統的 workqueue 中,多執行緒 workqueue 會有跟處理器核數量相同數量的 worker(執行緒),而在單一執行緒 workqueue 中整個系統只會有一個 worker(執行緒)。 因為在多執行緒 workqueue 裡 worker 數量需要跟核心數量一樣,而近年來處理器核心數量的增加,所以導致在開機的時候就會將原本預設的 32k PID 空間佔滿。並且多執行緒 workqueue 不僅佔用了許多資源,達到的並行效果也沒有令人滿意,甚至不太好。每個 workqueue 都維護各自的 worker pool,work items 也會競爭資源,導致死結情況更容易發生。 於是就有人重新設計了 workqueue ,CMWQ 就出現了。依照下列目的進行設計: * 要可以跟原本舊的 workqueue 共用 * 使用可以分享的 worker pool 來達到彈性的並行效果與不浪費資源 * 自動管理 worker pool 和並行程度來讓使用者不用擔心細節部分 ### CMWQ 用法 #### `alloc_workqueue` 建立一個新的 workqueue,這個函式會吃 3 個參數,分別是 `@name`、`@flags`、`@max_active`。 * `@name` 是 workqueue 的名稱,也可以是之後 rescuer 的名稱。 * `@flags` 跟 `@max_active` 會控制 workqueue 裡面的工作會如何被排程跟分配執行。 **flags** * `WQ_UNBOUND` > 任務會被排到一個 unbound 的 workqueue 中,並會被多個沒被綁在特定 CPU 的 worker 執行。這個 flag 會讓 workqueue 變成不提供並行的單純任務提供者。Worker pools 裡的工人會盡快開始執行 workqueue 裡的任務。Unbound workqueue 會犧牲 locality,但是在一些場合中會有幫助。像是:執行時間長並且會被密集執行的任務會希望把並行排程交給系統的排程器。 * `WQ_FREEZABLE` > 可以凍結的 workqueue 會參與系統的 suspend 機制,在 workqueue 裡的任務會被分配,但是會等到 workqueue 解凍時才執行。 * `WQ_MEM_RECLAIM` > 只要 workqueue 有參與記憶體空間的領回,就必須要有這個 flag,這個 workqueue 會保證儘管有記憶體壓力也會至少有一個工人。 * `WQ_HIGHPRI` > 當要排進 workqueue 的任務是有高優先權的,會被排入 highpri 的 workqueue。要注意的是一般的 workqueue 跟高優先權的 workqueue 是分開管理的,兩者不會跟對方溝通。 **max_active** 這個參數決定了每個處理器最多可以執行的任務數量,例如說 max_active 設定為 16 會使在 workqueue 中最多有 16 個任務被單個 CPU 同時執行,預設值為 0。 #### `destroy_workqueue` 吃一個 workqueue 參數,這個函式會安全地刪除一個 workqueue,等正在執行的任務完成後再進行刪除。 #### `queue_work` 這個函式會將任務排入 workqueue 中,吃兩個參數,分別是 workqueue 的名字跟要被排入的任務。 > 這邊只列出在 kecho 使用到的函式,更詳細的 API 說明可以去看 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/latest/core-api/workqueue.html#)。 ### 揣摩 Linux 核心開發者移除原本 `create_workqueue` 的考量 上方有提到換成 CMWQ 的好處了,我覺得最主要的原因是因為原本的 workqueue 不適合越來越多核處理器,會浪費許多資源,且並行的效果沒有令人滿意,所以設計出了新的 workqueue。 也有原因是因為越來越多的功能被加入 workqueue 之中,像是舊版的 workqueue 並沒有 flags 參數,而是將要使用的功能分成一個一個的參數分別輸入 workqueue,造成往後開發新功能造成麻煩。 在 [97e37d7](https://github.com/torvalds/linux/commit/97e37d7b9e65a6ac939f796f91081135b7a08acc) 中,作者提到了那時候的 workqueue 會吃 `@SingleThread` 跟 `@freezable` 這兩個參數來決定 workqueue 是不是單執行緒或是要不要有凍結功能。這個 commit 會將這兩個參數修改成 flags 形式,並且使用新的命名風格:`WQ_XXXXXX`。 ```diff +enum { + WQ_FREEZEABLE = 1 << 0, /* freeze during suspend */ + WQ_SINGLE_THREAD = 1 << 1, /* no per-cpu worker */ +}; extern struct workqueue_struct * -__create_workqueue_key(const char *name, int singlethread, int freezeable, +__create_workqueue_key(const char *name, unsigned int flags, struct lock_class_key *key, const char *lock_name); ``` ```diff struct workqueue_struct { + unsigned int flags; /* I: WQ_* flags */ struct cpu_workqueue_struct *cpu_wq; /* I: cwq's */ struct list_head list; /* W: list of all workqueues */ const char *name; /* I: workqueue name */ - int singlethread; - int freezeable; /* Freeze threads during suspend */ ``` 在 [502ca9d](https://github.com/torvalds/linux/commit/502ca9d819792e7d79b6e002afe9094c641fe410) 中,作者修改了原本的單執行緒 workqueue 讓他變成 shared worker pool friendly。原本的實作是限制 workqueue 只能使用一個特定的 CPU,但這個做法對要 worker pool 不太友善。所以使用了新的方法改善:使用動態的 CPU binding 跟 `cwq->limit`。因為在任何時刻,單執行緒的 workqueue 都會被任一個 CPU 綁住。 > 上述的 cwq 全名是 cpu workqueue ```diff enum { WQ_FREEZEABLE = 1 << 0, /* freeze during suspend */ - WQ_SINGLE_THREAD = 1 << 1, /* no per-cpu worker */ + WQ_SINGLE_CPU = 1 << 1, /* only single cpu at a time */ }; ``` ```diff +/** + * cwq_unbind_single_cpu - unbind cwq from single cpu workqueue processing + * @cwq: cwq to unbind + * + * Try to unbind @cwq from single cpu workqueue processing. If + * @cwq->wq is frozen, unbind is delayed till the workqueue is thawed. + * + * CONTEXT: + * spin_lock_irq(gcwq->lock). + */ +static void cwq_unbind_single_cpu(struct cpu_workqueue_struct *cwq) +{ + struct workqueue_struct *wq = cwq->wq; + struct global_cwq *gcwq = cwq->gcwq; + + BUG_ON(wq->single_cpu != gcwq->cpu); + /* + * Unbind from workqueue if @cwq is not frozen. If frozen, + * thaw_workqueues() will either restart processing on this + * cpu or unbind if empty. This keeps works queued while + * frozen fully ordered and flushable. + */ + if (likely(!(gcwq->flags & GCWQ_FREEZING))) { + smp_wmb(); /* paired with cmpxchg() in __queue_work() */ + wq->single_cpu = NR_CPUS; + } +} ```