Try   HackMD

2025q1 Homework3 (kxo)

contributed by < LinMarc1210 >

作業書寫規範:

  • 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
  • 文字訊息 (尤其是程式執行結果) 請避免用圖片來表示,否則不好搜尋和分類
  • 共筆書寫請考慮到日後協作,避免過多的個人色彩,用詞儘量中性
  • 不要在筆記內加入 [TOC] : 筆記左上方已有 Table of Contents (TOC) 功能,不需要畫蛇添足
  • 不要變更預設的 CSS 也不要加入任何佈景主題: 這是「開發紀錄」,用於評分和接受同儕的檢閱
  • 在筆記中貼入程式碼時,避免非必要的行號,也就是該手動將 c=cpp= 變更為 ccpp。行號只在後續討論明確需要行號時,才要出現,否則維持精簡的展現。可留意「你所不知道的 C 語言: linked list 和非連續記憶體」裡頭程式碼展現的方式
  • HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」
  • 留意科技詞彙的使用,請參見「資訊科技詞彙翻譯」及「詞彙對照表
  • 不要濫用 :::info, :::success, :::warning 等標示,儘量用清晰的文字書寫。:::danger 則僅限授課教師作為批注使用
  • 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景
  • 在中文敘述中,使用全形標點符號,例如該用「,」,而非 ","。注意書名號的使用,即 ,非「小於」和「大於」符號
  • 避免使用不必要的 emoji 字元

開發環境

$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ lscpu
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          39 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   16
  On-line CPU(s) list:    0-15
Vendor ID:                GenuineIntel
  Model name:             12th Gen Intel(R) Core(TM) i7-1260P
    CPU family:           6
    Model:                154
    Thread(s) per core:   2
    Core(s) per socket:   12
    Socket(s):            1
    Stepping:             3
    CPU(s) scaling MHz:   21%
    CPU max MHz:          4700.0000
    CPU min MHz:          400.0000
    BogoMIPS:             4992.00

研讀作業說明的筆記

筆記

Simrupt

先編譯並產生 simrupt.ko 檔案並使用以下命令掛載 simrupt 模組,並輸出 /dev/simrupt 的內容,可以看到每秒會輸出一個字元。

$ make
$ sudo insmod simrupt
$ sudo cat /dev/simrupt

使用 eBPF 追蹤 simrupt 的行為

bpftrace 用來追蹤的命令:sudo bpftrace [OPTIONS]

OPTIONS:
    -e 'program'   execute this program
    -l [search]    list probes

先搞懂幾個 bpftrace 的行為:

  • tracepoint: kernel static tracing
  • kprobe: kernel dynamic tracing,附加於要追蹤的函式的進入點
  • kretprobe: 附加於要追蹤的函式的返回點
  • comm: 目前執行緒的 command name,根據 comm 值可區分 deffered work 的執行來源:
    • kworker/uX:Y:執行於 workqueue context,具備可睡眠能力的獨立核心執行緒 (kernel thread for workqueue)
    • ksoftirqd/N:代表由 softirq daemon 延後執行 tasklet
    • swapper/N: idle,Ntid
    • 其他行程 (如 systemd-journal, cat 等):代表 softirq 被直接執行於該行程的核心脈絡中

追蹤 /dev/simrupt 開啟的狀態

使用以下命令追蹤:

$ 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);
}'

其概念可想成 tracepoint 設在 /dev/simrupt 的位置,並且當我們另外開啟新的視窗並輸入 sudo touch /dev/simrupt 的時候,可以在原本的視窗看到如以下輸出:

Opened /dev/simrupt by pid=9028, comm=touch
Opened /dev/simrupt by pid=9031, comm=touch
Opened /dev/simrupt by pid=9034, comm=touch

以上是重複執行三次 touch 的輸出,三次的 pid 不同但 comm 相同是因為三次都用 touch,所以 command name 都是 touch,但每次執行時會被當作不同的行程,因此有不同的 pid

觀察 simrupt 執行過程

使用以下命令觀察在我們輸入命令sudo cat /dev/simruptsimrupt 模組在執行 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);
}'

會得到類似以下的輸出(simrupt 每輸出一個字元就有一組):

work_func: comm=kworker/u64:3, pid=9329, tid=9329
tasklet_func: comm=ksoftirqd/9, pid=50, tid=50
work_func: comm=kworker/u64:2, pid=8536, tid=8536
tasklet_func: comm=swapper/9, pid=0, tid=0

