BPF への道: 技術的背景

目次

序章

BPFとは

歴史

構成

実行メカニズム

BPFとebpfの関係

BCC、bpftrace、IO バイザー

BCCプロジェクトのクイックスタート

execsnoop

生体潜時

動的計測: kprobes と uprobes

コンセプト

欠点

静的計測: トレースポイントと USDT

コンセプト

欠点

推奨される解決策

bpftrace を理解する: openat の追跡

技術的背景

グラフィカル BPF

BPF ヘルパー関数

bpf_probe_read()

bpf_probe_read がページ フォールト割り込みを無効にする理由

bpf_probe_read がページ フォールト割り込みを禁止する方法

サンプル

BPF システムコールコマンド

strace を使用して execsnoop を分析する

BPF プログラム タイプ

BPF マップ タイプ

BPF 同時実行制御

BPF sysfs インターフェース

BPF型フォーマット

BPFコアRE

BPF の限界

コール スタック バックトレース

フレーム ポインターに基づくコール スタック バックトレース

デバッグ情報に基づいてデバッグを行う

最後のブランチ レコード

オーク

シンボル

フレームグラフ

イベント ソース

Kプローブ

アップローブ

トレースポイント トレースポイント

USDT

パフォーマンス監視カウンター


序章

BPFとは

歴史

Berkeley Packet Filter の略で、ネットワーク パケット フィルタリング ツールのパフォーマンスを向上させるために 1992 年に誕生しました。

2014 年に Linux カーネルのメインラインに入りました。

使用方法に関しては、JavaScript に似ています。

構成

命令セット、ストレージ オブジェクト、補助機能、その他のパーツ。

実行メカニズム

一般に、次の 2 つの強制メカニズムがあります。

  1. 通訳者
  2. BPF 命令をローカライズされた命令に動的に変換するジャストインタイム JIT コンパイラ

実行前に、バリデーターのセキュリティー・チェックに合格する必要があります。これにより、BPF プログラム自体がクラッシュしないことが保証されます。

BPFとebpfの関係

現在の ebpf は、以前のものと一貫性を保つために、引き続き BPF と呼ばれます。

BCC、bpftrace、IO バイザー

BPF 命令を介して直接 BPF プログラムを作成するのは面倒なので、それをサポートする高水準言語があります。

BCC (BPF Compiler Collection、BPF) は、最初に BPF トレース プログラムを開発するために使用され、C 言語環境を提供し、ユーザー側インターフェースを実装するための lua および python 環境も提供しました. libbcc および libbpf ライブラリの前身でした. BPF プログラムでイベントを監視するためのライブラリ関数を提供します。

BCC 関数ライブラリには、70 以上のツールが用意されています。

bpftrace は、特に BPF ツールを作成するための高水準言語サポートを提供する新しいフロント エンドです。bpftrace も、libbcc および libpbf ライブラリの上に構築されています。

bpftrace は強力な 1 行のプログラムと短いスクリプトを作成するのが得意ですが、BCC は主に比較的複雑で大規模なバックエンド プロセスを開発します。

BCC と bpftrace は Linux カーネルには属していませんが、GITHUB の IO ViSor Linux 財団に属しています。

BCCプロジェクトのクイックスタート

execsnoop

bcc プロジェクトから、execve syscall をトレースすることによって機能します。

sudo execsnoop-bpfcc

効果

このツールを使用すると、業務負荷を確認できます。つまり、ある期間に自分のアイデアでプロセスが作成されているかどうかを確認できます。

生体潜時

コンセプト

ブロック デバイスのレイテンシ ヒストグラム (ディスク IO レイテンシ) を描画する

sudo biolatency-bpfcc

仮想マシン環境では、このコマンドはカーネル バージョン 5.19.0-35-generic では実行できません。

動的計測: kprobes と uprobes

コンセプト

本番環境で実行中のプログラムの任意の命令位置にウォッチポイントを挿入

欠点

バージョンが変更されると、インストルメント化された関数が直接名前変更または削除される可能性があり、これにより安定性の問題が発生し、BPF ツールが直接動作しなくなる可能性があります。

