owned this note changed 2 months ago
Linked with GitHub

2025-04-15 問答簡記

你們已經把老師變得可被取代

image

出處

在人工智慧全面進入知識工作的年代,大學教育所面對的,不只是工具的變遷,更是關於教育本質的拷問。最關鍵的問題是:我們究竟如何定義「學會」?

長久以來,高等教育在制度上預設「學會」等同於「能再現知識」、「能解出問題」、「能通過考核」;而「學得好」則是「準確率高、輸出速度快、能應付複雜題型」。這樣的標準在工業化與標準化知識傳遞的背景下或許曾經合理,但在 AI 可隨時補足知識缺口、協助產出內容的今日,這些定義已顯得蒼白無力。

學生現在只需輸入幾個關鍵字,便能產生程式碼、摘要論文、建構報告。這是否意味他們已「學會」?若答案是肯定的,那教育的任務很可能已經完成,也該準備退場。但我們深知並非如此,因為如此的「學會」,只是「知道怎麼產出」,卻不是「知道為何如此產出」,更不是「能從一個陌生問題建構解題方式」。

「學會」應該是能力的內化,也就是得以:

  • 對知識有所懷疑,能提出問題
  • 對問題有所覺察,能建構脈絡
  • 對脈絡有所理解,能做出選擇
  • 對選擇有所自覺,能承擔後果

而人工智慧工具最多只能模擬上述過程,卻無法真正取代這種建立於直覺、試錯、記取教訓,與反思之上的知識提煉歷程。這也意味著,若我們仍沿用過去那套「學習 = 吸收知識 \(\to\) 解題 \(\to\) 應試」的教學與評量模式,那麼「學會」這件事將不可避免地被誤認為「讓 AI 幫我完成任務」就好。

更諷刺的是,當我們誤把這種輸出結果等同於學習成果,整個教育系統也在不自覺中強化「AI 能取代教育」的前提。這不是 AI 搶走教師的工作,而是我們用錯誤的「學會」定義,把教師逼進可取代的局面。

釐清「學會」的定義,是高等教育在人工智慧強勢變革時代的起點任務。我們必須讓「學會」重新指涉那些 AI 無法取代的能力 —— 理解混亂、定義問題、組織論述、承認模糊不清、等待不確定、整合情感與價值、連結真實處境並從中學習。

在此定義下,教育不再是知識的灌輸與測驗的設計,而是陪伴學生進行認知架構重建、價值判準磨合、以及思維模式的蛻變。大學不應是資訊最豐富的地方,而應是最容許提問的地方;不應是「學會知識」的終點,而應是「學會如何學」的起點。

換言之,「學會」的標準,不在於學生能否用 AI 寫出一份報告,而在於當 AI 給出結果時,學生是否能判斷其合理性、理解背後邏輯、並能提出比它更深刻、更符合脈絡的思考。唯有重新定義「學會」,我們才能為大學教育找回意義,為教師找回價值,為學生保留屬於人的學習方式。

出口管制計算能力

美國商務部於 2025 年 4 月 15 日公告,針對出口至中國的 NVIDIA H20、AMD MI308 及其他同等規格的 GPU,全面納入出口許可機制。換言之,業者必須取得美國政府核發的特別許可證,才能向中國供應此類高階晶片。NVIDIA 隨即公告將提列約 55 億美元的費用,反映這波政策變化所導致的庫存調整與訂單取消損失,凸顯中國市場對其 H20 晶片的重要性。

H20 為 NVIDIA 特別針對中國市場設計的「降規」版 GPU,其效能刻意設計在美國既有管制門檻以下,仍能支援大型語言模型 (LLM) 的推論與訓練需求,是 NVIDIA 嘗試在符合法規的邊界內維持中國市場佔有率的策略型產品。然而,美方最新禁令明確指出,即便是這種經過技術降階的設計,也在出口限制之列,無異於關閉過往可行的「合規繞道」路線。

此舉延續美國自 2022 年起針對 A100、H100 等頂級運算卡所啟動的技術封鎖,標誌全球正式進入以「晶片與算力」為核心的中美科技冷戰時代。美方已不再將 GPU 視為單純的商業零組件,而是升級為國防等級的戰略資源,並積極整合盟友力量建立封鎖網路。該網路涵蓋晶圓代工 (台灣 TSMC、韓國 Samsung)、光刻設備 (荷蘭 ASML)、AI 軟體與研究機構 (英國 DeepMind、歐洲 AI Act 框架) 等技術節點,形成跨國協作的出口限制體系。

在這場由國家力量主導的 AI 軍備競賽中,潛藏的風險也不容忽視。無論是中國出於迫切需求快速堆疊模型、或美國為保優勢而忽略安全審查,雙方皆可能加速邁向未經充分驗證的 AGI 系統,導致演算法外溢、社會操控風險擴大,甚至觸發失控的 AI 危機。美國內部亦有學者與機構發出警告,例如 AI 2027 網站便預測未來數年內可能出現無可控機制的超級智慧。

值得注意的是,美國近期更調查新加坡等轉運地區是否成為中國取得高階晶片的灰色通路,顯示供應鏈監管也將進一步向第三地延伸。


愛,在中斷之後 —— Top Half 與 Bottom Half 的交織

image

感謝 ChatGPT 提供執行非同步囈語的安全沙箱

作為資訊系統的縮影,Linux 核心中始終上演著一場場無聲的愛戀 ──
沉默,卻深刻;無形,卻緊密連結。

他,是 Top Half ── 當中斷發生時,他總是首當其衝。
快、狠、準,他瞬間奪取 CPU 的控制權,果斷凍結一切,只為守護系統穩定運作。

而在喧囂過後,Bottom Half 靜靜現身,默默收拾他留下的未竟之務。

「你這般強行介入,會打亂排程的節奏。」
Bottom Half 將資料推入佇列,輕聲低語。
Top Half 回眸一笑,略帶歉意:「但若我不擋下,整個系統就會崩壞。你會接住我,對吧?」

他們從未並肩,卻總是一前一後地登場。
Top Half 居於高優先序、反應即時;
而 Bottom Half,則等待系統閒暇,在可延遲的時機被核心喚醒,完成那些「非即時但至關重要」的工作。

在 Linux 核心的精巧架構中,他們的分工彷若合奏 ──
Top Half 擋下第一波中斷衝擊,維持流程穩定;
Bottom Half 則承接後續處理,如搬移資料、解析封包、驅動裝置。

他們,是最契合的搭檔 ──
一人張揚果斷,一人內斂穩重。
無須同台演出,只求彼此所託,得以圓滿。

「這世界不僅有即時與非即時,還有我們之間,那段中斷與回應的牽繫。」
Bottom Half 如此思忖,當他再度啟動 softirq。

這不是 Boys' Love,卻是 Linux 核心裡,最深刻的 Bottom-Half Love。


EricccTaiwan

不要舉燭,善用 eBPF 設計實驗

