xdp-tutorial basic03-map-counterをやってみる

github.com

Assignment 1

現状の実装にbyteカウンタの機能を追加するというもの。

解いてみる

xdp_stats_mapのエントリの構造体にbyteカウンタのフィールドを追加する

eBPFマップは以下のように定義されている。

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, struct datarec);
    __uint(max_entries, XDP_ACTION_MAX);
} xdp_stats_map SEC(".maps");

エントリの構造体であるdatarecrx_bytesというbyteカウンタのフィールドを追加する。

file: basic03-map-counter/common_kern_user.h

@@ -7,6 +7,7 @@
 /* This is the data record stored in the map */
 struct datarec {
        __u64 rx_packets;
+       __u64 rx_bytes;
        /* Assignment#1: Add byte counters */
 };
packetの長さを求めて、それを上で作ったrx_bytesというフィールドに加算する。

xdp programのctxに指定されているxdp_mdというやつから、packetの内容が読める。

/* user accessible metadata for XDP packet hook
 * new fields must be added to the end of this structure
 */
struct xdp_md {
    __u32 data;
    __u32 data_end;
    __u32 data_meta;
    /* Below access go through struct xdp_rxq_info */
    __u32 ingress_ifindex; /* rxq->dev->ifindex */
    __u32 rx_queue_index;  /* rxq->queue_index  */

    __u32 egress_ifindex;  /* txq->dev->ifindex */
};

xdp_mdtaカーネルによってリマップされた後の形で、実際はxdp_buffxdp_rxq_infoの形になっているようだ。(問題文の説明にそう書いてあった。)

struct xdp_buff {
    void *data;
    void *data_end;
    void *data_meta;
    void *data_hard_start;
    unsigned long handle;
    struct xdp_rxq_info *rxq;
} __attribute__((preserve_access_index));

struct xdp_rxq_info {
    /* Structure does not need to contain all entries,
    * as "preserve_access_index" will use BTF to fix this...
    */
    struct net_device *dev;
    __u32 queue_index;
} __attribute__((preserve_access_index));

dataがパケットの先頭のアドレスでdata_endがパケットの末尾のアドレスであるようだ。
つまりはdataとdata_endの距離がパケット長になる。

ということで以下のように修正。

basic03-map-counter/xdp_prog_kern.c

@@ -25,8 +25,8 @@ struct {
 SEC("xdp")
 int  xdp_stats1_func(struct xdp_md *ctx)
 {
-       // void *data_end = (void *)(long)ctx->data_end;
-       // void *data     = (void *)(long)ctx->data;
+       void *data_end = (void *)(long)ctx->data_end;
+       void *data     = (void *)(long)ctx->data;
        struct datarec *rec;
        __u32 key = XDP_PASS; /* XDP_PASS = 2 */

@@ -39,13 +39,14 @@ int  xdp_stats1_func(struct xdp_md *ctx)
        if (!rec)
                return XDP_ABORTED;

+
        /* Multiple CPUs can access data record. Thus, the accounting needs to
         * use an atomic operation.
         */
        lock_xadd(&rec->rx_packets, 1);
-        /* Assignment#1: Add byte counters
-         * - Hint look at struct xdp_md *ctx (copied below)
-         *
+       __u64 bytes = data_end - data;
+       lock_xadd(&rec->rx_bytes, bytes);
+        /*
          * Assignment#3: Avoid the atomic operation
          * - Hint there is a map type named BPF_MAP_TYPE_PERCPU_ARRAY
          */
ユーザ空間のプログラム側でmap上のbyteカウンタの値を取得する

rx_packetsと同様にrx_bytesの値を取得するようにする。

xdp_load_and_stats.c

@@ -181,6 +187,7 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *
rec)

        /* Assignment#1: Add byte counters */
        rec->total.rx_packets = value.rx_packets;
+       rec->total.rx_bytes = value.rx_bytes;
        return true;
 }
ユーザ空間のプログラム側でmap上のbyteカウンタの情報を出力する

これもrx_packetsと同様にrx_bytesの値の情報を出力する。
(コード上のコメントに従えば、Mpbsを出力させる意図があるようだが、この後の検証でそんなにたくさんのデータを流す予定はないので、bpsに変更している。)

