Ubuntu Weekly Recipe

第695回 入門BPF CO-RE

この記事を読むのに必要な時間:およそ 10.5 分

BPFマップを利用したBPFプログラムとユーザーランドツールの間のデータの送受信

まず最初に,ここまではBPFプログラムの結果をbpf_printk()でトレースバッファーに出力していましたが,普段はユーザーランドに渡して,必要に応じて出力したいところです。そこで第690回と同じようにリングバッファーに出力してみましょう※1⁠。

※1
厳密に言うと,第690回ではPERFイベントのバッファーを使っていて,今回はよりジェネリックなリングバッファーを使っています。PERFイベントバッファーはCPUごとにバッファーが独立しているのに対して,リングバッファーはすべてのCPUで同じイベントを共有しています。リングバッファーのほうがモダンで汎用性が高く効率の良い実装になっているため,今後はリングバッファーを利用することが推奨されているようです。ちなみにlibbpf側のPERFイベントバッファー向けAPIはバージョン0.5あたりから大きく改造が入るようになりました。よってUbuntu 21.10のlibbpf 0.4でうまくいったコードも,より新しい環境でビルドすると「FOO is deprecated」の警告が出るようになるかもしれません。

この方法がわかれば,BPFプログラムから任意のデータ構造をユーザーランドが受け取れるようになります。

最初に比較的簡単な,BPFプログラムexecsnoop.bpf.c側の変更です。ここでは変更された箇所だけ抽出しています。

/* ユーザランドプログラムと送受信するデータ構造 */
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としてデータ構造を作りました。この時点ではPIDのみを保存します。リングバッファーは.mapsセクションにその情報を保存します。実際に確保するのはカーネルの役目です。BPF_MAP_TYPE_RINGBUFでリングバッファーであることを明示し,max_entriesでバッファーのサイズを指定しています。このサイズはbpf_map__set_max_entries()で変更可能です。

bpf_get_current_pid_tgid()第690回でも出てきましたね。実行中のタスクのPID/TGIDを取得するAPIで,今回はスレッドグループのIDを表示する想定で,上位32bitの値を使っています。

作成したデータは,bpf_ringbuf_output()でリングバッファーに保存します。引数は順番に,リングバッファーのポインタ,保存するデータのポインタ,データのサイズ,フラグです。

最後のフラグはユーザーランドにイベントを通知する際のタイミングをコントロールする場合に使われます。具体的にはリングバッファーに保存したタイミングでユーザーランドにシグナルを送るのか,通知を送らないのかを決めます。0を指定した場合は,バッファーの状況に応じて適切にシグナルを送ります。基本は0のままで良いはずです。

次にユーザーランド側のプログラムを確認してみましょう。

/* ユーザランドプログラムと送受信するデータ構造 */
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.バッファー名でアクセスできます。このバッファー名はBPFプログラム側で指定した.mapsセクション上のシンボル名です。ユーザーランドプログラムでは,このリングバッファーをbpf_map__fd()でファイルディスクリプターとして開きます。ring_buffer__new()にはこのファイルディスクリプターの他に,受信時のコールバック関数,コンテキスト,オプションを指定します。

コンテキストはコールバックの第一引数にそのまま渡されます。オプションは現状では何も使っていないようです。よってここではともにNULLを指定しています。

ring_buffer__poll()でポーリングを行います。何かイベントが届いたら,指定したコールバック関数が呼び出されるというわけです。タイムアウト値はepoll_wait()のそれと同じです。つまり-1を指定したらイベントが来るまでずっと待ち続けます。

データ受信時の処理を担う,コールバック関数側は受け取ったデータを処理する部分です。リングバッファーでは可変長のデータが届くこともあるため,本来はsizeをチェックする必要があるのですが,今回は特に何もしていません。単に受け取ったデータをstruct eventだと決め打ちして,その中身を表示しています。

ここまでの変更点は前述のリポジトリで,v1.1タグを付けています。よってリポジトリをclone済みなら,次の方法でも取得可能です。

$ git checkout r695/1 v1.1

あとは実際にビルドして試してみましょう。

$ make
$ sudo ./execsnoop
PID
 3967300
 3967301
 3967302

ひたすらPIDだけが表示されるようになりました。ちなみにbpf_printk()を呼ばないようにしたため,同時に別端末でsudo cat /sys/kernel/debug/tracing/trace_pipeを実行しても何も表示されなくなったはずです。

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。