静的計測: トレースポイントと USDT

コンセプト

本質的には、上記の動的インストルメンテーションにおけるインターフェイスの安定性の問題を解決し、安定したイベント名をコードに直接ハードコードし、開発者が直接維持することです。

USDT (ユーザー レベルの静的に定義されたトレース) は、このテクノロジについて説明しています。

欠点

ハードコーディングにより追加のメンテナンス コストが発生する

推奨される解決策

最初に静的追跡技術 (tracpoint または USDT) を使用し、それが十分でない場合は動的計測技術を使用します。

bpftrace を理解する: openat の追跡

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {printf("%s %s \n",comm,str(args->filename));}'

ここにいくつかの小さなヒントがあります:

  1. openat は Linux で open よりもはるかに多く呼び出されます
  2. bpftrace はルート権限を使用する必要があります
  3. bpftrace は一般的に単純で、比較的小さなコマンド ツールをいくつかサポートしていますが、BCC ツールはより強力です。

技術的背景

グラフィカル BPF

BPF ヘルパー関数

  1. マップ操作関数: BPF_MAP_LOOKUP_ELEM、BPF_MAP_UPDATE_ELEM、BPF_MAP_DELETE_ELEMなど
  2. メモリ操作関数: BPF_MEMCPY、BPF_MEMCPY_STR、BPF_MEMSETなど
  3. ネットワーク操作関数: BPF_SOCK_OPS_TCP_SOCK など
  4. 時刻操作関数: BPF_KTIME_GET_NS など
  5. システムコール操作関数:BPF_TRACE_PRINTK、BPF_GET_CURRENT_PID_TIDなど
  6. 数学計算関数: BPF_ADD、BPF_SUB、BPF_MUL、BPF_DIV など
  7. その他の機能: BPF_DEBUG、BPF_EXIT など

bpf_probe_read()

BPF でのメモリ アクセスは、BPF レジスタとスタック空間 (および補助関数による BPF マッピング テーブルへのアクセス) に制限されます. 他のメモリ (たとえば、BPF 以外のメモリ) にアクセスする場合は、bpf_probe_read() を使用する必要があります。

この関数は、プロセスのセキュリティをチェックし、ページ フォールト割り込みを禁止して、プローブ コンテキストでページ フォールト割り込みが発生しないようにします (そうしないと、カーネルの問題が発生する可能性があります)。

他にもヘルパー関数があります: bpf_probe_read_kernel, bpf_probe_read_user()

bpf_probe_read がページ フォールト割り込みを無効にする理由


Linux カーネルでは、BPF プログラムはカーネル空間のユーザー空間メモリにアクセスできます。BPF プログラムによってアクセスされたユーザー空間アドレス空間のページが物理メモリにない場合、ページ フォールト割り込みがトリガーされ、カーネルが対応するページをディスクからメモリに読み込み、物理メモリの割り当てを完了します。このプロセスには時間がかかります。

ただし、場合によっては、ページ フォールト割り込みに対応するページをすぐに読み取る必要がないこともあります。したがって、ページ フォールト割り込みを無効にすることで、時間のかかる物理メモリの割り当てを回避し、処理パフォーマンスを向上させることができます。

bpf_probe_read がページ フォールト割り込みを禁止する方法

現在のスレッドの終了ページ フォールト割り込みフラグ ビットを設定する必要がある

/* ページフォルト割り込みを無効にする */
void disable_page_fault(void)
{
    preempt_disable();
    現在-> フラグ |= PF_NOFREEZE;
    現在->mm->def_flags |= VM_FAULT_NOPAGE;
}

サンプル

bpf_probe_read 関数を使用して、skb の UDP パケットを読み取ります

UDP データ パケットを読み取るプロセスでは、最初に IP ヘッダーを読み取り、次に IP プロトコル タイプ フィールドに従って上位層プロトコルを UDP と判断し、次に UDP ヘッダーを読み取り、データ ペイロードを読み取る必要があります。以下は、bpf_probe_read() 関数を使用して UDP パケットを読み取るためのサンプル コードです。

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>