xdp_load_and_stats.c

@@ -118,12 +118,15 @@ static void stats_print(struct stats_record *stats_rec,
        struct record *rec, *prev;
        double period;
        __u64 packets;
+       __u64 bytes;
+       __u64 bits;
        double pps; /* packets per sec */
+       double bps; /* bits per sec */

        /* Assignment#2: Print other XDP actions stats  */
        {
                char *fmt = "%-12s %'11lld pkts (%'10.0f pps)"
-                       //" %'11lld Kbytes (%'6.0f Mbits/s)"
+                       " %'11lld bytes (%'6.0f bits/s)"
                        " period:%f\n";
                const char *action = action2str(XDP_PASS);
                rec  = &stats_rec->stats[0];
@@ -135,8 +138,11 @@ static void stats_print(struct stats_record *stats_rec,

                packets = rec->total.rx_packets - prev->total.rx_packets;
                pps     = packets / period;
+               bytes   = rec->total.rx_bytes - prev->total.rx_bytes;
+               bits    = bytes << 3;
+               bps     = bits / period;

-               printf(fmt, action, rec->total.rx_packets, pps, period);
+               printf(fmt, action, rec->total.rx_packets, pps, rec->total.rx_bytes, bps, period);
        }
 }

検証

$ make
    CC       xdp_load_and_stats
    CLANG    xdp_prog_kern.o
    LLC      xdp_prog_kern.o
$ sudo ./xdp_load_and_stats -d lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:211 attached on device:lo(ifindex:1)

Collecting stats from BPF map
 - BPF map (bpf_map_type:2) id:75 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

XDP-action
XDP_PASS               1 pkts (         4 pps)          90 bytes (  2877 bits/s) period:0.250223
XDP_PASS               2 pkts (         0 pps)         180 bytes (   360 bits/s) period:2.000256
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000286
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000314
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000300
XDP_PASS               4 pkts (         1 pps)         376 bytes (   784 bits/s) period:2.000297
XDP_PASS               4 pkts (         0 pps)         376 bytes (     0 bits/s) period:2.000259
XDP_PASS               4 pkts (         0 pps)         376 bytes (     0 bits/s) period:2.000316

packetのカウンタが2pktsから4pktsに上がっている部分がある。
ここのところで実は以下の通り、pingを打った。

$ping 127.0.0.1 -c 1

この部分のbytesカウンタを見ると180から376に上がっている。 つまり、このとき、loを通過したicmpパケット合計が196 bytes(376-180)であれば、正しく実装できたということ。

ここで取得したpcapを見てみる。

requestとreplyがそれぞれ98 bytesで合計196 bytesなのでうまくいっている。

Assignment 2

user 空間側のプログラムを修正してxdp_actionごとにstatsを集計するようにするというもの。

xdp_actionとは以下のこと。

linux/bpf.h

/* User return codes for XDP prog type.
 * A valid XDP program must return one of these defined values. All other
 * return codes are reserved for future use. Unknown return codes will
 * result in packet drops and a warning via bpf_warn_invalid_xdp_action().
 */
enum xdp_action {
    XDP_ABORTED = 0,
    XDP_DROP,
    XDP_PASS,
    XDP_TX,
    XDP_REDIRECT,
};

解いてみる

xdp_stats_mapの構造を見てみる

xdp_stats_mapは以下のようになっている。

xdp_prog_kern.c

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, struct datarec);
    __uint(max_entries, XDP_ACTION_MAX);
} xdp_stats_map SEC(".maps");

XDP_ACTION_MAXは以下のように定義されている。

common_kern_user.h

#ifndef XDP_ACTION_MAX
#define XDP_ACTION_MAX (XDP_REDIRECT + 1)
#endif

つまり、xdp_stats_mapはxdp_actionの種類分だけエントリがあるということ。
そして、xdp_stats_mapのkeyにはxdp_actionが指定される。

xdp_prog_kern.c

 __u32 key = XDP_PASS; /* XDP_PASS = 2 */

    /* Lookup in kernel BPF-side return pointer to actual data record */
    rec = bpf_map_lookup_elem(&xdp_stats_map, &key);