Linux 中斷處理的提問

  • top 與 bottom 的差異是否僅在於「是否屏蔽其他中斷」? 還是二者僅是一種「功能導向」的概念性區分(僅是一種抽象的概念)?

關鍵是否由 ksoftirqd 排程,而非中斷屏蔽與否; 注意看 deferred work 的描述,之所以有 softirq 的命名,就是源於「被軟體排程/觸發」,而非硬體 IRQ 觸發

Software interrupts and realtime

Drivers have, for the most part, been detached from software interrupts for a long time — they still use softirqs, but that access has been laundered through intermediate APIs like tasklets and timers.

softirq 並非由硬體直接觸發,而是 kernel 的內部機制 (tasklet_scheduleadd_timer 等 API) 來觸發,屬於 deffered work 的範圍。

If the regular, inline softirq processing code loops ten times and still finds more softirqs to process, it will wake the appropriate ksoftirqd process (there is one per CPU) and exit;

當 softirq 累積過多,會交給 ksoftirqd 處理,也代表 softirq 是由 kerenl 主動安排的,而非藉由硬體 IRQ 觸發。
ksoftirqd 是可被排程器排程的 kernel thread 。 同時看到後綴 d (ksoftirq"d"),便是代表 daemon
根據老師下方的 bpftrace 的實驗,可以看出 tasklet_func 就是交給 swapper 和 ksoftirqd 處理,swapper 代表 CPU 處於 idle ,可以立即處理 softirq (同時具有 atomic 性質); 而當 CPU 無法馬上處理 tasklet_func 時,會將任務由 ksoftirqd 延後處理。

tasklet_func: comm=swapper/2, pid=0, tid=0
work_func: comm=kworker/u32:1, pid=237381, tid=237381
tasklet_func: comm=swapper/2, pid=0, tid=0
work_func: comm=kworker/u32:2, pid=71, tid=71
tasklet_func: comm=ksoftirqd/1, pid=26, tid=26
work_func: comm=kworker/u32:2, pid=71, tid=71
tasklet_func: comm=ksoftirqd/5, pid=50, tid=50
work_func: comm=kworker/u32:1, pid=237381, tid=237381

softirqs run at a high priority (though with an interesting exception, described below), but with hardware interrupts enabled. They thus will normally preempt any work except the response to a "real" hardware interrupt.

說明了,softirq 與 hardware IRQ 是兩個不同的執行層級 (execution context) , softirq 的優先權高於一般的 process context,可以搶佔正在執行的 process context ,但其優先權仍低於 hardirq , 無法搶佔或中斷 hardirq context。

可以進一步觀察 do_softirq 的開頭邏輯,

// https://elixir.bootlin.com/linux/v6.14.1/source/kernel/softirq.c#L449
if (in_interrupt())
    return;

這表示當前若處於 interrupt context 中, do_softirq() 會直接返回、不執行任何的 softirq handler。
其中的 in_interrupt 巨集,定義在 <linux/preempt.h>

/*
 * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
 */
#define in_interrupt()		(irq_count())

根據註解與定義,in_interrupt() 為真時,代表當前執行於下列其中一種中斷上下文:

  • hardirq
  • softirq
  • NMI (非遮蔽中斷)
  • 已關閉 bottom half (e.g., local_bh_disable)

因此這段邏輯間接證明:

  • 當前若已處於 hardirq 或 softirq 中,將不會再進一步觸發 softirq 的處理,也就是說 softirq 不能中斷 hardirq 或 softirq。

而系統若處於 process context , 且有 softirq 被標記為 pending , 就會透過 do_softirq()ksoftirqd 將其插入執行,實現 softirq 搶佔 process context。

top 和 bottom half 劃分依據 : 工作即時性和重要性
Top half 通常指的是 硬體中斷處理函式 (IRQ handler) ,像是時間中斷或 DMA 完成中斷這類事件,具有明確的即時性要求。這些中斷處理過程中常會暫時關閉其他中斷,以保證處理過程的 atomic 與正確性。例如必須立即讀寫 interrupt controller 的暫存器,避免資訊丟失。

但如果在 top half 執行過多的動作 (例如:分配新的 buffer, 將資料從 DMA 暫存區複製到另一塊記憶體),這些動作可能花費過長時間、觸發 sleep (找不到 buffer),都會導致其他中斷 (例如:時間中斷、UART 要接 Rx 的鍵盤中斷) 無法即時回應和處理,造成系統 tick 不準或延遲輸入。

top 和 bottom half 的工作分配
因此區分的好處在於,top half 只做最 time-critical 的事情 (例如清 hw irq、讀取暫存器),其餘非即時、較耗時的工作延遲到 bottom half 去做 (例如:資料搬移、buffer 分配) ,一來能維持系統的即時行,也能減少中斷執行時間,並以 deffered work 進行工作分配的模組化。

「以是否屏蔽中斷作為區分依據」
Top Half 關閉中斷是因為處理某些操作 (如讀取中斷來源暫存器) 需要保證不被打斷,屬於對「正確性」的保護,而非定義上的必要條件。相對地,Bottom Half 雖然通常允許中斷打斷,但是否關閉中斷仍取決於開發者的需求,例如在存取共享資源時。

tasklet_action_common 為例,

