Persistent eBPF map object with bcc
Linux kernel 4.4以降には,eBPF map/programを"永続化"する仕組みが実装されている.ここではその概要を説明しつつ,iovisor/bccを使って実際にその動作を確かめてみる.
Background: eBPF objectを複数プロセスで共有したい
eBPFにmapというデータ構造があることはXDPの紹介エントリのサンプルコード内で軽く触れた.
通常,mapはそれを作成したプロセスからのみ読み書き可能である.しかし,いくつかのユースケースにおいては,mapを複数のプロセスから読み書きできると都合が良いことがある.たとえば何からの統計情報(例えばパケットカウンタ)をmapに保存してそれを外部に転送したい場合,「統計情報をmapに保存するプロセス」と「収集された統計情報を加工して外部に転送するプロセス」とに分割したほうが,実装の見通しの観点から望ましい.また,mapに何らかの設定情報(例えばIPルーティングテーブル)を保存してパケットを加工したい場合,「ルーティングテーブルを参照してパケットの加工を行うプロセス」と「ルーティングテーブルのアップデートを行うプロセス」とに分割したほうが,安定性の観点から望ましい.
Kernel 4.4以前から,mapのfile descriptorをunix domain socket経由で他プロセスに共有することで,複数プロセス間でmapを共有することは可能ではあった.
この方法は汎用的である反面,決して簡単とは言い難い.そこで登場したのが,今回紹介するeBPF objectに特化した仕組みである.
Solution: eBPF objectをBPF filesystemに"ピン付け"する
Linux kernel 4.4から,eBPF objectをファイルシステム上のパスに割り当てる (“pinning”) する仕組みが実装された.
まず,eBPF objectとファイルシステム上のパスのマッピングを行うファイルシステム (ここでは仮にbpffsと呼ぶことにする) が用意されている.このファイルシステムを任意のパス (一般に/sys/fs/bpf
) にマウントしておく.
linux/inode.c at master · torvalds/linux · GitHub
その上で,bpf(2)
syscallに追加された BPF_OBJ_PIN
コマンドを使ってpinningを行う.ファイルからeBPF objectを取得する際には同様に BPF_OBJ_GET
コマンドを使う.
簡単なサンプルが以下に用意されている:
- Pin: prototype-kernel/bpf.c at master · netoptimizer/prototype-kernel · GitHub
- Get: prototype-kernel/bpf.c at master · netoptimizer/prototype-kernel · GitHub
Example: bccを使って複数プロセス間でeBPF mapの読み書き
C言語のコードから直接syscallを使ってpinningを行う例は既にいくつかある (例1, 例2) ため,ここではbccを使ってpython経由でeBPF mapのpinningと他プロセスからの読み込みを試してみる.例として,受信したパケット数をeBPFプログラムでカウントしてeBPF mapに書き込み,それを他のプロセスから読み込む場合を考える.
Code
counter.py: eBPF mapへの書き込み & pinning
受信したパケット数をカウントしてeBPF map “packet_counter” に書き込むプログラムである.このプログラムはtc-bpf(8)
経由でネットワークインタフェースにアタッチされる.
from bcc import BPF, libbcc import pyroute2 import ctypes import time import sys code = """ #include <uapi/linux/bpf.h> #include <uapi/linux/pkt_cls.h> BPF_TABLE("array", uint32_t, long, packet_counter, 1); int counter(struct __sk_buff *ctx) { uint32_t index = 0; long *value = packet_counter.lookup(&index); if (value) *value += 1; return TC_ACT_OK; } """ def count_packet(iface, pin_path): bpf = BPF(text=code, cflags=["-w"]) func = bpf.load_func("counter", BPF.SCHED_CLS) counter = bpf.get_table("packet_counter") print("map fd: {}".format(counter.map_fd)) ret = libbcc.lib.bpf_obj_pin(counter.map_fd, ctypes.c_char_p(pin_path)) if ret != 0: raise Exception("Failed to pin map") print("Pinned at: {}".format(pin_path)) ip = pyroute2.IPRoute() ipdb = pyroute2.IPDB(nl=ip) idx = ipdb.interfaces[device].index ip.tc("add", "clsact", idx) ip.tc("add-filter", "bpf", idx, ":1", fd=func.fd, name=func.name, parent="ffff:fff2", classid=1, direct_action=True) print("Hit CTRL+C to stop") while True: try: print(counter[0].value) time.sleep(1) except KeyboardInterrupt: print("Removing filter from device") break ip.tc("del", "clsact", idx) ipdb.release() if __name__ == '__main__': iface = device = sys.argv[1] pin_path = sys.argv[2] count_packet(iface, pin_path)
実際にpinningを行っているのは下記の部分.現状,bccのpython bindingに専用のインタフェースが無いため,libbccの関数を直接呼び出している.
counter = bpf.get_table("packet_counter")
ret = libbcc.lib.bpf_obj_pin(counter.map_fd, ctypes.c_char_p(pin_path))
reader.py: pinningされたeBPF mapの読み出し
こちらのコードでmapの値を読み込む.
from bcc import libbcc, table import ctypes import sys class PinnedArray(table.Array): def __init__(self, map_path, keytype, leaftype, max_entries): map_fd = libbcc.lib.bpf_obj_get(ctypes.c_char_p(map_path)) if map_fd < 0: raise ValueError("Failed to open eBPF map") self.map_fd = map_fd self.Key = keytype self.Leaf = leaftype self.max_entries = max_entries def read_counter(map_path): counter = PinnedArray(map_path, ctypes.c_uint32, ctypes.c_long, 1) return counter[0].value if __name__ == '__main__': path = sys.argv[1] print(read_counter(path))
キモは下記の部分.下記コードでmapのファイルディスクリプタが取得できる.上記コードではbcc.table.Array
を再利用している.
map_fd = libbcc.lib.bpf_obj_get(ctypes.c_char_p(map_path))
実行例
まずはbpffsをマウント.
$ sudo mount -t bpf none /sys/fs/bpf $ mount | grep bpf none on /sys/fs/bpf type bpf (rw,relatime)
続いてcounter.py
を実行.
$ sudo python counter.py eth0 /sys/fs/bpf/counter map fd: 4 Pinned at: /sys/fs/bpf/counter Hit CTRL+C to stop 0 37 38 39 70 ...
/sys/fs/bpf/counter
が作成されていることが確認できる.
$ ls -l /sys/fs/bpf/counter -rw-------. 1 root root 0 Apr 8 14:23 /sys/fs/bpf/counter
この状態でreader.py
を実行.counter.py
側の出力とほぼ同じ値が取得できるはず.
$ sudo python reader.py /sys/fs/bpf/counter
253
なお,pinningしたobjectは生成元のプロセス(ここではcounter.py
)を終了したとしても削除されない.削除するには明示的にunlink
する必要がある.
$ ls -l /sys/fs/bpf/counter -rw-------. 1 root root 0 Apr 8 14:23 /sys/fs/bpf/counter $ sudo unlink /sys/fs/bpf/counter $ ls -l /sys/fs/bpf/counter ls: cannot access '/sys/fs/bpf/counter': No such file or directory
References
- Persistent BPF objects [LWN.net]
- Background, solutionのセクションに書いた内容がより詳しく述べられている
- 初期のパッチ(v1; 実際にマージされたのはv2)を元に書かれているため,実際の実装とは内容が一部異なる.(
BPF_PIN_FD
->BPF_OBJ_PIN
,BPF_GET_FD
->BPF_OBJ_GET
)
- bpf: add support for persistent maps/progs
- 初期実装のコミット.LWNの記事同様,背景が詳しく述べられている
- BPFの現在 // Speaker Deck
- Map pinningだけでなく,BPF全般の最近の動向が纏まっている