| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Stress every LRU lock-failure and orphan-recovery. |
| * perf_event NMI BPF on every online CPU does |
| * update+delete on a small LRU map; userspace threads on every CPU do |
| * the same from syscall context. |
| */ |
| #define _GNU_SOURCE |
| #include <pthread.h> |
| #include <sched.h> |
| #include <sys/syscall.h> |
| #include <linux/perf_event.h> |
| #include <test_progs.h> |
| #include "testing_helpers.h" |
| #include "lru_lock_nmi.skel.h" |
| |
| #define MAP_ENTRIES 64 |
| #define KEY_RANGE (MAP_ENTRIES * 2) |
| #define STRESS_NS (500 * 1000 * 1000ULL) |
| |
| struct hammer_arg { |
| int map_fd; |
| int cpu; |
| __u64 deadline_ns; |
| }; |
| |
| struct refill_arg { |
| int map_fd; |
| int cpu; |
| int per_cpu_quota; |
| int update_errors; |
| }; |
| |
| /* |
| * Pin the calling thread to @cpu. Uses dynamically-allocated CPU sets so |
| * we stay correct on hosts with @cpu >= CPU_SETSIZE (default 1024). |
| */ |
| static int pin_to_cpu(int cpu) |
| { |
| cpu_set_t *cs; |
| size_t cs_size; |
| int err; |
| |
| cs = CPU_ALLOC(cpu + 1); |
| if (!cs) |
| return -ENOMEM; |
| cs_size = CPU_ALLOC_SIZE(cpu + 1); |
| |
| CPU_ZERO_S(cs_size, cs); |
| CPU_SET_S(cpu, cs_size, cs); |
| err = pthread_setaffinity_np(pthread_self(), cs_size, cs); |
| CPU_FREE(cs); |
| return err; |
| } |
| |
| static void *hammer_thread(void *p) |
| { |
| struct hammer_arg *a = p; |
| int nr_possible_cpus = libbpf_num_possible_cpus(); |
| __u64 val[nr_possible_cpus]; |
| unsigned int seed; |
| __u32 key; |
| |
| memset(val, 0, sizeof(val)); |
| pin_to_cpu(a->cpu); |
| |
| seed = (unsigned int)a->cpu ^ (unsigned int)(uintptr_t)pthread_self(); |
| |
| while (get_time_ns() < a->deadline_ns) { |
| bool do_update = rand_r(&seed) & 1; |
| |
| key = rand_r(&seed) % KEY_RANGE; |
| if (do_update) |
| bpf_map_update_elem(a->map_fd, &key, val, BPF_ANY); |
| else |
| bpf_map_delete_elem(a->map_fd, &key); |
| } |
| return NULL; |
| } |
| |
| static void *refill_thread(void *p) |
| { |
| struct refill_arg *a = p; |
| int nr_possible_cpus = libbpf_num_possible_cpus(); |
| __u64 val[nr_possible_cpus]; |
| __u32 start, end, key; |
| |
| memset(val, 0, sizeof(val)); |
| pin_to_cpu(a->cpu); |
| |
| start = (__u32)a->cpu * (__u32)a->per_cpu_quota; |
| end = start + (__u32)a->per_cpu_quota; |
| for (key = start; key < end; key++) |
| if (bpf_map_update_elem(a->map_fd, &key, val, BPF_ANY)) |
| a->update_errors++; |
| return NULL; |
| } |
| |
| /* |
| * Drain the map, then refill it with each CPU inserting only its own |
| * quota of keys. |
| * After refill, lookup every key we inserted - a stranded node on any |
| * CPU's pool would have forced eviction. |
| */ |
| static int drain_then_verify_capacity(int map_fd, int nr_cpus) |
| { |
| int per_cpu_quota = MAP_ENTRIES / nr_cpus; |
| int total = per_cpu_quota * nr_cpus; |
| int nr_possible_cpus = libbpf_num_possible_cpus(); |
| pthread_t threads[nr_cpus]; |
| struct refill_arg args[nr_cpus]; |
| __u64 val[nr_possible_cpus]; |
| int i, hits = 0, nthreads = 0; |
| __u32 key; |
| |
| memset(val, 0, sizeof(val)); |
| |
| for (key = 0; key < KEY_RANGE; key++) |
| bpf_map_delete_elem(map_fd, &key); |
| |
| for (i = 0; i < nr_cpus; i++) { |
| args[i] = (struct refill_arg){ |
| .map_fd = map_fd, |
| .cpu = i, |
| .per_cpu_quota = per_cpu_quota, |
| }; |
| if (pthread_create(&threads[nthreads], NULL, refill_thread, &args[i]) == 0) |
| nthreads++; |
| } |
| for (i = 0; i < nthreads; i++) |
| pthread_join(threads[i], NULL); |
| |
| for (i = 0; i < nr_cpus; i++) |
| if (args[i].update_errors) |
| return -ENOMEM; |
| |
| for (key = 0; key < (__u32)total; key++) |
| if (bpf_map_lookup_elem(map_fd, &key, val) == 0) |
| hits++; |
| |
| return hits == total ? 0 : -EIO; |
| } |
| |
| static void run_variant(enum bpf_map_type type, __u32 map_flags, const char *name) |
| { |
| struct perf_event_attr attr = { |
| .size = sizeof(attr), |
| .type = PERF_TYPE_HARDWARE, |
| .config = PERF_COUNT_HW_CPU_CYCLES, |
| .freq = 1, |
| }; |
| int nr_cpus, max_cpus = 64; |
| struct bpf_link *links[max_cpus]; |
| pthread_t threads[max_cpus]; |
| struct hammer_arg args[max_cpus]; |
| struct lru_lock_nmi *skel = NULL; |
| int map_fd, i, err, nr_threads = 0, pmu_fd = -1; |
| __u64 deadline; |
| |
| nr_cpus = libbpf_num_possible_cpus(); |
| if (!ASSERT_GT(nr_cpus, 0, "num_cpus")) |
| return; |
| |
| if (nr_cpus > max_cpus) |
| nr_cpus = max_cpus; |
| |
| if (!test__start_subtest(name)) |
| return; |
| |
| memset(links, 0, sizeof(links)); |
| skel = lru_lock_nmi__open(); |
| if (!ASSERT_OK_PTR(skel, "skel_open")) |
| goto cleanup; |
| |
| err = bpf_map__set_type(skel->maps.lru_map, type); |
| if (!ASSERT_OK(err, "set_type")) |
| goto cleanup; |
| err = bpf_map__set_map_flags(skel->maps.lru_map, map_flags); |
| if (!ASSERT_OK(err, "set_flags")) |
| goto cleanup; |
| err = bpf_map__set_max_entries(skel->maps.lru_map, MAP_ENTRIES); |
| if (!ASSERT_OK(err, "set_max_entries")) |
| goto cleanup; |
| |
| err = lru_lock_nmi__load(skel); |
| if (!ASSERT_OK(err, "skel_load")) |
| goto cleanup; |
| |
| skel->bss->hits = 0; |
| map_fd = bpf_map__fd(skel->maps.lru_map); |
| attr.sample_freq = read_perf_max_sample_freq(); |
| |
| for (i = 0; i < nr_cpus; i++) { |
| pmu_fd = syscall(__NR_perf_event_open, &attr, -1, i, -1, 0); |
| if (pmu_fd < 0) { |
| if (i == 0 && |
| (errno == ENOENT || errno == EOPNOTSUPP)) { |
| test__skip(); |
| goto cleanup; |
| } |
| continue; |
| } |
| /* libbpf takes ownership of pfd on success */ |
| links[i] = bpf_program__attach_perf_event(skel->progs.oncpu, pmu_fd); |
| if (!links[i]) |
| close(pmu_fd); |
| } |
| |
| deadline = get_time_ns() + STRESS_NS; |
| for (i = 0; i < nr_cpus; i++) { |
| args[i].map_fd = map_fd; |
| args[i].cpu = i; |
| args[i].deadline_ns = deadline; |
| if (pthread_create(&threads[nr_threads], NULL, hammer_thread, &args[i]) == 0) |
| nr_threads++; |
| } |
| for (i = 0; i < nr_threads; i++) |
| pthread_join(threads[i], NULL); |
| |
| for (i = 0; i < nr_cpus; i++) { |
| if (links[i]) { |
| bpf_link__destroy(links[i]); |
| links[i] = NULL; |
| } |
| } |
| |
| ASSERT_GT(skel->bss->hits, 0, "nmi_bpf_ran"); |
| ASSERT_OK(drain_then_verify_capacity(map_fd, nr_cpus), "drain_then_verify_capacity"); |
| |
| cleanup: |
| for (i = 0; i < nr_cpus; i++) { |
| if (links[i]) |
| bpf_link__destroy(links[i]); |
| } |
| lru_lock_nmi__destroy(skel); |
| } |
| |
| void serial_test_lru_lock_nmi(void) |
| { |
| run_variant(BPF_MAP_TYPE_LRU_HASH, 0, "common_lru"); |
| run_variant(BPF_MAP_TYPE_LRU_HASH, BPF_F_NO_COMMON_LRU, "no_common_lru"); |
| run_variant(BPF_MAP_TYPE_LRU_PERCPU_HASH, 0, "percpu_lru"); |
| } |