LinuxのBPFとbccでデバッグする

最近公開されたスライドでLinuxのパフォーマンスチューニングとかDTraceで有名なBrendan GreggさんのLinux BPF Superpowersが面白かったのでBPFとbccに手を出してみました。

f:id:masami256:20160308002457p:plain

BPFLinuxカーネルのパケットフィルタリングの機能で、その名の通りBerkeley Packet Filterです。bcc「BPF Compiler Collection」というのはBPFを使いやすくするためのツールです。 BPFを使う場合、一番基本的なのはbpf(2)を使ってc言語で書くことだと思います。bccpythonc言語でBPFを使うようになっています。 (´-`).oO(BPFとbccの関係というのはkprobesとsystemtapの関係に近いような気がします

で、Berkeley Packet Filterでデバッグってなんだよ?ってなると思うのですが、それは正解ですね。ほんと、何でパケットフィルタでメモリリークの検出とかファイルシステムのパフォーマンス見てるのかよくわかりませんw

まあ、細かいことはさておき、使ってみましょう。多分最近のディストリビューションではBPFの機能は有効になっているんじゃないかと思うのですが、なってなかったら自前でビルドですね。ARCH LinuxカーネルはBFP有効になっています。必要なのはbccのパッケージでこれはAURにあります。あとはlinux-headers、linux-api-headers辺りも必要かもしれません。自分は自前ビルドのカーネルでやりました。

まず、サンプルを動かしてみましょう。githubからコードをクローンして、hello_world.pyを動かします。これはsys_cloneにprobeを登録して、関数が呼ばれるとHello, World!を表示します。prefixとしてkprobe__をつけるのがポイントです。

from bcc import BPF

BPF(text='void kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); }').trace_print()
masami@saga:~/codes/bcc/examples (master)$ sudo ./hello_world.py
    thread.rb:24-1937  [000] d..2   409.429035: : Hello, World!
    thread.rb:24-9544  [001] d..2   411.371223: : Hello, World!
    thread.rb:24-9544  [001] d..2   411.371430: : Hello, World! 

bpf_trace_printk()を使うとプロセス名(current->comm)、pid(current->pid)なんかは自動で表示してくれます。 text=としているところはc言語のコードです。これは以下のようにファイルから読み込ませることもできます。

BPF(src_file = "foo.c")

とまあ、基本的な使い方を抑えたところで実際に使ってみます。今回はcreate_new_namespaces()関数にprobeを登録してみました。 カーネル内のcreate_new_namespaces()が呼ばれたらプロセス名、pid、cloneのフラグを保存しておいて、それをperfと組み合わせてユーザーランドでデータを表示しています。

#!/usr/bin/env python

from bcc import BPF
import ctypes as ct

prog = """
#include <uapi/linux/ptrace.h>
#include <linux/nsproxy.h>

struct namespace_creator_t {
    u64 pid;
    char comm[256];
    u64 flags;
};

BPF_PERF_OUTPUT(events);

void kprobe__create_new_namespaces(struct pt_regs *ctx, unsigned long flags, struct task_struct *tsk,
struct user_namespace *user_ns, struct fs_struct *new_fs) 
{
    struct namespace_creator_t nc = {};
    u32 pid;

    nc.flags = flags;
    pid = bpf_get_current_pid_tgid();
    nc.pid = pid;
    bpf_get_current_comm(&nc.comm, sizeof(nc.comm));
    events.perf_submit(ctx, &nc, sizeof(nc));
}
"""

class NameSpaceCreator(ct.Structure):
    _fields_ = [
            ("pid", ct.c_ulonglong),
            ("comm", ct.c_char * 256),
            ("flags", ct.c_ulonglong)
    ]

def flags_string(flags):
    buf = []
    if flags & 0x04000000:
        buf.append("CLONE_NEWUTS ")
    if flags & 0x08000000:
        buf.append("CLONE_NEWIPC ")
    if flags & 0x10000000:
        buf.append("CLONE_NEWUSER ")
    if flags & 0x20000000:
        buf.append("CLONE_NEWPID ")
    if flags & 0x40000000:
        buf.append("CLONE_NEWNET ")

    s = ""
    for b in buf:
        s += b
    return s.strip()

def print_event(cpu, data, size):
    event = ct.cast(data, ct.POINTER(NameSpaceCreator)).contents
    print("pid %d(%s) flags: 0x%x(%s)" % (event.pid, event.comm, event.flags, flags_string(event.flags)))

print("Ctrl-c to stop")
b = BPF(text=prog)
b["events"].open_perf_buffer(print_event)
while 1:
    b.kprobe_poll()

実行するとこんな感じになります。

masami@saga:~/codes/ns_bfp$ sudo ./ns_bfp.py
Ctrl-c to stop
pid 5228(b'unshare') flags: 0x44000000(CLONE_NEWUTS CLONE_NEWNET)
pid 1427(b'chrome') flags: 0x20000011(CLONE_NEWPID)

pid5228は以下のようにunshare(1)を実行したときのものです。

masami@saga:~$ sudo unshare -u -n /bin/bash
[root@saga masami]#

pid1427はchromeでタブを一つ開いたときのものです。

とまあ、こんな感じで本来はパケットフィルタのBPFを使ってkprobeとperfを組み合わせた形でコードを書くことができます。 bccを使うとpythonc言語スクリプトを書けるので自由度がかなり高いですね。

Systems Performance: Enterprise and the Cloud

Systems Performance: Enterprise and the Cloud