# 2025q1 Homework3 (kxo)
contributed by < `LinMarc1210` >
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
## 開發環境
```shell
$ 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
```
## 研讀作業說明的筆記
> [筆記](https://hackmd.io/kN6LFVOcQaKj0YFpOdUm9w)
## Simrupt
先編譯並產生 `simrupt.ko` 檔案並使用以下命令掛載 simrupt 模組,並輸出 `/dev/simrupt` 的內容,可以看到每秒會輸出一個字元。
```shell
$ make
```
```shell
$ sudo insmod simrupt
```
```shell
$ 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,`N` 為 `tid`
- 其他行程 (如 `systemd-journal`, `cat` 等):代表 softirq 被直接執行於該行程的核心脈絡中
#### 追蹤 /dev/simrupt 開啟的狀態
使用以下命令追蹤:
```shell
$ 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/simrupt` ,`simrupt` 模組在執行 tasklet 與 workqueue 函式時的實際執行脈絡:
```shell
$ 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` 延遲處理。
:::info
在 softirq context 中,pid 通常為 0,因其執行於 interrupt context,不屬於一般行程。
:::
#### 使用 bpftrace 分別追蹤 tasklet 和 workqueue
追蹤 `simrupt_tasklet_func` (tasklet) 或 `simrupt_work_func` (workqueue) 都可以在要監控的函式進入點與返回點分別加入 `kprobe` 和 `kretprobe`,如以下命令:
- 追蹤 tasklet
```shell
$ 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)
```shell
$ 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);
}
'
```
用 `kprobe` 加 `kretprobe` 的組合可以得到 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_func` ,`simrupt_tasklet_func` 再呼叫 `simrupt_work_func` ,我們用 kprobe 來觀察他們的關係,就算將 `kprobe` 追蹤函式的順序調換,也不影響輸出的順序。
```shell
$ 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);
}
'
```
```shell
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 問答簡記](https://hackmd.io/V7bD2XjnQuCZqDMt4ZizGA?view#%E4%BD%BF%E7%94%A8-bpftrace-%E5%88%86%E6%9E%90%E7%94%B1-timer-%E8%A7%B8%E7%99%BC%E7%9A%84%E5%9F%B7%E8%A1%8C%E8%84%88%E7%B5%A1) ,得知 `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 以「直接」方式於該行程內立即執行。
#### 彙整工作佇列延遲時間的直方圖
```shell
$ 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 回傳狀態
```shell
$ 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");
}
'
```
:::danger
TODO: 藉由 eBPF 觀察 workqueue 排入佇列後,如何由 CPU 排程?當系統存在 real-time (或 deadline) class 的任務時,是否影響 workqueue 的 work 排程呢?
:::