eBPF で Linux の動作を観察する

Linux 上でプログラムを動かしていると、「ネットワーク通信はどれくらいの頻度で起こっているのか」「ファイルの読み込みはいつ発生しているのか」といったことが気になることがあります。 今回は eBPF を使って、そのような Linux 内部の動きを観察します。まず sched_switch が起きたらフラグを立てる小さなプログラムを作り、その後で発展的な利用方法と観測対象を整理します。 最後に、実用例として bpftrace と、自分が作っている procinsh を紹介します。

1. eBPF とは何か

eBPF は、Linux kernel にプログラムをロードし、決められた地点で実行させるための仕組みです。 例えばスレッドが切り替わる地点にプログラムを attach すると、そのイベントが発生するたびに kernel 内で自分の処理が呼ばれます。 イベントの引数を読み、時刻を記録し、kernel 内で集計してから userspace に結果を渡せます。 ロード、attach、解放までの流れは libbpf の公式解説 にまとまっています。

2. eBPF を使ってみる

使用環境は Ubuntu 26.04 LTS(x86_64)、Linux kernel 7.0.0-15-generic、clang 21.1.8、libbpf 1.6.3、bpftool 7.7.0 です。 本文の kernel ソースは、この Ubuntu kernel のベースである Linux v7.0 を参照します。 実行するマシンは Linux 環境で管理権限を使えることを前提にしています。

sched_switch を確認する

tracepoint は、kernel のソースコードにあらかじめ置かれた観測用のフックです。 そこに処理を登録すると、kernel がその地点を通るたびに登録した処理が呼ばれます。 eBPF プログラムをこのフックに attach できます(tracepoint の公式解説)。

sched_switch は、scheduler が現在のタスクから次のタスクへ切り替える際の tracepoint です。 Linux v7.0 では、__schedule() の trace_sched_switch() 呼び出し の直後に context_switch() があります。 イベントには prev_pid、next_pid、それぞれの名前と優先度、切り替え前の状態などが含まれます。 定義と各フィールドへの代入は include/trace/events/sched.h で確認できます。 今回はイベントの引数は読み取らず、イベントが起きて BPF が実行されたことだけを確認します。

eBPF 側のコード

サンプル一式は articles/ebpf-hello/ にあります。 hello.bpf.c は次のコードだけです。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

int ran;

