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を共有することは可能ではあった.

frsyuki.hatenablog.com

この方法は汎用的である反面,決して簡単とは言い難い.そこで登場したのが,今回紹介する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 コマンドを使う.

簡単なサンプルが以下に用意されている:

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を行っているのは下記の部分.現状,bccpython 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全般の最近の動向が纏まっている