diff --git a/libbpf-tools/.gitignore b/libbpf-tools/.gitignore index bfceccf328d7..b67d7af45a08 100644 --- a/libbpf-tools/.gitignore +++ b/libbpf-tools/.gitignore @@ -9,6 +9,7 @@ /drsnoop /execsnoop /filelife +/funclatency /hardirqs /llcstat /numamove diff --git a/libbpf-tools/Makefile b/libbpf-tools/Makefile index bd561a7be5dc..9e07c73a76af 100644 --- a/libbpf-tools/Makefile +++ b/libbpf-tools/Makefile @@ -26,6 +26,7 @@ APPS = \ drsnoop \ execsnoop \ filelife \ + funclatency \ hardirqs \ llcstat \ numamove \ @@ -47,6 +48,7 @@ COMMON_OBJ = \ $(OUTPUT)/syscall_helpers.o \ $(OUTPUT)/errno_helpers.o \ $(OUTPUT)/map_helpers.o \ + $(OUTPUT)/uprobe_helpers.o \ # .PHONY: all diff --git a/libbpf-tools/funclatency.bpf.c b/libbpf-tools/funclatency.bpf.c new file mode 100644 index 000000000000..c2f9fdd2b17e --- /dev/null +++ b/libbpf-tools/funclatency.bpf.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2021 Google LLC. */ +#include "vmlinux.h" +#include +#include +#include +#include "funclatency.h" +#include "bits.bpf.h" + +const volatile pid_t targ_tgid; +const volatile int units; + +/* key: pid. value: start time */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_PIDS); + __type(key, u32); + __type(value, u64); +} starts SEC(".maps"); + +__u32 hist[MAX_SLOTS]; + +SEC("kprobe/dummy_kprobe") +int BPF_KPROBE(dummy_kprobe) +{ + u64 id = bpf_get_current_pid_tgid(); + u32 tgid = id >> 32; + u32 pid = id; + u64 nsec; + + if (targ_tgid && targ_tgid != tgid) + return 0; + nsec = bpf_ktime_get_ns(); + bpf_map_update_elem(&starts, &pid, &nsec, BPF_ANY); + + return 0; +} + +SEC("kretprobe/dummy_kretprobe") +int BPF_KRETPROBE(dummy_kretprobe) +{ + u64 *start; + u64 nsec = bpf_ktime_get_ns(); + u64 id = bpf_get_current_pid_tgid(); + u32 pid = id; + u64 slot, delta; + + start = bpf_map_lookup_elem(&starts, &pid); + if (!start) + return 0; + + delta = nsec - *start; + + switch (units) { + case USEC: + delta /= 1000; + break; + case MSEC: + delta /= 1000000; + break; + } + + slot = log2l(delta); + if (slot >= MAX_SLOTS) + slot = MAX_SLOTS - 1; + __sync_fetch_and_add(&hist[slot], 1); + + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/libbpf-tools/funclatency.c b/libbpf-tools/funclatency.c new file mode 100644 index 000000000000..b225e7a47d05 --- /dev/null +++ b/libbpf-tools/funclatency.c @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +/* Copyright (c) 2021 Google LLC. + * + * Based on funclatency from BCC by Brendan Gregg and others + * 2021-02-26 Barret Rhoden Created this. + * + * TODO: + * - support uprobes on libraries without -p PID. (parse ld.so.cache) + * - support regexp pattern matching and per-function histograms + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "funclatency.h" +#include "funclatency.skel.h" +#include "trace_helpers.h" +#include "map_helpers.h" +#include "uprobe_helpers.h" + +#define warn(...) fprintf(stderr, __VA_ARGS__) + +static struct prog_env { + int units; + pid_t pid; + unsigned int duration; + unsigned int interval; + unsigned int iterations; + bool timestamp; + char *funcname; +} env = { + .interval = 99999999, + .iterations = 99999999, +}; + +const char *argp_program_version = "funclatency 0.1"; +const char *argp_program_bug_address = ""; +static const char args_doc[] = "FUNCTION"; +static const char program_doc[] = +"Time functions and print latency as a histogram\n" +"\n" +"Usage: funclatency [-h] [-m|-u] [-p PID] [-d DURATION] [ -i INTERVAL ]\n" +" [-T] FUNCTION\n" +" Choices for FUNCTION: FUNCTION (kprobe)\n" +" LIBRARY:FUNCTION (uprobe a library in -p PID)\n" +" :FUNCTION (uprobe the binary of -p PID)\n" +" PROGRAM:FUNCTION (uprobe the binary PROGRAM)\n" +"\v" +"Examples:\n" +" ./funclatency do_sys_open # time the do_sys_open() kernel function\n" +" ./funclatency -m do_nanosleep # time do_nanosleep(), in milliseconds\n" +" ./funclatency -u vfs_read # time vfs_read(), in microseconds\n" +" ./funclatency -p 181 vfs_read # time process 181 only\n" +" ./funclatency -p 181 c:read # time the read() C library function\n" +" ./funclatency -p 181 :foo # time foo() from pid 181's userspace\n" +" ./funclatency -i 2 -d 10 vfs_read # output every 2 seconds, for 10s\n" +" ./funclatency -mTi 5 vfs_read # output every 5 seconds, with timestamps\n" +; + +static const struct argp_option opts[] = { + { "milliseconds", 'm', NULL, 0, "Output in milliseconds"}, + { "microseconds", 'u', NULL, 0, "Output in microseconds"}, + {0, 0, 0, 0, ""}, + { "pid", 'p', "PID", 0, "Process ID to trace"}, + {0, 0, 0, 0, ""}, + { "interval", 'i', "INTERVAL", 0, "Summary interval in seconds"}, + { "duration", 'd', "DURATION", 0, "Duration to trace"}, + { "timestamp", 'T', NULL, 0, "Print timestamp"}, + { NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help"}, + {}, +}; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + struct prog_env *env = state->input; + long duration, interval, pid; + + switch (key) { + case 'p': + errno = 0; + pid = strtol(arg, NULL, 10); + if (errno || pid <= 0) { + warn("Invalid PID: %s\n", arg); + argp_usage(state); + } + env->pid = pid; + break; + case 'm': + if (env->units != NSEC) { + warn("only set one of -m or -u\n"); + argp_usage(state); + } + env->units = MSEC; + break; + case 'u': + if (env->units != NSEC) { + warn("only set one of -m or -u\n"); + argp_usage(state); + } + env->units = USEC; + break; + case 'd': + errno = 0; + duration = strtol(arg, NULL, 10); + if (errno || duration <= 0) { + warn("Invalid duration: %s\n", arg); + argp_usage(state); + } + env->duration = duration; + break; + case 'i': + errno = 0; + interval = strtol(arg, NULL, 10); + if (errno || interval <= 0) { + warn("Invalid interval: %s\n", arg); + argp_usage(state); + } + env->interval = interval; + break; + case 'T': + env->timestamp = true; + break; + case 'h': + argp_state_help(state, stderr, ARGP_HELP_STD_HELP); + break; + case ARGP_KEY_ARG: + if (env->funcname) { + warn("Too many function names: %s\n", arg); + argp_usage(state); + } + env->funcname = arg; + break; + case ARGP_KEY_END: + if (!env->funcname) { + warn("Need a function to trace\n"); + argp_usage(state); + } + if (env->duration) { + if (env->interval > env->duration) + env->interval = env->duration; + env->iterations = env->duration / env->interval; + } + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +static const char *unit_str(void) +{ + switch (env.units) { + case NSEC: + return "nsec"; + case USEC: + return "usec"; + case MSEC: + return "msec"; + }; + + return "bad units"; +} + +static int attach_kprobes(struct funclatency_bpf *obj) +{ + long err; + + obj->links.dummy_kprobe = + bpf_program__attach_kprobe(obj->progs.dummy_kprobe, false, + env.funcname); + err = libbpf_get_error(obj->links.dummy_kprobe); + if (err) { + warn("failed to attach kprobe: %ld\n", err); + return -1; + } + + obj->links.dummy_kretprobe = + bpf_program__attach_kprobe(obj->progs.dummy_kretprobe, true, + env.funcname); + err = libbpf_get_error(obj->links.dummy_kretprobe); + if (err) { + warn("failed to attach kretprobe: %ld\n", err); + return -1; + } + + return 0; +} + +static int attach_uprobes(struct funclatency_bpf *obj) +{ + char *binary, *function; + char bin_path[PATH_MAX]; + off_t func_off; + int ret = -1; + long err; + + binary = strdup(env.funcname); + if (!binary) { + warn("strdup failed"); + return -1; + } + function = strchr(binary, ':'); + if (!function) { + warn("Binary should have contained ':' (internal bug!)\n"); + return -1; + } + *function = '\0'; + function++; + + if (resolve_binary_path(binary, env.pid, bin_path, sizeof(bin_path))) + goto out_binary; + + func_off = get_elf_func_offset(bin_path, function); + if (func_off < 0) { + warn("Could not find %s in %s\n", function, bin_path); + goto out_binary; + } + + obj->links.dummy_kprobe = + bpf_program__attach_uprobe(obj->progs.dummy_kprobe, false, + env.pid ?: -1, bin_path, func_off); + err = libbpf_get_error(obj->links.dummy_kprobe); + if (err) { + warn("Failed to attach uprobe: %ld\n", err); + goto out_binary; + } + + obj->links.dummy_kretprobe = + bpf_program__attach_uprobe(obj->progs.dummy_kretprobe, true, + env.pid ?: -1, bin_path, func_off); + err = libbpf_get_error(obj->links.dummy_kretprobe); + if (err) { + warn("Failed to attach uretprobe: %ld\n", err); + goto out_binary; + } + + ret = 0; + +out_binary: + free(binary); + + return ret; +} + +static int attach_probes(struct funclatency_bpf *obj) +{ + if (strchr(env.funcname, ':')) + return attach_uprobes(obj); + return attach_kprobes(obj); +} + +static volatile bool exiting; + +static void sig_hand(int signr) +{ + exiting = true; +} + +static struct sigaction sigact = {.sa_handler = sig_hand}; + +int main(int argc, char **argv) +{ + static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .args_doc = args_doc, + .doc = program_doc, + }; + struct funclatency_bpf *obj; + int err; + struct tm *tm; + char ts[32]; + time_t t; + + err = argp_parse(&argp, argc, argv, 0, NULL, &env); + if (err) + return err; + + sigaction(SIGINT, &sigact, 0); + + err = bump_memlock_rlimit(); + if (err) { + warn("failed to increase rlimit: %d\n", err); + return 1; + } + + obj = funclatency_bpf__open(); + if (!obj) { + warn("failed to open BPF object\n"); + return 1; + } + + obj->rodata->units = env.units; + obj->rodata->targ_tgid = env.pid; + + err = funclatency_bpf__load(obj); + if (err) { + warn("failed to load BPF object\n"); + return 1; + } + + err = attach_probes(obj); + if (err) + goto cleanup; + + printf("Tracing %s. Hit Ctrl-C to exit\n", env.funcname); + + for (int i = 0; i < env.iterations && !exiting; i++) { + sleep(env.interval); + + printf("\n"); + if (env.timestamp) { + time(&t); + tm = localtime(&t); + strftime(ts, sizeof(ts), "%H:%M:%S", tm); + printf("%-8s\n", ts); + } + + print_log2_hist(obj->bss->hist, MAX_SLOTS, unit_str()); + } + + printf("Exiting trace of %s\n", env.funcname); + +cleanup: + funclatency_bpf__destroy(obj); + + return err != 0; +} diff --git a/libbpf-tools/funclatency.h b/libbpf-tools/funclatency.h new file mode 100644 index 000000000000..a28fc2c5a1a6 --- /dev/null +++ b/libbpf-tools/funclatency.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +#pragma once + +#define MAX_PIDS 102400 +#define MAX_SLOTS 25 + +enum units { + NSEC, + USEC, + MSEC, +}; diff --git a/libbpf-tools/uprobe_helpers.c b/libbpf-tools/uprobe_helpers.c new file mode 100644 index 000000000000..ff979461f6db --- /dev/null +++ b/libbpf-tools/uprobe_helpers.c @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +/* Copyright (c) 2021 Google LLC. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define warn(...) fprintf(stderr, __VA_ARGS__) + +/* + * Returns 0 on success; -1 on failure. On sucess, returns via `path` the full + * path to the program for pid. + */ +int get_pid_binary_path(pid_t pid, char *path, size_t path_sz) +{ + ssize_t ret; + char proc_pid_exe[32]; + + if (snprintf(proc_pid_exe, sizeof(proc_pid_exe), "/proc/%d/exe", pid) + >= sizeof(proc_pid_exe)) { + warn("snprintf /proc/PID/exe failed"); + return -1; + } + ret = readlink(proc_pid_exe, path, path_sz); + if (ret < 0) { + warn("No such pid %d\n", pid); + return -1; + } + if (ret >= path_sz) { + warn("readlink truncation"); + return -1; + } + path[ret] = '\0'; + + return 0; +} + +/* + * Returns 0 on success; -1 on failure. On success, returns via `path` the full + * path to a library matching the name `lib` that is loaded into pid's address + * space. + */ +int get_pid_lib_path(pid_t pid, const char *lib, char *path, size_t path_sz) +{ + FILE *maps; + char *p; + char proc_pid_maps[32]; + char line_buf[1024]; + + if (snprintf(proc_pid_maps, sizeof(proc_pid_maps), "/proc/%d/maps", pid) + >= sizeof(proc_pid_maps)) { + warn("snprintf /proc/PID/maps failed"); + return -1; + } + maps = fopen(proc_pid_maps, "r"); + if (!maps) { + warn("No such pid %d\n", pid); + return -1; + } + while (fgets(line_buf, sizeof(line_buf), maps)) { + if (sscanf(line_buf, "%*x-%*x %*s %*x %*s %*u %s", path) != 1) + continue; + /* e.g. /usr/lib/x86_64-linux-gnu/libc-2.31.so */ + p = strrchr(path, '/'); + if (!p) + continue; + if (strncmp(p, "/lib", 4)) + continue; + p += 4; + if (strncmp(lib, p, strlen(lib))) + continue; + p += strlen(lib); + /* libraries can have - or . after the name */ + if (*p != '.' && *p != '-') + continue; + + fclose(maps); + return 0; + } + + warn("Cannot find library %s\n", lib); + fclose(maps); + return -1; +} + +/* + * Returns 0 on success; -1 on failure. On success, returns via `path` the full + * path to the program. + */ +static int which_program(const char *prog, char *path, size_t path_sz) +{ + FILE *which; + char cmd[100]; + + if (snprintf(cmd, sizeof(cmd), "which %s", prog) >= sizeof(cmd)) { + warn("snprintf which prog failed"); + return -1; + } + which = popen(cmd, "r"); + if (!which) { + warn("which failed"); + return -1; + } + if (!fgets(path, path_sz, which)) { + warn("fgets which failed"); + pclose(which); + return -1; + } + /* which has a \n at the end of the string */ + path[strlen(path) - 1] = '\0'; + pclose(which); + return 0; +} + +/* + * Returns 0 on success; -1 on failure. On success, returns via `path` the full + * path to the binary for the given pid. + * 1) pid == x, binary == "" : returns the path to x's program + * 2) pid == x, binary == "foo" : returns the path to libfoo linked in x + * 3) pid == 0, binary == "" : failure: need a pid or a binary + * 4) pid == 0, binary == "bar" : returns the path to `which bar` + * + * For case 4), ideally we'd like to search for libbar too, but we don't support + * that yet. + */ +int resolve_binary_path(const char *binary, pid_t pid, char *path, size_t path_sz) +{ + if (!strcmp(binary, "")) { + if (!pid) { + warn("Uprobes need a pid or a binary\n"); + return -1; + } + return get_pid_binary_path(pid, path, path_sz); + } + if (pid) + return get_pid_lib_path(pid, binary, path, path_sz); + + if (which_program(binary, path, path_sz)) { + /* + * If the user is tracing a program by name, we can find it. + * But we can't find a library by name yet. We'd need to parse + * ld.so.cache or something similar. + */ + warn("Can't find %s (Need a PID if this is a library)\n", binary); + return -1; + } + return 0; +} + +/* + * Opens an elf at `path` of kind ELF_K_ELF. Returns NULL on failure. On + * success, close with close_elf(e, fd_close). + */ +static Elf *open_elf(const char *path, int *fd_close) +{ + int fd; + Elf *e; + + if (elf_version(EV_CURRENT) == EV_NONE) { + warn("elf init failed\n"); + return NULL; + } + fd = open(path, O_RDONLY); + if (fd < 0) { + warn("Could not open %s\n", path); + return NULL; + } + e = elf_begin(fd, ELF_C_READ, NULL); + if (!e) { + warn("elf_begin failed: %s\n", elf_errmsg(-1)); + close(fd); + return NULL; + } + if (elf_kind(e) != ELF_K_ELF) { + warn("elf kind %d is not ELF_K_ELF\n", elf_kind(e)); + elf_end(e); + close(fd); + return NULL; + } + *fd_close = fd; + return e; +} + +static void close_elf(Elf *e, int fd_close) +{ + elf_end(e); + close(fd_close); +} + +/* Returns the offset of a function in the elf file `path`, or -1 on failure. */ +off_t get_elf_func_offset(const char *path, const char *func) +{ + off_t ret = -1; + int fd = -1; + Elf *e; + Elf_Scn *scn; + Elf_Data *data; + GElf_Shdr shdr[1]; + GElf_Sym sym[1]; + size_t shstrndx; + char *n; + + e = open_elf(path, &fd); + + if (elf_getshdrstrndx(e, &shstrndx) != 0) + goto out; + + scn = NULL; + while ((scn = elf_nextscn(e, scn))) { + if (!gelf_getshdr(scn, shdr)) + continue; + if (!(shdr->sh_type == SHT_SYMTAB || shdr->sh_type == SHT_DYNSYM)) + continue; + data = NULL; + while ((data = elf_getdata(scn, data))) { + for (int i = 0; gelf_getsym(data, i, sym); i++) { + n = elf_strptr(e, shdr->sh_link, sym->st_name); + if (!n) + continue; + if (GELF_ST_TYPE(sym->st_info) != STT_FUNC) + continue; + if (!strcmp(n, func)) { + ret = sym->st_value; + goto out; + } + } + } + } + +out: + close_elf(e, fd); + return ret; +} diff --git a/libbpf-tools/uprobe_helpers.h b/libbpf-tools/uprobe_helpers.h new file mode 100644 index 000000000000..c8a758036efb --- /dev/null +++ b/libbpf-tools/uprobe_helpers.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2021 Google LLC. */ +#ifndef __UPROBE_HELPERS_H +#define __UPROBE_HELPERS_H + +#include +#include + +int get_pid_binary_path(pid_t pid, char *path, size_t path_sz); +int get_pid_lib_path(pid_t pid, const char *lib, char *path, size_t path_sz); +int resolve_binary_path(const char *binary, pid_t pid, char *path, size_t path_sz); +off_t get_elf_func_offset(const char *path, const char *func); + +#endif /* __UPROBE_HELPERS_H */