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");
エントリの構造体であるdatarec
にrx_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_buff
とxdp_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)
解いてみる
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