BPFマップを利用したBPFプログラムとユーザーランドツールの間のデータの送受信
まず最初に,bpf_
でトレースバッファーに出力していましたが,
- ※1
- 厳密に言うと,
第690回ではPERFイベントのバッファーを使っていて, 今回はよりジェネリックなリングバッファーを使っています。PERFイベントバッファーはCPUごとにバッファーが独立しているのに対して, リングバッファーはすべてのCPUで同じイベントを共有しています。リングバッファーのほうがモダンで汎用性が高く効率の良い実装になっているため, 今後はリングバッファーを利用することが推奨されているようです。ちなみにlibbpf側のPERFイベントバッファー向けAPIはバージョン0. 5あたりから大きく改造が入るようになりました。よってUbuntu 21. 10のlibbpf 0. 4でうまくいったコードも, より新しい環境でビルドすると 「FOO is deprecated」 の警告が出るようになるかもしれません。
この方法がわかれば,
最初に比較的簡単な,execsnoop.
)
/* ユーザランドプログラムと送受信するデータ構造 */
struct event {
pid_t pid;
};
/* データを格納するリングバッファー */
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024 /* 256 KiB */);
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int syscalls__execve(struct trace_event_raw_sys_enter *ctx)
{
struct event event = {};
/* execve()呼び出し元のスレッドグループIDの取得 */
event.pid = bpf_get_current_pid_tgid() >> 32;
/* リングバッファーに保存 */
bpf_ringbuf_output(&events, &event, sizeof(event), 0);
return 0;
}
ポイントは次のとおりです。
- BPFプログラムとユーザーランドの間でやりとりするデータ構造を決めておく
- そのデータは
.maps
セクションで定義されたバッファーに保存する
今回はstruct event
」.maps
セクションにその情報を保存します。実際に確保するのはカーネルの役目です。BPF_
でリングバッファーであることを明示し,max_
でバッファーのサイズを指定しています。このサイズはbpf_
で変更可能です。
bpf_
は第690回でも出てきましたね。実行中のタスクのPID/
作成したデータは,bpf_
でリングバッファーに保存します。引数は順番に,
最後のフラグはユーザーランドにイベントを通知する際のタイミングをコントロールする場合に使われます。具体的にはリングバッファーに保存したタイミングでユーザーランドにシグナルを送るのか,
次にユーザーランド側のプログラムを確認してみましょう。
/* ユーザランドプログラムと送受信するデータ構造 */
struct event {
pid_t pid;
};
/* リングバッファーからデータ受信時の処理 */
int handle_event_cb(void *ctx, void *data, size_t size)
{
struct event *event = data;
fprintf(stdout, "% 8d\n", event->pid);
return 0;
}
int main(void)
{
struct execsnoop_bpf *obj;
struct ring_buffer *rb = NULL;
int err;
obj = execsnoop_bpf__open();
if (!obj) {
fprintf(stderr, "failed to open BPF object\n");
return 1;
}
if (execsnoop_bpf__load(obj)) {
fprintf(stderr, "failed to load BPF object\n");
goto cleanup;
}
if (execsnoop_bpf__attach(obj)) {
fprintf(stderr, "failed to attach BPF object\n");
goto cleanup;
}
/* リングバッファーのオープン */
rb = ring_buffer__new(bpf_map__fd(obj->maps.events), handle_event_cb, NULL, NULL);
if (!rb) {
rb = NULL;
fprintf(stderr, "failed to open ring buffer: %d\n", err);
goto cleanup;
}
fprintf(stdout, "PID\n");
for (;;) {
/* リングバッファーをポーリングする処理 */
err = ring_buffer__poll(rb, 100 /* ms of timeout */);
if (err < 0 && errno != EINTR) {
fprintf(stderr, "failed perf_buffer_poll(): %s\n", strerror(errno));
goto cleanup;
}
}
cleanup:
/* リングバッファーの解放 */
ring_buffer__free(rb);
execsnoop_bpf__destroy(obj);
return 0;
}
こちらのポイントは次のとおりです。
- リングバッファーを開いてポーリングする
- データを受信したときのコールバック関数を用意する
リングバッファーはobj->maps.バッファー名
」.maps
セクション上のシンボル名です。ユーザーランドプログラムでは,bpf_
でファイルディスクリプターとして開きます。ring_
にはこのファイルディスクリプターの他に,
コンテキストはコールバックの第一引数にそのまま渡されます。オプションは現状では何も使っていないようです。よってここではともにNULLを指定しています。
ring_
でポーリングを行います。何かイベントが届いたら,epoll_
のそれと同じです。つまり-1
」
データ受信時の処理を担う,size
をチェックする必要があるのですが,struct event
だと決め打ちして,
ここまでの変更点は前述のリポジトリで,v1.
タグを付けています。よってリポジトリをclone済みなら,
$ git checkout r695/1 v1.1
あとは実際にビルドして試してみましょう。
$ make $ sudo ./execsnoop PID 3967300 3967301 3967302
ひたすらPIDだけが表示されるようになりました。ちなみにbpf_
を呼ばないようにしたため,sudo cat /sys/
」