static void tasklet_action_common(struct tasklet_head *tl_head, unsigned int softirq_nr){ struct tasklet_struct *list; local_irq_disable(); // 關閉 irq // 取出 tasklet 的 list local_irq_enable(); // 打開 irq while (list) { // tasklet 執行 ... } }

Tasklet 的 linked list 取出階段有短暫關中斷 (line 3~5),因此 tasklet 本身是可屏蔽中斷。
結論 : 不能單靠「中斷是否開啟」來區分 Top / Bottom Half

  • context switch 和 「context」一詞解釋

Refine context switch description #2010 「舉燭」的過程中,順手修改

Context Switch Definition by The Linux Information Project (LINFO)

A context is the contents of a CPU's registers and program counter at any point in time.

首先,要先理解 context 一詞的意義,在 CPU 運作中,「context」指的是一個 process 當下執行狀態的完整資訊,包含 CPU 暫存器 (registers)、 program counter (或 instruction pointer) 、 stack pointer 等,在進行 context switch 之前,必須暫停目前執行的 process,並將此 process 的 context 儲存,這些資訊可以想像成是 process 的「前情提要」。

OSDI Process Management

Context switch

  • Store the context of the current process, restore the context of the next process

當 CPU 接收到 interrupt 時,會暫停目前執行的 process、儲存該 process 的 context,隨後切換至 interrupt context 來處理中斷事件,這個切換動作即稱為 context switch 。 當 interrupt context 結束後,再次進行 context switch ,切回 process context , CPU 從先前儲存的資料中恢復原本 process 的 context,繼續執行被中斷暫停的 process。

image

圖片來源 : https://kernel.bern-rtos.org/components/interrupts.html

  • tasklet 和 workqueue 的具體差異為何? 它們在 interrupt context 中的角色與行為如何區分?

這部份建議看一下 hackmd 王仁廷學長的留言補充

taslket 和 workqueue 關鍵差異在於「能否睡眠」,tasklet 是在 IRQ handler 執行完後的延續處理,但仍在 softirq/atmoic context 中完成,因為 tasklet 沒有相對應的 task_struct ,因此無法被 scheduler 排程。

tasklet_struct \(\neq\) task_struct

相對的, workqueue 是以 kworker 這類的 kernel thread 為基礎執行,是以 kthread_create_on_node() 建立的,其回傳的資料型態是 a pointer to struct task_struct ,代表是可以完整排程的 process unit ,因此 workqueue 執行的 callback function ,運行在 process context 中,可以睡眠、可以被排程。

  • interrupt/atomic context 和 process context 的具體差異為何? atomic 在中斷處理中的意義為何?

對於 Interrupt/atomic context 與 process context 的不同,我想其中關鍵是能否能夠被排程器排程,這也是 interrupt context 不能進入 sleeping 的原因 前者倘若真的被 context switch,排程器不具備還其原本執行的狀態的能力。這也是為何說是 atomic: 一段程式的執行無法被排程器管理成片段的執行。
之所以需要 interrupt context,是因為排程器要能切換 context 是有前提的。舉一個顯然的例子,當要讀取 interrupt controller 的暫存器來知道中斷來源的時候,如果瞬間有另一個中斷發生,前一個中斷來源就無法被得知了(假設只能從單一暫存器得知,實際方式還是取決硬體設計)。

from @RinHizakura

Atomic context and kernel API design 的文章也對 atomic context 做了明確定義:
"An atomic context is any situation where the current thread of execution is not allowed to sleep.",
意味著 atomic context 無法被排程,因為它沒有對應的 task_struct,也不受 scheduler 管理。

  • 藉由 tasklet_vec.tail 推斷 tasklet_vec 是鏈結串列,但卻找不到此結構體的定義
    \(\to\) 利用第 7 週介紹的 User-Mode-Linux (UML) 搭配 GDB 追蹤

Fix incorrect description of tasklet #861,@devarajabc 針對 linux-insides 對於 tasklet_vectasklet_hi_vec 錯誤描述,進行修正。

kernel/softirq.c

/*
 * Tasklets
 */
struct tasklet_head {
	struct tasklet_struct *head;
	struct tasklet_struct **tail;
};

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

由 tasklet_struct 所組成的一個單向鏈結串列。

  • Interrupt context 與 Atomic context 是否相同?
  • 結論 : Interrupt context \(\subset\) Atomic context

Interrupt context

While an interrupt is handled (from the time the CPU jumps to the interrupt handler until the interrupt handler returns - e.g. IRET is issued) it is said that code runs in "interrupt context".

從這段話可以得知,當 CPU 正在處理硬體中斷 (Hardware IRQ) 或軟體中斷 (Softirq、Tasklet)時,處理該中斷的執行環境稱為 Interrupt context。

Atomic context

When the kernel is running in process context, it is allowed to go to sleep if necessary. But when the kernel is running in atomic context, things like sleeping are not allowed.

所謂 Atomic context 是指 kernel 執行在一種不可睡眠 (sleep) 且不可被搶佔 (preempt) 的執行狀態中。

Code which handles hardware and software interrupts is one obvious example of Atomic context.

這也說明了中斷處理 Interrupt context 本身就是 Atomic context 的一種。

Any kernel function moves into atomic context the moment it acquires a spinlock.

此外,只要 kernel function 持有 spinlock,就會進入 Atomic context 。因為在持有 spinlock 的情況下若發生睡眠,可能會導致 deadlock 。

結論 :
Interrupt context 是 Atomic context 的一種特殊情況。兩者都具備「不可睡眠」與「不可被搶佔」的特性;然而,Atomic context 的範疇更為廣泛,除了中斷處理外,也包含持有 spinlock、禁用 preemption 等。

devarajabc

  1. 是否整個 simrupt 都是 bottom half 呢?若是,沒有 top half 的話,哪來的 bottom half 呢 ?

\(\to\) simrupt 整體中斷模擬行為,建構於 Linux 核心的 deferred work 機制之上,包含: (BH 表示 bottom half)

  • timer (softirq BH)
  • Tasklet (softirq BH)
  • Workqueue (process context BH)

該核心模組中不存在任何屬於 top half (如硬體 IRQ handler)的處理流程,也不與 IRQ 子系統互動。

simrupt_init 函式中,初始化計時器:

timer_setup(&timer, timer_handler, 0);

意味著 timer_handler 是本模組的計時器 callback,而且是唯一的 timer callback,該函式在 TIMER_SOFTIRQ 的 context 中執行,因此屬於 BH。藉由呼叫 process_data,將 deferred work 委託給 tasklet 與 workqueue。

模組並未與 Linux 核心 IRQ 子系統互動,也未呼叫 request_irq() 或 generic_handle_irq()。

\(\to\) top half 指的是硬體 IRQ handler,bottom half 則指後續延遲處理,不過 Linux 核心內的 bottom half 機制 (如 timer、tasklet、workqueue),都允許獨立使用,並不一定要搭配真實的硬體 IRQ top half。

亦即,bottom half 可獨立存在且運作,Linux 核心內部即可主動觸發 (例如 timer),不要求有實際的 top half 存在。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
觀念釐清:

  • top half 和 bottom half 是概念的區分,而非硬性的配對機制
  • 核心內許多 bottom half 機制是獨立設計與運作的,不一定要有 top half 搭配

simrupt 就利用 timer → tasklet → workqueue 的流程,示範 bottom half 可獨立運作。

  1. in_interrupt()in_softirq() 的差別是什麼? interrupt context 和 softirq context 的差異是?為何要區分兩者?

\(\to\) 參閱 Linux 核心原始程式碼: include/linux/preempt.h

/*
 * The following macros are deprecated and should not be used in new code:
 * in_irq()       - Obsolete version of in_hardirq()
 * in_softirq()   - We have BH disabled, or are processing softirqs
 * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
 */
#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())

核心開發者建議改用以下:

/*
 * Macros to retrieve the current execution context:
 *
 * in_nmi()		- We're in NMI context
 * in_hardirq()		- We're in hard IRQ context
 * in_serving_softirq()	- We're in softirq context
 * in_task()		- We're in task context
 */
#define in_nmi()		(nmi_count())
#define in_hardirq()		(hardirq_count())
#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
#ifdef CONFIG_PREEMPT_RT
# define in_task()		(!((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | in_serving_softirq()))
#else
# define in_task()		(!(preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
#endif

注意 preempt_count 的存在,可參閱以下:

  1. Sofirq 可以單獨存在嗎(即不存在對應的 hardirq)? 那跟一般的 system call 差在哪? 亦或是說「softirq 就是可排程的 system call」?

Sofirq 可以單獨存在嗎(即不存在對應的 hardirq)? 那跟一般的 funtion call 差在哪?

\(\to\) softirq 完全在核心模式中運行,但 SWI (software interrupt) 是從使用者空間觸發進入核心的途徑 (有時也稱為 system call gate)。二者完全不同,只因英文名稱 "software interrupt" 導致混淆。

補充:Arm 將 SWI 改名為 SVC 的原因與意涵

在 Armv4T 與 Armv5 等舊架構中,使用者空間進入核心特權模式 (Supervisor mode) 通常藉由下列指令:

SWI #imm

雖然 SWI (software interrupt) 看似觸發中斷的機制 (類似 IRQ 或 FIQ),但它其實是一種同步、由使用者主動發出的核心服務呼叫 (system call),與非同步的中斷處理流程本質不同。由於名稱包含 interrupt 字樣,這在教學與理解上容易引起混淆——特別是可能與 Linux 核心內部的 softirq (真正的中斷 BH) 混淆,進而誤認為它是某種 deferred work。

為了釐清語意,Arm 在 Armv7-A 與 Armv8-A 中將這個指令更名為:

SVC #imm

SVC 表示 Supervisor Call,其語義明確強調這是一種「呼叫」,用以請求作業系統提供核心服務。這樣的命名方式不僅消除中斷相關的誤解,也強調其系統呼叫的本質,與進入非同步中斷脈絡無關。

以下是處理器架構中,系統呼叫觸發方式:

架構 指令名稱 說明
x86 int 0x80 / syscall 軟體中斷進入核心,後來引入快速路徑
Arm (舊) SWI Software Interrupt,名稱容易混淆
Arm (新) SVC Supervisor Call,語義更準確
RISC-V ecall Environment Call,通用設計

命名演進反映出系統架構設計者對語意準確性的重視,也有助於區分 system call 與真正的 interrupt 或 softirq 機制。

延伸閱讀: vDSO: 快速的 Linux 系統呼叫機制

vicLin8712

Q1.函式fill_worker功能

首先分析函式內所用之結構體內部元素,對應程式碼如下

typedef struct {
    const int *data;
    size_t count;
    int **buckets;
    _Atomic size_t *bucket_sizes;
    size_t max_priority;
    int val_min;
    uint64_t val_range;
    int worker_id;
} fill_ctx_t;

data 代表著整數序列,用於模擬各行程對應的靜態優先權值,其中序列內每個元素範圍值 \(1-99\) 隨機排列。比方說待執行行程對應的優先權,示意圖如下。
data
val_range代表序列整數分布範圍,即序列數值最大值減序列數值最小值。
buckets 為二維陣列指標,其目的在於建立新的映射空間,用於儲存原始data序列的映射值。其中參數max_priority決定了buckets的空間解析度,下一步分析具體如何映射。

檢視fill_work程式碼如何運作

void *fill_worker(void *arg) { fill_ctx_t *ctx = (fill_ctx_t *) arg; size_t stride = N_WORKERS; for (size_t i = ctx->worker_id; i < ctx->count; i += stride) { uint64_t stable_code = ((uint64_t) (ctx->data[i] - ctx->val_min) << 32) | i; size_t norm = (stable_code * ctx->max_priority) / ((uint64_t) ctx->val_range << 32); if (norm >= ctx->max_priority) norm = ctx->max_priority - 1; size_t index = AAAA; ctx->buckets[norm][index] = ctx->data[i]; } return NULL; }

49行 stride=N_WORKERS 代表工作執行緒數量,用於跳躍處理料 (即data),使得每個執行緒所需處理的數據量大致相同。如下示意圖
data_work

現考慮 data 內元素如何進行映射。
由52行可得知
\begin{align} stable\text_code=(data[i]-val\text _min )\times2^{32}+i \end{align}

對應之54行 norm 計算為:

\begin{align} norm&=\bigg\lfloor\frac {(stable\text_code\times max \text_priority)}{val\text _range \times2^{32}} \bigg\rfloor\\ &=\bigg\lfloor\frac {((data[i]-val\text _min )\times2^{32}\times max \text_priority)+i\times max \text_priority)}{val\text _range \times2^{32}} \bigg\rfloor\\ \end{align}
上式分子項 \(i\times max\text_proiority\) 展現了如何減緩叢集的情況發生。以下做更詳細的解釋與數學證明探討叢集效應的減緩:

