perf, ftraceのしくみ

Linuxのトレーサーであるperfやftraceのツールの使い方に関する情報は結構ありますが,構造に関してはあまり見つけられなかったため,ここに簡単に調べたことをまとめようかと思います.(ツールの使い方の説明はあんまりしないです.)

この文章はLinux 4.15のソースに基づいています.

全体像

そもそもLinuxのトレーサーとひとえに言ってもperfとかftraceとかkprobeとかuprobeとかいろいろありすぎて一体どうなっているんだという感じなので簡単に関係を図示しています.

f:id:mm_i:20180304224739p:plain

実際はいろいろと複雑に絡み合ってるのでなかなか可視化するのが難しいですが,まぁ一つの見方だと思ってください.

大雑把には以下のように分類できます.

  • ユーザランドのツール
    • perf, perf-tools, bcc, trace-cmd
  • インタフェース
    • perf_event_open(2), bpf(2), ioctl(2), debugfs (tracefs), ...
  • トレーサー, フレームワーク
    • perf, ftrace, eBPF
  • event, data source
    • Performance counter
    • tracepoint
    • kprobe
    • uprobe
    • mcount

ftrace

ftraceは主にカーネルのコードのトレースを目的としたフレームワークです. ftraceという名前の通り,function tracingが主機能の一つですが,実際には以下のようなトレーサーが含まれています.

  • function
    • 関数呼び出しの記録
  • functrion_graph
    • 関数の戻りも記録
  • event tracer
    • tracepoint, kprobe, uprobe
  • latency tracer
    • wakeup
      • wakeup時間までのトレース
    • irqsoff
      • 割り込み停止時間トレース
    • ...
  • mmio tracer
  • ...

ftraceとのやりとりはユーザスペースからはdebugfsを通じておこないます. debugfsは /sys/kernel/debug/ にマウントされていることが多いと思います. 特にdebugfsの中のtracing/ディレクトリ以下がftrace部分です. Linux 4.1から,(主にセキュリティのため)debugfsをマウントしたくない場合でもftraceが利用できるように,tracefsが導入されています. これは従来のdebugfsのtracing部分を分離したものです. debugfsがマウントされている場合,互換性のためにdebug/tracingにtracefsがマウントされるようになっています.

ftraceを利用する場合はまず使用するトレーサーを選択します. トレーサーはトレース結果をリングバッファへ出力します.このバッファへはtracefs経由でアクセスすることが可能です. ftraceにはトレース結果のフィルタリングや,あるイベント時にトレースを開始するなどの機能があります.

トレーサーのコードは主に kernel/trace以下に存在します.

tracefsによるftraceの使い方は以下が参考になります.

以下ではfunction tracing, tracepint, kprobeについて簡単に説明します.

function trace

function tracingはgccのプロファイリング機能を利用します.

gccでは-pgオプションをつけてコンパイルすると,関数呼び出しがmcountという関数の呼び出しに変換されます.

例:

# w/o -pg
% echo "int main(){return 0;}" | gcc -x c -O0 -S -fno-asynchronous-unwind-tables -o- -
        .file   ""
        .text
        .globl  main
        .type   main, @function
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $0, %eax
        popq    %rbp
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.2.0-8ubuntu3) 7.2.0"
        .section        .note.GNU-stack,"",@progbits
# w/ -pg
% echo "int main(){return 0;}" | gcc -pg -x c -O0 -S -fno-asynchronous-unwind-tables -o- -
        .file   ""
        .text
        .globl  main
        .type   main, @function
main:
        pushq   %rbp
        movq    %rsp, %rbp
1:      call    *mcount@GOTPCREL(%rip)
        movl    $0, %eax
        popq    %rbp
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.2.0-8ubuntu3) 7.2.0"
        .section        .note.GNU-stack,"",@progbits

ユーザランドのプログラムの場合,glibcに含まれるmcountの関数とリンクされます. このmcountの関数はいわゆるトランポリンコードとして動作し,mcountの関数内で記録をとることで関数呼び出しがトレースできます.

ftraceのfunction traceも基本は同じですが,カーネル内の全ての関数呼び出しをmcountを経由してしまうと性能が大幅に低下することは想像に難くありません. そこで,ftraceでは-pg付きでコンパイルしたのち,mcountのcall命令をnopに置き換えるということをします(これはCONFIG_DYNAMIC_FTRACE=yのときですが,普通ftraceを使う場合は有効にするはず). この処理はカーネルビルド時におこないます.どこにmcountのcall命令が存在したかというのはカーネル内の__start_mcount_loc__end_mcount_locの間に保持しておきます.

