Try   HackMD

紀錄:一對一討論

Linux workqueue 如何建立和排程?

為什麼需要 Concurrency Managed Workqueue ?

在 CMWQ 之前的 workqueue 實作中,worker thread 的數量是與被建立的 workqueue 數量直接掛勾的,每個 workqueue 都有自己獨立的 work pool。意味著使用者每建立一個新的 workqueue,系統上的 worker thread 總量便會增加。實作中包含了兩種 worker thread 的建立方式:

  • multi threaded (MT) wq : 在每個 CPU 上會有一個 worker thread,work item 可以分配不同的 CPU 上執行
  • single threaded (ST) wq : 僅以單一的 worker thread 來完成系統上所有的 work item

在 MT wq 的部份,這代表系統需要建立與 CPU 數量乘上 workqueue 總量等價的 worker thread,這是很嚴重的的問題,但 multi threaded (MT) wq 浪費了大量的資源,但所提供的並行度仍然不是讓人滿意,因為在 MT workqueue 中,每個 CPU核心 上的關於這個 workqueue 只有一個 work thread。這個 work thread 負責處理該核心上分配的所有 work item。換句話說,同一個 workqueue 上的 所有 work items 都需要 依次排隊 由該核心上的線程來處理,這些 work items 會「爭奪」這個核心上唯一的 execution context,從而影響並行度。而 ST wq 是為整個系統提供唯一的 context,因此所有的 work 都是一一排隊執行,造成 concurrency level 不佳,雖然對資源的使用量比較低。

總結:

  • ST workqueue 的並行度比較低,但資源消耗少,適合處理較輕量的工作負載。
  • MT workqueue 提高並行度,但消耗大量資源,且無法靈活地根據系統需求調整線程的數量,這導致其並行效果不佳。

cmwq 是一個重新實現的工作隊列,重點是以下目標:

  1. 保持與原始 workqueue API的相容性。
  2. 捨棄各個 workqueue 獨立對應一組 worker-pools 的作法。取而代之,所有 workqueue 會共享 per-CPU worker pools,並按需提供靈活的 concurrency level,避免大量的資源耗損。
  3. 自動調節 work pool 和並行度,讓 API 的使用者不用關心這些細節。

參考資料:Linux 核心設計: Concurrency Managed Workqueue(CMWQ)
參考資料:Workqueue

workqueue 建立

下列這五種為 workqueue 建立的 (API):

  • alloc_workqueue()
  • alloc_ordered_workqueue()
  • create_workqueue()
  • create_freezable_workqueue()
  • create_singlethread_workqueue()

這些 API 雖然名稱不同,但底層實作邏輯最終都會呼叫 alloc_workqueue() ,因此 alloc_workqueue() 是核心唯一真正的函式,其餘的只是為了語意清楚或向後相容所包裝的巨集。

今天說明 create_workqueue()alloc_workqueue 兩種:

#define create_workqueue(name)						\
	alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))

由上述這段程式碼可以知道 create_workqueue() 主要是將簡單的建立 workqueue 過程封裝起來,並提供了一個簡單的介面來建立一個綁定的 workqueue 。它是 alloc_workqueue() 的一個簡化版本。

create_workqueue 只要輸入字串就能建立,但有限制 Workqueue 其中的參數 WQ_MEM_RECLAIM 為有可能在記憶體回收路徑(memory reclaim paths)中使用的 workqueue ,必須設置這個旗標(WQ_MEM_RECLAIM)。有了這個旗標,即使系統處於記憶體壓力狀態,該 workqueue 也保證至少會有一個可執行的執行緒。

其中的參數 1 為決定每個 CPU 上可以同時指派給該 workqueue (wq)之 work item 的最大執行緒數量

@max_active determines the maximum number of execution contexts per CPU which can be assigned to the work items of a wq.

Workqueue 可以得知舊的 create_*workqueue() API 都已經「棄用」,並預計在未來的 Linux 核心版本中被移除(刪掉)。

The original create_*workqueue() functions are deprecated and scheduled for removal.

而在閱讀 workqueue.h 時我發現到其中的錯字因此發了 [PATCH] workqueue: fix typo in comment

後來收到 Tejun Heo回覆

Applied to wq/for-6.16.

Thanks.