int mybpf_prog(struct __sk_buff *skb)
{
    ボイド * データ = (ボイド *)(長い) skb-> データ;
    void *data_end = (void *)(long)skb->data_end;

    struct ethdr *eth = データ;
    もし (eth + 1 > data_end)
        0 を返します。
    
    // IP ヘッダーを読み取る
    struct iphdr iph;
    (bpf_probe_read(&iph, sizeof(iph), (void *)(eth + 1)) != 0) の場合
        0 を返します。

    if (iph.protocol == IPPROTO_UDP) {
        // UDP ヘッダーを読み取る
        struct udphdr ええと;
        if (bpf_probe_read(&uh, sizeof(uh), (void *)((unsigned char *)iph + (iph.ihl * 4))) != 0)
            0 を返します。
            
        // パケットの全長を計算する
        unsigned int len = ntohs(iph.tot_len) - (iph.ihl * 4) - sizeof(uh);
        if (len <= 0) // パケット長エラー
            0 を返します。
            
        // データ ペイロードの読み取り
        unsigned char ペイロード[len];
        if (bpf_probe_read(&payload, len, (void *)((unsigned char *)uh + sizeof(uh))) != 0)
            0 を返します。

        // 読み取りデータ ペイロードを処理する
        ...
        
        1 を返します。
    }

    0 を返します。
}

BPF システムコールコマンド

strace を使用して execsnoop を分析する

sudo strace -ebpf execsnoop-bpfcc

bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\274\4\0\0\274\4\0\0H\17 \0\0\0\0\0\0\0\0\0\2"..., btf_log_buf=NULL, btf_size=5148, btf_log_size=0, btf_log_level=0}, 28) = 3
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERF_EVENT_ARRAY, key_size=4, value_size=4, max_entries=128, map_flags=0, inner_map_fd=0, map_name="events", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0 、btf_vmlinux_value_type_id=0、map_extra=0}、72) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=510, insns=0x7ff36007d000, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 19, 17), prog_flags=0, prog_name="syscall__execve"、prog_ifindex=0、expected_attach_type=BPF_CGROUP_INET_INGRESS、prog_btf_fd=3、func_info_rec_size=8、func_info=0x5645a446e430、func_info_cnt=1、line_info_rec_size=16、line_info=0x5645a4a88f2 0, line_info_cnt=252, attach_btf_id=0, attach_prog_fd=0, fd_array=NULL}, 144) = 5
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=82, insns=0x7ff36021b7d0, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 19, 17), prog_flags=0, prog_name="do_ret_sys_exec", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, func_info=0x5645a446e430, func_info_cnt=1, line_info_rec_size=16, line_info=0x5645a348d 760、line_info_cnt=28、attach_btf_id=0、attach_prog_fd=0、 fd_array=NULL}, 144) = 7
PCOMM PID PPID RET ARGS
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, キー=0x7ff35babc690, 値=0x7ff35babc590, フラグ=BPF_ANY}, 144) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, キー=0x7ff35babc590, 値=0x7ff35babc690, フラグ=BPF_ANY}, 144) = 0
^Cstrace: プロセス 4540 が切り離されました

PS: strace を使用すると本質的に ptrace が使用されるため、strace を直接使用することは避けるのが最善です。これにより、ターゲット プロセスの実行速度が大幅に低下し、パフォーマンスが元の 1% に直接低下する可能性がありますが、その利点はサポートできることです。 bpf システム コール 変換、たとえば、BPF_PROG_LOAD を出力できます

BPF プログラム タイプ

さまざまな bpf プログラム タイプによって、BPF プログラムがマウントできるイベントのタイプと、イベントのパラメータが定義されます。主にトレース目的で使用される BPF プログラムのタイプは次のとおりです。

BPF マップ タイプ

このうち、BPF_MAP_TYPE_PERF_EVENT_ARRAY は、カーネルに取り込まれた情報をユーザーに転送することができ、execsnoop はこのタイプを使用します。