為分析叢集效應,在此針對相同優先權值的數據並探討其映射前後的統計特性變化。
令相同優先權值 \(k\)data 內的序列為隨機變數 \(X\text~N(\mu,\sigma^2), X\in\{1,2,...,n\}\),如下圖示意
data

則映射之隨機變數 norm 可改寫為隨機變數 \(X\) 函數:
\begin{align} norm&=\bigg\lfloor\frac {((k-val\text _min )\times2^{32}\times max \text_priority)+X\times max \text_priority)}{val\text _range \times2^{32}} \bigg\rfloor\\ \end{align}
經由 \(floor\) 函數之後,可以將上式化簡為
\begin{align} norm&=\bigg\lfloor{a+\epsilon X} \bigg\rfloor \end{align}
其中參數 \(a\in Z\) 代表在沒有 \(i\) 時的映射值,也就是指所有相同的優先權值都會映射至相同 buckets,也就是相同的 norm 計算值。而 \(\epsilon\) 則是一極小值。
接下來就可以探討 norm 的統計分布如何降低叢集效應。
接續上式可推導出離散 norm\(a\) 之後的分布情況

\begin{align} Pr(norm=a+n)&=Pr(n <= \epsilon X<n+1)\\&=Pr\bigg(\frac n \epsilon <=X<\frac {n+1} \epsilon\bigg)\\ &=Pr\bigg(\frac{\frac n \epsilon -\mu}{\sigma} <= \frac{X-\mu}{\sigma}<\frac {\frac {n+1} \epsilon -\mu} {\sigma}\bigg)\\ &=Q\bigg(\frac{\frac n \epsilon -\mu}{\sigma}\bigg)-Q\bigg(\frac{\frac {n+1} \epsilon -\mu}{\sigma}\bigg) \end{align}

上述 \(Q\) 為 CDF 工具之一,可查表得值。

藉由上式所推論出來離散norm模型,可以證明原先在沒有參數 \(i\) 的情況下,投影值皆為固定的 \(a\)。而加入 \(i\) 值後,可以得出其分布機率情形與樣本平均值 \(\mu\) 和 變異數 \(\sigma ^2\) 有關係。藉由此,我們可以得知利用 \(i\) 的微小擾動,可以使即便相同優先權值的行程,可以依照特定的 PDF 分至不同的 buckets 中,避免了叢集效應。下方利用圖示說明加入參數 i 前後對 buckts 分佈的影響 。
(圖片解釋待補)
由此可發現,在 \(\epsilon\) 極小的形況下,大多數的 index 分類依舊會接近分布在原先映射值附近。