alloc_workqueue() 可以不只像上述 create_*workqueue() 可以輸入字串和最大執行緒數量還能輸入其他旗標如下列所示:
WQ_BH

  • BH workqueues 總是以「每個 CPU 各自一組的方式存在,且其中的 work items 會在排入該工作的 CPU 上、以 softirq 的上下文中、依照排入順序執行。
  • 所有 BH workqueue 都必須設定 max_active = 0 且唯一允許的額外旗標是 WQ_HIGHPRI
  • BH work items 不能 sleep。不過,delayed queueingflushcancel 等,仍然是支援的。

WQ_UNBOUND

  • 被排入 unbound workqueueswork items ,會由一組不綁定任何特定 CPU 的特殊 worker-pool 來處理。
  • 這使得這種 workqueues 的行為就像是一個簡單的「執行環境提供者」,而不進行並行管理。
  • unbound worker-pool 會儘可能立刻啟動 work item 的執行。相較於綁定型 workqueue ,它犧牲了 CPU 快取區域性(locality),但在以下情況中特別有用:
    • 當預期 workqueues 的並行需求會大幅波動時
    • 針對長時間執行的、CPU 密集型的工作負載

WQ_FREEZABLE

  • 具有可凍結(freezable)屬性的 workqueues 會參與系統暫停(suspend)操作中的凍結階段(freeze phase)。
  • 在這個階段,workqueues 中的 work items 會被清空,並且在系統 thaw 之前,不會執行任何新的 work item

WQ_MEM_RECLAIM

  • 所有可能在記憶體回收流程中被使用的 workqueues,必須加上這個旗標。
  • 即使系統處於記憶體壓力狀態,該佇列仍保證會有至少一個 worker thread 可以執行。

WQ_HIGHPRI

  • 此旗標表示該佇列的 work items,會排入目標 CPU 的 高優先序(highpri)worker-pool。
  • 高優先序的 worker-pool 是由具有較高執行優先權(較低 nice 值的 worker thread 所服務的)
  • 高優先與一般 worker-pool 是彼此獨立的:它們有各自的 thread pool 與並行控制機制

WQ_CPU_INTENSIVE

  • 屬於 CPU 密集型 workqueueswork items ,不會被納入並行限制(concurrency level)的計算中。
    換句話說,這些可執行的 CPU 密集型 work item 不會阻止同一個 worker-pool 中的其他工作開始執行。
  • 這對於那些預期會長時間佔用 CPU 資源的 bound 工作特別有用,因為它們的執行可以交由系統排程器來管理(而不是由 workqueue 的並行控制機制管理)。
  • 儘管 CPU 密集型的 work item 不會計入並行限制,它們的執行啟動仍然會受到並行管理的控制。也就是說,如果同一個 worker-pool 中有可執行的非 CPU 密集型 work item ,可能會延遲 CPU 密集型 work item 的啟動。
  • 這個旗標 WQ_CPU_INTENSIVEunbound workqueues 來說是無意義的。
enum wq_flags {
	WQ_BH			= 1 << 0, /* execute in bottom half (softirq) context */
	WQ_UNBOUND		= 1 << 1, /* not bound to any cpu */
	WQ_FREEZABLE		= 1 << 2, /* freeze during suspend */
	WQ_MEM_RECLAIM		= 1 << 3, /* may be used for memory reclaim */
	WQ_HIGHPRI		= 1 << 4, /* high priority */
	WQ_CPU_INTENSIVE	= 1 << 5, /* cpu intensive workqueue */
	WQ_SYSFS		= 1 << 6, /* visible in sysfs, see workqueue_sysfs_register() */
        ...
        ...

提交任務的流程

在成功建立 workqueue 之後,接下來的步驟就是將待執行的 work_struct 提交至 workqueue,以便讓 workqueue 開始處理。在提交 work_struct 之前要先定義與初始化 work_struct 後才提交 work_struct 。提交 work_struct 的函式有兩個:

