--- tags: Linux Kernel --- # 2023q1 Homework7 (ktcp) contributed by < [`chun61205`](https://github.com/chun61205) > > [ktcp](https://hackmd.io/@sysprog/linux2023-ktcp/%2F%40sysprog%2Flinux2023-ktcp-a) ## 研究 CMWQ ### Why CMWQ 根據 Linux 核心文件。 > In the original wq implementation, a multi threaded (MT) wq had one worker thread per CPU and a single threaded (ST) wq had one worker thread system-wide. A single MT wq needed to keep around the same number of workers as the number of CPUs. The kernel grew a lot of MT wq users over the years and with the number of CPU cores continuously rising, some systems saturated the default 32k PID space just booting up. 原本的 workqueue 實作會讓每個 worker 管理自己的 workqueue,因此 MT 會因為 CPU (worker) 數量的增加,且不一定每個 workqueue 都會保持運作,因此會造成效能不佳。 CMWQ 的優勢如下: > 1. 拆開 workqueue 與 worker-pools,可單純地將 work 放入 queue 中不必在意如何分配 worker 去執行,根據設定的 flags 決定如何分配,適時的做切換,減少 worker 的 idle 情況,讓系統使用率提升 > 2. 若任務長時間佔用系統資源(或是有 blocking 的情況產生),CMWQ 會動態建立新的執行緒並分配給其他的 CPU 執行,避免過多的執行緒產生 > 3. 使不同的任務之間能被更彈性的執行(所有的 workqueue 共享),會根據不同的優先級執行 ![](https://i.imgur.com/Y6vSpO6.png) 經過實驗後,也可以發現 CMWQ 的執行並不會因為 thread 的數量提升而導致執行時間提升。 ### CPU 排程器和 workqueue/CMWQ 的互動 > [Linux Workqueue - 魅族内核团队](https://kernel.meizu.com/linux-workqueue.html) #### worker_pool 1. Bound 類型:綁定特定的 CPU,使管理的 worker 執行在指定的 CPU 上執行,而每個 CPU 中會有二個 worker-pools 一個為高優先級的,另一個給普通優先級的,透過不同的 flags 影響 workqueue 的執行優先度 ![](https://i.imgur.com/1IzNrY6.png) ![](https://i.imgur.com/WHPUsRs.png) 2. Unbound 類型:thread pool 用於處理不綁定特定 CPU,其 thread pool 是動態變化,透過設定 workqueue 的屬性建立對應的 worker-pools Unbound 類型的 workqueue 則又分為兩種, unbound_std_wq 和 ordered_wq 。 1. unbound_std_wq : 一個 node 對應一個 worker_pool ![](https://i.imgur.com/zViH9Wz.png) ![](https://i.imgur.com/X5fVTDa.png) 2. ordered_wq :所有 node 對應一個 worker_pool ![](https://i.imgur.com/RWJW0B2.png) ![](https://i.imgur.com/w2N64hg.png) 大致上了解 workqueue 的運作模式後就可以看到程式碼。 #### workqueue 創建 > [`workqueue.h`](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue.h), [`workqueue_internal.h`](https://elixir.bootlin.com/linux/latest/source/kernel/workqueue_internal.h), [`workqueue.c`](https://elixir.bootlin.com/linux/latest/source/kernel/workqueue.c) ```c // kecho_mod.c struct workqueue_struct *kecho_wq; kecho_wq = alloc_workqueue(MODULE_NAME, bench ? 0 : WQ_UNBOUND, 0); ``` ```c // echo_server.c queue_work(kecho_wq, work); ``` 簡單來說,在 kecho 中,因為需要使用 CMWQ ,所以需要自己創建 workqueue ,並利用 `queue_work` 來使用,若是沒有特別指定,則會使用預設的 workqueue 。 接下來可以看看 linux 核心原始程式碼。 根據 [`workqueue.h`](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue.h) 說明 ```c /** * queue_work - queue work on a workqueue * @wq: workqueue to use * @work: work to queue * * Returns %false if @work was already on a queue, %true otherwise. * * We queue the work to the CPU on which it was submitted, but if the CPU dies * it can be processed by another CPU. * * Memory-ordering properties: If it returns %true, guarantees that all stores * preceding the call to queue_work() in the program order will be visible from * the CPU which will execute @work by the time such work executes, e.g., * * ... ``` `queue_work` 會將 `work` 放到 `kecho_wq` 排列,如果 `work` 已經存在於 `kecho_wq` 中,則會回傳 `false` ,否則回傳 `true`。 如果往下拆解 `queue_work` 會發現,要將 `work` 加入 `kecho_wq` 會經過 `queue_work` -> `queue_work_on` -> `__queue_work` -> `insert_work` 才會添加進去。 可以看到 ```c static inline bool queue_work(struct workqueue_struct *wq, struct work_struct *work) { return queue_work_on(WORK_CPU_UNBOUND, wq, work); } ``` 呼叫 `queue_work_on` 的時候 `WORK_CPU_UNBOUND` 會作為參數傳遞,代表不綁定 CPU 。而在 `queue_work_on` 中又會呼叫 `__queue_work` 並傳遞 `WORK_CPU_UNBOUND` ,接著在 `__queue_work` 中 CPU 就會被指定成不綁定 CPU 。 ```c // __queue_work if (wq->flags & WQ_UNBOUND) { if (req_cpu == WORK_CPU_UNBOUND) cpu = wq_select_unbound_cpu(raw_smp_processor_id()); pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu)); } else { if (req_cpu == WORK_CPU_UNBOUND) cpu = raw_smp_processor_id(); pwq = per_cpu_ptr(wq->cpu_pwqs, cpu); } ``` 從上面的程式碼可以發現,如果使用 `queue_work` ,則會自動指定不綁定 CPU 。根據 [`workqueue.h`](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue.h) 如果要指定特定 CPU 可以使用 `schedule_work_on` ```c /** * schedule_work_on - put work task on a specific cpu * @cpu: cpu to put the work task on * @work: job to be done * * This puts a job on a specific cpu */ static inline bool schedule_work_on(int cpu, struct work_struct *work) ``` #### 研究 pwq > [任务工厂- Linux 中的workqueue 机制[二] - 知乎专栏](https://zhuanlan.zhihu.com/p/94561631) pwq 是 pool workqueue 的縮寫,意思是 workqueue 和 worker pool 的中間人。 首先可以看到 pwq 的結構的說明 ```c /* * The per-pool workqueue. While queued, the lower WORK_STRUCT_FLAG_BITS * of work_struct->data are used for flags and the remaining high bits * point to the pwq; thus, pwqs need to be aligned at two's power of the * number of flag bits. */ ``` 這裡使用到 data alignment 的方法,將該結構的最後的 $4$ bits 用於儲存顏色,該結構前 $4$ bits 用於儲存 pointer 。 ![](https://hackmd.io/_uploads/B1pkkpEV2.png) ```c /* * nr_active management and WORK_STRUCT_INACTIVE: * * When pwq->nr_active >= max_active, new work item is queued to * pwq->inactive_works instead of pool->worklist and marked with * WORK_STRUCT_INACTIVE. int nr_active; /* L: nr of active works */ int max_active; /* L: max active works */ struct list_head inactive_works; /* L: inactive works */ struct list_head pwqs_node; /* WR: node on wq->pwqs */ struct list_head mayday_node; /* MD: node on wq->maydays */ ``` 搭配上面的圖和相關程式碼,可以發現 pwq 也是一個 list ,當 `pwq->nr_active >= max_active` 新的 work 就會被加入到 inactive_works 等待。而這些 `pwqs_node` ,每一個 node 對應到一個 work 和一個 worker pool ,透過這樣的方式來分配 work 到不同的 worker pool 上。 回去看到 `insert_work` , ```c set_work_pwq(work, pwq, extra_flags); ``` 就可以發現在插入 work 的時候就會指定 pwq 。 ## 將 CMWQ 引入 kHTTPd