複数のエントリを扱えるように改良する

xdp_load_and_statsプログラムではxdp_stats_mapのエントリをstats_recordという構造体に入れて扱う。
stats_recordはxdp_stats_mapのエントリの生の構造体であるdata_recをラップしたものになっている。

xdp_load_and_stats.c

struct record {
    __u64 timestamp;
    struct datarec total; /* defined in common_kern_user.h */
};

struct stats_record {
    struct record stats[1];
};

stats_recordがxdp_stats_mapのエントリをxdp_action個数分保存できるように修正する。

mapからxdp_actionの個数分だけ、エントリを取り出す。

xdp_load_and_stats.c

@@ -97,7 +97,7 @@ struct record {
 };

 struct stats_record {
-       struct record stats[1]; /* Assignment#2: Hint */
+       struct record stats[XDP_ACTION_MAX];
 };

xdp_load_and_stats.c

@@ -194,10 +194,11 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *rec)
 static void stats_collect(int map_fd, __u32 map_type,
                          struct stats_record *stats_rec)
 {
-       /* Assignment#2: Collect other XDP actions stats  */
-       __u32 key = XDP_PASS;
-
-       map_collect(map_fd, map_type, key, &stats_rec->stats[0]);
+       map_collect(map_fd, map_type, XDP_ABORTED, &stats_rec->stats[XDP_ABORTED]);
+       map_collect(map_fd, map_type, XDP_DROP, &stats_rec->stats[XDP_DROP]);
+       map_collect(map_fd, map_type, XDP_PASS, &stats_rec->stats[XDP_PASS]);
+       map_collect(map_fd, map_type, XDP_TX, &stats_rec->stats[XDP_TX]);
+       map_collect(map_fd, map_type, XDP_REDIRECT, &stats_rec->stats[XDP_REDIRECT]);
 }

 static void stats_poll(int map_fd, __u32 map_type, int interval)

statsの出力を修正する

xdp_load_and_stats.c