% sudo cat /proc/kallsyms | grep mcount
ffffffffbc83d1c0 T __start_mcount_loc
ffffffffbc886f90 T __stop_mcount_loc

この情報を使ってftraceでfunction tracingする際に対象箇所のコードを書き換えてmcountを呼ぶようにします. こうすることで,ftraceを利用していないときのオーバヘッドをほぼ0に抑えています.

参考までに,手元の環境でfunction traceのオンオフでddを実行した際の実行時間は以下のようになりました.

トレースオフ

# current_tracer = nop
% time dd if=/dev/zero of=/dev/null bs=1 count=500k
512000+0 records in
512000+0 records out
512000 bytes (512 kB, 500 KiB) copied, 0.471769 s, 1.1 MB/s
dd if=/dev/zero of=/dev/null bs=1 count=500k  0.13s user 0.34s system 99% cpu 0.473 total

トレースオン

# current_tracder = function_graph
% time dd if=/dev/zero of=/dev/null bs=1 count=500k
512000+0 records in
512000+0 records out
512000 bytes (512 kB, 500 KiB) copied, 5.88682 s, 87.0 kB/s
dd if=/dev/zero of=/dev/null bs=1 count=500k  0.17s user 5.72s system 99% cpu 5.898 total

トレースオン時は実行時間が10倍以上になっています. (ただし,実際にトレースする際は,全ての関数をトレースしても訳がわからなくなるので,フィルタリングを掛けたり一部の処理部分だけトレースを有効化すると思います).

もう少し具体的な構造の説明は以下が参考になります.

x86でのmcountの実装は arch/x86/kernel/ftrace.c, arch/x86/kernel/ftrace_64.Sにあります.

余談ですが,Linux 4.0から導入されたライブパッチはこのmcountのフックを利用しています. (mcountからパッチされた関数を呼び出す).

tracepoint (static event)

tracepointはカーネルのコード内で簡単にprobe functionを定義できるようにするための仕組みです. やっていることを単純化すると,ソース内で以下のようにprobe functinonを呼び出します.

if(event_on){
    callback()
}

実際には一つのトレースポイントに複数の関数を登録することが可能です. (カーネルモジュールからも登録が可能です()). カーネル内の1000以上の箇所でtracepointが定義されています.

tracepointeの定義の詳細はマクロが多用されていて非常に分かりにくいですが,どうも以下のようになっているみたいです.

例: sched_process_exec

https://github.com/torvalds/linux/blob/v4.15/include/trace/events/sched.h#L301

TRACE_EVENT(sched_process_exec,

    TP_PROTO(struct task_struct *p, pid_t old_pid,
         struct linux_binprm *bprm),

    TP_ARGS(p, old_pid, bprm),

    TP_STRUCT__entry(
        __string(   filename,   bprm->filename   )
        __field(    pid_t,      pid     )
        __field(    pid_t,      old_pid     )
    ),

    TP_fast_assign(
        __assign_str(filename, bprm->filename);
        __entry->pid     = p->pid;
        __entry->old_pid = old_pid;
    ),

    TP_printk("filename=%s pid=%d old_pid=%d", __get_str(filename),
          __entry->pid, __entry->old_pid)
);

これらのマクロは include/linux/tracepoint.hで定義されています. 各マクロの細かい説明はこちらをみてください.

TRACE_EVENTマクロは最終的にDECLARE_TRACEとして展開されます.

https://github.com/torvalds/linux/blob/v4.15/include/linux/tracepoint.h#L185