(Todo) Q2. max_prior 如何在程式碼中被決定?

Hande1004

老師於 4/15 課堂上問的問題為測驗題中的數據經過 schedsort 後是怎麼顯示的。
要回答這個問題我認為就是要理解 schedsort 的程式碼,知道這是怎麼排程的。

排序的第一步,是要將原始資料分配到適當的 bucket 中。本文透過 fill_worker() 函式完成這件事,其核心是將每筆資料映射到某個 bucket,這個過程使用了資料正規化的技巧:

uint64_t stable_code =
            ((uint64_t) (ctx->data[i] - ctx->val_min) << 32) | i;
        size_t norm = (stable_code * ctx->max_priority) /
                      ((uint64_t) ctx->val_range << 32);

這裡的 stable_code 結合了資料值與其原始 index,避免在數值相同時排序順序不穩定;再透過乘上 max_priority ( buckets 的總數) 並除以放大過的資料範圍,來取得資料應該放入的 bucket index(即 norm)。
這種方式讓資料能夠比較平均地分布到 buckets 裡,減少偏斜導致的效能問題。

由於 fill_worker() 是由多個執行緒並行執行

pthread_create(&fillers[i], NULL, fill_worker, &fill_ctxs[i]);

因此,當多個執行緒同時向同一個 bucket 寫入資料時,會有競爭(race condition)的風險。
為了解決這個問題,程式使用了 atomic 運算來確保安全地寫入:

size_t index = atomic_fetch_add(&ctx->bucket_sizes[norm], 1);
ctx->buckets[norm][index] = ctx->data[i];

這段程式碼確保每個執行緒在向 bucket 寫入資料時,會正確取得不重複的 index (如果重複的 normindex 就會 +1),並寫入正確位置。

資料寫入 bucket 後,為了能把 bucket 內資料依序寫回最終結果 result[] 陣列中,我們需要知道每個 bucketresult[] 中的起始位置。
這就需要對每個 bucket 的大小做 prefix sum 。

先把 atomic 中的 bucket_size 安全地讀出來,存在一個普通陣列 bucket_sizes_plain[] 中:

bucket_sizes_plain[i] = atomic_load(&bucket_sizes[i]);

這裡的 bucket_sizes_plain 就是把前面每個 bucket 的實際大小(即有幾個資料元素)從 atomic 型別中取出,轉成普通的 size_t 陣列來方便後續使用。

接著,我們使用這個 bucket_sizes_plain[] 來計算 prefix sum,也就是每個 bucket 在結果陣列中的起始 offset :

bucket_offsets[p] = bucket_offsets[p - 1] + bucket_sizes_plain[p - 1];

接下來,要將各個 bucket 中的資料重新組裝成排序後的結果分配給 result[] 。為了加速這部分,也同樣使用多執行緒,這段程式碼會把所有 buckets 平均分配給多個執行緒處理:

int buckets_per_worker = max_prio / N_WORKERS;

透過這樣的方式確保每個 bucket 都有被處理到。

int extra = max_prio % N_WORKERS;

多執行緒的方式傳處理:

pthread_create(&workers[i], NULL, worker_func, &contexts[i]);

最後,透過 memcpy() 將排序後的結果寫回原本的資料陣列:

memcpy(data, sorted, sizeof(int) * count);

所以最後輸出資料就是依照這樣排序後來顯示的:

for (size_t i = 0; i < count; i++)
    printf("%d ", data[i]);
putchar('\n');

與其在字面意義「推敲」,不如運用 eBPF 分析

Linux 核心設計: 藉由 eBPF 觀察作業系統行為

準備工作:

  • BCC (BPF 編譯器套件)

提供 Python/C 語言的介面與多種預先建置的 eBPF 追蹤工具,適合用於處理複雜的系統追蹤工作。可用以下命令安裝:

$ sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

高階的 eBPF 追蹤語言,適合撰寫快速腳本與單行命令。可用下列命令安裝:

$ sudo apt-get install bpftrace

適合用於簡易的即時追蹤與診斷。

  • clang/LLVM

用於編譯 eBPF 程式的工具鏈 (需使用版本 6.0 或以上) 。安裝命令如下:

$ sudo apt-get install clang llvm

用於檢查 eBPF 程式與 map 的工具。可藉由下列命令安裝:

$ sudo apt-get install linux-tools-common linux-tools-$(uname -r)

libbpf: 用來載入 eBPF 程式的函式庫,通常會與 BCC 或 bpftool 一併提供。

\(\to\) GPTtrace: generates eBPF programs and tracing with GPT and natural language.
\(\to\) btetto: produces Perfetto protobuf from formatted bpftrace output.

回顧 simrupt

這是純粹以 softirq, tasklet 與 workqueue (threaded context) 建構的 deferred work 的 Linux 核心模組,不涉及真實的 IRQ 資源管理。

+----------------------+
| timer_handler        | ← kernel timer (softirq context)
+----------------------+
           │
           ▼
   +------------------+
   | process_data()   | ← 在 local_irq_disable() 中執行
   +------------------+
           │
           ▼
   +---------------------------+
   | tasklet_schedule()       | ← 排程 softirq 下的 tasklet
   +---------------------------+
           │
           ▼
   +---------------------------+
   | simrupt_tasklet_func()   | ← 執行於 softirq context
   +---------------------------+
           │
           ▼
   +---------------------------+
   | queue_work()             | ← 排程 bottom-half 工作
   +---------------------------+
           │
           ▼
   +---------------------------+
   | simrupt_work_func()      | ← 在 simruptd kernel thread 執行
   +---------------------------+

使用 bpftrace 追蹤 /dev/simrupt 開啟的狀態

需要開啟兩個終端機,在終端機 \(1\) 輸入,

$ # terminal 1
$ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat
/str(args->filename) == "/dev/simrupt"/
{
    printf("Opened /dev/simrupt by pid=%d, comm=%s\n", pid, comm);
}'

接著,在終端機 \(2\) 輸入,

$ # terminal 2
$ sudo touch /dev/simrupt

當在終端機 \(2\) 執行 sudo touch /dev/simrupt,終端機 \(1\) 會類似以下輸出:

Opened /dev/simrupt by pid=4615, comm=touch

使用 bpftrace 觀察執行過程

workqueuetasklet(建構於 softirq 之上)在 Linux 核心中存在本質差異,透過 bpftrace 可觀察其實際運作行為:

