事件追蹤

作者:

Theodore Ts’o

更新者:

李澤帆 (Li Zefan) 和 Tom Zanussi

1. 簡介

追蹤點(參見使用 Linux 核心追蹤點)無需建立自定義核心模組即可透過事件追蹤基礎設施註冊探測函式。

並非所有追蹤點都可以使用事件追蹤系統進行追蹤;核心開發者必須提供程式碼片段,定義追蹤資訊如何儲存到追蹤緩衝區,以及追蹤資訊應如何列印。

2. 使用事件追蹤

2.1 透過“set_event”介面

可用於追蹤的事件可以在檔案 /sys/kernel/tracing/available_events 中找到。

要啟用某個特定事件,例如“sched_wakeup”,只需將其回顯到 /sys/kernel/tracing/set_event。例如:

# echo sched_wakeup >> /sys/kernel/tracing/set_event

注意

需要使用“>>”,否則它會首先停用所有事件。

要停用某個事件,將事件名稱前加上感嘆號回顯到 set_event 檔案:

# echo '!sched_wakeup' >> /sys/kernel/tracing/set_event

要停用所有事件,將空行回顯到 set_event 檔案:

# echo > /sys/kernel/tracing/set_event

要啟用所有事件,將 *:**: 回顯到 set_event 檔案:

# echo *:* > /sys/kernel/tracing/set_event

事件被組織成子系統,例如 ext4、irq、sched 等,完整的事件名稱格式為:<subsystem>:<event>。子系統名稱是可選的,但它會顯示在 available_events 檔案中。子系統中的所有事件可以透過 <subsystem>:* 語法指定;例如,要啟用所有 irq 事件,可以使用以下命令:

# echo 'irq:*' > /sys/kernel/tracing/set_event

set_event 檔案也可以用來只啟用與特定模組相關的事件:

# echo ':mod:<module>' > /sys/kernel/tracing/set_event

這將啟用模組 <module> 中的所有事件。如果模組尚未載入,該字串將被儲存,當匹配 <module> 的模組載入時,事件啟用操作就會生效。

:mod: 之前的文字將被解析以指定模組建立的特定事件:

# echo '<match>:mod:<module>' > /sys/kernel/tracing/set_event

上述命令將啟用 <match> 匹配的任何系統或事件。如果 <match>"*",它將匹配所有事件。

要只啟用系統中某個特定事件:

# echo '<system>:<event>:mod:<module>' > /sys/kernel/tracing/set_event

如果 <event>"*",它將匹配給定模組中系統內的所有事件。

2.2 透過“enable”開關

可用的事件也列在 /sys/kernel/tracing/events/ 目錄層次結構中。

要啟用事件“sched_wakeup”:

# echo 1 > /sys/kernel/tracing/events/sched/sched_wakeup/enable

要停用它:

# echo 0 > /sys/kernel/tracing/events/sched/sched_wakeup/enable

要啟用 sched 子系統中的所有事件:

# echo 1 > /sys/kernel/tracing/events/sched/enable

要啟用所有事件:

# echo 1 > /sys/kernel/tracing/events/enable

讀取這些 enable 檔案時,有四種結果:

  • 0 - 此檔案影響的所有事件都已停用

  • 1 - 此檔案影響的所有事件都已啟用

  • X - 存在事件啟用和停用混雜的情況

  • ? - 此檔案不影響任何事件

2.3 啟動選項

為了方便早期啟動除錯,使用啟動選項:

trace_event=[event-list]

event-list 是一個逗號分隔的事件列表。事件格式請參閱 2.1 節。

3. 定義一個啟用事件的追蹤點

請參閱 samples/trace_events 中提供的示例。

4. 事件格式

每個追蹤事件都有一個與之關聯的“format”檔案,其中包含每個已記錄事件欄位的描述。此資訊可用於解析二進位制追蹤流,也是查詢可用於事件過濾器(參見第 5 節)的欄位名稱的地方。

它還顯示了將用於以文字模式列印事件的格式字串,以及用於效能分析的事件名稱和 ID。

每個事件都有一組關聯的common欄位;這些欄位以common_為字首。其他欄位因事件而異,對應於該事件的 TRACE_EVENT 定義中定義的欄位。

格式中的每個欄位都有以下形式:

field:field-type field-name; offset:N; size:N;

其中 offset 是欄位在追蹤記錄中的偏移量,size 是資料項的大小(以位元組為單位)。

例如,以下是“sched_wakeup”事件顯示的資訊:

# cat /sys/kernel/tracing/events/sched/sched_wakeup/format

name: sched_wakeup
ID: 60
format:
        field:unsigned short common_type;       offset:0;       size:2;
        field:unsigned char common_flags;       offset:2;       size:1;
        field:unsigned char common_preempt_count;       offset:3;       size:1;
        field:int common_pid;   offset:4;       size:4;
        field:int common_tgid;  offset:8;       size:4;

        field:char comm[TASK_COMM_LEN]; offset:12;      size:16;
        field:pid_t pid;        offset:28;      size:4;
        field:int prio; offset:32;      size:4;
        field:int success;      offset:36;      size:4;
        field:int cpu;  offset:40;      size:4;