BPF 同時実行制御

Linux 5.1 でスピン ロック ヘルパーが追加されるまで、BPF には常に同時実行制御がありませんでしたが、これらはトレーサーではまだ利用できません。

トレースを使用すると、並列スレッドが BPF マップ フィールドを並行して検索および更新できるため、あるスレッドが別のスレッドからの更新を上書きすると、破損が発生します。これは、失われた更新の問題 (失われた更新) とも呼ばれます。つまり、同時読み取りと書き込みが重複すると、更新が失われます。トレース フロントエンド BCC および bpftrace は、この破損を回避するために、可能な場合は CPU ごとのハッシュおよび配列マップ タイプを使用します。これにより、論理 CPU ごとにインスタンスが作成されます (PERCPU タイプで BPF_MAP_TYPE_PERCPU_HASH を使用するとします)。

これは、BPF_MAP_TYPE_PERCPU_HASH タイプの使用方法です。

strace -febpf bpftrace -e 'k:vfs_read { @ = count(); }'

これは同時実行制御を使用しない方法です

strace -febpf bpftrace -e 'k:vfs_read { @++; }'

カウントを比較すると、通常のハッシュではイベントが 0.01% 過小評価されていることがわかります。

比較すると0.01%の誤差があります。

BPF sysfs インターフェース

Linux 4.4 では、BPF は、通常 /sys/fs/bpf にマウントされる仮想ファイル システムを介して BPF プログラムとマップを公開するコマンドを導入しました。これは「固定」と呼ばれ、さまざまな方法で使用できます。デーモンと同様に永続的な BPF プログラムを作成し、それらをロードしたプロセスが終了した後も実行を継続できます。また、ユーザーランド プログラムが実行中の BPF プログラムと対話する別の方法も提供します。つまり、BPF マップを読み書きできます。

BPF型フォーマット

ターゲット プログラムのソース コードがなく、一部の BPF ツールの作成が困難な場合、この問題を解決できる BTF テクノロジがここにあります。

しかし、BTF 技術はまだ開発中です。

BPFコアRE

BPF の Compile Once - Run Everywhere プロジェクトは、BPF プログラムを一度 BPF バイトコードにコンパイルして保存し、配布して他のシステムで実行できるようにすることを目的としています。これにより、BPF コンパイラ (LLVM および Clang) をどこにでもインストールする必要がなくなります。これは、スペースに制約のある組み込み Linux にとって課題です。また、BPF 可観測性ツールの実行時にコンパイラを実行するための実行時の CPU とメモリのコストも回避されます。

CO-REも現在開発中です。

BPF の限界

  1. カーネル関数を勝手に使わない
  2. BPF スタックのサイズは 512 (MAX_BPF_STACK) を超えることはできません. これには解決策があります: マップされた記憶領域を使用します.

コール スタック バックトレース

BPF は、コール スタック情報を格納するための専用のマッピング テーブル構造を提供します。これにより、フレーム ポインター ベースまたは ORC ベースのコール スタック バックトレース情報を保存できます。

フレーム ポインターに基づくコール スタック バックトレース

この技術は主に、関数呼び出しスタック フレーム リストの先頭は常に特定のレジスタ (x86_64 では RBP) に格納され、この関数呼び出しの戻りアドレスは常に RBP が指す位置にあるという前提に基づいています。値に固定オフセット Shift (+8) を加えたもの。

これは、RBP を読み取った後、RBP 値が先頭にあるリンク リストをトラバースし、固定オフセット位置でリターン アドレスを取得することで、プログラムの実行を中断した後、どのデバッガでもスタック バックトラックを簡単に実行できることを示しています。

PS: gcc コンパイラでは、デフォルトで関数フレーム ポインタはなく、RBP は通常のレジスタとして使用されますが、パフォーマンスの向上はそれほど高くないため、このデフォルトの動作を有効にすることをお勧めします。

-fno-omit-frame-pointer

デバッグ情報に基づいてデバッグを行う

つまり、gcc の後に -g -wall を追加します。