  • queue_work()
  • schedule_work()

schedule_work() 的作用是將 work_struct 加入 kernel 的預設 workqueue 中,而另外queue_work()用來將 work_struct 提交到指定的 workqueue 中的函式,但觀察下列 work_struct 可以發現到其中並沒有 task_struct 因此不會被排程器直接排程,所以我們要觀察到底執行這個 work_struct 是哪一個執行緒

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

於是我觀察了 kxo 這個核心模組是由哪個執行緒來執行 work_struct ,我在一端的終端機輸入了下列命來觀察:

sudo bpftrace -e '
kprobe:ai_one_work_func {
    printf("ai_one_work_func: comm=%s, pid=%d, tid=%d\n", comm, pid, tid);
}'

並在另外一個終端機執行 sudo ./xo-user 這個命令後得出了下列的結果:

ai_one_work_func: comm=kworker/u32:1, pid=47849, tid=47849

可以發現到實際執行 work_struct 的執行緒為 kworker 就和 Workqueue 中所述的一樣:對於執行緒型 workqueue,由特別用途的執行緒(稱為 kworker)一個接一個地從 queue 中取出並執行函式。如果佇列中沒有工作,這些 worker thread 就會變成閒置狀態。這些 worker thread 是由 worker pool 管理的。

For threaded workqueues, special purpose threads, called [k]workers, execute the functions off of the queue, one after the other. If no work is queued, the worker threads become idle. These worker threads are managed in worker-pools.

worker thread 閒置的情形也可以觀察的到:

ps -p 49452 -o pid,ppid,stat,cmd
    PID    PPID STAT CMD
  49452       2 I    [kworker/u32:6-flush-259:5]

在觀察 Linux 核心原始程式碼後可以發現,當呼叫 schedule_work() 並傳入某個 work item 時,系統首先會檢查該工作是否已經處於 pending 狀態。若否,該工作將會被加入至其對應之 workqueue_struct 所屬的 pool_workqueue(簡稱 pwq)中。

pool_workqueue 是 workqueue 在某個 worker_pool 上的具體對應實體,負責與底層的執行緒池協作,並管理實際的工作排程流程。

此時,該 work item 會被掛入 worker_pool 的 worklist,表示它正排隊等待被執行。若此 workqueue 為 threaded 類型,系統會喚醒對應的 kworker 執行緒來處理該工作。kworker 是由核心動態建立的核心執行緒,具備標準的 task_struct 結構,因此可以被 Linux 的排程器納入排程流程中,與一般使用者行程一樣,接受時間片管理與 CPU 分派。

一旦被喚醒,kworker 執行緒將會依序從 worklist 中取出各個 work item,並呼叫其對應的工作函式(即 work_struct 結構中的 func 函數指標)進行執行,直到佇列清空,或當前無待處理的工作為止。

在 Linux 中,任務有五種排程類別和六種排程策略,我們要查看 kworker 是被 cpu 以哪個排程策略排程的,因此我輸入了下列命令並觀察結果:

wu@wu-Pro-E500-G6-WS720T:~$ chrt -p 47849
pid 47849's current scheduling policy: SCHED_OTHER
pid 47849's current scheduling priority: 0

可以發現到 kworker 使用的排程策略為 SCHED_OTHER 也就是 SCHED_NORMAL 就是和一般使用者空間任務使用相同排程,之所以能找到這點是因為每個有 task_struct 的都有 sched_class 這個欄位。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

而在 sched(7) — Linux manual page 中提到 SCHED_OTHER 排程策略只能用於靜態優先權為 0 的執行緒。當一個執行緒使用此策略時,排程器會從所有靜態優先權為 0 的執行緒清單中,選擇下一個要執行的對象。

在這個清單內部,執行緒的選擇並非隨機,而是依據動態優先權的值進行排序

這個動態優先權也就是 Linux EEVDF 所使用的 virtual deadline,它是以 vruntime 為基礎,加上由 nice 值導出的執行時間預期值來計算,進而精確控制公平性與排程延遲具體,計算公式如下:

	/*
	 * EEVDF: vd_i = ve_i + r_i / w_i
	 */
	se->deadline = se->vruntime + calc_delta_fair(se->slice, se);

換句話說:

virtual deadline=vruntime+sched_entity slicetask weight

當一個執行緒的 nice 值越小(即優先權越高),它所對應的 task_weight 就越大。而在 calc_delta_fair() 中,當 nice 值越小時,slice / weight 的結果越小,使得 virtual_deadline 的值越小,也就是說該執行緒越容易被優先排程;

而更新 vruntime 為以下公式進行計算:

curr->vruntime += calc_delta_fair(delta_exec, curr);

而其中的 delta_exec 為下列:

delta_exec = update_curr_se(rq, curr);

update_curr_se 函式的主要作用是:
利用 rq_clock_task(rq) 取得該 CPU 上當前最新的任務時鐘時間,再用這個時間減掉 curr->exec_start(該任務上一次開始執行的時間點),計算出此次執行的時間差 delta_exec。
接著,將 exec_start 更新為當前時間,並將計算出的 delta_exec 累加到 sum_exec_runtime 中,紀錄該任務的累計執行時間。

計算出 delta_exec 後,即可用它更新當前任務的 vruntime。
calc_delta_fair() 函式會根據該任務的權重(se->load.weight)對實際執行時間 delta 進行調整,將其換算成任務的虛擬執行時間。
當權重不是預設值 NICE_0_LOAD 時,函式會呼叫 __calc_delta() 來計算調整後的虛擬執行時間,並回傳最終結果。

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);

	return delta;
}