print fmt: "task %s:%d [%d] success=%d [%03d]", REC->comm, REC->pid,
           REC->prio, REC->success, REC->cpu

此事件包含 10 個欄位,前 5 個是通用欄位,後 5 個是事件特定的。此事件的所有欄位都是數值型,除了“comm”是字串型,這種區別對於事件過濾很重要。

5. 事件過濾

可以透過將布林“過濾器表示式”與追蹤事件關聯起來,在核心中對它們進行過濾。事件一旦被記錄到追蹤緩衝區,其欄位就會與該事件型別關聯的過濾器表示式進行檢查。欄位值“匹配”過濾器的事件將出現在追蹤輸出中,而值不匹配的事件將被丟棄。沒有過濾器關聯的事件匹配所有內容,並且在未為事件設定過濾器時是預設行為。

5.1 表示式語法

一個過濾器表示式由一個或多個“謂詞”組成,這些謂詞可以使用邏輯運算子“&&”和“||”進行組合。謂詞只是一個子句,它將已記錄事件中包含的欄位的值與常量值進行比較,並根據欄位值是否匹配(1)或不匹配(0)返回 0 或 1。

field-name relational-operator value

括號可用於提供任意的邏輯分組,雙引號可用於防止 shell 將運算子解釋為 shell 元字元。

可用於過濾器的欄位名稱可在追蹤事件的“format”檔案中找到(參見第 4 節)。

關係運算符取決於被測試欄位的型別:

數值欄位可用的運算子是:

==, !=, <, <=, >, >=, &

字串欄位的運算子是:

==, !=, ~

