Upgrade to Pro — share decks privately, control downloads, hide ads and more …

bpf_arenaとstruct_opsを使うのが難しかった話

Avatar for KANAI Masumi KANAI Masumi
November 06, 2026

 bpf_arenaとstruct_opsを使うのが難しかった話

Avatar for KANAI Masumi

KANAI Masumi

November 06, 2026

More Decks by KANAI Masumi

Other Decks in Technology

Transcript

  1. ⾃⼰紹介 • ⾦井真澄 (@masu-mi.bsky.social) • 2021年2⽉ BBSakura Networks 株式会社 中途⼊社

    ◦ ⼊社前、モバイルコア ◦ ⼊社後、モバイルコアむ • つ • 新卒はウェブ企業 ◦ • も ろそうだな い ば市在住 メディアサービス・オブジェクトストレージ開発 学⽣時代は、完全WKB解析と⾮断熱遷移
  2. 会社紹介 持分比率 100 % 49 % 51 % 2019.08~ IX事業

    ネットワーク ソリューション ソフトウェア事業 ネットワーククラウド サービスの開発 ソフトウェア ソリューション 3
  3. 作ったものと動機 s3fs-fuseの代替ファイルシステムを試験⽤に作りま • た FUSEのs3fsは、openやreadのたびにデーモンに処理 ‒ 早 折り返 ほどレイテンシ 渡りS3と通信

    る 改善 る ‒ キャッシュ層を追加 て効果を測定 たい ‒ メモリ消費、追加実装 有益 ? ‒ Page Cacheのスケジューラ 素直な実装だ どカーネルモジュールなので実装 楽 • キャッシュ機能を拡充 る ‒ eBPFでカーネル内で判断 る ‒ prewarm などアプリケーション側の知識で追加動作を差 込みたい 5
  4. s3fs-ebpfの構成 ユーザー空間、VFS、⾃作FS、eBPF 、それぞれ別のメモリ領域を参照 る アプリ hitならdaemonまで⾏かない daemon ユーザー空間 GET S3

    /dev/s3fs_ebpf カーネル read(2) VFS generic_file_read_iter page cacheのmiss eBPFプログラム fentry、struct_ops ⾃作FS(kmod) read_folio、evict、KFunc 参照するメモリ領域 page cache ファイルの中⾝ ユーザー空間のプロセス 既存のカーネルと外部 出典:masu-mi/s3fs-ebpf(pcacheモード) BPF map 状態と統計 bpf_arena 書いた中⾝ ⾃作FS(kmod) chunk表 kmodのキャッシュ eBPF eBPFの呼び出 6
  5. 使ったeBPF機能 難 ったのはbpf_arenaとstruct_opsで、関連 てfentryとKFuncも扱う 機能 使った場所 利⽤⽬的 struct_ops ⾃作FSの関数 ⾃作FS内のキャッシュ操作を交換可能に

    る fentry read() 読み取りイベントを観測 bpf_arena daemonとeBPF間 ファイル共有、daemon ら通知(予定) KFunc(BPFへ関数提供) カーネルモジュールで登録 ⻑いループ処理のverifier迂回⽬的 出典:masu-mi/s3fs-ebpf、Linux 6.12 7
  6. 使ったeBPF機能 難 ったのはbpf_arenaとstruct_opsで、関連 てfentryとKFuncも扱う 機能 使った場所 採⽤判断 struct_ops ⾃作FSの関数 ⾃作FS内のキャッシュ操作を交換可能に

    る -> OK fentry read() 読み取りイベントを観測 -> OK bpf_arena daemonとeBPF間 ファイル共有 -> NG、daemon ら通知(検討中) KFunc(BPFへ関数提供) カーネルモジュールで登録 ⻑いループ処理のverifier迂回⽬的 -> (may_goto, bpf_loop, ) 出典:masu-mi/s3fs-ebpf、Linux 6.12 8
  7. struct_ops アプリ daemon ユーザー空間 GET S3 /dev/s3fs_ebpf カーネル read(2) VFS

    generic_file_read_iter page cacheのmiss eBPFプログラム fentry、struct_ops ⾃作FS(kmod) read_folio、evict、KFunc 参照するメモリ領域 page cache ファイルの中⾝ ユーザー空間のプロセス 既存のカーネルと外部 出典:masu-mi/s3fs-ebpf(pcacheモード) BPF map 状態と統計 bpf_arena 旧構成のみ ⾃作FS(kmod) chunk表 kmodのキャッシュ eBPF eBPFの呼び出 9
  8. struct_opsの仕組み struct_opsは構造体を登録で るmap、メンバにeBPFプログラム(fd)も登録で る SEC(".struct_ops.link") struct s3fs_policy_ops sieve_pick_ops = {

    .on_admit = (void *)sp_admit, .on_hit = (void *)sp_hit, .pick_victim = (void *)sp_pick, /* ... on_evict, on_insert, ... */ }; 値のメンバの例(sched_ext_ops) SEC(".struct_ops.link") struct s3fs_policy_ops my_ops = { メンバ BPF プログラムの型(BTF)をverifier 関数のほ に持てる 検査 に普通の値(名前・上限値など)もメンバ 型定義はカーネル側(tcp_congestion_ops、 sched_ext_ops、6.9以降はkmodも) 型定義 たモジュール init_memberで受 ⼀式をstruct_opsのmapの値と て登録 bpf_linkで有効に る 取る 、 .on_evict = (void *)my_on_evict, .flags = S3FS_POLICY_F_NO_ON_HIT, .name = "my_policy", }; 10
  9. struct_opsの仕組み struct_opsは構造体を登録で SEC(".struct_ops.link") るmap、メンバにeBPFプログラム(fd)も登録で る BPF_F_LINKフラグ v6.4で追加 struct s3fs_policy_ops sieve_pick_ops

    = { .on_admit = (void *)sp_admit, .on_hit = (void *)sp_hit, .pick_victim = (void *)sp_pick, /* ... on_evict, on_insert, ... */ }; 値のメンバの例(sched_ext_ops) SEC(".struct_ops.link") struct s3fs_policy_ops my_ops = { メンバ BPF プログラムの型(BTF)をverifier 関数のほ に持てる 検査 に普通の値(名前・上限値など)もメンバ 型定義はカーネル側(tcp_congestion_ops、 sched_ext_ops、6.9以降はkmodも) 型定義 たモジュール init_memberで受 ⼀式をstruct_opsのmapの値と て登録 bpf_linkで有効に る 取る 、 .on_evict = (void *)my_on_evict, .flags = S3FS_POLICY_F_NO_ON_HIT, .name = "my_policy", }; 11
  10. 登録から差し替えまでの流れ mapを書いて登録を準備 、linkの作成、更新、解放でkmodの関数 呼ばれる ユーザー空間の操作 カーネルでの処理 kmodの関数 BPF_MAP_UPDATE_ELEM(mapの 値を書く) メンバ

    とに検査、trampolineでkdataを埋 め、READYに る init_member、validate BPF_LINK_CREATE linkを作り、kdataを公開 reg(kdata) BPF_LINK_UPDATE(新しいmapの fd) linkの接続先を新 BPF_LINK_DETACHか、最後のfdの close 接続を外 る いmapに付 替える update(new, old) unreg(kdata) mapの値は書き換えられないので、差し替えは新しいmapへの付け替えで⾏う 出典:Linux 6.12 kernel/bpf/bpf_struct_ops.c 12
  11. kmodでのstruct_opsの登録 kmodは表を1つ⽤意 、register_bpf_struct_opsで登録 る struct s3fs_policy_ops { (*pick_victim)(void); ... };

    // 1 static struct s3fs_policy_ops stubs = { stub, ... }; static struct bpf_struct_ops st_ops = { 1 vtableの型と、member との空 関数(cfi_stubs)を⽤意 る 2 bps_struct_ops構造体のインス タンスを作る 3 linkの作成で公開、更新で差 え、解放で撤去 る 4 module_initで型を登録 る // 2 .verifier_ops = &vops, .cfi_stubs = &stubs, .init = init, .init_member = init_member, .reg = reg, .unreg = unreg, .update = update, .owner = THIS_MODULE, .name = "s3fs_policy_ops", }; // 中で reg: { rcu_assign_pointer(active, kdata); /*...*/ } unreg: { // 3 rcu_assign_pointer(active, NULL); 替 synchronize_rcu(); } update: rcu_assign_pointer(active, kdata); synchronize_rcu(); register_bpf_struct_ops(&st_ops, s3fs_policy_ops); // 4 公開と撤去はポインタ1本で⾏い、撤去の後はsynchronize_rcuで待つ 13
  12. kmodが⽤意する部品 struct_opsの型をkmodで提供 るには、検査と公開の部品を べて書 部品 やること なぜ要るか vtableの型と登録 型をBTFに出 BPF側のstructとmemberを名前で照合

    る cfi_stubs member verifier_ops 使えるhelperと引数の読み⽅ struct_opsのprogは れで検査 init、init_member、validate 登録時、メンバ と、全メンバの後に 値のメンバと組み合わ を検査 る フレームワーク 必 呼ぶ reg、unreg vtableを公開 kmod ど update BPF_LINK_UPDATEで差 呼び出し側の wrapper rcu_dereference てNULLなら既定値 とに同 型の空関数 、撤去 る 替える ないと登録 -EINVAL。CFIの型を ら読む ら取る れる をフレームワークは知らない ないと-EOPNOTSUPP attachの前はvtable ない fmod_retの約15⾏に対し、約220⾏になった 14
  13. 呼び出しの流れ fentryは元の関数に処理を⾜ fentry struct_ops 、struct_opsは実装そのものを置 換える nopを書 換えたcall trampoline BPF

    prog (call) kmodの間接call CFIの検査 trampoline 元の関数 続 BPF prog (call) 1回の呼び出しのコストはほぼ同じ stuct_opsは CFI(Kernel Control Flow Integrity: カーネル制御フロー整合性)が挟まる 出典:Linux 6.12 arch/x86/net/bpf_jit_comp.c、kernel/bpf/trampoline.c 15
  14. fentryのtrampoline fentryは、関数の先頭の5バイトのnopを、trampolineへのcallに書 換える attach前 bpf_trampoline(JITが作る) generic_file_read_iter: 引数のレジスタをスタックに保存 endbr64 nop5 /*

    was: call __fentry__ */ __bpf_prog_enter push %rbp attach後 generic_file_read_iter: endbr64 BPF prog __bpf_prog_exit call bpf_trampoline push %rbp 関数の続 へ戻る attachしていない関数のコストは、nop 1つだけ 出典:Linux 6.12 kernel/bpf/trampoline.c、arch/x86/net/bpf_jit_comp.c 16
  15. CFIとtrampoline CFIは、関数ポインタで呼ぶ先の関数の型を、ハッシュで確 める 呼ぶ側:kmodのops->on_evict(ino) movl $(-hash), %r10d addl -15(%r11), %r10d

    jz 1f ud2 呼ばれる側:struct_opsのtrampoline __cfi_tramp: movl $hash, %eax /* callee's hash */ /* from cfi_stubs */ nop x 11 tramp: /* type mismatch */ endbr64 1: call *%r11 push %rbp ... trampolineは実⾏時に作るのでハッシュを持た 、cfi_stubsの同 eBPF プログラム memberの関数 struct_ops⽤のmapを更新時に、カーネルはkCFU付 呼ばれる側の後半で call xxx とeBPFプログラム呼び出 ら写 のtrampコードを⽣成 ⼊っている cfi_stubsがないと、trampolineに付ける型のハッシュが決まらない 出典:Linux 6.12 arch/x86/net/bpf_jit_comp.c、kernel/bpf/bpf_struct_ops.c 17
  16. アルゴリズムが使うアクセス情報 どのアクセス情報をど まで省 る は、使うアルゴリズムで決まる アルゴリズム 使うアクセス情報 省ける情報 SIEVE(参照ビットが⽴ったものを残す) 参照ビット(1ビット)

    回数と時刻 LRU(最近使ったものを残す) 最終アクセスの順序 な S3-FIFO(⼩さいFIFOで再参照されたものだけ をメインに移す) アクセス回数(上限3) 3を超える回数 W-TinyLFU(推定頻度でadmitを決める) cacheにないinoも含めたアクセス頻 度 な 持つ情報を変えると、BPF mapは作り直しになる 18
  17. vtableの範囲 使うアクセス情報 違うので、アルゴリズムを⾜ たびにvtableに関数 増えた 関数 kmodとBPFのどちらが何を持つか on_evict ino とに追い出

    てよい を0/1で返 。順序はkmod on_insert、on_remove、pick_victim BPF 順序を持ち、kmod 返ったinoを確 on_size 容量の計上はkmod、予算の配分はBPF on_hit 数えるのはkmod、使い⽅はBPF missの途中のino kmodは追い出 決める める ないので、policyに⼊れない 提案はBPF、確定はkmodと分けないと状態がずれる 出典:docs/design/design-review-checklist.md §3 19
  18. bpf_arena アプリ daemon ユーザー空間 GET S3 /dev/s3fs_ebpf カーネル read(2) VFS

    generic_file_read_iter page cacheのmiss eBPFプログラム fentry、struct_ops ⾃作FS(kmod) read_folio、evict、KFunc 参照するメモリ領域 page cache ファイルの中⾝ ユーザー空間のプロセス 既存のカーネルと外部 出典:masu-mi/s3fs-ebpf(pcacheモード) BPF map 状態と統計 bpf_arena 旧構成のみ ⾃作FS(kmod) chunk表 kmodのキャッシュ eBPF eBPFの呼び出 20
  19. bpf_arenaの使い⽅ bpf_arenaはユーザー空間とeBPF間の共有メモリ(最⼤4 GiB) struct { __uint(type, BPF_MAP_TYPE_ARENA); __uint(map_flags, BPF_F_MMAPABLE); __uint(max_entries,

    1024); /* pages, up to 4 GiB */ } arena SEC(".maps"); struct node __arena *n = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0); daemonはmapのfdをmmap 、同 ノードを普通のポインタで辿る __arena 付いたものは、コンパイラ arenaの中のメモリと て扱い、参照外 の前にcastを⼊れる 要件:Linux 6.9以降。__arenaの⾃動のcastにはclang 19以降 出典:Linux 6.12 kernel/bpf/arena.c 21
  20. arenaのアドレス解決 eBPF、ユーザープロセスでmmap先のアドレス 異なるので解決⽅法 必要 →ポインタはユーザー空間のアドレスを格納、BPFは下位32bitに⾃分の基底を⾜ て読む user_vm_start user_vm arenaのmmap page

    同 kern_vm 物理ページ 4 GiBの予約 arenaの範囲 kern_vm_start kern_vm_start + (u32)user_vm_start page stored n->next : 0x00007f7d_26200040 (user VA) cast_kern : w2 = w2 -> 0x26200040 (offset from kern_vm_start) load : r3 = *(u64 *)(r2 + r12 + off) r12 = kern_vm_start 上位32bitを捨てるので、壊れた値でも予約領域の外に出ない 出典:Linux 6.12 kernel/bpf/arena.c、kernel/bpf/verifier.c 22
  21. BPF_F_NO_USER_CONV arenaのmapにBPF_F_NO_USER_CONVを付 ると、cast_user 1命令になる 付ける場所:arenaのmapの定義 __uint(map_flags, BPF_F_MMAPABLE | BPF_F_NO_USER_CONV); 既定:JITが出す7命令

    付けたとき mov mov w2, w1 shl r2, 32 or r2, user_vm_start >> 32 rol r2, 32 xor r11, r11 test w2, w2 w2, w1 verifier 32bitのmovに書 換え、下位32bitだ を残 引 換えに、ユーザー空間はポインタをそのまま辿れない cmove r2, r11 上位32bitにuser_vm_startを戻 、NULLは0のまま 読み書きでkern_vm_startを⾜すのは、付けても付けなくても同じ 出典:Linux 6.12 arch/x86/net/bpf_jit_comp.c、kernel/bpf/verifier.c 23
  22. BPF_F_NO_USER_CONV arenaのmapにBPF_F_NO_USER_CONVを付 ると、cast_user 1命令になる 付ける場所:arenaのmapの定義 __uint(map_flags, BPF_F_MMAPABLE | BPF_F_NO_USER_CONV); 既定:JITが出す7命令

    付けたとき mov mov w2, w1 shl r2, 32 or r2, user_vm_start >> 32 rol r2, 32 xor r11, r11 test w2, w2 w2, w1 verifier 32bitのmovに書 換え、下位32bitだ を残 引 換えに、ユーザー空間はポインタをそのまま辿れない ユーザー空間でのarenaの開始アドレス(即値) cmove r2, r11 上位32bitにuser_vm_startを戻 、NULLは0のまま 読み書きでkern_vm_startを⾜すのは、付けても付けなくても同じ 出典:Linux 6.12 arch/x86/net/bpf_jit_comp.c、kernel/bpf/verifier.c 24
  23. cilium/ebpfでのloadの順序 cilium/ebpfでは、arenaを先に作ってmmap て ら、progをload る LoadAndAssign mapを作る user_vm_start 2段のload arenaだ

    NewMap progをload 拒否 れる 決まる前にaddr_space_castを検査 る unix.Mmap LoadAndAssign MapReplacements loadで る mmapの前にloadすると、addr_space_castが拒否される 25
  24. addr_space_castのMarshalの不具合 cilium/ebpfの古い版は、addr_space_castのoffを0に書 換えていた .oを読む デコード (だいたい逆アセ) mapのfdや kfuncのIDを埋める Marshalで バイト列に戻

    BPF_PROG_LOAD cilium/ebpf loadの前に⾏うリンクとrelocation clangが出した命令 Marshalのあと code off imm ALU64 MOV X off = 1 imm = 1 ALU64 MOV X off = 0 imm = 1 BPF_MOV uses reserved fieldsで拒否 26
  25. addr_space_castのMarshalの不具合 cilium/ebpfの古い版は、addr_space_castのoffを0に書 換えていた .oを読む デコード (だいたい逆アセ) mapのfdや kfuncのIDを埋める Marshalで バイト列に戻

    BPF_PROG_LOAD cilium/ebpf loadの前に⾏うリンクとrelocation clangが出した命令 Marshalのあと code off imm ALU64 MOV X off = 1 imm = 1 ALU64 MOV X off = 0 imm = 1 BPF_MOV uses reserved fieldsで拒否 MOV命令クラスは off={ 8,16,32 } のみだったaddr_space_cast(1)が追加 27
  26. addr_space_castのMarshalの不具合 cilium/ebpfの古い版は、addr_space_castのoffを0に書 換えていた .oを読む デコード (だいたい逆アセ) mapのfdや kfuncのIDを埋める Marshalで バイト列に戻

    BPF_PROG_LOAD cilium/ebpf loadの前に⾏うリンクとrelocation clangが出した命令 Marshalのあと code off imm ALU64 MOV X off = 1 imm = 1 ALU64 MOV X off = 0 imm = 1 BPF_MOV uses reserved fieldsで拒否 修正はmainに⼊ったが、タグ付きのリリースにはまだない 28
  27. arenaで残る回避策 loadの順序のほ にも、ツールとカーネルの両⽅に回避策 残る 課題 回避策 __arenaのグローバル変数(.addr_space.1)を、cilium/ebpfが 読み込めない arenaの先頭アドレスとオフセットをctxで渡 、毎回組み⽴てる

    clang 18以下ではcastなしでビルドが通る clang 19未満を拒否 、.oにcast progがarenaのmapを参照しないと関連付かない asm volatileで参照を作る kmodはkern_vmcaを使えない(未export、例外テーブルがな い) ユーザー側のページをpin て読む(GUP、1回約1.7 µs) ある テストで確 める kmodはarenaのページをpinして読むしかなく、主なread経路から外した 29
  28. ループの書き⽅ 6.12では、can_loopを⾜ ばarenaのlinked listをBPFのまま辿れる 拒否された形 6.12で通る形:can_loop(may_goto命令) while (n) { while

    (n && can_loop) { sum += n->payload; sum += n->payload; n = n->next; n = n->next; } } // BPF program is too large. // processed 1000001 insns 上限:1回の実⾏で、subprog 検査:周回の状態 前と同 とに合計8,388,608回(BPF_MAX_LOOPS)。使い切るとループを抜 る になれば打ち切るので、100万命令の予算を周回 とには使わない 資料:commit 011832b97b31「bpf: Introduce may_goto instruction」、selftests/bpf/bpf_experimental.h(can_loop) ループのためにKFuncを書く必要はほぼない 出典:Linux 6.12 kernel/bpf/verifier.c、selftests/bpf/bpf_experimental.h、bpf_arena_list.h 30
  29. s3fs代替の性能 open+read+closeの⼤ な差は、openでS3を確認 ない分 ら来ていた 構造だけ 独⾃キャッシュ 全体 open+read+close(p50) 3.2×

    140× 180× open+read+close、openでS3を確認 1.7× 1.27× 1.2× ⼩さいファイル10,000個のcat(warm) 2.9× 34× 39× cold read 300 MiB 0.74× 1.44× 1.27× 新規100 MiBのdurable write 0.97× 1.20× 1.04× 既存10 MiBの途中に4 KiB書く 0.66× 0.71× 0.68× warmのSQLite(OLTP) 1.89× 2.54× 0.97× 場⾯ 構造だ :両⽅キャッシュな 。独⾃キャッシュ:両⽅openのたびにpage cacheを捨てる。全体:両⽅ べてのキャッシュを使う。値は s3fs-fuseの時間÷s3fs-ebpfの時間 s3fs-fuseは新鮮 と⼀貫性のため、openのたびにHEAD HEAD る(open_check=1) 、kernel_cacheな ではpage cacheも捨てる。s3fs-ebpfは2⾏⽬だ 出典:docs/journal/benchmark-results.md §18.48、§18.51、§18.52 openで 32
  30. 得られた知⾒ openでS3を確認 ない設計 、⼤ な差の源だった • open+read+closeの180×は、openでS3を確認 • 同 仕事を

    ない設計の値で、確認 ると1.2× た構造の差は1.7×で、kmodを⼊れてまで速 なる場⾯は少ない • warmの実アプリはpage cacheのhitで終わり、FS全体の差は±3%に収まる • policyの差はhit rateで23 pt、missに約13 ms • 汎⽤の先読みは、simulatorでGET る条件では時間で18% 増え、hit rateも改善 出典:benchmark-results.md §18.48、§18.50〜§18.52、ann-workload-results.md な った 33
  31. 採⽤基準と設計上の注意 機能 とに、採⽤ る条件と、設計で守る と 分 れた 機能 採⽤する条件 設計上の注意

    struct_ops カーネル側に判断点の⼀式 めて差 替えたい bpf_arena BPFとユーザー空間 ポインタを含む構造を共有 、読むの その2者だ BPF以外のカーネルのコンテキスト ら読まない。4GiB以下の データ構造でユーザー空間 ら更新あり。ライブラリの対応を 確 める fentry 既存の関数を変え に観測 る 汎⽤関数のhot pathではtrampolineより中⾝ 象を判定 、共有値への書 込みは間引 KFunc カーネルの状態を読む窓⼝や、ロック BPFで書 ない処理を置 あり、実⾏中にまと 、外 フラグを残 要るなど 提案はBPF、確定はカーネルと分 、容量の計上やロックはカー ネルに残 。差 替えはRCUのポインタ1本で⾏う 効 。先頭で対 can_loopで書 るループの代わりに書 ない。ロック、コンテ キスト、終了性の保証はkmodの責任 34