diff --git a/tools/filetop.py b/tools/filetop.py index c01a8e810c02..3a1a17f981c2 100755 --- a/tools/filetop.py +++ b/tools/filetop.py @@ -20,18 +20,22 @@ import argparse import os import stat +import re from subprocess import call +MAX_DENTRY_DEPTH_VAL = 32 + # arguments -examples = """examples: - ./filetop # file I/O top, 1 second refresh - ./filetop -C # don't clear the screen - ./filetop -p 181 # PID 181 only - ./filetop -d /home/user # trace files in /home/user directory only - ./filetop 5 # 5 second summaries - ./filetop 5 10 # 5 second summaries, 10 times only - ./filetop 5 --read-only # 5 second summaries, only read operations traced - ./filetop 5 --write-only # 5 second summaries, only write operations traced +examples = f"""examples: + ./filetop # file I/O top, 1 second refresh + ./filetop -C # don't clear the screen + ./filetop -p 181 # PID 181 only + ./filetop -d /home/user # trace files in /home/user directory only + ./filetop -d /home/user -R # trace files in /home/user and subdirectories (max depth {MAX_DENTRY_DEPTH_VAL}) + ./filetop 5 # 5 second summaries + ./filetop 5 10 # 5 second summaries, 10 times only + ./filetop 5 --read-only # 5 second summaries, only read operations traced + ./filetop 5 --write-only # 5 second summaries, only write operations traced """ parser = argparse.ArgumentParser( description="File reads and writes by process", @@ -60,6 +64,8 @@ help=argparse.SUPPRESS) parser.add_argument("-d", "--directory", type=str, help="trace this directory only") +parser.add_argument("-R", "--recursive", action="store_true", + help=f"when used with -d, also trace files in subdirectories (max depth {MAX_DENTRY_DEPTH_VAL})") args = parser.parse_args() interval = int(args.interval) @@ -80,6 +86,28 @@ #undef DNAME_INLINE_LEN #define DNAME_INLINE_LEN __BCC_DNAME_INLINE_LEN +// Limit dentry parent walk depth to satisfy eBPF verifier loop +#ifndef MAX_DENTRY_DEPTH +#define MAX_DENTRY_DEPTH MAX_DENTRY_DEPTH_VALUE +#endif + +static __always_inline int not_under_inode(struct dentry *de, unsigned long target_ino) +{ + struct dentry *pde = de; + int found = 0; + #pragma unroll + for (int i = 0; i < MAX_DENTRY_DEPTH; i++) { + if (!pde->d_parent) + break; + pde = pde->d_parent; + if (pde->d_inode && pde->d_inode->i_ino == target_ino) { + found = 1; + break; + } + } + return !found; +} + // the key for the output summary struct info_t { unsigned long inode; @@ -169,6 +197,9 @@ } """ +bpf_text = bpf_text.replace('MAX_DENTRY_DEPTH_VALUE', str(MAX_DENTRY_DEPTH_VAL)) +m = re.search(r'#define\s+MAX_DENTRY_DEPTH\s+(\d+)', bpf_text) +MAX_DENTRY_DEPTH = int(m.group(1)) if m else 32 if args.tgid: bpf_text = bpf_text.replace('TGID_FILTER', 'tgid != %d' % args.tgid) else: @@ -180,12 +211,24 @@ if args.directory: try: directory_inode = os.lstat(args.directory)[stat.ST_INO] - print(f'Tracing directory: {args.directory} (Inode: {directory_inode})') - bpf_text = bpf_text.replace('DIRECTORY_FILTER', 'file->f_path.dentry->d_parent->d_inode->i_ino != %d' % directory_inode) + if args.recursive: + print( + f'Tracing directory recursively: {args.directory} ' + f'(Inode: {directory_inode}, max depth: {MAX_DENTRY_DEPTH_VAL})' + ) + directory_filter = "not_under_inode(de, %d)" % directory_inode + else: + print(f'Tracing directory: {args.directory} (Inode: {directory_inode})') + directory_filter = "de->d_parent->d_inode->i_ino != %d" % directory_inode + + bpf_text = bpf_text.replace('DIRECTORY_FILTER', directory_filter) except (FileNotFoundError, PermissionError) as e: print(f'Error accessing directory {args.directory}: {e}') exit(1) else: + if args.recursive: + print("Error: --recursive can only be used with -d/--directory option") + exit(1) bpf_text = bpf_text.replace('DIRECTORY_FILTER', '0') if debug or args.ebpf: