mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-11-03 14:15:17 +08:00
docs: add comprehensive BPF Arena tutorial detailing features, use cases, and examples
This commit is contained in:
@@ -1,233 +1,256 @@
|
||||
# BPF Workqueues Tutorial
|
||||
# eBPF Tutorial by Example: BPF Workqueues for Asynchronous Sleepable Tasks
|
||||
|
||||
## What are BPF Workqueues?
|
||||
Ever needed your eBPF program to sleep, allocate memory, or wait for device I/O? Traditional eBPF programs run in restricted contexts where blocking operations crash the system. But what if your HID device needs timing delays between injected key events, or your cleanup routine needs to sleep while freeing resources?
|
||||
|
||||
BPF workqueues allow you to schedule **asynchronous work** from BPF programs. This enables:
|
||||
- Deferred processing
|
||||
- Non-blocking operations
|
||||
- Background task execution
|
||||
- Sleepable context for long-running operations
|
||||
This is what **BPF Workqueues** enable. Created by Benjamin Tissoires at Red Hat in 2024 for HID-BPF device handling, workqueues let you schedule asynchronous work that runs in process context where sleeping and blocking operations are allowed. In this tutorial, we'll explore why workqueues were created, how they differ from timers, and build a complete example demonstrating async callback execution.
|
||||
|
||||
## The Problem
|
||||
## Introduction to BPF Workqueues: Solving the Sleep Problem
|
||||
|
||||
### Before bpf_wq: Limitations of bpf_timer
|
||||
### The Problem: When eBPF Can't Sleep
|
||||
|
||||
**bpf_timer** runs in **softirq context**, which has severe limitations:
|
||||
- ❌ Cannot sleep
|
||||
- ❌ Cannot use `kzalloc()` (memory allocation)
|
||||
- ❌ Cannot wait for device I/O
|
||||
- ❌ Cannot perform any blocking operations
|
||||
Before BPF workqueues existed, developers had `bpf_timer` for deferred execution. Timers work great for scheduling callbacks after a delay, perfect for updating counters or triggering periodic events. But there's a fundamental limitation that made timers unusable for certain critical use cases: **bpf_timer runs in softirq (software interrupt) context**.
|
||||
|
||||
### Real-World Use Case: HID Device Handling
|
||||
Softirq context has strict rules enforced by the kernel. You cannot sleep or wait for I/O - any attempt to do so will cause kernel panics or deadlocks. You cannot allocate memory using `kzalloc()` with `GFP_KERNEL` flag because memory allocation might need to wait for pages. You cannot communicate with hardware devices that require waiting for responses. Essentially, you cannot perform any blocking operations that might cause the CPU to wait.
|
||||
|
||||
**Problem**: HID (Human Interface Devices - keyboards, mice, tablets) devices need to:
|
||||
1. **React to events asynchronously** - Transform input, inject new events
|
||||
2. **Communicate with hardware** - Re-initialize devices after sleep/wake
|
||||
3. **Perform device I/O** - Send commands, wait for responses
|
||||
This limitation became a real problem for Benjamin Tissoires at Red Hat when he was developing HID-BPF in 2023. HID devices (keyboards, mice, tablets, game controllers) frequently need operations that timers simply can't handle. Imagine implementing keyboard macro functionality where pressing F1 types "hello" - you need 10ms delays between each keystroke for the system to properly process events. Or consider a device with buggy firmware that needs re-initialization after system wake - you must send commands and wait for hardware responses. Timer callbacks in softirq context can't do any of this.
|
||||
|
||||
**These operations require sleepable context!**
|
||||
As Benjamin Tissoires explained in his kernel patches: "I need something similar to bpf_timers, but not in soft IRQ context... the bpf_timer functionality would prevent me to kzalloc and wait for the device."
|
||||
|
||||
## The Solution: bpf_wq
|
||||
### The Solution: Process Context Execution
|
||||
|
||||
Developed by **Benjamin Tissoires** (Red Hat) in 2024 as part of HID-BPF work.
|
||||
In early 2024, Benjamin proposed and developed **bpf_wq** - essentially "bpf_timer but in process context instead of softirq." The kernel community merged it into Linux v6.10+ in April 2024. The key insight is simple but powerful: by running callbacks in process context (through the kernel's workqueue infrastructure), BPF programs gain access to the full range of kernel operations.
|
||||
|
||||
### Key Quote from Kernel Patches:
|
||||
> "I need something similar to bpf_timers, but not in soft IRQ context...
|
||||
> the bpf_timer functionality would prevent me to kzalloc and wait for the device"
|
||||
Here's what changes with process context:
|
||||
|
||||
### What bpf_wq Provides:
|
||||
- ✅ **Sleepable context** - Can perform blocking operations
|
||||
- ✅ **Memory allocation** - Can use `kzalloc()` safely
|
||||
- ✅ **Device I/O** - Can wait for hardware responses
|
||||
- ✅ **Asynchronous execution** - Deferred work without blocking main path
|
||||
| Feature | bpf_timer (softirq) | bpf_wq (process) |
|
||||
|---------|---------------------|------------------|
|
||||
| **Can sleep?** | ❌ No - will crash | ✅ Yes - safe to sleep |
|
||||
| **Memory allocation** | ❌ Limited flags only | ✅ Full `kzalloc()` support |
|
||||
| **Device I/O** | ❌ Cannot wait | ✅ Can wait for responses |
|
||||
| **Blocking operations** | ❌ Prohibited | ✅ Fully supported |
|
||||
| **Latency** | Very low (microseconds) | Higher (milliseconds) |
|
||||
| **Use case** | Time-critical fast path | Sleepable slow path |
|
||||
|
||||
## Real-World Applications
|
||||
Workqueues enable the classic "fast path + slow path" pattern. Your eBPF program handles performance-critical operations immediately in the fast path, then schedules expensive cleanup or I/O operations to run asynchronously in the slow path. The fast path stays responsive while the slow path gets the capabilities it needs.
|
||||
|
||||
### 1. HID Device Quirks and Fixes
|
||||
### Real-World Applications
|
||||
|
||||
**Problem**: Many HID devices have firmware bugs or quirks requiring workarounds.
|
||||
The applications span multiple domains. **HID device handling** was the original motivation - injecting keyboard macros with timing delays, fixing broken device firmware dynamically without kernel drivers, re-initializing devices after wake from sleep, transforming input events on the fly. All these require sleepable operations that only workqueues can provide.
|
||||
|
||||
**Before bpf_wq**: Write kernel drivers, recompile kernel
|
||||
**With bpf_wq**: Load BPF program to fix device behavior dynamically
|
||||
**Network packet processing** benefits from async cleanup patterns. Your XDP program enforces rate limits and drops packets in the fast path (non-blocking), while a workqueue cleans up stale tracking entries in the background. This prevents memory leaks without impacting packet processing performance.
|
||||
|
||||
**Example Use Cases**:
|
||||
- Transform single key press into macro sequence
|
||||
- Fix devices that forget to send button release events
|
||||
- Invert mouse coordinates for broken hardware
|
||||
- Re-initialize device after wake from sleep
|
||||
**Security monitoring** can apply fast rules immediately, then use workqueues to query reputation databases or external threat intelligence services. The fast path makes instant decisions while the slow path updates policies based on complex analysis.
|
||||
|
||||
### 2. Network Packet Processing
|
||||
**Resource cleanup** defers expensive operations. Instead of blocking the main code path while freeing memory, closing connections, or compacting data structures, you schedule a workqueue to handle cleanup in the background.
|
||||
|
||||
**Problem**: Rate limiting requires tracking state and cleaning up old entries.
|
||||
## Implementation: Simple Workqueue Test
|
||||
|
||||
**Before**: Either block packet processing OR leak memory
|
||||
**With bpf_wq**:
|
||||
- Fast path: Check limits, drop packets (non-blocking)
|
||||
- Slow path: Workqueue cleans up stale entries (async)
|
||||
Let's build a complete example that demonstrates the workqueue lifecycle. We'll create a program that triggers on the `unlink` syscall, schedules async work, and verifies that both the main path and workqueue callback execute correctly.
|
||||
|
||||
### 3. Security and Monitoring
|
||||
|
||||
**Problem**: Security decisions need to consult external services or databases.
|
||||
|
||||
**Before**: All decisions must be instant (no waiting)
|
||||
**With bpf_wq**:
|
||||
- Fast path: Apply known rules immediately
|
||||
- Slow path: Query reputation databases, update policy
|
||||
|
||||
### 4. Resource Cleanup
|
||||
|
||||
**Problem**: Freeing resources (memory, connections) can be expensive.
|
||||
|
||||
**Before**: Block main path during cleanup
|
||||
**With bpf_wq**: Defer cleanup to background workqueue
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Comparison: bpf_timer vs bpf_wq
|
||||
|
||||
| Feature | bpf_timer | bpf_wq |
|
||||
|---------|-----------|--------|
|
||||
| **Context** | Softirq (interrupt) | Process (workqueue) |
|
||||
| **Can sleep?** | ❌ No | ✅ Yes |
|
||||
| **Memory allocation** | ❌ No | ✅ Yes |
|
||||
| **Device I/O** | ❌ No | ✅ Yes |
|
||||
| **Latency** | Very low (μs) | Higher (ms) |
|
||||
| **Use case** | Time-critical | Sleepable operations |
|
||||
|
||||
### When to Use Each
|
||||
|
||||
**Use bpf_timer when:**
|
||||
- You need microsecond-level precision
|
||||
- Operations are fast and non-blocking
|
||||
- You're just updating counters or state
|
||||
|
||||
**Use bpf_wq when:**
|
||||
- You need to sleep or wait
|
||||
- You need memory allocation
|
||||
- You need device/network I/O
|
||||
- Cleanup can happen later
|
||||
|
||||
## Code Example: Why Workqueue Matters
|
||||
|
||||
### ❌ Cannot Do with bpf_timer (softirq):
|
||||
```c
|
||||
// This FAILS in bpf_timer callback (softirq context)
|
||||
static int timer_callback(void *map, int *key, void *value)
|
||||
{
|
||||
// ERROR: Cannot allocate in softirq!
|
||||
struct data *d = kmalloc(sizeof(*d), GFP_KERNEL);
|
||||
|
||||
// ERROR: Cannot sleep in softirq!
|
||||
send_device_command_and_wait(device);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Works with bpf_wq (workqueue):
|
||||
```c
|
||||
// This WORKS in bpf_wq callback (process context)
|
||||
static int wq_callback(void *map, int *key, void *value)
|
||||
{
|
||||
// OK: Can allocate in process context
|
||||
struct data *d = kmalloc(sizeof(*d), GFP_KERNEL);
|
||||
|
||||
// OK: Can sleep/wait in process context
|
||||
send_device_command_and_wait(device);
|
||||
|
||||
// OK: Can do blocking I/O
|
||||
write_to_file(log_file, data);
|
||||
|
||||
kfree(d);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Historical Timeline
|
||||
|
||||
1. **2022**: Benjamin Tissoires starts HID-BPF work
|
||||
2. **2023**: Realizes bpf_timer limitations for HID device I/O
|
||||
3. **Early 2024**: Proposes bpf_wq as "bpf_timer in process context"
|
||||
4. **April 2024**: bpf_wq merged into kernel (v6.10+)
|
||||
5. **2024-Present**: Used for HID quirks, rate limiting, async cleanup
|
||||
|
||||
## Key Takeaway
|
||||
|
||||
**bpf_wq exists because real-world device handling and resource management need sleepable, blocking operations that bpf_timer cannot provide.**
|
||||
|
||||
It enables BPF programs to:
|
||||
- Fix hardware quirks without kernel drivers
|
||||
- Perform async cleanup without blocking
|
||||
- Wait for I/O without hanging the system
|
||||
- Do "slow work" without impacting "fast path"
|
||||
|
||||
**Bottom line**: bpf_wq brings true asynchronous, sleepable programming to BPF!
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Workqueue Structure
|
||||
|
||||
Embed a `struct bpf_wq` in your map value:
|
||||
### Complete BPF Program: wq_simple.bpf.c
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Simple BPF workqueue example */
|
||||
#include <vmlinux.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include "bpf_experimental.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
/* Element with embedded workqueue */
|
||||
struct elem {
|
||||
int value;
|
||||
struct bpf_wq work; // Embedded workqueue
|
||||
int value;
|
||||
struct bpf_wq work;
|
||||
};
|
||||
|
||||
/* Array to store our element */
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_ARRAY);
|
||||
__type(value, struct elem);
|
||||
__uint(type, BPF_MAP_TYPE_ARRAY);
|
||||
__uint(max_entries, 1);
|
||||
__type(key, int);
|
||||
__type(value, struct elem);
|
||||
} array SEC(".maps");
|
||||
```
|
||||
|
||||
### 2. Initialize and Schedule
|
||||
/* Result variables */
|
||||
__u32 wq_executed = 0;
|
||||
__u32 main_executed = 0;
|
||||
|
||||
```c
|
||||
/* Workqueue callback - runs asynchronously in workqueue context */
|
||||
static int wq_callback(void *map, int *key, void *value)
|
||||
{
|
||||
struct elem *val = value;
|
||||
/* This runs later in workqueue context */
|
||||
wq_executed = 1;
|
||||
val->value = 42; /* Modify the value asynchronously */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Main program - schedules work */
|
||||
SEC("fentry/do_unlinkat")
|
||||
int test_workqueue(void *ctx)
|
||||
{
|
||||
struct elem *val = bpf_map_lookup_elem(&array, &key);
|
||||
struct bpf_wq *wq = &val->work;
|
||||
struct elem init = {.value = 0}, *val;
|
||||
struct bpf_wq *wq;
|
||||
int key = 0;
|
||||
|
||||
// Initialize workqueue
|
||||
bpf_wq_init(wq, &array, 0);
|
||||
main_executed = 1;
|
||||
|
||||
// Set callback function
|
||||
bpf_wq_set_callback(wq, callback_fn, 0);
|
||||
/* Initialize element in map */
|
||||
bpf_map_update_elem(&array, &key, &init, 0);
|
||||
|
||||
// Schedule async execution
|
||||
bpf_wq_start(wq, 0);
|
||||
/* Get element from map */
|
||||
val = bpf_map_lookup_elem(&array, &key);
|
||||
if (!val)
|
||||
return 0;
|
||||
|
||||
return 0;
|
||||
/* Initialize workqueue */
|
||||
wq = &val->work;
|
||||
if (bpf_wq_init(wq, &array, 0) != 0)
|
||||
return 0;
|
||||
|
||||
/* Set callback function */
|
||||
if (bpf_wq_set_callback(wq, wq_callback, 0))
|
||||
return 0;
|
||||
|
||||
/* Schedule work to run asynchronously */
|
||||
if (bpf_wq_start(wq, 0))
|
||||
return 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Callback Execution
|
||||
### Understanding the BPF Code
|
||||
|
||||
The program demonstrates the complete workqueue workflow from initialization through async execution. We start by defining a structure that embeds a workqueue. The `struct elem` contains both application data (`value`) and the workqueue handle (`struct bpf_wq work`). This embedding pattern is critical - the workqueue infrastructure needs to know which map contains the workqueue structure, and embedding it in the map value establishes this relationship.
|
||||
|
||||
Our map is a simple array with one entry, chosen for simplicity in this example. In production code, you'd typically use hash maps to track multiple entities, each with its own embedded workqueue. The global variables `wq_executed` and `main_executed` serve as test instrumentation, letting userspace verify that both code paths ran.
|
||||
|
||||
The workqueue callback shows the signature that all workqueue callbacks must follow: `int callback(void *map, int *key, void *value)`. The kernel invokes this function asynchronously in process context, passing the map containing the workqueue, the key of the entry, and a pointer to the value. This signature gives the callback full context about which element triggered it and access to the element's data. Our callback sets `wq_executed = 1` to prove it ran, and modifies `val->value = 42` to demonstrate that async modifications persist in the map.
|
||||
|
||||
The main program attached to `fentry/do_unlinkat` triggers whenever the `unlink` syscall executes. This gives us an easy way to activate the program - userspace just needs to delete a file. We set `main_executed = 1` immediately to mark the synchronous path. Then we initialize an element and store it in the map using `bpf_map_update_elem()`. This is necessary because the workqueue must be embedded in a map entry.
|
||||
|
||||
The workqueue initialization follows a three-step sequence. First, `bpf_wq_init(wq, &array, 0)` initializes the workqueue handle, passing the map that contains it. The verifier uses this information to validate that the workqueue and its container are properly related. Second, `bpf_wq_set_callback(wq, wq_callback, 0)` registers our callback function. The verifier checks that the callback has the correct signature. Third, `bpf_wq_start(wq, 0)` schedules the workqueue to execute asynchronously. This call returns immediately - the main program continues executing while the kernel queues the work for later execution in process context.
|
||||
|
||||
The flags parameter in all three functions is reserved for future use and should be 0 in current kernels. The pattern allows future extensions without breaking API compatibility.
|
||||
|
||||
### Complete User-Space Program: wq_simple.c
|
||||
|
||||
```c
|
||||
static int callback_fn(void *map, int *key, void *value)
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Userspace test for BPF workqueue */
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "wq_simple.skel.h"
|
||||
|
||||
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
|
||||
{
|
||||
struct elem *val = value;
|
||||
return vfprintf(stderr, format, args);
|
||||
}
|
||||
|
||||
// This runs asynchronously in workqueue context
|
||||
val->value = 42;
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct wq_simple_bpf *skel;
|
||||
int err, fd;
|
||||
|
||||
return 0;
|
||||
libbpf_set_print(libbpf_print_fn);
|
||||
|
||||
/* Open and load BPF application */
|
||||
skel = wq_simple_bpf__open_and_load();
|
||||
if (!skel) {
|
||||
fprintf(stderr, "Failed to open and load BPF skeleton\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Attach tracepoint handler */
|
||||
err = wq_simple_bpf__attach(skel);
|
||||
if (err) {
|
||||
fprintf(stderr, "Failed to attach BPF skeleton\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("BPF workqueue program attached. Triggering unlink syscall...\n");
|
||||
|
||||
/* Create a temporary file to trigger do_unlinkat */
|
||||
fd = open("/tmp/wq_test_file", O_CREAT | O_WRONLY, 0644);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
unlink("/tmp/wq_test_file");
|
||||
}
|
||||
|
||||
/* Give workqueue time to execute */
|
||||
sleep(1);
|
||||
|
||||
/* Check results */
|
||||
printf("\nResults:\n");
|
||||
printf(" main_executed = %u (expected: 1)\n", skel->bss->main_executed);
|
||||
printf(" wq_executed = %u (expected: 1)\n", skel->bss->wq_executed);
|
||||
|
||||
if (skel->bss->main_executed == 1 && skel->bss->wq_executed == 1) {
|
||||
printf("\n✓ Test PASSED!\n");
|
||||
} else {
|
||||
printf("\n✗ Test FAILED!\n");
|
||||
err = 1;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
wq_simple_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
### Understanding the User-Space Code
|
||||
|
||||
### 1. Simple Workqueue Test (`wq_simple`)
|
||||
The userspace program orchestrates the test and verifies results. We use the skeleton API from libbpf which embeds the compiled BPF bytecode in a C structure, making loading trivial. The `wq_simple_bpf__open_and_load()` call compiles (if needed), loads the BPF program into the kernel, and creates all maps in one operation.
|
||||
|
||||
Basic demonstration:
|
||||
- Workqueue initialization on syscall entry
|
||||
- Async callback execution
|
||||
- Verification of both sync and async paths
|
||||
After loading, `wq_simple_bpf__attach()` attaches the fentry program to `do_unlinkat`. From this point, any unlink syscall will trigger our BPF program. We deliberately trigger this by creating and immediately deleting a temporary file. The `open()` creates `/tmp/wq_test_file`, we close the fd, then `unlink()` deletes it. This deletion enters the kernel's `do_unlinkat` function, triggering our fentry probe.
|
||||
|
||||
Here's the critical timing aspect: workqueue execution is asynchronous. Our main BPF program schedules the work and returns immediately. The kernel queues the callback for later execution by a kernel worker thread. This is why we `sleep(1)` - giving the workqueue time to execute before we check results. In production code, you'd use more sophisticated synchronization, but for a simple test, sleep is sufficient.
|
||||
|
||||
After the sleep, we read global variables from the BPF program's `.bss` section. The skeleton provides convenient access through `skel->bss->main_executed` and `skel->bss->wq_executed`. If both are 1, we know the synchronous path (fentry) and async path (workqueue callback) both executed successfully.
|
||||
|
||||
## Understanding Workqueue APIs
|
||||
|
||||
The workqueue API consists of three essential functions that manage the lifecycle. **`bpf_wq_init(wq, map, flags)`** initializes a workqueue handle, establishing the relationship between the workqueue and its containing map. The map parameter is crucial - it tells the verifier which map contains the value with the embedded `bpf_wq` structure. The verifier uses this to ensure memory safety across async execution. Flags should be 0 in current kernels.
|
||||
|
||||
**`bpf_wq_set_callback(wq, callback_fn, flags)`** registers the function to execute asynchronously. The callback must have the signature `int callback(void *map, int *key, void *value)`. The verifier checks this signature at load time and will reject programs with mismatched signatures. This type safety prevents common async programming errors. Flags should be 0.
|
||||
|
||||
**`bpf_wq_start(wq, flags)`** schedules the workqueue to run. This returns immediately - your BPF program continues executing synchronously. The kernel queues the callback for execution by a worker thread in process context at some point in the future. The callback might run microseconds or milliseconds later depending on system load. Flags should be 0.
|
||||
|
||||
The callback signature deserves attention. Unlike `bpf_timer` callbacks which receive `(void *map, __u32 *key, void *value)`, workqueue callbacks receive `(void *map, int *key, void *value)`. Note the key type difference - `int *` vs `__u32 *`. This reflects the evolution of the API and must be matched exactly or the verifier rejects your program. The callback runs in process context, so it can safely perform operations that would crash in softirq context.
|
||||
|
||||
## When to Use Workqueues vs Timers
|
||||
|
||||
Choose **bpf_timer** when you need microsecond-precision timing, operations are fast and non-blocking, you're updating counters or simple state, or implementing periodic fast-path operations like statistics collection or packet pacing. Timers excel at time-critical tasks that must execute with minimal latency.
|
||||
|
||||
Choose **bpf_wq** when you need to sleep or wait, allocate memory with `kzalloc()`, perform device or network I/O, or defer cleanup operations that can happen later. Workqueues are perfect for the "fast path + slow path" pattern where critical operations happen immediately and expensive processing runs asynchronously. Examples include HID device I/O (keyboard macro injection with delays), async map cleanup (preventing memory leaks), security policy updates (querying external databases), and background processing (compression, encryption, aggregation).
|
||||
|
||||
The fundamental trade-off is latency vs capability. Timers have lower latency but restricted capabilities. Workqueues have higher latency but full process context capabilities including sleeping and blocking I/O.
|
||||
|
||||
## Compilation and Execution
|
||||
|
||||
Navigate to the bpf_wq directory and build:
|
||||
|
||||
```bash
|
||||
$ sudo ./wq_simple
|
||||
cd /home/yunwei37/workspace/bpf-developer-tutorial/src/features/bpf_wq
|
||||
make
|
||||
```
|
||||
|
||||
The Makefile compiles the BPF program with the experimental workqueue features enabled and generates a skeleton header.
|
||||
|
||||
Run the simple workqueue test:
|
||||
|
||||
```bash
|
||||
sudo ./wq_simple
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
BPF workqueue program attached. Triggering unlink syscall...
|
||||
|
||||
Results:
|
||||
@@ -237,132 +260,28 @@ Results:
|
||||
✓ Test PASSED!
|
||||
```
|
||||
|
||||
### 2. Real-World: Rate Limiter with Async Cleanup (`rate_limiter`)
|
||||
The test verifies that both the synchronous fentry probe and the asynchronous workqueue callback executed successfully. If the workqueue callback didn't run, `wq_executed` would be 0 and the test would fail.
|
||||
|
||||
**Production-ready example** showing practical workqueue usage:
|
||||
## Historical Timeline and Context
|
||||
|
||||
**Problem**:
|
||||
- Track packet rates per source IP
|
||||
- Drop packets exceeding 100 pps
|
||||
- Clean up stale entries without blocking packet processing
|
||||
Understanding how workqueues came to exist helps appreciate their design. In 2022, Benjamin Tissoires started work on HID-BPF, aiming to let users fix broken HID devices without kernel drivers. By 2023, he realized `bpf_timer` limitations made HID device I/O impossible - you can't wait for hardware responses in softirq context. In early 2024, he proposed `bpf_wq` as "bpf_timer in process context," collaborating with the BPF community on the design. The kernel merged workqueues in April 2024 as part of Linux v6.10. Since then, they've been used for HID quirks, rate limiting, async cleanup, and other sleepable operations.
|
||||
|
||||
**Solution with Workqueues**:
|
||||
- **Fast path**: Check/update rate limits, drop if needed
|
||||
- **Slow path (async)**: Workqueue removes entries older than 10 seconds
|
||||
- **Zero blocking**: Cleanup runs in background
|
||||
The key quote from Benjamin's patches captures the motivation perfectly: "I need something similar to bpf_timers, but not in soft IRQ context... the bpf_timer functionality would prevent me to kzalloc and wait for the device."
|
||||
|
||||
```bash
|
||||
$ sudo ./rate_limiter eth0
|
||||
=== BPF Rate Limiter with Workqueue Cleanup ===
|
||||
Interface: eth0 (ifindex=2)
|
||||
Rate limit: 100 packets/sec per IP
|
||||
Cleanup: Async workqueue removes stale entries (>10s old)
|
||||
This real-world need drove the design. Workqueues exist because device handling and resource management require sleepable, blocking operations that timers fundamentally cannot provide.
|
||||
|
||||
Press Ctrl+C to stop...
|
||||
## Summary and Next Steps
|
||||
|
||||
Time Total Pkts Dropped Active IPs Cleanups
|
||||
-----------------------------------------------------------------------
|
||||
1234 45123 1234 150 12
|
||||
1235 46789 1456 152 15
|
||||
...
|
||||
```
|
||||
BPF workqueues solve a fundamental limitation of eBPF by enabling sleepable, blocking operations in process context. Created specifically to support HID device handling where timing delays and device I/O are essential, workqueues unlock powerful new capabilities for eBPF programs. They enable the "fast path + slow path" pattern where performance-critical operations execute immediately while expensive cleanup and I/O happen asynchronously without blocking.
|
||||
|
||||
**Key Features**:
|
||||
1. **In-kernel rate limiting** - No userspace involvement for packet decisions
|
||||
2. **Per-IP tracking** - Hash map stores state for each source IP
|
||||
3. **Async cleanup** - Workqueue prevents memory leaks without blocking packets
|
||||
4. **Real-time stats** - Monitor performance and efficiency
|
||||
Our simple example demonstrates the core workqueue lifecycle: embedding a `bpf_wq` in a map value, initializing and configuring it, scheduling async execution, and verifying the callback runs in process context. This same pattern scales to production use cases like network rate limiting with async cleanup, security monitoring with external service queries, and device handling with I/O operations.
|
||||
|
||||
## Use Cases
|
||||
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
### 1. Rate Limiting
|
||||
Schedule delayed actions to enforce rate limits:
|
||||
```c
|
||||
// Defer packet drop decision
|
||||
bpf_wq_start(wq, 0); // Execute in background
|
||||
```
|
||||
## References
|
||||
|
||||
### 2. Batch Processing
|
||||
Accumulate events and process in batches:
|
||||
```c
|
||||
// Collect events in map
|
||||
// Workqueue processes batch periodically
|
||||
```
|
||||
- **Original Kernel Patches:** Benjamin Tissoires' HID-BPF and bpf_wq patches (2023-2024)
|
||||
- **Linux Kernel Source:** `kernel/bpf/helpers.c` - workqueue implementation
|
||||
- **Tutorial Repository:** <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/features/bpf_wq>
|
||||
|
||||
### 3. Heavy Computations
|
||||
Offload expensive operations:
|
||||
```c
|
||||
// Main path: fast, non-blocking
|
||||
// Workqueue: slow processing (parsing, crypto)
|
||||
```
|
||||
|
||||
### 4. Cleanup Tasks
|
||||
Defer resource cleanup:
|
||||
```c
|
||||
// Free memory, close connections in background
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cd /home/yunwei37/workspace/bpf-developer-tutorial/src/features/bpf_wq
|
||||
make
|
||||
|
||||
# Run simple test
|
||||
sudo ./wq_simple
|
||||
|
||||
# Run rate limiter (requires network interface)
|
||||
sudo ./rate_limiter lo # Use loopback for testing
|
||||
sudo ./rate_limiter eth0 # Use real interface
|
||||
|
||||
# Generate test traffic
|
||||
ping -f localhost # Flood ping to trigger rate limiting
|
||||
```
|
||||
|
||||
## Key APIs
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `bpf_wq_init(wq, map, flags)` | Initialize workqueue |
|
||||
| `bpf_wq_set_callback(wq, fn, flags)` | Set callback function |
|
||||
| `bpf_wq_start(wq, flags)` | Schedule async execution |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux kernel 6.6+ (workqueue support)
|
||||
- Root/sudo access
|
||||
- libbpf, clang, bpftool
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
bpf_wq/
|
||||
├── wq_simple.bpf.c # BPF workqueue program
|
||||
├── wq_simple.c # Userspace loader
|
||||
├── bpf_experimental.h # Workqueue helper definitions
|
||||
├── Makefile # Build system
|
||||
├── README.md # This file
|
||||
└── .gitignore # Ignore build artifacts
|
||||
```
|
||||
|
||||
## Advantages Over Alternatives
|
||||
|
||||
| Approach | Blocking | Context Switches | Complexity |
|
||||
|----------|----------|-----------------|------------|
|
||||
| **Synchronous** | Yes | No | Low |
|
||||
| **Userspace notification** | No | Yes (many) | High |
|
||||
| **BPF workqueue** | No | Minimal | Medium |
|
||||
|
||||
BPF workqueues provide the best balance of performance and flexibility for async operations!
|
||||
|
||||
## Summary
|
||||
|
||||
BPF workqueues enable **true asynchronous programming** in BPF:
|
||||
- ✅ Non-blocking main path
|
||||
- ✅ Deferred execution
|
||||
- ✅ Sleepable context support
|
||||
- ✅ Minimal overhead
|
||||
- ✅ Type-safe callbacks
|
||||
|
||||
Perfect for scenarios where you need to do work later without blocking the fast path!
|
||||
Example adapted from Linux kernel BPF selftests with educational enhancements. Requires Linux kernel 6.10+ for workqueue support. Complete source code available in the tutorial repository.
|
||||
|
||||
Reference in New Issue
Block a user