glob (~) 接受萬用字元 (*,?) 和字元類 ([)。例如:

prev_comm ~ "*sh"
prev_comm ~ "sh*"
prev_comm ~ "*sh*"
prev_comm ~ "ba*sh"

如果欄位是指向使用者空間的指標(例如 sys_enter_openat 中的“filename”),則必須在欄位名稱後附加“.ustring”:

filename.ustring ~ "password"

因為核心需要知道如何從使用者空間中檢索指標指向的記憶體。

您可以將任何長型別轉換為函式地址並按函式名稱搜尋:

call_site.function == security_prepare_creds

當欄位“call_site”落入“security_prepare_creds”函式地址範圍內時,上述命令將進行過濾。也就是說,它將比較“call_site”的值,如果它大於或等於“security_prepare_creds”函式的起始地址且小於該函式的結束地址,則過濾器將返回 true。

“.function”字尾只能附加到 long 型別的值,並且只能與“==”或“!=”進行比較。

Cpumask 欄位或編碼 CPU 編號的標量欄位可以使用使用者提供的 cpulist 格式的 cpumask 進行過濾。格式如下:

CPUS{$cpulist}

cpumask 過濾可用的運算子是:

& (交集), ==, !=

例如,這將過濾其 .target_cpu 欄位存在於給定 cpumask 中的事件:

target_cpu & CPUS{17-42}

5.2 設定過濾器

透過將過濾器表示式寫入給定事件的“filter”檔案來設定單個事件的過濾器。

例如:

# cd /sys/kernel/tracing/events/sched/sched_wakeup
# echo "common_preempt_count > 4" > filter

一個稍微複雜一點的例子:

# cd /sys/kernel/tracing/events/signal/signal_generate
# echo "((sig >= 10 && sig < 15) || sig == 17) && comm != bash" > filter

如果表示式中有錯誤,設定時會得到“Invalid argument”錯誤,並且透過檢視過濾器(例如)可以看到錯誤的字串和錯誤訊息:

# cd /sys/kernel/tracing/events/signal/signal_generate
# echo "((sig >= 10 && sig < 15) || dsig == 17) && comm != bash" > filter
-bash: echo: write error: Invalid argument
# cat filter
((sig >= 10 && sig < 15) || dsig == 17) && comm != bash
^
parse_error: Field not found

目前,錯誤指示符(‘^’)總是出現在過濾器字串的開頭;但即使沒有更準確的位置資訊,錯誤訊息也應該有用。

5.2.1 過濾器限制

如果對指向環形緩衝區之外的核心或使用者空間記憶體的字串指標 (char *) 設定過濾器,出於安全原因,最多會將 1024 位元組的內容複製到臨時緩衝區進行比較。如果記憶體複製失敗(指標指向不應訪問的記憶體),則字串比較將被視為不匹配。

5.3 清除過濾器

要清除事件的過濾器,將“0”寫入事件的過濾器檔案。

要清除子系統中所有事件的過濾器,將“0”寫入子系統的過濾器檔案。

5.4 子系統過濾器

為了方便,可以透過將過濾器表示式寫入子系統根目錄下的過濾器檔案,將子系統中每個事件的過濾器作為一個組進行設定或清除。但是請注意,如果子系統中任何事件的過濾器缺少子系統過濾器中指定的欄位,或者由於任何其他原因無法應用過濾器,則該事件的過濾器將保留其先前的設定。這可能導致意外的過濾器混合,從而導致令人困惑(對於可能認為不同過濾器生效的使用者)的追蹤輸出。只有僅引用通用欄位的過濾器才能保證成功傳播到所有事件。

以下是一些子系統過濾器示例,也說明了上述觀點:

清除 sched 子系統中所有事件的過濾器:

# cd /sys/kernel/tracing/events/sched
# echo 0 > filter
# cat sched_switch/filter
none
# cat sched_wakeup/filter
none

為 sched 子系統中的所有事件設定一個只使用通用欄位的過濾器(所有事件最終都使用相同的過濾器):

# cd /sys/kernel/tracing/events/sched
# echo common_pid == 0 > filter
# cat sched_switch/filter
common_pid == 0
# cat sched_wakeup/filter
common_pid == 0

嘗試為 sched 子系統中的所有事件設定一個使用非通用欄位的過濾器(除了那些具有 prev_pid 欄位的事件外,所有事件都保留其舊過濾器):

# cd /sys/kernel/tracing/events/sched
# echo prev_pid == 0 > filter
# cat sched_switch/filter
prev_pid == 0
# cat sched_wakeup/filter
common_pid == 0

5.5 PID 過濾

與頂級 events 目錄位於同一目錄下的 set_event_pid 檔案存在,它將過濾所有不包含在 set_event_pid 檔案中列出的 PID 的任務的追蹤事件。

# cd /sys/kernel/tracing
# echo $$ > set_event_pid
# echo 1 > events/enable

將只追蹤當前任務的事件。

要新增更多 PID 而不丟失已包含的 PID,請使用“>>”。

# echo 123 244 1 >> set_event_pid

6. 事件觸發器

追蹤事件可以被設定為有條件地呼叫觸發器“命令”,這些命令可以採用各種形式,詳述如下;例如,當追蹤事件被觸發時,啟用或停用其他追蹤事件,或者呼叫堆疊追蹤。每當一個帶有附加觸發器的追蹤事件被呼叫時,與該事件關聯的觸發器命令集就會被呼叫。任何給定的觸發器還可以額外關聯一個與第 5 節(事件過濾)中描述的相同形式的事件過濾器——只有當被呼叫的事件透過關聯的過濾器時,命令才會被呼叫。如果沒有過濾器與觸發器關聯,它總是透過。

透過將觸發器表示式寫入給定事件的“trigger”檔案來向特定事件新增和移除觸發器。

給定事件可以關聯任意數量的觸發器,但受限於各個命令在這方面的任何限制。

事件觸發器是在“軟”模式之上實現的,這意味著無論何時一個追蹤事件關聯了一個或多個觸發器,即使它實際上沒有啟用,也會以“軟”模式啟用該事件。也就是說,追蹤點將被呼叫,但不會被追蹤,當然除非它實際被啟用。這種機制允許即使對於未啟用的事件,觸發器也能被呼叫,並且還允許使用當前的事件過濾器實現有條件地呼叫觸發器。

事件觸發器的語法大致基於 set_ftrace_filter “ftrace 過濾器命令”的語法(參見ftrace - 函式追蹤器的“過濾器命令”部分),但存在主要差異,並且實現目前與它沒有任何關聯,因此請注意不要在這兩者之間進行泛化。

注意

寫入 trace_marker (參見ftrace - 函式追蹤器) 也可以啟用寫入 /sys/kernel/tracing/events/ftrace/print/trigger 的觸發器。

6.1 表示式語法

透過將命令回顯到“trigger”檔案來新增觸發器:

# echo 'command[:count] [if filter]' > trigger

透過將相同的命令(但以“!”開頭)回顯到“trigger”檔案來移除觸發器:

# echo '!command[:count] [if filter]' > trigger

[if filter] 部分在移除時不會用於匹配命令,因此在“!”命令中省略它會達到與包含它相同的效果。

過濾器語法與上面“事件過濾”一節中描述的相同。

為了方便使用,目前使用“>”寫入觸發檔案僅新增或移除單個觸發器,並且沒有明確的“>>”支援(“>”實際上行為類似於“>>”)或截斷支援以移除所有觸發器(您必須為每個新增的觸發器使用“!”)。

6.2 支援的觸發命令

支援以下命令:

  • enable_event/disable_event

    當觸發事件發生時,這些命令可以啟用或停用另一個追蹤事件。當這些命令註冊時,另一個追蹤事件被啟用,但處於“軟”停用模式。也就是說,追蹤點將被呼叫,但不會被追蹤。只要有觸發器可以觸發它,事件追蹤點就保持此模式。

    例如,以下觸發器會導致在進入 read 系統呼叫時追蹤 kmalloc 事件,末尾的 :1 指定此啟用只發生一次:

    # echo 'enable_event:kmem:kmalloc:1' > \
        /sys/kernel/tracing/events/syscalls/sys_enter_read/trigger
    

    以下觸發器導致在 read 系統呼叫退出時停止追蹤 kmalloc 事件。此停用操作在每次 read 系統呼叫退出時發生:

    # echo 'disable_event:kmem:kmalloc' > \
        /sys/kernel/tracing/events/syscalls/sys_exit_read/trigger
    

    格式是:

    enable_event:<system>:<event>[:count]
    disable_event:<system>:<event>[:count]
    

    要刪除上述命令:

    # echo '!enable_event:kmem:kmalloc:1' > \
        /sys/kernel/tracing/events/syscalls/sys_enter_read/trigger
    
    # echo '!disable_event:kmem:kmalloc' > \
        /sys/kernel/tracing/events/syscalls/sys_exit_read/trigger
    

    請注意,每個觸發事件可以有任意數量的 enable/disable_event 觸發器,但每個被觸發事件只能有一個觸發器。例如,sys_enter_read 可以有同時啟用 kmem:kmalloc 和 sched:sched_switch 的觸發器,但不能有兩個 kmem:kmalloc 版本,例如 kmem:kmalloc 和 kmem:kmalloc:1,或‘kmem:kmalloc if bytes_req == 256’和‘kmem:kmalloc if bytes_alloc == 256’(它們可以組合成 kmem:kmalloc 上的單個過濾器)。

  • stacktrace

    每當觸發事件發生時,此命令會在追蹤緩衝區中轉儲堆疊跟蹤。

    例如,以下觸發器每次命中 kmalloc 追蹤點時都會轉儲堆疊跟蹤:

    # echo 'stacktrace' > \
          /sys/kernel/tracing/events/kmem/kmalloc/trigger
    

    以下觸發器會在 kmalloc 請求發生前 5 次(大小 >= 64K)時轉儲堆疊跟蹤:

    # echo 'stacktrace:5 if bytes_req >= 65536' > \
          /sys/kernel/tracing/events/kmem/kmalloc/trigger
    

    格式是:

    stacktrace[:count]
    

    要刪除上述命令:

    # echo '!stacktrace' > \
          /sys/kernel/tracing/events/kmem/kmalloc/trigger
    
    # echo '!stacktrace:5 if bytes_req >= 65536' > \
          /sys/kernel/tracing/events/kmem/kmalloc/trigger
    

    後者也可以更簡單地透過以下方式刪除(不帶過濾器):

    # echo '!stacktrace:5' > \
          /sys/kernel/tracing/events/kmem/kmalloc/trigger
    

    請注意,每個觸發事件只能有一個 stacktrace 觸發器。

  • snapshot

    此命令使快照在觸發事件發生時被觸發。

    以下命令在塊請求佇列深度大於 1 被拔出時建立快照。如果您當時正在追蹤一組事件或函式,則快照追蹤緩衝區將在觸發事件發生時捕獲這些事件:

    # echo 'snapshot if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    只快照一次:

    # echo 'snapshot:1 if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    要刪除上述命令:

    # echo '!snapshot if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    
    # echo '!snapshot:1 if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    請注意,每個觸發事件只能有一個快照觸發器。

  • traceon/traceoff

    當指定的事件發生時,這些命令會開啟和關閉追蹤。引數決定了追蹤系統開啟和關閉的次數。如果未指定,則沒有限制。

    以下命令會在塊請求佇列深度大於 1 首次拔出時關閉追蹤。如果您當時正在追蹤一組事件或函式,您可以檢查追蹤緩衝區以檢視導致觸發事件的事件序列:

    # echo 'traceoff:1 if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    當 nr_rq > 1 時始終停用追蹤:

    # echo 'traceoff if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    要刪除上述命令:

    # echo '!traceoff:1 if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    
    # echo '!traceoff if nr_rq > 1' > \
          /sys/kernel/tracing/events/block/block_unplug/trigger
    

    請注意,每個觸發事件只能有一個 traceon 或 traceoff 觸發器。

  • hist

    此命令將事件命中聚合到一個雜湊表中,該雜湊表以一個或多個追蹤事件格式欄位(或堆疊跟蹤)為鍵,並以一個或多個追蹤事件格式欄位和/或事件計數(hitcount)派生的一組執行總數為值。

    有關詳細資訊和示例,請參閱事件直方圖

7. 核心追蹤事件 API

在大多數情況下,追蹤事件的命令列介面已經足夠。然而,有時應用程式可能需要比透過簡單的系列連結命令列表示式所能表達的更復雜的關係,或者組合命令集可能過於繁瑣。一個例子是應用程式需要“監聽”追蹤流以維護核心狀態機,例如,檢測排程程式中何時發生非法核心狀態。

追蹤事件子系統提供了一個核心 API,允許模組或其他核心程式碼隨意生成使用者定義的“合成”事件,這些事件可用於增強現有追蹤流和/或發出特定重要狀態已發生的訊號。

類似的核心 API 也可用於建立 kprobe 和 kretprobe 事件。

合成事件和 k/ret/probe 事件 API 都建立在更底層的“dynevent_cmd”事件命令 API 之上,該 API 也可用於更專業的應用程式,或作為其他更高級別追蹤事件 API 的基礎。

為這些目的提供的 API 描述如下,並允許以下功能:

  • 動態建立合成事件定義

  • 動態建立 kprobe 和 kretprobe 事件定義

  • 從核心程式碼追蹤合成事件

  • 底層的“dynevent_cmd”API

7.1 動態建立合成事件定義

有幾種方法可以從核心模組或其他核心程式碼建立新的合成事件。

第一種方法是一步到位地建立事件,使用 synth_event_create()。在此方法中,要建立的事件名稱和定義欄位的陣列會提供給 synth_event_create()。如果成功,呼叫後將存在具有該名稱和欄位的合成事件。例如,要建立一個新的“schedtest”合成事件:

ret = synth_event_create("schedtest", sched_fields,
                         ARRAY_SIZE(sched_fields), THIS_MODULE);

此示例中的 sched_fields 引數指向一個 struct synth_field_desc 陣列,其中每個元素都按型別和名稱描述一個事件欄位:

static struct synth_field_desc sched_fields[] = {
      { .type = "pid_t",              .name = "next_pid_field" },
      { .type = "char[16]",           .name = "next_comm_field" },
      { .type = "u64",                .name = "ts_ns" },
      { .type = "u64",                .name = "ts_ms" },
      { .type = "unsigned int",       .name = "cpu" },
      { .type = "char[64]",           .name = "my_string_field" },
      { .type = "int",                .name = "my_int_field" },
};

有關可用型別,請參閱 synth_field_size()

如果 field_name 包含 [n],則該欄位被視為靜態陣列。

如果 field_names 包含 [] (無下標),則該欄位被視為動態陣列,它在事件中只會佔用容納陣列所需的空間。

因為事件的空間是在分配欄位值給事件之前預留的,所以使用動態陣列意味著下面描述的分段式核心 API 不能與動態陣列一起使用。但是,其他非分段式核心 API 可以與動態陣列一起使用。

如果事件是從模組內部建立的,則必須將指向該模組的指標傳遞給 synth_event_create()。這將確保在模組移除時,追蹤緩衝區不會包含不可讀的事件。

至此,事件物件已準備好用於生成新事件。

在第二種方法中,事件分幾個步驟建立。這允許動態建立事件,而無需事先建立和填充欄位陣列。

要使用此方法,應首先使用 synth_event_gen_cmd_start()synth_event_gen_cmd_array_start() 建立一個空或部分空的合成事件。對於 synth_event_gen_cmd_start(),應提供事件名稱以及一個或多個引數對,每對引數代表一個“type field_name;”欄位規範。對於 synth_event_gen_cmd_array_start(),應提供事件名稱以及一個 struct synth_field_desc 陣列。在呼叫 synth_event_gen_cmd_start()synth_event_gen_cmd_array_start() 之前,使用者應使用 synth_event_cmd_init() 建立並初始化一個 dynevent_cmd 物件。

例如,建立一個包含兩個欄位的新“schedtest”合成事件:

struct dynevent_cmd cmd;
char *buf;

/* Create a buffer to hold the generated command */
buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

/* Before generating the command, initialize the cmd object */
synth_event_cmd_init(&cmd, buf, MAX_DYNEVENT_CMD_LEN);

ret = synth_event_gen_cmd_start(&cmd, "schedtest", THIS_MODULE,
                                "pid_t", "next_pid_field",
                                "u64", "ts_ns");

或者,使用包含相同資訊的 struct synth_field_desc 欄位陣列:

ret = synth_event_gen_cmd_array_start(&cmd, "schedtest", THIS_MODULE,
                                      fields, n_fields);

合成事件物件建立後,可以填充更多欄位。欄位透過 synth_event_add_field() 一個接一個地新增,提供 dynevent_cmd 物件、欄位型別和欄位名稱。例如,要新增一個名為“intfield”的新 int 欄位,應進行以下呼叫:

ret = synth_event_add_field(&cmd, "int", "intfield");

有關可用型別,請參閱 synth_field_size()。如果 field_name 包含 [n],則該欄位被視為陣列。

也可以使用 add_synth_fields() 和一個 synth_field_desc 陣列一次性新增一組欄位。例如,這將只新增前四個 sched_fields

ret = synth_event_add_fields(&cmd, sched_fields, 4);

如果您已經有一個“type field_name”形式的字串,可以使用 synth_event_add_field_str() 直接新增它;它還會自動在字串末尾新增一個“;”。

新增完所有欄位後,應透過呼叫 synth_event_gen_cmd_end() 函式來最終確定並註冊事件:

ret = synth_event_gen_cmd_end(&cmd);

此時,事件物件已準備好用於追蹤新事件。

7.2 從核心程式碼追蹤合成事件

要追蹤合成事件,有幾種選項。第一種選項是在一次呼叫中追蹤事件,使用帶有可變數量值的 synth_event_trace(),或使用帶有值陣列的 synth_event_trace_array()。第二種選項可以避免預先形成值陣列或引數列表的需要,透過 synth_event_trace_start()synth_event_trace_end() 以及 synth_event_add_next_val()synth_event_add_val() 來分段新增值。

7.2.1 一次性追蹤合成事件

要一次性追蹤合成事件,可以使用 synth_event_trace()synth_event_trace_array() 函式。

synth_event_trace() 函式傳入代表合成事件的 trace_event_file(可以使用 trace_get_event_file() 透過合成事件名稱、“synthetic”作為系統名稱和追蹤例項名稱(如果使用全域性追蹤陣列則為 NULL)來檢索),以及可變數量的 u64 引數,每個合成事件欄位一個,以及傳入值的數量。

因此,要追蹤與上述合成事件定義對應的事件,可以使用以下程式碼:

ret = synth_event_trace(create_synth_test, 7, /* number of values */
                        444,             /* next_pid_field */
                        (u64)"clackers", /* next_comm_field */
                        1000000,         /* ts_ns */
                        1000,            /* ts_ms */
                        smp_processor_id(),/* cpu */
                        (u64)"Thneed",   /* my_string_field */
                        999);            /* my_int_field */

所有 vals 都應轉換為 u64,字串 vals 只是指向字串的指標,轉換為 u64。字串將使用這些指標複製到事件中為字串預留的空間。

或者,可以使用 synth_event_trace_array() 函式完成相同的操作。它傳入代表合成事件的 trace_event_file(可以使用 trace_get_event_file() 透過合成事件名稱、“synthetic”作為系統名稱和追蹤例項名稱(如果使用全域性追蹤陣列則為 NULL)來檢索),以及一個 u64 陣列,每個合成事件欄位一個。

要追蹤與上述合成事件定義對應的事件,可以使用以下程式碼:

u64 vals[7];

vals[0] = 777;                  /* next_pid_field */
vals[1] = (u64)"tiddlywinks";   /* next_comm_field */
vals[2] = 1000000;              /* ts_ns */
vals[3] = 1000;                 /* ts_ms */
vals[4] = smp_processor_id();   /* cpu */
vals[5] = (u64)"thneed";        /* my_string_field */
vals[6] = 398;                  /* my_int_field */

“vals”陣列只是一個 u64 陣列,其數量必須與合成事件中的欄位數量匹配,並且必須與合成事件欄位的順序相同。

所有 vals 都應轉換為 u64,字串 vals 只是指向字串的指標,轉換為 u64。字串將使用這些指標複製到事件中為字串預留的空間。

為了追蹤一個合成事件,需要一個指向追蹤事件檔案的指標。trace_get_event_file() 函式可以用來獲取它——它會在給定的追蹤例項中找到該檔案(在這種情況下為 NULL,因為使用的是頂層追蹤陣列),同時防止包含它的例項被銷燬:

schedtest_event_file = trace_get_event_file(NULL, "synthetic",
                                            "schedtest");

在追蹤事件之前,應該以某種方式啟用它,否則合成事件實際上不會出現在追蹤緩衝區中。

要從核心啟用合成事件,可以使用 trace_array_set_clr_event()(它並非特定於合成事件,因此確實需要明確指定“synthetic”系統名稱)。

要啟用事件,向其傳遞“true”:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", true);

要停用它,傳遞 false:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", false);

最後,可以使用 synth_event_trace_array() 實際追蹤事件,追蹤後事件應在追蹤緩衝區中可見:

ret = synth_event_trace_array(schedtest_event_file, vals,
                              ARRAY_SIZE(vals));

要移除合成事件,應停用事件,並使用 trace_put_event_file() 將追蹤例項“放回”:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", false);
trace_put_event_file(schedtest_event_file);

如果這些都成功了,就可以呼叫 synth_event_delete() 來刪除事件:

ret = synth_event_delete("schedtest");

7.2.2 分段追蹤合成事件

要使用上述分段方法追蹤合成事件,使用 synth_event_trace_start() 函式“開啟”合成事件追蹤:

struct synth_event_trace_state trace_state;

ret = synth_event_trace_start(schedtest_event_file, &trace_state);

它被傳入代表合成事件的 trace_event_file(使用上述相同的方法),以及一個指向 struct synth_event_trace_state 物件的指標,該物件在使用前將歸零,並用於維護此呼叫與後續呼叫之間的狀態。

一旦事件被開啟,這意味著在追蹤緩衝區中為其保留了空間,就可以設定單個欄位。有兩種方法可以做到這一點:一種是逐個設定事件中的每個欄位,無需查詢;另一種是按名稱設定,需要查詢。兩者之間的權衡是賦值的靈活性與每個欄位查詢的成本。

要一次性按順序賦值而無需查詢,應使用 synth_event_add_next_val()。每次呼叫都會傳入 synth_event_trace_start() 中使用的相同 synth_event_trace_state 物件,以及要設定事件中下一個欄位的值。設定完每個欄位後,“遊標”指向下一個欄位,該欄位將由後續呼叫設定,如此繼續直到所有欄位按順序設定完畢。使用此方法時,與上述示例相同的呼叫序列將是(不含錯誤處理程式碼):

/* next_pid_field */
ret = synth_event_add_next_val(777, &trace_state);

/* next_comm_field */
ret = synth_event_add_next_val((u64)"slinky", &trace_state);

/* ts_ns */
ret = synth_event_add_next_val(1000000, &trace_state);

/* ts_ms */
ret = synth_event_add_next_val(1000, &trace_state);

/* cpu */
ret = synth_event_add_next_val(smp_processor_id(), &trace_state);

/* my_string_field */
ret = synth_event_add_next_val((u64)"thneed_2.01", &trace_state);

/* my_int_field */
ret = synth_event_add_next_val(395, &trace_state);

要以任意順序賦值,應使用 synth_event_add_val()。每次呼叫都會傳入 synth_event_trace_start() 中使用的相同 synth_event_trace_state 物件,以及要設定的欄位的欄位名稱和要設定的值。使用此方法時,與上述示例相同的呼叫序列將是(不含錯誤處理程式碼):

ret = synth_event_add_val("next_pid_field", 777, &trace_state);
ret = synth_event_add_val("next_comm_field", (u64)"silly putty",
                          &trace_state);
ret = synth_event_add_val("ts_ns", 1000000, &trace_state);
ret = synth_event_add_val("ts_ms", 1000, &trace_state);
ret = synth_event_add_val("cpu", smp_processor_id(), &trace_state);
ret = synth_event_add_val("my_string_field", (u64)"thneed_9",
                          &trace_state);
ret = synth_event_add_val("my_int_field", 3999, &trace_state);

請注意,如果 synth_event_add_next_val()synth_event_add_val() 在同一個事件追蹤中使用,則它們是不相容的——兩者只能使用其中之一,不能同時使用。

最後,事件在“關閉”之前不會實際被追蹤,這透過使用 synth_event_trace_end() 完成,該函式只接受之前呼叫中使用的 struct synth_event_trace_state 物件:

ret = synth_event_trace_end(&trace_state);

請注意,無論任何新增呼叫是否失敗(例如由於傳入了錯誤的欄位名稱),synth_event_trace_end() 都必須在最後呼叫。

7.3 動態建立 kprobe 和 kretprobe 事件定義

要從核心程式碼建立 kprobe 或 kretprobe 追蹤事件,可以使用 kprobe_event_gen_cmd_start()kretprobe_event_gen_cmd_start() 函式。

要建立 kprobe 事件,應首先使用 kprobe_event_gen_cmd_start() 建立一個空或部分空的 kprobe 事件。事件名稱和探測位置應與一個或多個表示探測欄位的引數一起提供給此函式。在呼叫 kprobe_event_gen_cmd_start() 之前,使用者應使用 kprobe_event_cmd_init() 建立並初始化一個 dynevent_cmd 物件。

例如,建立一個包含兩個欄位的新“schedtest”kprobe 事件:

struct dynevent_cmd cmd;
char *buf;

/* Create a buffer to hold the generated command */
buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

/* Before generating the command, initialize the cmd object */
kprobe_event_cmd_init(&cmd, buf, MAX_DYNEVENT_CMD_LEN);

/*
 * Define the gen_kprobe_test event with the first 2 kprobe
 * fields.
 */
ret = kprobe_event_gen_cmd_start(&cmd, "gen_kprobe_test", "do_sys_open",
                                 "dfd=%ax", "filename=%dx");

kprobe 事件物件建立後,可以填充更多欄位。欄位可以使用 kprobe_event_add_fields() 新增,提供 dynevent_cmd 物件以及可變引數列表的探測欄位。例如,要新增幾個附加欄位,可以進行以下呼叫:

ret = kprobe_event_add_fields(&cmd, "flags=%cx", "mode=+4($stack)");

新增完所有欄位後,應透過呼叫 kprobe_event_gen_cmd_end()kretprobe_event_gen_cmd_end() 函式來最終確定和註冊事件,具體取決於啟動的是 kprobe 還是 kretprobe 命令:

ret = kprobe_event_gen_cmd_end(&cmd);

或者

ret = kretprobe_event_gen_cmd_end(&cmd);

此時,事件物件已準備好用於追蹤新事件。

類似地,可以使用 kretprobe_event_gen_cmd_start() 建立一個 kretprobe 事件,其中包含探測名稱、位置以及諸如 $retval 等附加引數:

ret = kretprobe_event_gen_cmd_start(&cmd, "gen_kretprobe_test",
                                    "do_sys_open", "$retval");

類似於合成事件的情況,可以使用以下程式碼啟用新建立的 kprobe 事件:

gen_kprobe_test = trace_get_event_file(NULL, "kprobes", "gen_kprobe_test");

ret = trace_array_set_clr_event(gen_kprobe_test->tr,
                                "kprobes", "gen_kprobe_test", true);

最後,同樣類似於合成事件,可以使用以下程式碼釋放 kprobe 事件檔案並刪除事件:

trace_put_event_file(gen_kprobe_test);

ret = kprobe_event_delete("gen_kprobe_test");

7.4 “dynevent_cmd”底層 API

核心內部的合成事件和 kprobe 介面都建立在更底層的“dynevent_cmd”介面之上。此介面旨在為合成事件和 kprobe 介面等更高級別介面提供基礎,這些介面可以作為示例使用。

基本思想很簡單,就是提供一個通用的層,可用於生成追蹤事件命令。然後可以將生成的命令字串傳遞給追蹤事件子系統中已存在的命令解析和事件建立程式碼,以建立相應的追蹤事件。

簡而言之,其工作方式是:高階介面程式碼建立一個 struct dynevent_cmd 物件,然後使用 dynevent_arg_add()dynevent_arg_pair_add() 兩個函式構建命令字串,最後使用 dynevent_create() 函式執行該命令。介面的詳細資訊如下所述。

構建新命令字串的第一步是建立並初始化一個 dynevent_cmd 例項。例如,這裡我們在棧上建立一個 dynevent_cmd 並初始化它:

struct dynevent_cmd cmd;
char *buf;
int ret;

buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

dynevent_cmd_init(cmd, buf, maxlen, DYNEVENT_TYPE_FOO,
                  foo_event_run_command);

dynevent_cmd 初始化需要提供使用者指定的緩衝區和緩衝區長度(可以使用 MAX_DYNEVENT_CMD_LEN,但由於它通常過大(2k)而無法舒適地放在棧上,因此動態分配),一個 dynevent 型別 ID,旨在用於檢查後續 API 呼叫是否針對正確的命令型別,以及一個指向事件特定的 run_command() 回撥的指標,該回調將被呼叫以實際執行事件特定的命令函式。

完成之後,可以透過連續呼叫新增引數的函式來構建命令字串。

要新增單個引數,請定義並初始化一個 struct dynevent_argstruct dynevent_arg_pair 物件。下面是可能最簡單的引數新增示例,它只是將給定字串作為以空格分隔的引數附加到命令中:

struct dynevent_arg arg;

dynevent_arg_init(&arg, NULL, 0);

arg.str = name;

ret = dynevent_arg_add(cmd, &arg);

arg 物件首先使用 dynevent_arg_init() 初始化,在這種情況下,引數為 NULL 或 0,這意味著沒有可選的健全性檢查函式或附加到引數末尾的分隔符。

這是一個更復雜的示例,使用“arg pair”,它用於建立一個由兩個元件組成一個單元的引數,例如“type field_name;”引數或簡單的表示式引數,例如“flags=%cx”:

struct dynevent_arg_pair arg_pair;

dynevent_arg_pair_init(&arg_pair, dynevent_foo_check_arg_fn, 0, ';');

arg_pair.lhs = type;
arg_pair.rhs = name;

ret = dynevent_arg_pair_add(cmd, &arg_pair);

同樣,arg_pair 首先被初始化,在這種情況下,使用了一個回撥函式來檢查引數的健全性(例如,對的任何一部分都不是 NULL),以及一個用於在對之間新增運算子的字元(這裡沒有)和一個附加到 arg_pair 末尾的分隔符(這裡是“;”)。

還有一個 dynevent_str_add() 函式,可以用來直接新增字串,不帶空格、分隔符或引數檢查。

可以進行任意數量的 dynevent_*_add() 呼叫來構建字串(直到其長度超過 cmd->maxlen)。當所有引數都已新增並且命令字串完整時,唯一剩下要做的事情就是執行命令,這隻需呼叫 dynevent_create() 即可實現:

ret = dynevent_create(&cmd);

此時,如果返回值為 0,則動態事件已建立並可以使用。

有關 API 的詳細資訊,請參閱 dynevent_cmd 函式定義本身。