SEC("tracepoint/sched/sched_switch")
int on_switch(void *ctx)
{
    ran = 1;
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

SEC("tracepoint/sched/sched_switch") で attach 先を指定します。 sched_switch が起きると on_switch() が呼ばれ、初期値 0 の ran を 1 にします。 ctx にはイベントの情報が渡されますが、ここでは使いません。

userspace 側のコード

hello.c は、BPF プログラムをロードして attach し、1 秒後に ran を読みます。

#include <stdio.h>
#include <unistd.h>
#include "hello.skel.h"

int main(void)
{
    struct hello_bpf *skel = hello_bpf__open_and_load();
    if (!skel)
        return 1;

    int err = hello_bpf__attach(skel);
    if (!err) {
        sleep(1);
        int ran = skel->bss->ran;
        puts(ran ? "sched_switch: BPF ran!" : "sched_switch: no event observed");
        err = !ran;
    }
    hello_bpf__destroy(skel);
    return err ? 1 : 0;
}

hello_bpf__open_and_load() でロードし、hello_bpf__attach() でイベントに結び付けます。 sleep(1) の間にも scheduler は動くので、BPF が実行されれば ran が 1 になります。 hello_bpf__destroy() で detach して終了します。

hello.skel.h は bpftool gen skeleton が生成するヘッダです。 BPF のバイトコードとロード用の関数を含み、BPF 側のグローバル変数にも skel->bss->ran としてアクセスできます。 生成結果の全体は hello.skel.h に置きました。 struct hello_bpf 内の ran に関係する定義を抜き出すと、次のようになります。

struct hello_bpf__bss {
    int ran;
} *bss;

この bss を通して、BPF 側の int ran を userspace から読めます。 リンク先は使用環境での生成例で、run.sh は実行のたびにヘッダを生成します。 詳細は libbpf の skeleton の説明 と bpftool のマニュアル にあります。

sched_switch
    ↓
BPF: ran = 1
    ↓ グローバル変数を保持する BPF Map
userspace: ran を読む
    ↓
"sched_switch: BPF ran!"

ビルド、実行する

Ubuntu 26.04 で依存パッケージをインストールし、リポジトリのルートで実行します。

sudo apt install build-essential clang pkg-config libbpf-dev libelf-dev zlib1g-dev \
    linux-tools-common "linux-tools-$(uname -r)"
bash articles/ebpf-hello/run.sh

run.sh が BPF オブジェクト、skeleton、userspace の実行ファイルを順番にビルドし、sudo で実行します。 イベントを受け取れれば、次のメッセージを表示して終了します。

sched_switch: BPF ran!

3. eBPF の発展的な利用方法

BPF Map と userspace

今回の ran は、BPF プログラムと userspace の間で状態を共有しています。 このグローバル変数を保持する Map は libbpf が用意し、skeleton を通して userspace に memory map します(libbpf の説明)。

より多くの状態を保持したい場合は、用途に合わせて Map を定義します。

Map 形
Hash key から value を引く
Array 0 から始まる整数 index
Per-CPU Map CPU ごとに別の value を持つ Hash / Array など

仕様は Hash と Array の公式ドキュメント、実装は hashtab.c と arraymap.c にあります。 userspace からは bpf() system call を通じて Map の値を取得・更新することもできます(Map の API)。 集計値ではなく個々のイベントを送りたいときには、BPF ring buffer も使えます。

eBPF では何を観測できるのか

sched_switch 以外にも、次のようなポイントを観測できます。

レイヤ 観測したいこと attach 先の例・ソース
process fork / exec / exit sched_process_fork / sched_process_exec / sched_process_exit(sched.h)
CPU / scheduler 実行時間、context switch、実行待ち時間 sched_switch / sched_wakeup(sched.h)
syscall enter / exit、引数、戻り値、latency raw_syscalls:sys_enter / sys_exit(syscalls.h)
file I/O open / read / write / close syscall のイベント、VFS 関数への kprobe / fentry(read_write.c)
block I/O request / completion / latency block_rq_issue / block_rq_complete(block.h)
network connect / accept、再送、packet、socket syscall、tcp_retransmit_skb(tcp.h)、XDP / tc
memory page fault / allocation / reclaim x86 の page fault tracepoint、kmem.h、vmscan.h
kernel function 特定関数の引数・戻り値・所要時間 vfs_read() への kprobe / kretprobe、fentry / fexit
userspace function アプリや共有ライブラリの関数呼び出し fopen() への uprobe / uretprobe(attach の例)

4. eBPF の実例

bpftrace

bpftrace は、eBPF を使って Linux を動的にトレースするための高水準言語とツールです。 tracepoint、kprobe、uprobe、perf event などを短いスクリプトから利用でき、scheduler、system call、I/O、userspace の関数呼び出しなどを観測できます(bpftrace のドキュメント)。

例えば、この記事で使った sched_switch も次の 1 行で観測できます。

sudo bpftrace -e 'tracepoint:sched:sched_switch { @switches = count(); }'

実行中に sched_switch が発生するたびに @switches が増え、終了時に合計が表示されます。 今回のサンプルでは C で eBPF プログラムを書き、libbpf でロードして attach しましたが、bpftrace では同じ tracepoint への attach と集計を短い記述で行えます。 sched_switch を使った例は One-Liner Tutorial の Scheduler Tracing にもあります。

procinsh

procinsh は、自分が作っている Linux の活動を 3D で眺めるためのソフトウェアです。 ここではファイル I/O やソケット通信のイベントを eBPF で取得し、ホスト上でデータが流れる様子を可視化しています(ファイル I/O の実装、ソケット通信の実装)。下の画像で白い丸で表示されているデータは eBPF 経由で取得したデータです。

eBPF 経由で取得した通信の様子

5. まとめ

eBPF を使うと、Linux 内部のイベントに処理を attach して、自分が必要な情報を集められます。 今回の例では sched_switch に attach し、BPF 側でフラグを立て、userspace から読んで実行を確認しました。