@@ -123,14 +123,14 @@ static void stats_print(struct stats_record *stats_rec,
        double pps; /* packets per sec */
        double bps; /* bits per sec */

-       /* Assignment#2: Print other XDP actions stats  */
-       {
+       int xdpact = 0;
+       for (xdpact = 0; xdpact < XDP_ACTION_MAX; xdpact++) {
                char *fmt = "%-12s %'11lld pkts (%'10.0f pps)"
                        " %'11lld bytes (%'6.0f bits/s)"
                        " period:%f\n";
-               const char *action = action2str(XDP_PASS);
-               rec  = &stats_rec->stats[0];
-               prev = &stats_prev->stats[0];
+               const char *action = action2str(xdpact);
+               rec  = &stats_rec->stats[xdpact];
+               prev = &stats_prev->stats[xdpact];

                period = calc_period(rec, prev);
                if (period == 0)

動かしてみる

xdp_actionごとにstatsが出力される。ただし、kernel側で動くプログラムにはタッチしてないので、XDP_PASSしかカウンタは上がらない。

$ sudo ./xdp_load_and_stats --dev=lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:52 attached on device:lo(ifindex:1)

Collecting stats from BPF map
 - BPF map (bpf_map_type:2) id:11 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250372
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_PASS               1 pkts (         4 pps)          90 bytes (  2876 bits/s) period:0.250382
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_PASS               2 pkts (         0 pps)         180 bytes (   360 bits/s) period:2.000472
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000388
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000387
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000387
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000386
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000386

Assignment3

atomicな加算処理はコストが高いので、これをやめて、BPF_MAP_TYPE_PERCPU_ARRAYのeBPFマップを使うというもの。

atomicな加算とは何か?

__sync_fetch_and_addはコンパイラで提供される組み込み関数でこれを使うとatomicな加算が実現できる。

#ifndef lock_xadd
#define lock_xadd(ptr, val) ((void) __sync_fetch_and_add(ptr, val))
#endif

BPF_MAP_TYPE_PERCPU_ARRAYとは何か?

CPU毎にメモリ領域を割り当ててくれるBPF_MAP_TYPE_ARRAY。

CPU毎にメモリ領域がそれぞれ存在するため、値の取り出しをするときは以下のようにcpuを指定してやる必要がある。

void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)

docs.kernel.org

解いてみる

MAPの種類を変更する

xdp_prog_kern.c

@@ -9,7 +9,7 @@
  * - The idea is to keep stats per (enum) xdp_action
  */
 struct {
-       __uint(type, BPF_MAP_TYPE_ARRAY);
+       __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
        __type(key, __u32);
        __type(value, struct datarec);
        __uint(max_entries, XDP_ACTION_MAX);

atomicな加算をやめて、普通に加算する

xdp_prog_kern.c

@@ -43,13 +43,9 @@ int  xdp_stats1_func(struct xdp_md *ctx)
        /* Multiple CPUs can access data record. Thus, the accounting needs to
         * use an atomic operation.
         */
-       lock_xadd(&rec->rx_packets, 1);
+       rec->rx_packets++;
        __u64 bytes = data_end - data;
-       lock_xadd(&rec->rx_bytes, bytes);
-        /*
-         * Assignment#3: Avoid the atomic operation
-         * - Hint there is a map type named BPF_MAP_TYPE_PERCPU_ARRAY
-         */
+       rec->rx_bytes += bytes;

        return XDP_PASS;
 }

ユーザ空間での値の集計

(問題文でcpuごとの値の集計を行うコードが提供されているので、それをそのまま使っている。)

xdp_load_and_stats.c

@@ -158,11 +158,26 @@ void map_get_value_array(int fd, __u32 key, struct datarec *value)
 /* BPF_MAP_TYPE_PERCPU_ARRAY */
 void map_get_value_percpu_array(int fd, __u32 key, struct datarec *value)
 {
-       /* For percpu maps, userspace gets a value per possible CPU */
-       // unsigned int nr_cpus = libbpf_num_possible_cpus();
-       // struct datarec values[nr_cpus];
+       /* For percpu maps, user space gets a value per possible CPU */
+       unsigned int nr_cpus = libbpf_num_possible_cpus();
+       struct datarec values[nr_cpus];
+       __u64 sum_bytes = 0;
+       __u64 sum_pkts = 0;
+       int i;
+
+       if ((bpf_map_lookup_elem(fd, &key, values)) != 0) {
+               fprintf(stderr,
+                       "ERR: bpf_map_lookup_elem failed key:0x%X\n", key);
+               return;
+       struct datarec values[nr_cpus];
+       __u64 sum_bytes = 0;
+       __u64 sum_pkts = 0;
+       int i;
+
+       if ((bpf_map_lookup_elem(fd, &key, values)) != 0) {
+               fprintf(stderr,
+                       "ERR: bpf_map_lookup_elem failed key:0x%X\n", key);
+               return;
+       }

-       fprintf(stderr, "ERR: %s() not impl. see assignment#3", __func__);
+       /* Sum values from each CPU */
+       for (i = 0; i < nr_cpus; i++) {
+               sum_pkts  += values[i].rx_packets;
+               sum_bytes += values[i].rx_bytes;
+       }
+       value->rx_packets = sum_pkts;
+       value->rx_bytes   = sum_bytes;
 }

 static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *rec)
@@ -177,7 +192,8 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *
rec)
                map_get_value_array(fd, key, &value);
                break;
        case BPF_MAP_TYPE_PERCPU_ARRAY:
-               /* fall-through */
+               map_get_value_percpu_array(fd, key, &value);
+               break;
        default:
                fprintf(stderr, "ERR: Unknown map_type(%u) cannot handle\n",
                        map_type);

解説

カーネル空間

  • 適当なCPUでマップを更新する。

ユーザ空間

  • 各CPUに個別に割当らているマップが持つカウンタを全て足し合わせて、それをstatsとして表示する。

動かしてみる

$ sudo ./xdp_load_and_stats --dev=lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:74 attached on device:lo(ifindex:1)

 Collecting stats from BPF map
  - BPF map (bpf_map_type:6) id:19 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

  XDP-action
  XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250211
  XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250183
  XDP_PASS               1 pkts (         4 pps)          90 bytes (  2878 bits/s) period:0.250184
  XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250184
  XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250184
  XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000381
  XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000383