perf, ftraceのしくみ
Linuxのトレーサーであるperfやftraceのツールの使い方に関する情報は結構ありますが,構造に関してはあまり見つけられなかったため,ここに簡単に調べたことをまとめようかと思います.(ツールの使い方の説明はあんまりしないです.)
この文章はLinux 4.15のソースに基づいています.
全体像
そもそもLinuxのトレーサーとひとえに言ってもperfとかftraceとかkprobeとかuprobeとかいろいろありすぎて一体どうなっているんだという感じなので簡単に関係を図示しています.
実際はいろいろと複雑に絡み合ってるのでなかなか可視化するのが難しいですが,まぁ一つの見方だと思ってください.
大雑把には以下のように分類できます.
- ユーザランドのツール
- 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
- 割り込み停止時間トレース
- ...
- wakeup
- 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の使い方は以下が参考になります.
- ftrace - Function Tracer, https://github.com/torvalds/linux/blob/v4.15/Documentation/trace/ftrace.txt
- Steven Rostedt, Debugging the kernel using Ftrace - part 1, https://lwn.net/Articles/365835/, 2009.
- Steven Rsotedt, Secrets of the Ftrace function tracer, https://lwn.net/Articles/370423/, 2010.
- mhiramat, Ftraceでカーネルの一部の処理を追いかける方法, https://qiita.com/mhiramat/items/42a6af4f3c289ad37095, 2016.
- Andrej Yemelianov, Kernel Tracing with Ftrace, https://blog.selectel.com/kernel-tracing-ftrace/, 2017.
以下では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倍以上になっています. (ただし,実際にトレースする際は,全ての関数をトレースしても訳がわからなくなるので,フィルタリングを掛けたり一部の処理部分だけトレースを有効化すると思います).
もう少し具体的な構造の説明は以下が参考になります.
- function tracer guts, https://github.com/torvalds/linux/tree/master/Documentation/trace/ftrace-design.txt
- Steven Rostedt, Ftrace Kernel Hooks: More than just tracing, https://www.linuxplumbersconf.org/2014/ocw/system/presentations/1773/original/ftrace-kernel-hooks-2014.pdf, 2014.
- mcountの動的書き換え方法
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に関しては以下に資料があります.
- Using the Linux Kernel Tracepoints, https://github.com/torvalds/linux/blob/master/Documentation/trace/tracepoints.txt
- Event Tracing, https://github.com/torvalds/linux/blob/master/Documentation/trace/events.txt
- Steven Rostedt, Using the TRACE_EVENT() macro (Part 1), https://lwn.net/Articles/379903/, 2010.
- Steven Rostedt, Using the TRACE_EVENT() macro (Part 2), https://lwn.net/Articles/381064/, 2010.
- Steven Rostedt, Using the TRACE_EVENT() macro (Part 3), https://lwn.net/Articles/383362/, 2010.
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に関しては以下が参考になります.
- Sudhanshu Goswami, An introduction to KProbes, https://lwn.net/Articles/132196/, 2005.
- Kprobe-based Event Tracing, https://github.com/torvalds/linux/blob/v4.15/Documentation/trace/kprobetrace.txt
- kretprobe: Linuxの備忘録とか・・・, http://wiki.bit-hive.com/north/pg/kretprobe, 2012.
- Uprobe-tracer: Uprobe-based Event Tracing, https://github.com/torvalds/linux/blob/v4.15/Documentation/trace/uprobetracer.txt
その他のtracer
mmio tracerに関して,簡単に処理を追ってみます.
struct tracerでtracefsでやりとりする際の関数の定義をしているようです.
- 初期化
- ioremap, iounmap時にtraceする
ioremap_trace_core
=>__trace_mmiotrace_map()
=>mmio_trace_mapping()
call_filter_check_discard
でフィルタリング
- この後
trace_buffer_unlock_commit()
=>trace_buffer_unlock_commit_regs()
=>__buffer_unlock_commit()
=>ring_buffer_write()
でring bufferへ書き込み
trace-cmd
実際にftraceを利用する場合には,ftraceのフロントエンドであるtrace-cmdが利用できます. ftraceのメンテナであるSteven Rostedt氏が直々に開発しています.
- man page of trace-cmd, http://man7.org/linux/man-pages/man1/trace-cmd.1.html
- Steven Rostedt, Ftrace Profiling, https://events.static.linuxfound.org/sites/events/files/slides/collab-2015-ftrace-profiling.pdf, 2015.
ちなみに,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の使い方は以下が参考になります.
- https://github.com/torvalds/linux/tree/master/tools/perf/Documentation
- perf toolのドキュメント (man page)
- Brendan Gregg, perf Examples, http://www.brendangregg.com/perf.html
- 神資料
- perf wiki, https://perf.wiki.kernel.org/index.php/Main_Page
- Paul J. Drongowski, PERF tutorial: Finding execution hot spots, http://sandsoftwaresound.net/perf/perf-tutorial-hot-spots/
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); ...
もう少し具体的には,以下のようになっています.
- kprobe
kprobe_dispatcher
=>kprobe_perf_func
=>perf_trace_buf_submit
- uprobe
uprobe_dispatcher
=>uprobe_perf_func
=> ... =>perf_trace_buf_submit
- tracepoint
DECLARE_EVENT_CLASS
=>perf_trace_run_bpf_submit
=>perf_tp_event
- 後述するeBPFプログラムの呼び出しと共通化されている.
- syscall
perf_syscall_enter
=>perf_trace_buf_submit
perf_syscall_exit
=>perf_trace_buf_submit
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のプログラムがアタッチできるようになっています. 具体的には,以下のカーネルのバージョンで機能が追加されています.
- 4.1: kprobe (commit)
BPF_PROG_TYPE_KPROBE
kprobe_perf_func
=>trace_call_bpf
- 4.3: uprobe (commit)
BPF_PROG_TYPE_KPROBE
- prog typeはkprobeのものを利用
uprobe_perf_func
=>trace_call_bpf
- 4.7: tracepoint (commit)
BPF_PROG_TYPE_TRACEPOINT
DECLARE_EVENT_CLASS
=>perf_trace_run_bpf_submit
=>trace_call_bpf
- sysenter, sysexitは他のtracepointと扱いが異なるため,特別な処理が必要 (c.f. bpf: add support for
sys_enter_*
andsys_exit_*
tracepoints
- 4.9: perf software / hardware event (commit)
BPF_PROG_TYPE_PERF_EVENT
__perf_event_overflow
=>READ_ONCE(event->overflow_handler)(event, data, regs);
=>bpf_overflow_handler
- sampling counterがオーバーフローした際にbpfプログラムが呼ばれる
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に出力するサンプルがあります. 概略は以下の通りです.
BPF_MAP_TYPE_PERF_EVENT_ARRAY
のbpf arrayを作成 (trace_output_kern.c#6)- ユーザランド側で
perf_event_attr.type = PERF_TYPE_SOFTWARE, .config = PERF_COUNT_SW_BPF_OUTPUT
としてperf_event_open
(trace_output_user.c#L162) bpf_map_update_elem()
でBPF arrayとperf のfdとの対応付 (trace_output_user.c#L165)bpf_map_update_elem()
=>bpf_fd_array_map_update_elem()
map_fd_get_ptr()
はperf_event_fd_array_get_ptr
(perf_event_array_map_ops
で定義)bpf_event_entry_gen
でbpf_map_update_elemU()
の引数で渡したperf eventのfdに対応するperf_fileを格納array->ptrs[index]
にその情報が保存される
- perf のfdに対してmmap (trace_output_user.c#L41)
- 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年代からずっと開発されているのでいろいろとツールが揃っていると思います. SystemTapのwikiにsystemtap, 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も見てみるといいと思います.