これには、DWARF の ELF のデバッグ情報が含まれています。

ELF ファイルのデバッグ関連ファイル セグメントは、.eh_frame および .debug_frame です。

欠点は、これにより実行可能ファイルが非常に大きくなることです

libjvm.so = 17M

libjvmd.so = 222M

最後のブランチ レコード

つまり、LBR はハードウェア バッファに記録されるインターの特別な技術であり、この技術には追加のオーバーヘッドはありません。

ただし、サポート レコードの深さには制限があります。

BPF は LBR をサポートしていません

オーク

新しいデバッグ情報形式 - Oops 巻き戻し機能 (ORC) は、スタック バックトラッキング要件のために特別に設計されています。DWARF 形式と比較して、この形式を使用するとプロセッサの要件が低くなります. ORC も ELF ファイル セグメントを使用し、現在の Linux カーネルはいくつかのサポートを提供します.

現在、ユーザー モードでの ORC コール スタックのサポートは開発されていません。

カーネルでの ORC ベースのコール スタック トレースバックは、perf_callchain_kernel 関数でサポートできます。

シンボル

現在、コール スタック情報はカーネル内のアドレス データの形式で記録されており、これらのアドレスはユーザー モード プログラムによってシンボル (関数名など) に変換できます。

しかし、作業のこの部分はまだ完了していません。

フレームグラフ

フレーム グラフは、プログラムの CPU 使用率を表示するための視覚化ツールです。フレーム グラフの基本的な使用方法と解釈方法を次に示します。

  1. 座標軸: フレーム グラフの y 軸は呼び出しスタックを識別し、関数呼び出しの深さを上から下に表します。x 軸は CPU 時間 (ミリ秒、秒、またはその他の単位) を表し、左から右にプログラムの実行時間を表します。
  2. 色: フレーム グラフの色は、関数呼び出しによって消費される CPU 時間またはカウンター スペースの相対値を示します。
  3. 幅: フレーム グラフの各長方形の幅は、問題のコードの CPU 時間またはカウンターの測定値を表します。
  4. ツール: たとえば、Flamegraph、perf、およびその他のフレーム グラフ生成ツールは、四角形の順序、配色など、さまざまな設定の有効化をサポートしています。

上記の情報に基づいて、フレーム グラフを解釈する方法を学びましょう。

  1. 開始点と終了点: フレーム グラフの開始点と終了点は、通常、プログラムの開始点と終了点であり、通常、フレーム グラフではかなり幅の広いプレートと明るい色が使用されます。
  2. 幅: フレーム グラフで幅の広い四角形は、プログラム実行中のリソース消費が比較的高く、それに応じてコードの実行時間が長くなることを示します。
  3. 色: フレーム グラフの色は薄緑から濃い緑まであります. 薄緑の領域はプログラムで比較的時間がかかりオーバーヘッドの少ない領域であり, 濃い緑の領域はより多くの時間消費を追求しています.
  4. 繰り返し呼び出し: フレーム グラフでは、同じ関数を繰り返し呼び出すと同じボックスが表示され、ボックスの幅は呼び出された回数と実行時間の相対的なサイズを示します。
  5. スタック情報: フレーム グラフ内の各関数呼び出しの名前と呼び出しスタック情報は、プログラムのパフォーマンスの問題を特定して調整するためにも使用できます。

イベント ソース

Kプローブ

kprobes と kretprobes の概念

kprobes は、カーネルを再起動せずにカーネルの動的計測サポートを提供します。

kretprobes を使用して、カーネル関数の戻り値を計測して戻り値を取得するか、kprobes と kretprobes を使用して関数を同時に計測し、カーネル関数呼び出しの時間を取得できます。