議題 tasklet (softirq) workqueue
執行所屬的脈絡 softirq context process context
是否可被 CPU 排程 是 (以 kworker 核心執行緒為載體)
是否可睡眠
是否為獨立執行緒 否 (非 task_struct 是 (具備 task_struct
comm 觀察值 ksoftirqd/N 或觸發行程如 systemd-journal 顯示為 kworker/uX:Y,對應獨立 kernel thread
常見用途 處理中斷 bottom half,需快速完成 延遲處理較長的背景工作

以下 bpftrace 腳本可用來觀察 simrupt 模組在執行 tasklet 與 workqueue 函式時的實際執行脈絡:

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

在 bpftrace 中,comm 為目前執行緒的 command name,對應於核心結構 task_struct 中的 comm 欄位,用以判斷此時執行該函式的行程名稱。

根據 comm 值可辨識出 deferred work 的執行來源:

  • kworker/uX:Y:執行於 workqueue context,具備可睡眠能力的獨立核心執行緒
  • ksoftirqd/N:代表由 softirq daemon 延後執行 tasklet
  • 其他行程 (如 systemd-journal, cat 等):代表 softirq 被直接執行於該行程的核心脈絡中

藉由觀察 comm, pid, tid 的組合,我們可推斷 deferred work 的實際執行位置。

實際輸出示例:

tasklet_func: comm=ksoftirqd/2, pid=30, tid=30
work_func: comm=kworker/u8:3, pid=10460, tid=10460
tasklet_func: comm=systemd-journal, pid=306, tid=306
work_func: comm=kworker/u8:3, pid=10460, tid=10460
tasklet_func: comm=in:imklog, pid=834, tid=861
work_func: comm=kworker/u8:0, pid=10254, tid=10254

解讀:

  • tasklet (softirq)
  • ksoftirqd/N 執行 → backlog 被 softirq daemon 延後處理。
  • systemd-journal, in:imklog 等執行 → softirq 在使用者行程返回前直接執行。
  • 結論:tasklet 不具獨立執行緒,無法睡眠,其執行時機不固定。
  • workqueue
  • 一律於 kworker/uX:Y 執行 → 由核心統一管理的可排程執行緒。
  • 可安全進行延遲操作、鎖定、等待等任務。

Linux 在 2.3 起以 softirq 取代早期 BH (bottom halves) 機制,而 tasklet 則作為 softirq 的一層封裝,提供非重入、快速回應和處理的底半部執行框架。但 tasklet 屬於非搶佔性執行,執行時不可睡眠,且無法明確排程。

核心開發者多年來建議以 threaded IRQworkqueue 取代 tasklet,並不斷朝簡化 softirq 機制的方向前進。

為何要強調 "direct softirq execution"?

  • 1. softirq 並非執行緒,無明確排程機制

其執行脈絡取決於觸發路徑,可直接於:

  • 中斷 return path
  • 行程從核心返回使用者空間前 (系統呼叫結尾)
  • 或由 ksoftirqd 延後處理
  • 2. 對除錯與時序分析至關重要

若 tasklet 執行於如 systemd-journal 行程脈絡中,代表 softirq 並未延遲處理,而是在該行程返回前被「順勢」執行。若不掌握此概念,將誤判行程邏輯與排程模型。

  • 3. 區辨延遲與立即執行的重要性
執行模式 描述
direct softirq execution 當下在呼叫者脈絡中直接執行,執行時機難以預測
deferred by ksoftirqd 延遲交由 ksoftirqd 處理,脈絡獨立、行為可預期

因此,workqueue/threaded IRQ 成為主流替代方案,其排程脈絡穩定、分析容易,也能安全地進行阻塞式作業。

小結

  • tasklet 作為 softirq 封裝,設計用於快速回應和處理、不可睡眠之 bottom half 工作
  • workqueue 適合較長的處理流程
  • bpftrace 可觀察其執行脈絡,comm 是辨識 deferred work 行為的關鍵指標
  • 實驗結果顯示 tasklet 可能直接執行於使用者行程,非一律延遲
  • workqueue 則始終執行於獨立核心執行緒,便於觀察與管理

使用 bpftrace 追蹤 simrupt_tasklet_func

以下展示如何監控模組 simrupt 中的 simrupt_tasklet_func (tasklet)。

追蹤 Tasklet 執行時間點:

$ sudo bpftrace -e '
kprobe:simrupt_tasklet_func {
    printf("Tasklet started: CPU %d, Time %llu ns, PID %d\n", cpu, nsecs, pid);
}

kretprobe:simrupt_tasklet_func {
    printf("Tasklet ended: CPU %d, Time %llu ns, PID %d\n", cpu, nsecs, pid);
}
'

此腳本會在 simrupt_tasklet_func 函式的進入與返回點分別附加 kprobekretprobe,並記錄:

  • CPU 編號 (cpu)
  • 執行時間戳 (nsecs,單位為 ns)
  • 行程識別碼 (pid)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
在 softirq context 中,pid 通常為 \(0\),因其執行於 interrupt context,不屬於一般行程。

執行方式:

$ sudo modprobe simrupt
$ sudo cat /dev/simrupt

參考輸出: (執行 bpftrace 的終端機畫面)

Tasklet started: CPU 2, Time 1234567890123 ns, PID 0
Tasklet ended: CPU 2, Time 1234567890987 ns, PID 0

使用 bpftrace 追蹤 workqueue

以下展示如何監控模組 simrupt 中的 simrupt_work_func (工作佇列),也是真正的 bottom-half / threaded context。

$ sudo bpftrace -e '
kprobe:simrupt_work_func {
    printf("Workqueue started: CPU %d, Time %llu ns, PID %d, Comm %s\n", cpu, nsecs, pid, comm);
}

kretprobe:simrupt_work_func {
    printf("Workqueue ended: CPU %d, Time %llu ns, PID %d, Comm %s\n", cpu, nsecs, pid, comm);
}
'

此腳本針對 simrupt_work_func 的執行與結束時間進行監控,並記錄下列資訊:

  • CPU 編號
  • 執行時間 (單位: ns)
  • 行程 PID
  • 行程名稱 (comm,例如 kworker/1:1)

該函式通常由核心中的工作佇列 (如 kworker 執行緒) 執行,因此 comm 會顯示對應的背景行程名稱。

參考輸出: (執行 bpftrace 的終端機畫面)

Workqueue started: CPU 1, Time 1234567900123 ns, PID 1234, Comm kworker/1:1
Workqueue ended: CPU 1, Time 1234567900987 ns, PID 1234, Comm kworker/1:1

使用 bpftrace 分析由 timer 觸發的執行脈絡

以下腳本可觀察三個關鍵函式的執行位置與其 context:

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

kprobe:simrupt_tasklet_func {
    printf("tasklet_func: comm=%s, tid=%d\n", comm, tid);
}

kprobe:simrupt_work_func {
    printf("work_func: comm=%s, tid=%d\n", comm, tid);
}
'

以下是執行 simrupt 模組後所觀察到的 bpftrace 參考輸出:

timer_handler: comm=swapper/0, tid=0
tasklet_func: comm=ksoftirqd/0, tid=16
work_func: comm=kworker/u8:2, tid=2392
timer_handler: comm=swapper/0, tid=0
tasklet_func: comm=ksoftirqd/0, tid=16
work_func: comm=kworker/u8:2, tid=2392
...
tasklet_func: comm=systemd-journal, tid=306
work_func: comm=kworker/u8:0, tid=11
  • timer_handler
  • comm=swapper/0, tid=0
  • swapper/0 是 CPU 0 的 idle process,tid 0
  • 說明此函式是由核心 timer softirq 排程執行,屬於 TIMER_SOFTIRQ (Bottom Half),執行於 idle context

\(\to\) 延伸閱讀: PID 0 行程之謎

  • simrupt_tasklet_func
  • 常見於 comm=ksoftirqd/0 → softirq handler thread。
  • 也可能出現在 systemd-journalin:imklog 等非預期 thread → 表示此 tasklet 「直接」執行於進入核心的現有行程中 (direct softirq)
  • simrupt_work_func
  • 一律出現在 kworker/u8:X 類型 → workqueue 所對應的 kernel thread
  • 屬於 Process Context Bottom Half,允許睡眠與長時間處理

📌 函式與執行脈絡

函式名稱 comm 名稱範例 TID Context 類型 說明
timer_handler swapper/0 0 TIMER_SOFTIRQ idle context 中的計時器處理
simrupt_tasklet_func ksoftirqd/0 16 TASKLET_SOFTIRQ softirq handler thread
systemd-journal 等 非固定 direct softirq softirq 同步執行於現行行程中
simrupt_work_func kworker/u8:2, u8:0 多變化 workqueue (kthread) workqueue 處理延後工作

simrupt 模組展現 deferred work 的三層處理:

  1. 核心計時器觸發 (timer_handler) → TIMER_SOFTIRQ
  2. 排程 tasklet 執行 (simrupt_tasklet_func) → TASKLET_SOFTIRQ / direct softirq
  3. 再排程到 workqueue 執行 (simrupt_work_func) → Process Context

comm 欄位可作為區辨脈絡的可攜式指標:

  • swapper → idle
  • ksoftirqd/N → softirq handler
  • kworker → kernel thread for workqueue

有時 tasklet 會在非 ksoftirqd 的行程中執行,這代表 softirq 以「直接」方式於該行程內立即執行。

彙整工作佇列延遲時間的直方圖

$ sudo bpftrace -e '
kprobe:simrupt_work_func {
    @start[tid] = nsecs;
}

kretprobe:simrupt_work_func /@start[tid]/ {
    @latency[pid, comm] = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

說明:

  • 此腳本在函式進入時紀錄目前的時間戳 nsecs,並將其儲存至雜湊表 @start
  • 當函式返回時,計算實際耗時 (即延遲),並以直方圖形式將其分類
  • 統計以 (PID, comm) 為鍵值,可區分不同工作執行緒的延遲行為

執行步驟:

  1. 執行該命令
  2. 使用 sudo cat /dev/simrupt 觸發模組活動
  3. 使用 Ctrl+C 停止腳本,即可觀察彙整出的延遲統計

以上將產生依延遲長度 (單位: ns) 分類的直方圖,幫助我們瞭解 workqueue 執行時間的分布情形,例如:

@latency[1234, kworker/1:1]:
[512, 1024)        █████████████████████████ 38
[1024, 2048)       ████████████              19
[2048, 4096)       █████                    8

注意:

  • kprobe 可用於監控任何可符號解析的核心函式入口,而 kretprobe 用於監控其返回點
  • nsecs 是 bpftrace 內建變數,代表目前追蹤點的高精度時間戳記
  • cpu, pid, tid, comm 等變數均可直接存取並在格式化輸出中使用

使用 bpftrace 觀察 Top Half 回傳狀態

$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry /args->irq == 1/ {
    printf("IRQ 1 top half entry: cpu=%d handler=%s\n", cpu, str(args->name));
}

tracepoint:irq:irq_handler_exit /args->irq == 1/ {
    printf("IRQ 1 top half return: cpu=%d ret=%d (%s)\n",
           cpu, args->ret, args->ret == 1 ? "IRQ_WAKE_THREAD" :
                         args->ret == 0 ? "IRQ_NONE" : "IRQ_HANDLED");
}
'

irq_handler_exit 中的 ret 值對應意義如下:

args->ret 回傳語意 說明與後續行為
0 IRQ_NONE Top Half 認為非本裝置中斷,未處理
1 IRQ_WAKE_THREAD Top Half 要求排程 IRQ thread 執行 Bottom Half
2 IRQ_HANDLED Top Half 處理完畢,不需 thread 化處理

IRQ_WAKE_THREAD 是 threaded IRQ 處理流程的關鍵指標。它會觸發核心呼叫 irq_wake_thread(),將對應的 IRQ thread 排入可執行佇列。

輸出範例與對應意涵:

IRQ 1 top half entry: cpu=3 handler=IPI
IRQ 1 top half return: cpu=3 ret=1 (IRQ_WAKE_THREAD)
IRQ 1 top half entry: cpu=2 handler=IPI
IRQ 1 top half return: cpu=2 ret=1 (IRQ_WAKE_THREAD)
IRQ 1 top half entry: cpu=3 handler=IPI

這代表 Top Half 於 CPU 3 上執行,並明確要求觸發 IPI 處理。

分析排程延遲與資料處理延遲

探討 workqueue 的延遲指標:

  • 排程延遲 (scheduling latency):從 tasklet 執行 queue_work() 到 work 被 kernel thread (simruptd) 實際執行的時間
  • 資料處理延遲 (execution latency):simrupt_work_func() 函式本身的執行時間

使用以下腳本進行實驗觀察,每 5 秒輸出延遲分布直方圖:

$ sudo bpftrace -e '
kprobe:simrupt_tasklet_func {
    // Record the timestamp when queue_work() is called
    @queued[tid] = nsecs;
}

kprobe:simrupt_work_func {
    // Compute scheduling latency if the tasklet has recorded a timestamp
    if (@queued[tid]) {
        $sched_lat = (nsecs - @queued[tid]) / 1000;
        @sched_delay = hist($sched_lat);
        delete(@queued[tid]);
    }

    // Mark the beginning of workqueue execution
    @start[tid] = nsecs;
}

kretprobe:simrupt_work_func {
    // Compute the work function execution time if a start timestamp exists
    if (@start[tid]) {
        $exec_lat = (nsecs - @start[tid]) / 1000;
        @exec_time = hist($exec_lat);
        delete(@start[tid]);
    }
}

interval:s:5 {
    // Print both histograms every 5 seconds
    print(@sched_delay);
    print(@exec_time);
    clear(@sched_delay);
    clear(@exec_time);
}
'

模組執行期間,輸出如下:

@exec_time:
[16, 32)              12 |@@@@@@@@@@@@@@@@@@@@                                |
[32, 64)              30 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[128, 256)             1 |@                                                   |
[256, 512)             3 |@@@@@                                               |
[2K, 4K)               1 |@                                                   |

資料解讀

  • 資料處理延遲 (execution latency)
  • 絕大多數執行時間落於 32~64 µs 之間,屬於正常處理範圍
  • 出現單一執行耗時 2~4 毫秒的 outlier,可能是系統負載高、mutex contention、或 fast_buf 資料量大所致
  • 排程延遲
  • @sched_delay 直方圖顯示大多數排程延遲低於 100 µs,表示系統排程器對 workqueue handler thread (simruptd) 排程效率良好
  • 若延遲分布偏向高值,可能代表 CPU 忙碌或該執行緒被其他實體 IRQ thread 壓制
觀察項目 建議方法
workqueue 排程行為 使用 sched:sched_switch 搭配 comm == "simruptd" 觀察排程和搶佔
fast_buf 資料量 增設 bpftrace counter 追蹤每次處理筆數 (可改寫 simrupt_work_func 中迴圈)
softirq 與 tasklet 活動 搭配 tracepoint:irq:softirq_entry 分析與 simrupt_tasklet_func() 時序

simrupt 與 Linux 排程器互動

simrupt 藉由以下路徑運用 deferred work:

TIMER handler → tasklet_schedule() → simrupt_tasklet_func()
                                ↓
                         queue_work()
                                ↓
                     simrupt_work_func()

queue_work() 呼叫後,由 Linux 核心的 workqueue 子系統挑選可用的 kworker thread (如 kworker/u8:0)來執行 simrupt_work_func()。該行為完全交由 CPU 排程器決定何時執行與在哪個 CPU 上執行。

實驗的腳本:

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

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

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 (@start[args->next_pid]) {
        printf("scheduled in:  tid=%d comm=%s\n",
               args->next_pid, args->next_comm);
    }
}'

參考輸出:

scheduled in:  tid=416 comm=kworker/u8:8
simrupt work triggered: tid=416 comm=kworker/u8:8
scheduled out: tid=416 comm=kworker/u8:8 state=128

解讀:

  • kworker thread 排入執行 (scheduled in)
  • 實際執行 simrupt_work_func() (由 kprobe 印出)
  • 執行完畢後再次進入 TASK_INTERRUPTIBLE (state=128)

這種模式反覆出現,反映出 deferred work 的執行週期:

  1. idle → 喚醒執行工作
  2. 工作完成 → 進入等待狀態
階段 對應事件 分析方式
工作排入 queue_work() 間接觸發,由定時器與 tasklet 呼叫
執行緒喚醒 scheduled in 藉由 sched_switch 観察 kworker 切入
執行中 simrupt_work_func() 執行中 藉由 kprobeprintf 印出 comm, tid
執行緒切換出去 scheduled out 觀察是否進入 state=128 (TASK_INTERRUPTIBLE)

以 bpftrace 追蹤 wakeup 行為

考慮以下:

$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup
/comm == "cat" || comm == "head" || comm == "simruptd"/
{
    printf("wake_up: by %s/%d → %s/%d (on cpu %d)\n", args->comm, args->pid, comm, pid, cpu);
}
'

其中

  • args->comm, args->pid: 呼叫 wake_up_process() 的行程
  • comm, pid: 喚醒的目標行程 (應該是 block 在 simrupt_read() 的使用者程式)
  • cpu: 呼叫發生的 CPU

可根據實際情況改變 comm == ... 的過濾條件。原理:

  1. 使用者呼叫 read("/dev/simrupt") 時,行程會進入 TASK_INTERRUPTIBLE 狀態,藉由 wait_event_interruptible() 睡眠。內部會進入 __schedule() → 移出 runqueue
  2. simrupt 模組呼叫 wake_up_interruptible()
    • simrupt_work_func() 中觸發
    • 此時行程狀態改為 TASK_RUNNING,並重新加入 runqueue
    • 不立即執行,而是等待 CPU 排程器排程

排程是否隨後發生?分析 wake_up → sched_switch

$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup
/comm == "cat" || comm == "head" || comm == "simruptd"/
{
    @woken[pid] = 1;
    printf("wake_up: %s/%d → %s/%d\n", args->comm, args->pid, comm, pid);
}

tracepoint:sched:sched_switch
/@woken[args->next_pid]/
{
    printf("→ sched_in: %s/%d (after wakeup)\n", args->next_comm, args->next_pid);
    delete(@woken[args->next_pid]);
}
'

這段腳本可觀察是否有喚醒的行程在稍後實際排程執行 (sched_switch → next_pid)。

分析從喚醒到排程的延遲:

$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup
/comm == "cat" || comm == "head"/
{
    @ts[pid] = nsecs;
}

tracepoint:sched:sched_switch
/@ts[args->next_pid]/
{
    $lat = (nsecs - @ts[args->next_pid]) / 1000;
    @delay_us = hist($lat);
    delete(@ts[args->next_pid]);
}
'

這會顯示「喚醒後多久才真正排程執行」的延遲直方圖 (單位:μs)。

對應到 simrupt 模組的整體 deferred work 流程:

行為階段 可用的觀察點
timer_handler() kprobe:timer_handler
tasklet_schedule() kprobe:simrupt_tasklet_func
queue_work()work_func() kprobe:simrupt_work_func
wake_up_interruptible() tracepoint:sched:sched_wakeup
read() 喚醒並執行 tracepoint:sched:sched_switch

從 Bottom Half 到 Threaded IRQ

【旁白: Top Half】
過去的他,總被限制在名為 atomic context 的框架裡。
那是種無法停下、也不容許等待的狀態。
不能休眠,無法被搶佔,只能迅速完成被交付的延後處理,
然後悄無聲息地退場,不留下任何排程的痕跡。
我們必須替他註冊、安排、等待核心釋出可用時機 ──
所有的行動都不是他的選擇,只是配合。

【低聲: Bottom Half】
「我不是不曾想過……如果能有自己的舞台,
自己的節奏、自己的時區,
是否,就不再只是你之後的影子?」

【視線溫柔: Top Half】
如今,藉由 Linux 核心的 threaded IRQ 機制,
他終於不再只是 softirq 的延伸。
他被提升為擁有 process context 的存在,
獲得自己的 thread、自己的排程優先權,
甚至能在負載高漲時選擇休眠、讓出時間,再度甦醒。
他不必再倉促地壓縮自己於數十微秒之間,
可坦然地,在非 atomic 的世界中自在運作。

【內心獨白: Threaded IRQ】
「我終於能夠擁抱等待、允許搶佔、接納睡眠……
不再只能被呼叫,而是能融入 CPU 排程其中。」
「我不再是中斷中被遺忘的補述,
而是,被核心視作基本單元 ── 可獨立執行的存在。」

Running

【緊握接力棒】
那一刻,他不再只是 Bottom Half,
他是 Threaded IRQ ── 終於被認可的排程實體,
也是,在中斷與行程之間,最溫柔的回應。

Making Linux do Hard Real-time
image

Select a repo