這裡的 NICE_0_LOAD 為下列定義:

# define SCHED_FIXEDPOINT_SHIFT		10
# define NICE_0_LOAD_SHIFT	(SCHED_FIXEDPOINT_SHIFT + SCHED_FIXEDPOINT_SHIFT)
# define NICE_0_LOAD		(1L << NICE_0_LOAD_SHIFT)

也就是說,這個值等同於

220,與我觀察到的 nice 值為 0 的任務的 se.load.weight 完全一致。

wu@wu-Pro-E500-G6-WS720T:~$ ps -p 3184 -o ni
 NI
  0 
wu@wu-Pro-E500-G6-WS720T:~$ cat /proc/3184/sched
chrome (3184, #threads: 36)
-------------------------------------------------------------------
se.exec_start                                :      90128878.494935
se.vruntime                                  :       1225168.254057
se.sum_exec_runtime                          :        851903.234532
se.nr_migrations                             :                64974
nr_switches                                  :              3695116
nr_voluntary_switches                        :              3689914
nr_involuntary_switches                      :                 5202
se.load.weight                               :              1048576
se.avg.load_sum                              :                   45
se.avg.runnable_sum                          :                47134
se.avg.util_sum                              :                47134
se.avg.load_avg                              :                    0
se.avg.runnable_avg                          :                    0
se.avg.util_avg                              :                    0
se.avg.last_update_time                      :       90128493667328
se.avg.util_est                              :                    7
uclamp.min                                   :                    0
uclamp.max                                   :                 1024
effective uclamp.min                         :                    0
effective uclamp.max                         :                 1024
policy                                       :                    0
prio                                         :                  120
clock-delta                                  :                   64
mm->numa_scan_seq                            :                    0
numa_pages_migrated                          :                    0
numa_preferred_nid                           :                   -1
total_numa_faults                            :                    0
current_node=0, numa_group_id=0
numa_faults node=0 task_private=0 task_shared=0 group_private=0 group_shared=0

在觀察完下列結果後我去觀察 nice 為 -20 的 se.load.weight:

wu@wu-Pro-E500-G6-WS720T:~$ cat /proc/54902/sched
kworker/u33:1 (54902, #threads: 1)
-------------------------------------------------------------------
se.exec_start                                :      88173523.146110
se.vruntime                                  :       2135742.629337
se.sum_exec_runtime                          :         43077.473473
se.nr_migrations                             :                   98
nr_switches                                  :                  552
nr_voluntary_switches                        :                  451
nr_involuntary_switches                      :                  101
se.load.weight                               :             90891264
se.avg.load_sum                              :                   13
se.avg.runnable_sum                          :                14410
se.avg.util_sum                              :                14410
se.avg.load_avg                              :                    3
se.avg.runnable_avg                          :                    0
se.avg.util_avg                              :                    0
se.avg.last_update_time                      :       88172628609024
se.avg.util_est                              :                  613
uclamp.min                                   :                    0
uclamp.max                                   :                 1024
effective uclamp.min                         :                    0
effective uclamp.max                         :                 1024
policy                                       :                    0
prio                                         :                  100
clock-delta                                  :                  104
numa_pages_migrated                          :                    0
numa_preferred_nid                           :                   -1
total_numa_faults                            :                    0
current_node=0, numa_group_id=0
numa_faults node=0 task_private=0 task_shared=0 group_private=0 group_shared=0

觀察到 se.load.weight 為 90,891,264 也就是 nice 值為 -20 的權重 88761 乘上

210,約為 nice 值為 0 的權重大約 86 倍。根據 __calc_delta 的註解,其計算方式為 delta_exec * weight / lw.weight。因此,nice 值較大的任務其對應的 delta 會較小,導致 vruntime 增加較慢,進而使得 virtual_deadline 也較小。這使得這類任務更容易在排程中被優先選中執行

/*
 * delta_exec * weight / lw.weight
 *   OR
 * (delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT
 *
 * Either weight := NICE_0_LOAD and lw \e sched_prio_to_wmult[], in which case
 * we're guaranteed shift stays positive because inv_weight is guaranteed to
 * fit 32 bits, and NICE_0_LOAD gives another 10 bits; therefore shift >= 22.
 *
 * Or, weight =< lw.weight (because lw.weight is the runqueue weight), thus
 * weight/lw.weight <= 1, and therefore our shift will also be positive.
 */
static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)

觀察高優先序的 worker thread 和一般 worker thread

為了觀察高優先序的 work thread 與一般 work thread 在排程層面上的差異,我首先透過以下命令,檢查一般 work pool 中的 work thread 所使用的 nice 值:

wu@wu-Pro-E500-G6-WS720T:~$ ps -p 47849 -o ni
 NI
  0

而在上述 workqueue 建立中也有提到有一個旗幟叫做WQ_HIGHPRI,此旗標表示該佇列的 work items,會排入目標 CPU 的高優先序 worker-pool。
因此我將原本 kxo 的 workqueue 加上這個旗幟來去觀察他的 nice 值,下列為測試結果:

kxo_workqueue = alloc_workqueue("kxod", WQ_UNBOUND | WQ_HIGHPRI, WQ_MAX_ACTIVE);
ai_two_work_func: comm=kworker/u33:1, pid=54902, tid=54902
wu@wu-Pro-E500-G6-WS720T:~$ chrt -p 54902
pid 54902's current scheduling policy: SCHED_OTHER
pid 54902's current scheduling priority: 0
wu@wu-Pro-E500-G6-WS720T:~$ ps -p 54902 -o ni
 NI
-20

可以發現到高優先序的 worker-pool 的 worker thread 的 nice 值為 -20

為了驗證 nice 值是否會影響排程行為,我設計並執行了以下測試:

首先,我建立了兩個綁定型的 workqueue:

  • 一個為高優先權 workqueue,其對應的 worker thread nice 值為 -20
  • 另一個為一般優先權的 workqueue,其 nice 值為預設的 0

接著,我將一個包含 udelay(1000000) 的任務分別排入這兩個 workqueue,並設計為重複排程。

透過 perfetto 這個工具我觀察到在 20 秒內高優先權 worker thread 佔據了絕大部分的執行時間,也就是下列這張圖:

Screenshot from 2025-05-18 16-17-23

而要如何區隔一般任務和 workqueue 的任務呢?就是要觀察任務的 comm ,如果是 workqueue 的任務 comm 會等於 kworker/ 而一般使用者空間任務的 comm 會是可識別的應用名稱,例如 bash、chrome、sleep 等等,種類多且與實際執行程式有關,像下列我所觀察到的例子:

3184 wu        20   0   32.9g 471040 278300 S   2.7   1.4  20:31.02 chrome 
   wu@wu-Pro-E500-G6-WS720T:~$ chrt -p 3184
pid 3184's current scheduling policy: SCHED_OTHER
pid 3184's current scheduling priority: 0

為了觀察實際 kworker 排程的情況我使用 bpftrace 追蹤 kxo 與 Linux 排程器互動

sudo bpftrace -e '
kprobe:ai_one_work_func {
    printf("ai_one_work_func work triggered: tid=%d comm=%s\n", tid, comm);
    @start[tid] = nsecs;
    @done[tid] = 0;
}

kretprobe:ai_one_work_func /@start[tid]/ {
    $dur = (nsecs - @start[tid]) / 1000;
    printf("ai_one_work_func work done: tid=%d duration=%lluus\n", tid, $dur);
    @done[tid] = 1;
}

kprobe:ai_two_work_func {
    printf("ai_two_work_func work triggered: tid=%d comm=%s\n", tid, comm);
    @start[tid] = nsecs;
    @done[tid] = 0;
}

kretprobe:ai_two_work_func /@start[tid]/ {
    $dur = (nsecs - @start[tid]) / 1000;
    printf("ai_two_work_func work done: tid=%d duration=%lluus\n", tid, $dur);
    @done[tid] = 1;
}

tracepoint:sched:sched_switch {
    if (@start[args->prev_pid]) {
        printf("scheduled out: tid=%d comm=%s state=%d\n",
               args->prev_pid, args->prev_comm, args->prev_state);

        if (@done[args->prev_pid]) {
            delete(@start[args->prev_pid]);
            delete(@done[args->prev_pid]);
        }
    }

}'

下列為追蹤的結果:

ai_one_work_func work triggered: tid=309 comm=kworker/u32:9
ai_one_work_func work done: tid=309 duration=704185us
scheduled out: tid=309 comm=kworker/u32:9 state=256
ai_two_work_func work triggered: tid=309 comm=kworker/u32:9
ai_two_work_func work done: tid=309 duration=3473us
scheduled out: tid=309 comm=kworker/u32:9 state=128
ai_one_work_func work triggered: tid=309 comm=kworker/u32:9
ai_one_work_func work done: tid=309 duration=439878us
scheduled out: tid=309 comm=kworker/u32:9 state=256
ai_two_work_func work triggered: tid=6518 comm=kworker/u32:1
ai_two_work_func work done: tid=6518 duration=416us
scheduled out: tid=6518 comm=kworker/u32:1 state=128

我們接下來觀察 /include/linux/sched.h 中關於 tsk->__state 的部分,其中定義了:

#define TASK_DEAD			0x00000080 /* 128*/
#define TASK_WAKEKILL			0x00000100 /* 256 */

根據我的觀察,在 kworker 中,如果執行的任務函式運行時間比較長,任務完成後通常會保留該執行緒並標記為 TASK_WAKEKILL,以便日後能快速重用這個執行緒。這樣可以避免頻繁創建與銷毀 kworker,節省系統開銷。

相對地,當 kworker 所執行的任務非常短時,核心可能會直接將該執行緒狀態標記為 TASK_DEAD,意味著該執行緒不再重用,將被釋放以回收資源,但實際回收可能延遲。

我認為這可能是核心排程策略的一部分:當任務函式的執行時間較長時,系統往往傾向於保留該 kworker 執行緒,這可能是因為系統預期仍有後續任務會到來,或該執行緒剛完成工作時,idle pool 中的可用 worker 數量不足。

透過保留已處理過這些長時間任務的 kworker,系統能在下一次有類似工作抵達時,直接喚醒既有的執行緒,而無需重新建立新的 worker。這種重複使用的機制有助於減少執行緒建立與銷毀的成本,提升整體執行效率與響應速度。

worker thread 跟 numa node

從 create_worker() 函式的實作中可以觀察到,實際建立 kernel thread 的動作是透過下列這兩行完成的:

worker = alloc_worker(pool->node);
worker->task = kthread_create_on_node(worker_thread, worker,
						      pool->node, "%s", id_buf);

這行程式碼呼叫了 kthread_create_on_node(),用來建立並命名一個 kernel thread。建立後的 thread 會處於停止狀態,你需要使用 wake_up_process() 才能啟動它。

新建立的 thread 會使用 SCHED_NORMAL 排程政策也就是一般任務(一般的 CFS 調度),並且預設允許在所有 CPU 上執行(無 CPU affinity 限制)。

其中第三個參數是輸入 NUMA node 或是 NUMA_NO_NODE ,如果是 NUMA node 的話是預期這個 thread 最終會綁定在某個特定的 CPU 上,要提供該 CPU 所屬的 NUMA node。這樣可以讓該 thread 的 kernel stack(以及其他執行相關的資料結構)分配在對應的 NUMA 記憶體節點上,提升記憶體親和性(memory locality)。否則,就請傳入 NUMA_NO_NODE 在目前呼叫 kthread_create_on_node 的 cpu 上建立

而如果是使用 kthread_create() 這個函式來建立 thread 的話,實際上它是一個巨集包裝,如下所示:

#define kthread_create(threadfn, data, namefmt, arg...) \
	kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

可以看到它實際上會呼叫 kthread_create_on_node(),並將 NUMA_NO_NODE 傳入,代表 不指定特定的 NUMA node。這表示:

  • 新建立的 thread 的資料結構(如 task_struct、stack)會根據目前執行 thread 所在的 CPU,自動分配在該 CPU 所屬的 NUMA node 上(這是預設行為)。

而在 create_worker 中還有下列這段程式碼:

kthread_bind_mask(worker->task, pool_allowed_cpus(pool));

這是將這個 worker thread(worker->task)綁定在 pool_allowed_cpus(pool) 所回傳的 CPU mask 上,使它只能在指定的 CPU 上被 scheduler 調度執行,而其中的 pool_allowed_cpus 為下列這段:

static cpumask_t *pool_allowed_cpus(struct worker_pool *pool)
{
	if (pool->cpu < 0 && pool->attrs->affn_strict)
		return pool->attrs->__pod_cpumask;
	else
		return pool->attrs->cpumask;
}

如果兩者同時成立,表示 work pool 未指定特定 CPU,但要嚴格限制 CPU 親和性,這時返回 __pod_cpumask,而另一個 affn_strict 為如果為而 __pod_cpumask 的定義可以在 linux/include/linux/workqueue.h 中找到,而另一個 affn_strict 若未設定,workqueue 將盡力在 __pod_cpumask 範圍內啟動 worker ,但排程器可自由將其遷移 __pod_cpumask 到範圍外的 CPU 執行,反之,若 affn_strict 被設定,worker 則只允許在 __pod_cpumask 範圍內運行。

	/**
	 * @__pod_cpumask: internal attribute used to create per-pod pools
	 *
	 * Internal use only.
	 *
	 * Per-pod unbound worker pools are used to improve locality. Always a
	 * subset of ->cpumask. A workqueue can be associated with multiple
	 * worker pools with disjoint @__pod_cpumask's. Whether the enforcement
	 * of a pool's @__pod_cpumask is strict depends on @affn_strict.
	 */
	cpumask_var_t __pod_cpumask;

__pod_cpumask 跟 numa 節點的關係?

不然,返回一般的 CPU 掩碼 cpumask。

官方文件指出,WQ_UNBOUND 所排入的工作項目會由一組特殊的 worker pool 處理。這些 worker 並不綁定在任何特定的 CPU,因此理論上應該可以在任意 CPU 上執行。

不過,實際上 WQ_UNBOUND 是有 CPU affinity 的:它會優先嘗試使用當前執行緒所在的 CPU,前提是該 CPU 存在於 wq_unbound_cpumask 中(目前允許 unbound workqueues 執行的 CPU 清單)。

當前 CPU 如果不在允許 wq_unbound_cpumask ,系統會進一步透過 round-robin 機制,從上次選用的 CPU 開始,依序掃描下一顆可用的 CPU,條件是該 CPU 必須同時存在於:

  • wq_unbound_cpumask
  • cpu_online_mask
static int wq_select_unbound_cpu(int cpu)
{
	int new_cpu;

	if (likely(!wq_debug_force_rr_cpu)) {
		if (cpumask_test_cpu(cpu, wq_unbound_cpumask))
			return cpu;
	} else {
		pr_warn_once("workqueue: round-robin CPU selection forced, expect performance impact\n");
	}

	new_cpu = __this_cpu_read(wq_rr_cpu_last);
	new_cpu = cpumask_next_and(new_cpu, wq_unbound_cpumask, cpu_online_mask);
	if (unlikely(new_cpu >= nr_cpu_ids)) {
		new_cpu = cpumask_first_and(wq_unbound_cpumask, cpu_online_mask);
		if (unlikely(new_cpu >= nr_cpu_ids))
			return cpu;
	}
	__this_cpu_write(wq_rr_cpu_last, new_cpu);

	return new_cpu;
}

資料來源

TODO: 紀錄關於 Linux Suspend/Resume 的提問

經過測試不管是哪種程度的 suspend 都會在 suspend_enter 階段做 sync filesystem,嘗試將 dirty page 寫回磁碟中,但是根據 System Sleep States 所述 memory 只有在 Hibernation 的情況下才會斷電,因此 memory 只要不是 Hibernation 狀況下都不會消失,那是不是只要不是使用 Hibernation 就能不用 sync filesystem 不去將dirty page 寫回磁碟中?

This state (also referred to as STR or S2RAM), if supported, offers significant energy savings as everything in the system is put into a low-power state, except for memory。

  • suspend_enter 階段可能會做 sync filesystem,嘗試將 dirty page 寫回磁碟中。

    • 這裡所謂的「嘗試」,代表寫入動作不保證完全成功;若寫入失敗會發生什麼事,假如資料遺漏的話呢?
  • 當系統喚醒後,控制權會交還給平台韌體或直接給 boot loader(依系統配置而定),接著會啟動一個新的核心實例,稱為恢復核心(restore kernel)。這個恢復核心會從持久性儲存裝置中搜尋並載入休眠映像(hibernation image),以還原系統至休眠前的狀態。

    • 搜尋並載入休眠映像(hibernation image)這個動作,只有在系統進行休眠(hibernate)並重新開機後,boot loader 才會啟動相關流程嗎?
      至於 boot loader 如何知道這次開機是為了從休眠中恢復?
    • 那還原之後 restore核心的記憶體位置會怎麼處理?
  • Runtime Suspend CallbackRuntime Resume Callback 中提到,如果 resume 回傳錯誤,則 PM core 將此視為致命錯誤,需要先透過特殊的 helper 將其狀態復原為 "active" 或 "suspended"

    • 如何知道要恢復到"active" 還是 "suspended"是根據回傳的 error code ?
  • Atosuspend 中提到:驅動裝置應該等到裝置處於非活動狀態至少一段時間後,才主動將其至於低功耗的狀態。

    • 那這個一段時間後是怎麼計算的?
  • 目前實驗一

TODO: 提問的智慧

在閱讀 How-To-Ask-Questions-The-Smart-Way 後做了下列的筆記:
在提問之前需要做好以下事項:

  1. 嘗試在你準備提問的論壇的舊文章中搜尋答案。
  2. 嘗試上網搜尋來找到答案。
  3. 嘗試閱讀手冊來找到答案。
  4. 嘗試閱讀常見問題文件(FAQ)來找到答案。
  5. 嘗試自己檢查或試驗來找到答案
  6. 向你身邊的強者朋友打聽來找到答案。
  7. 如果你是程式開發者,請嘗試閱讀原始碼來找到答案

當你在完成上述幾點後,主動表達自己願意在尋找答案的過程中付出努力,這就是一個非常良好的開始。舉例來說,像是「有人可以給我一點提示嗎?」這類的話語,不只是表達你有求知的態度,更讓人感受到:只要有人能為你指出正確的方向,你就有能力也有決心把事情完成。

其二,我學到的是:使用有意義且具體明確的標題,是一種有效吸引專家目光的方式。控制在 50 字以內的標題,能快速傳達問題核心。在「目標」部分明確指出出問題的對象或環境;在「差異」部分描述實際行為與預期結果之間的落差。
例如:「X.org 6.8.1 的滑鼠游標在某牌顯示卡 MV1005 晶片組下出現變形」。

其三,我學到的是:使用清晰、正確、精確且合乎文法的語句同樣非常重要。語言表達反映一個人的思維習慣,若在文字上不注意細節,往往也會在寫程式和思考時顯得粗心大意。

其四,我學到的是:精確的描述問題並言之有物和話不在多而在精也就是下列幾點:

  • 仔細、清楚地描述你的問題。
  • 描述問題發生的環境。
  • 描述在提問前你是怎樣去研究和理解這個問題的。
  • 描述在提問前為確定問題而採取的診斷步驟。
  • 描述最近做過什麼可能相關的硬體或軟體變更。
  • 盡可能的提供一個可以重製這個問題的既定環境的方法

這是我在 How-To-Ask-Questions-The-Smart-Way 中所學到最重要的,儘量去揣測收件人會怎樣反問你,在他提問的時候預先給他答案。

那為什麼要話不在多而在精呢?原因在於下列三點:

  1. 表現出你為簡化問題付出了努力,這可以使你得到回答的機會增加
  2. 簡化問題使你更有可能得到有用的答案
  3. 在精鍊你的bug報告的過程中,你很可能就自己找到了解決方法或權宜之計。

其五,我學到的是:描述問題症狀而非猜測必須確保自己完整描述了問題的實際症狀,而不是僅僅傳達你對問題的解釋或推測。

其六,我學到的是:描述目標而不是過程。當你想弄清楚某件事的做法時,應該在開頭清楚說明你想達成的目標,接著再具體說明你在實作過程中卡住的步驟,這樣他人才更容易理解並提供有效的協助。

其七,我學到的是:清楚明確地表達你的需求。要明確表述需要回答者做什麼(如提供指點、發送一段程式碼、檢查你的 patch 、或是其他等等),就最有可能得到有用的答案。

以上是我在 How-To-Ask-Questions-The-Smart-Way 中所學到的提問的技巧。