#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \
   extern struct tracepoint __tracepoint_##name;         \
   static inline void trace_##name(proto)               \
   {                               \
       if (static_key_false(&__tracepoint_##name.key))       \
           __DO_TRACE(&__tracepoint_##name,        \
               TP_PROTO(data_proto),           \
               TP_ARGS(data_args),         \
               TP_CONDITION(cond), 0);            \
       if (IS_ENABLED(CONFIG_LOCKDEP) && (cond)) {       \
           rcu_read_lock_sched_notrace();          \
           rcu_dereference_sched(__tracepoint_##name.funcs);\
           rcu_read_unlock_sched_notrace();        \
       }                           \
   }
    ...

この分岐では static-key と呼ばれる仕組みを利用しています. if (static_key_false(&__tracepoint_##name.key))の部分は最初nopとしてコンパイルされます. 後からtracepointを有効にするとき,その部分を__DO_TRACE()を実行するようなjmp命令に書き換えます. (ちなみに,最新のドキュメントにはstatic_key_false()はdeprecatedと書いてありますが,普通に利用されてますね..)

__DO_TRACE()の中でコールバック関数を呼び出します.

ここで定義される trace_##name() をフックしたい場所から呼びます. sched_process_execは以下から呼ばれています.

https://github.com/torvalds/linux/blob/v4.15/fs/exec.c#L1683

...
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }
...

で,これだけだとtracepointが定義しただけで,それに対応するコールバック関数は何も登録されていません.

sched_process_execを定義しているsched.hでは,ヘッダの末尾で以下のファイルをincludeしています.

https://github.com/torvalds/linux/blob/v4.15/include/trace/events/sched.h#L576

/* This part must be outside protection */
#include <trace/define_trace.h>

このdefine_trace.hですが,trace/trace_events.hをインクルードしたのち,もう一度sched.hをインクルードします. (TRACE_INCLUDEの部分でincludeされます)

https://github.com/torvalds/linux/blob/v4.15/include/trace/define_trace.h

...
#include <trace/trace_events.h>
#include <trace/perf.h>
...
#define TRACE_HEADER_MULTI_READ

#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)
...

trace_events.hの中で,DECLARE_EVENT_CLASSなどのマクロが再定義されます. 従って,shced.hを2回目にインクルードした際はこれらのマクロが適用されます.

https://github.com/torvalds/linux/blob/v4.15/include/trace/trace_events.h#L757

#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print) \
_TRACE_PERF_PROTO(call, PARAMS(proto));                    \
static char print_fmt_##call[] = print;                  \
static struct trace_event_class __used __refdata event_class_##call = { \
   .system         = TRACE_SYSTEM_STRING,          \
   .define_fields      = trace_event_define_fields_##call, \
   .fields         = LIST_HEAD_INIT(event_class_##call.fields),\
   .raw_init       = trace_event_raw_init,         \
   .probe          = trace_event_raw_event_##call,     \
   .reg            = trace_event_reg,          \
   _TRACE_PERF_INIT(call)                      \
};

#undef DEFINE_EVENT
#define DEFINE_EVENT(template, call, proto, args)          \
                                   \
static struct trace_event_call __used event_##call = {           \
   .class          = &event_class_##template,      \
   {                               \
       .tp         = &__tracepoint_##call,     \
   },                              \
   .event.funcs        = &trace_event_type_funcs_##template,   \
   .print_fmt      = print_fmt_##template,         \
   .flags          = TRACE_EVENT_FL_TRACEPOINT,        \
};                                 \
static struct trace_event_call __used                    \
__attribute__((section("_ftrace_events"))) *__event_##call = &event_##call

DEFINE_EVENTマクロにより,_ftrace_eventsセクションにstruct trace_event_callのデータが格納されます.

ftraceの初期化時にこの情報を利用してトレースポイントのイベントをリストに追加します.

https://github.com/torvalds/linux/blob/v4.15/kernel/trace/trace_events.c#L3085

static __init int event_trace_enable(void)
...
    for_each_event(iter, __start_ftrace_events, __stop_ftrace_events) {

        call = *iter;
        ret = event_init(call);
        if (!ret)
            list_add(&call->list, &ftrace_events);

eventの有効化はftrace_event_enable_disableでおこないます.

https://github.com/torvalds/linux/blob/v4.15/kernel/trace/trace_events.c#L456

static int __ftrace_event_enable_disable(struct trace_event_file *file,
                     int enable, int soft_disable)
{
...
            ret = call->class->reg(call, TRACE_REG_REGISTER, file);
...

このreg()は,上のDECLARE_EVENT_CLASSで定義したtrace_event_reg()です.

https://github.com/torvalds/linux/blob/v4.15/kernel/trace/trace_events.c#L286

int trace_event_reg(struct trace_event_call *call,
            enum trace_reg type, void *data)
{
    struct trace_event_file *file = data;

    WARN_ON(!(call->flags & TRACE_EVENT_FL_TRACEPOINT));
    switch (type) {
    case TRACE_REG_REGISTER:
        return tracepoint_probe_register(call->tp,
                         call->class->probe,
                         file);
    case TRACE_REG_UNREGISTER:
...

ここのtracepoint_probe_register()により,ftraceのコールバック関数がトレースポイントに登録されます. 関数を最初に登録する際はsatatic keyの分岐の部分も書き換えます.

ちなみに,call->class->probeというのはDECLARE_EVENT_CLASSによって定義されたtrace_event_raw_event_##callで,これは以下のようになっています.

https://github.com/torvalds/linux/blob/v4.15/include/trace/trace_events.h#L698

static notrace void                            \
trace_event_raw_event_##call(void *__data, proto)          \
{                                   \
    struct trace_event_file *trace_file = __data;          \
    struct trace_event_data_offsets_##call __maybe_unused __data_offsets;\
    struct trace_event_buffer fbuffer;             \
    struct trace_event_raw_##call *entry;              \
    int __data_size;                       \
                                    \
    if (trace_trigger_soft_disabled(trace_file))          \
        return;                           \
                                    \
    __data_size = trace_event_get_offsets_##call(&__data_offsets, args); \
                                    \
    entry = trace_event_buffer_reserve(&fbuffer, trace_file,    \
                 sizeof(*entry) + __data_size);       \
                                    \
    if (!entry)                           \
        return;                           \
                                    \
    tstruct                             \
                                    \
    { assign; }                         \
                                    \
    trace_event_buffer_commit(&fbuffer);                \
}

trace_event_buffer_commit()によってring bufferへ出力をおこないます.

ftraceではtracepointのeventのオンオフだけでなく,eventに応じたトレースの開始/終了の切り替えなどができるようになっています.

tracepointに関しては以下に資料があります.

kprobe (dynamic event)

kprobeはカーネルコード内に動的にフックポイントを追加するための仕組みです. アイディアの基本はフックしたい箇所のコードをブレークポイント命令で書き換えることです. これにより命令単位でカーネル内のほぼ全ての場所のフックが可能になります. (kprobe自身のコードなどはフック不可能です.NOKPROBE_SYMBOLマクロを使うとそのアドレスが_kprobe_blacklistセクションに登録され,そのアドレス範囲に対するkprobeが禁止されます).

kprobeとtracepointを比較すると,kprobeはtracepointの上位互換のような気もしますが,kprobeはアドレス単位でフックをおこなうためバイナリに依存してしまうのに対し,tracepointの方はバイナリ変更の影響を受けません.(ただし,カーネル開発者側的には一度導入したtracepointを保守する責任が発生するといえます). tracepiontの方がデータ構造の取得などは楽かと思います. またブレークポイントのフックの方がtracepintのif文によるフックよりかはオーバヘッドが大きいと思います(といっても影響が出るほど大きくはないと思います). あとはkprobeは動的にコードを書き換えるため,そういう意味ではtracepointの方が安定性があるといえます. とはいってもkprobeも多分本体に導入されてから10年近く経ちますし,特に利用に問題はないかと思います.

kprobeの使い方は,samples/kprobesが参考になります. ブレークポイント箇所の命令を実行する前に呼ばれるpre handlerと,命令実行後に呼ばれるpost handlerを設定してregister_kprobe()を呼びます.

ftraceの観点からみると,tracefsによってkprobeを設定しようとする際,kprobe_events_opsに従ってprobes_write => trace_parse_run_command => trace_run_command の中で createfnのコールバック関数が呼ばれ,結局 create_trace_kprobeが実行されます. ここでregister_trace_kprobe => register_kprobe_event => __register_trace_kprobe の中で register_kprobe()されます.

このときkprobeに登録される関数は,alloc_trace_kprobeの中で tk->rp.kp.pre_handler = kprobe_dispatcher;として kprobe_dispathcerが設定されています. kprobe_dispathcerの中から呼ばれる __kprobe_trace_funcでring bufferへの書き込みをおこなっています.

また,kretprobeという,関数のreturnをフックするための仕組みも提供されています. 関数のreturnをフックしたいというよくあるニーズに答えるために導入されたんだと思います. 実装的には単純にretをkprobeでフックするのではなく,カーネルのentryをkprobeでフックし,その際にスタック上の戻りアドレスを書き換えてret時にkretprobeのトランポリンコードを呼ぶようにしているようです(かしこい).

また,uprobeというkprobeのユーザランド版もあります. ちなみに,uprobeはinodeと紐づける形で登録します.

kprobe/uprobeに関しては以下が参考になります.

その他のtracer

mmio tracerに関して,簡単に処理を追ってみます.

struct tracerでtracefsでやりとりする際の関数の定義をしているようです.

mmiotraceの例

trace-cmd

実際にftraceを利用する場合には,ftraceのフロントエンドであるtrace-cmdが利用できます. ftraceのメンテナであるSteven Rostedt氏が直々に開発しています.

ちなみに,githubのリポジトリの方はかなり古いので注意が必要です.

perf

perfとはLinuxに存在するパフォーマンスモニタリングのための機能です.perf_eventともいいます. perfという名称のユーザスペース用ツールも開発されており,単にperfと言った場合はこのツールを指すことが多いかと思います. ちょっとややこしいので,ここではカーネルの機能の方はperf_eventと書くことにします.

perf(の前進)はもともとPerformance counters for Linux (PCL) という名前だったみたいなので,察するにCPUのperfomance counterへのアクセス手段の提供が当初の目的だったんだと思います. ただし,今ではpermance counter以外のイベントにも対応しています. perf listコマンドによって対応しているイベントの確認ができます.

perf_eventではeventを以下のように分類しています.

  • PERF_TYPE_HARDWARE
  • PERF_TYPE_HW_CACHE
  • PERF_TYPE_RAW
  • PERF_TYPE_SOFTWARE
  • PERF_TYPE_TRACEPOINT
  • PERF_TYPE_BREAKPOINT

hardware, hw_cache, raw がCPUのperfomance counterのイベントに対応します.

perf_event_open(2)システムコールによって,一つのperf eventに対応したfile descriptorが入手できます. このfdに対してread()などをすることでeventのカウンタにアクセスします.

また,perfではイベントのカウンタに2種類あります. 一つはcounting counterで,イベントの発生回数を得るために利用します.read()するとカウンタの値が得られます. もう一つがsampling counterで,このカウンタの場合,N回のイベントごとに設定したコールバック関数を呼びます. perf statで得られるのはcounting counterの値,perf recordで得られるのはsamping counterの結果です.

ちなみにこれは余談ですが,perf_event_open(2)のman pageはシステムコールの中でおそらくもっとも長いです.

% git clone https://github.com/mkerrisk/man-pages && cd man-pages/man2
% find ./ -name "*.2" | parallel wc {} | sort -nr | head
3331 13388 88727 ./perf_event_open.2
2796 12584 78237 ./ptrace.2
2281  9245 58337 ./keyctl.2
2102  9984 58088 ./fcntl.2
1938  9262 58178 ./futex.2
1756  7672 45635 ./open.2
1598  6891 43519 ./prctl.2
1368  6316 37622 ./clone.2
1179  5042 32922 ./bpf.2
1100  5122 33092 ./seccomp.2

ユーザスペースツールのperfの使い方は以下が参考になります.

PERF_TYPE_HARDWARE, HW_CACHE, RAW

これらのイベントはCPUのperfomance counterのアクセスに利用します. perfomance counterとはCPUについているイベントのモニタリング機能のことです. IntelのCPUの場合SDMの18,19章あたりに書いてあります. どんなイベントが取れるのかはCPUごとに異なりますが,特に一般的なイベントをPERF_EVENT_HARDWARE, PERF_EVENT_HW_CACHEに分類しています. PERF_EVENT_HARDWARE, PERF_EVENT_HW_CACHEには以下のようなものがあります.

% sudo perf list | grep -i hardware
  branch-instructions OR branches                    [Hardware event]
  branch-misses                                      [Hardware event]
  bus-cycles                                         [Hardware event]
  cache-misses                                       [Hardware event]
  cache-references                                   [Hardware event]
  cpu-cycles OR cycles                               [Hardware event]
  instructions                                       [Hardware event]
  ref-cycles                                         [Hardware event]
  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  LLC-load-misses                                    [Hardware cache event]
  LLC-loads                                          [Hardware cache event]
  LLC-store-misses                                   [Hardware cache event]
  LLC-stores                                         [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  dTLB-loads                                         [Hardware cache event]
  dTLB-store-misses                                  [Hardware cache event]
  dTLB-stores                                        [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  iTLB-loads                                         [Hardware cache event]
  node-load-misses                                   [Hardware cache event]
  node-loads                                         [Hardware cache event]
  node-store-misses                                  [Hardware cache event]
  node-stores                                        [Hardware cache event]

IntelのCPUの話を少しだけすると,intelのcpuではperfomance counterのeventをarchitectural performance eventsとnon-architectural performance events (model-specific performance events)の二つに分けています. architectgural performance counter(クロック数とか)の値はIA32_FIXED_CTR[0-2]レジスタから取得可能です. それ以外のeventは,IA32_PERFEVTSELxレジスタでどのeventを取りたいかを設定します. そのイベントの結果はIA32_PMCxに格納されます. これらのレジスタは全てMSRです.従って,wrmsr/rdmsrでアクセスします. また,CR4.PCE (Performance-Monitoring Counter enable) = 1のとき,rdpmc命令を使ってユーザランドからIA32_PMCxの値を読むことが可能です.rdpmcの方がrdmsrよりも早いらしいです(参考).

IA32_PMCxレジスタの数は限られています.CPUによりますが,2個とか4個とか6個とかです. そこでperfではレジスタ数以上のイベントを記録する場合,ラウンドロビンによって適当な時間間隔でレジスタを共有します. 従って,最終的に得られる値はあくまで推定値となります. 正確な値が必要な場合には取得するイベントを絞る必要があります. このあたりはperf wikiに書いてあります.

perfomance counterにはオーバーフローすると割り込みを発生させる機能があります. sampling counterはこれを利用します. 特に,クロック数などのイベントを基準として,適当な間隔で割り込みを発生させ,割り込み発生時のripの記録を取ることでプロファイリングができます.

PERF_EVENT_HARDWARE, PERF_EVENT_HW_CACHE以外のCPU固有のイベントにアクセスするにはPERF_TYPE_RAWを利用して直接イベントの番号を指定します. ちなみに,Linux 4.10付近からプロセッサ毎の固有のPMU eventを名前で参照できるようになっています. perf listしたときにKernel PMU Eventと書かれているものがこれです. この情報はtools/perf/pmu-event/arch/以下のjsonファイルで定義されているようです.

PERF_TYPE_SOFTWARE

perf listしたときにsoftware eventと表示されるやつです.context switchなどがあります.

% sudo perf list | grep -i software
  alignment-faults                                   [Software event]
  bpf-output                                         [Software event]
  context-switches OR cs                             [Software event]
  cpu-clock                                          [Software event]
  cpu-migrations OR migrations                       [Software event]
  dummy                                              [Software event]
  emulation-faults                                   [Software event]
  major-faults                                       [Software event]
  minor-faults                                       [Software event]
  page-faults OR faults                              [Software event]
  task-clock                                         [Software event]

これはどうなっているのかというと,それぞれのイベント箇所で明示的にperf_sw_event()を呼んでいます.

perf_sw_event() => __perf_sw_event() => ___perf_sw_event => do_pwerf_sw_event => perf_swevent_event

PERF_TYPE_TRACEPOINT

ftraceでも使われていたtracepoint, kprobe, uprobeなどのイベントがPERF_TYPE_TRACEPOINTです. kprobeやuprobeはftraceによって登録されたコールバック関数(kprobe_dispatcher, uprobe_dispatcher)の中からperf_trace_buf_submit()が呼ばれています. tracepointの場合はdefine_trace.hの中で,perf.hが呼ばれ,この中でperf_event用のtracepointのコールバック関数が定義されています. このコールバック関数の登録はftraceのコールバック関数のところでおこなっています.

https://github.com/torvalds/linux/blob/v4.15/kernel/trace/trace_events.c#L305

int trace_event_reg(struct trace_event_call *call,
            enum trace_reg type, void *data)
{
    struct trace_event_file *file = data;

    WARN_ON(!(call->flags & TRACE_EVENT_FL_TRACEPOINT));
    switch (type) {
    case TRACE_REG_REGISTER:
        return tracepoint_probe_register(call->tp,
                         call->class->probe,
                         file);
    case TRACE_REG_UNREGISTER:
        tracepoint_probe_unregister(call->tp,
                        call->class->probe,
                        file);
        return 0;

#ifdef CONFIG_PERF_EVENTS
    case TRACE_REG_PERF_REGISTER:
        return tracepoint_probe_register(call->tp,
                         call->class->perf_probe,
                         call);
        ...

もう少し具体的には,以下のようになっています.

perf_trace_buf_submit() => perf_tp_event => perf_swevent_event

となり処理が継続されます.

PERF_TYPE_BREAKPOINT

これはハードウェアブレークポイントに対応したイベントです.

普通はkprobeやuprobeを使えばいいので使用例がほとんどど見つかりませんが,以下のように利用できます.

% sudo cat /proc/kallsyms| grep sys_brk
ffffffffbb400930 T sys_brk
% sudo perf stat -e mem:0xffffffffbb400930:x ls
bin  perf.data work

 Performance counter stats for 'ls':

                 3      mem:0xffffffffbb400930:x

       0.001000937 seconds time elapsed

具体的なフォーマットはman page参照.

ブレークポイントの設定部分は,ptraceなどからも利用されるようです.

USDT (SDT Event)

perf_eventとは直接は関係ないですが,USDTあるいはSDTと呼ばれるユーザスペースのプログラムでtracepointのようなトレースを実現する方法があります. これはもともとはDtraceで存在していた機能のようで,SystemTapがサポートしています. また,perfも最近対応しています(https://lwn.net/Articles/618956/).

プログラムのソースコードの適当な箇所にUSDTのprobeを埋め込むと,それ自体はnopとしてコンパイルされます. USDTの情報がELFの.note.stapsdtセクションに格納されるので,後からその情報を利用してuprobeでフックすれば目的の箇所でのフックができます. perf buildid-cacheコマンドで,.note.stapsdtの情報に基づいてイベントが追加できます.

SDTのイベントはperf listでSDT eventとして見えます.

% sudo perf list | grep SDT
  sdt_libc:lll_lock_wait_private                     [SDT event]
  sdt_libc:longjmp                                   [SDT event]
  sdt_libc:longjmp_target                            [SDT event]
  sdt_libc:memory_arena_new                          [SDT event]
  sdt_libc:memory_arena_retry                        [SDT event]
  sdt_libc:memory_arena_reuse                        [SDT event]
  sdt_libc:memory_arena_reuse_free_list              [SDT event]
  sdt_libc:memory_arena_reuse_wait                   [SDT event]
...

perf-tools

perfとftraceを利用したパフォーマンス解析ツールとして,perf-toolsがあります. perfやftrace (tracefs)のラッパーとして動作します.(perf-toolsという名称ですが,ftraceも使っています).

ただし,今ではbccのツールでperf-toolsでできたことは全てできるんじゃないかと思います.

perf ftrace

若干ややこしいですが,perfコマンドにもftraceのラッパーが含まれており,perf ftraceコマンドで利用できます. perf ftrace recordとして簡単にfunction traceの結果が記録できます.

straceとの比較

システムコール呼び出しのトレースをおこなうstraceやライブラリ関数呼び出しのトレースをおこなうltraceといったコマンドがありますが,これらはいずれもptraceを使用しています. straceの場合,システムコール発行/終了時にSIGTRAPを送信します. ltraceの場合はライブラリ関数呼び出しのPLT部分をブレークポイントでフックし,SIGTRAPを送信します.

perfを使ってシステムコールをトレースすることは可能ですが,straceと比べてperfはシグナルを介さずに記録を取ることができるため,高速に動作します. 以下に簡単な例を示します.

strace

% time strace -eaccept dd if=/dev/zero of=/dev/null bs=1 count=500k
512000+0 records in
512000+0 records out
512000 bytes (512 kB, 500 KiB) copied, 18.8414 s, 27.2 kB/s
+++ exited with 0 +++
strace -eaccept dd if=/dev/zero of=/dev/null bs=1 count=500k  2.67s user 20.21s system 121% cpu 18.847 total

perf

% time perf record -e 'syscalls:sys_enter_accept' dd if=/dev/zero of=/dev/null bs=1 count=500k
512000+0 records in
512000+0 records out
512000 bytes (512 kB, 500 KiB) copied, 0.490177 s, 1.0 MB/s
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.013 MB perf.data ]
perf record -e 'syscalls:sys_enter_accept' dd if=/dev/zero of=/dev/null   0.20s user 0.40s system 93% cpu 0.635 total

その他

  • /proc/sys/kernel/perf_event_paranoidの値でperfの実行にCAP_SYS_ADMINが必要かどうか設定できます.
2   allow only user-space measurements (default since Linux 4.6).
1   allow both kernel and user measurements (default before Linux 4.6).
0   allow access to CPU-specific data but not raw tracepoint samples.
-1  no restrictions.

perfとeBPF

Linux Kernel 4.1以降,perfのeventに対してeBPFのプログラムがアタッチできるようになっています. 具体的には,以下のカーネルのバージョンで機能が追加されています.

BPFプログラムはperf_event_open()で得られたfdに対してioctl(fd, PERF_EVENT_IOC_SET_BPF, prog_fd);を実行してアタッチします. kprobe, uprobeやtracepointはperf側へイベントを渡す際にtrace_call_bpf()を呼ぶようになっています.カウンタオーバフロー時にbpfプログラムが呼ばれる訳ではないです.(そういう意味ではperfのeventにアタッチしているというよりかは,kprobe等に直接アタッチしているといった方が適切かもしれないです). ブレークポイントイベントに対してはBPFプログラムはアタッチできないようです.

BPFプログラムからは,bpf_trace_printk()を利用してftraceのring bufferへの出力,bpf_perf_event_output()でperfのring bufferへの出力ができます.

sample/bpf/trace_output_kern.c, sample/bpf/trace_output_user.cにBPF側からperfのring bufferに出力するサンプルがあります. 概略は以下の通りです.

  1. BPF_MAP_TYPE_PERF_EVENT_ARRAYのbpf arrayを作成 (trace_output_kern.c#6)
  2. ユーザランド側でperf_event_attr.type = PERF_TYPE_SOFTWARE, .config = PERF_COUNT_SW_BPF_OUTPUTとしてperf_event_open (trace_output_user.c#L162)
  3. bpf_map_update_elem()でBPF arrayとperf のfdとの対応付 (trace_output_user.c#L165)
  4. perf のfdに対してmmap (trace_output_user.c#L41)
  5. BPFプログラム側ではbpf_perf_event_outputを使って出力 (trace_output_kern.c#24)

また,sample/bpf/tracex6_kern.c, sample/bpf/tracex6_user.cにBPF側からperfのカウンタにアクセスする例があります.

実際にこれらの機能を利用する場合はbccを利用するのがいいかと思います.

perfコマンドからも,イベントをBPFのプログラムでフィルタリングできるようになっています (参考)

ftraceとeBPF

Linux 4.15の時点ではftarceのイベントに対してeBPFプログラムはアタッチできません. 昨年末にBPF_PROG_TYPE_FTRACEの提案がありましたが(パッチ),これはBPFをトレースのオンオフの切り替えだけに使うという限定されてたものだったという点や,そもそもパッチ自体にいろいろ問題があったということで採用にはいたってません. 今後追加される可能性は十分あるんじゃないかと思います.

その他トレーシングツール

perfやftraceはカーネルと共に開発されていますが,その他独自に開発されているトレーシングツールがいくつかあります. (主にカーネルモジュールの形で利用します).

特に有名なのがSystemTapで,kprobeやuprobe, tracepointなどに対応し,SystemTap Scriptという形で実質的にC言語でフックした箇所に処理が追加できるのでかなり自由度が高いと思います. もちろんその分安全性には気をつける必要はあります.また,最近bpfのバックエンドも追加されたようです. 他にも代表的なツールにLTTngがあります. SystemTapもLTTngも2000年代からずっと開発されているのでいろいろとツールが揃っていると思います. SystemTapwikisystemtap, dtrace, LTTng, perfの比較があります. あんまりSystemTapを使ったことがないのではっきりとは分かりませんが,多分最近になってようやくftrace, perf, eBPFでSystemTapでできたことの多く(+α)ができるようになってる感じなんじゃないかと思います.

また,特に組み込み向けの軽量なトレーシングツールとしてLuaを使ったdynamic tracingができるktapというのがあります(Huaweiが開発). ただ,これはLinux本体にマージされそうになるも丁度eBPFのマージとぶつかったりして結局マージされず,今では更新は止まっているみたいです.

最近も開発されているトレーシングツールとしてはsysdigというのもあります. これは公式曰く "sysdig as strace + tcpdump + htop + iftop + lsof + wireshark" で,カーネルトレースの用途などには使えませんが,コンテナサポートを全面に押し出しているものなのでそういう用途には便利かもしれないです.

まとめ

perfやftrace周りの処理の概要について簡単に書きました.

結局のところどれを使えばいいんだという話ですが,まぁ自分が好きなのを使えばいいんじゃないでしょうか(ぉ. とりあえず,performance counterの値を知りたいのならperf,カーネルコードのちゃんとしたトレースを取るならftraceですし,あとは今ならbccのツールで手軽に目的のことができるんことが多いんじゃないかなと思います. 場合によってはSystemTapやLTTngも見てみるといいと思います.