原理

  1. ブレークポイントの登録: Kprobes は、カーネルの動的メモリ割り当てテクノロジを使用して、カーネルの Web サーバー コードの主要な場所にブレークポイントを登録します。
  2. ブレークポイントのトリガー: カーネルが登録されたブレークポイントの位置まで実行されると、カーネルはすぐに実行を中断し、この時点で Kprobes によって登録されたハンドラーの実行を開始します。
  3. ハンドラー: Kprobes のハンドラーは、ブレークポイントでカーネル関数呼び出しにフックしてパフォーマンス データを記録したり、デバッグまたはプロファイリングの目的でカーネル状態を変更したりできるユーザー定義関数にすることができます。
  4. 処理完了: 処理プログラムが完了した後、Kprobes は中断された場所で残りのコードを実行し続け、カーネル コードのトレースまたは処理を完了します。

Kprobes インターフェイス

c言語でポート処理関数とリターン処理関数を記述し、register_kprobe()を呼び出して登録する必要があります。

現在は主に BCC または bpftrace を使用しています。

BCC は以下を提供します。

attach_kprobe()
attach_kretprobe()

bpftrace のデモは次のとおりです。

bpftrace -e 'kprobe:vfs_* {@[プローブ] = count()}'

アップローブ

コンセプト

ユーザー モード プログラムの動的インストルメンテーションを提供します。

kprobes と同様に、原理は kprobes に似ています。

原理

指定したアドレスにジャンプ命令を挿入することで、そのアドレスに出入りするCPUの実行フローを遮断し、その前後に指定した処理プログラムを実行します。このメソッドは、バイナリ コードを破壊することなく、プログラムの実行状態を監視および変更できます。

インターフェース

Ftrace に基づいて、/sys/kernel/debug/tracing/uprobe_events に: このファイルに特定の文字列を書き込んで uprobes を開くか閉じる

perf_event_open()

BCC には 2 つのインターフェイスが用意されています。

attach_uprobe
attach_uretprobe

トレースポイント トレースポイント

コンセプト

静的計測

原理

カーネル コードを記述する場合、開発者はトレースポイント マクロ定義を使用してトレースポイントを事前定義できます。これらのトレースポイントとそのパラメーターは、コンパイル時にカーネル バイナリで修正されます。

プログラムの実行中にトレース機能が有効になっている場合、プログラムが対応するトレース ポイントまで実行されると、対応するトレースポイント コールバック関数がトリガーされます。このコールバック関数をコードで定義して、必要な操作を実装できます。トレースポイントは事前に定義されているため、余分なアセンブリ命令の挿入を回避でき、プログラムのパフォーマンスへの影響を軽減できます。


インターフェース

BCCが提供する

tracepoint_probe()

USDT

コンセプト

ユーザー・モードの事前定義された静的追跡により、ユーザー空間の追跡ポイント・メカニズムが提供されます

BPFとUSDT

USDT().enable_probe()

パフォーマンス監視カウンター

パフォーマンス監視カウンターは、プログラムの実行中にさまざまなシステム ハードウェアの状態を測定するために使用されるカウンターです。これらは通常、プロセッサの基盤となるハードウェアによって提供され、システムで実行されている多数のカウンターを使用して、システムのパフォーマンスとボトルネックを監視できます。パフォーマンス監視カウンターは、命令実行、CPU キャッシュ パフォーマンス、メモリ サマリーなどのさまざまな指標を測定し、システム全体のパフォーマンスと最適化ソリューションの効果を評価できます。

ソフトウェア開発の過程で、パフォーマンス監視カウンターを使用することで、プログラムのパフォーマンスのボトルネックをより正確に分析できます。開発者は、さまざまなカウンター インジケーターを組み合わせてシステムのボトルネックを特定し、コードとアルゴリズムを最適化してプログラムのパフォーマンスを向上させることができます。

一般的なパフォーマンス監視カウンターには、CPU 動作サイクル (Clocks)、命令実行数 (Instruction)、キャッシュ ヒット率 (Cache Hits)、メモリ アクセス レイテンシ (Memory Latency) などがあります。これらのカウンターは通常、専用ツールまたはシステム コマンド ライン インターフェイス (Linux システムによって提供されるパフォーマンス ツール、Intel VTune、AMD CodeXL、およびその他のパフォーマンス監視ツールなど) を介して取得できます。

Guess you like

Origin blog.csdn.net/qq_32378713/article/details/129811548