可觀察到 workqueue 都是執行於獨立核心執行序,由核心統一管理並且可排程。tasklet 則可能直接執行於使用者行程,也有可能由 ksoftirqd 延遲處理。

在 softirq context 中,pid 通常為 0,因其執行於 interrupt context,不屬於一般行程。

使用 bpftrace 分別追蹤 tasklet 和 workqueue

追蹤 simrupt_tasklet_func (tasklet) 或 simrupt_work_func (workqueue) 都可以在要監控的函式進入點與返回點分別加入 kprobekretprobe,如以下命令:

  • 追蹤 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);
    ​​​​}
    ​​​​'
    
  • 追蹤 workqueue (真正的 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);
    ​​​​}
    ​​​​'
    

kprobekretprobe 的組合可以得到 CPU 編號、執行開始與結束時間 (單位: ns)、行程 PID、行程名稱 (comm,例如 kworker/1:1) 等資訊。一樣執行 sudo cat /dev/simrupt 以在模組執行時監控函式:

  • tasklet:
    ​​​​Tasklet started: CPU 12, Time 7810371240793 ns, PID 0
    ​​​​Tasklet ended: CPU 12, Time 7810371285112 ns, PID 0
    ​​​​Tasklet started: CPU 0, Time 7810475261760 ns, PID 0
    ​​​​Tasklet ended: CPU 0, Time 7810475310086 ns, PID 0
    
  • workqueue:
    ​​​​Workqueue started: CPU 12, Time 7729771508263 ns, PID 7848, Comm kworker/u64:1
    ​​​​Workqueue ended: CPU 12, Time 7729771549360 ns, PID 7848, Comm kworker/u64:1
    ​​​​Workqueue started: CPU 15, Time 7729875156804 ns, PID 9583, Comm kworker/u64:5
    ​​​​Workqueue ended: CPU 15, Time 7729875190097 ns, PID 9583, Comm kworker/u64:5
    

分析由 timer 觸發的執行脈絡

在 simrupt 模組中函式呼叫的關係是 timer_handler 呼叫 simrupt_tasklet_funcsimrupt_tasklet_func 再呼叫 simrupt_work_func ,我們用 kprobe 來觀察他們的關係,就算將 kprobe 追蹤函式的順序調換,也不影響輸出的順序。

$ sudo bpftrace -e '
kprobe:simrupt_work_func {
    printf("work_func: comm=%s, tid=%d\n", comm, tid);
}
kprobe:simrupt_tasklet_func {
    printf("tasklet_func: comm=%s, tid=%d\n", comm, tid);
}
kprobe:timer_handler {
    printf("timer_handler: comm=%s, tid=%d\n", comm, tid);
}
'
timer_handler: comm=swapper/0, tid=0
tasklet_func: comm=swapper/0, tid=0
work_func: comm=kworker/u64:3, tid=10027
timer_handler: comm=chrome, tid=4062
tasklet_func: comm=chrome, tid=4062
work_func: comm=kworker/u64:5, tid=9583

接下來參考 2025-04-15 問答簡記 ,得知 simrupt 展現 deffered work 的三層處理:

函式名稱 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 處理延後工作
  1. 核心計時器觸發 (timer_handler) → TIMER_SOFTIRQ
  2. 排程 tasklet 執行 (simrupt_tasklet_func) → TASKLET_SOFTIRQ / direct softirq
  3. 再排程到 workqueue 執行 (simrupt_work_func) → Process Context

有時 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]);
}
'

與前面追蹤 simrupt_work_func 相同,只是這次將時間戳儲存至雜湊表 @start 中,函式返回就會用 (pid, comm) 區分不同工作執行緒的延遲行為。

執行完 sudo cat /dev/simrupt 並在另一個執行 bpftrace 的視窗按下 CTRL+C 就可看到直方圖:

@latency[5408, kworker/u64:22]: 
[8K, 16K)              2 |@@@@                                                |
[16K, 32K)            25 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[32K, 64K)             6 |@@@@@@@@@@@@                                        |
[64K, 128K)            1 |@@                                                  |

@latency[6135, kworker/u64:56]: 
[8K, 16K)              2 |@@@@                                                |
[16K, 32K)            24 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[32K, 64K)             9 |@@@@@@@@@@@@@@@@@@@                                 |

觀察 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");
}
'

TODO: 藉由 eBPF 觀察 workqueue 排入佇列後,如何由 CPU 排程?當系統存在 real-time (或 deadline) class 的任務時,是否影響 workqueue 的 work 排程呢?