Welcome back to my series on ebpf; in the last post, we learned how to write a simple XDP-based packet filter. In this post, we’ll continue the work on eBPF to make it easier to write more complex programs. Yes, I promised to write a load balancer but instead opted to add support for global variables to hello-ebpf, documenting it in this short post.
When we want to configure our eBPF program, say to set a simple logLevel
setting, we currently have only one option: We could create an array map with one entry, our configuration value, and then use the usual methods to set its value and retrieve it. In Java, this would look like:
@BPFMapDefinition(maxEntries = 1) BPFArray<Integer> logLevel; void setLogLevel(int level) { logLevel.set(0, level); }
In the ebpf program itself, see Hello eBPF: Recording data in basic eBPF maps (2) for more information; the value would be used as:
struct { // this is auto-generated by hello-ebpf __uint (type, BPF_MAP_TYPE_ARRAY); __type (key, u32); __type (value, s32); __uint (max_entries, 1); } logLevel SEC(".maps"); s32 getLogLevel() { u32 zero = 0; return *bpf_map_lookup_elem(&map, &zero); }
Memory Segmentation
This is quite cumbersome, especially as C already has a concept of global variables. Why couldn’t we just use these:
s32 logLevel; s32 getLogLevel() { return logLevel; }
A program’s memory at runtime is split into multiple segments:
Segments as BPF Maps
Starting with Linux 5.2, d8eca5bbb2be (“bpf: implement lookup-free direct value access for maps”), we can directly access segments from the user-land as if they are a single-valued array map and can use the BPF Type Format information for every segment to see where each global variable is placed.
But how can we expose this to the user in user-land in a usable manner? We can extend the preprocessor to do its magic:
final GlobalVariable<Integer> logLevel = new GlobalVariable(/* initial value */ 42); // later program.logLevel.set(...); // or program.logLevel.get();
It is essential to state that the eBPF program can change the global variables, too, allowing us to have a simple communication channel between user-land and kernel-land.
This mechanism isn’t limited to scalar values; you can also store more complex values:
@Type record Server(int ip, @Size(10) int[] ports, int portsCount) {} final GlobalVariable<Server> server = new GlobalVariable<>(new Server(..., new int[]{22, 23, 0, 0, 0, 0, 0, 0, 0, 0}, 2));
Conclusion
Using global variables, we can easily configure our eBPF and communicate between user-land and kernel-land. Add some preprocessor magic, and we have a powerful new feature in hello-ebpf. With this at hand, we can finally start writing a load balancer.
Thanks for joining us on the journey to create an eBPF library for Java. I’ll see you in two weeks for the next installment.
This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone. Thanks to Dylan Reimerink for answering all my questions and sharing all his knowledge on eBPF; this blog post is based on one of his answers on StackOverflow.