1#!/usr/bin/env python 2# @lint-avoid-python-3-compatibility-imports 3# 4# filetop file reads and writes by process. 5# For Linux, uses BCC, eBPF. 6# 7# USAGE: filetop.py [-h] [-C] [-r MAXROWS] [interval] [count] 8# 9# This uses in-kernel eBPF maps to store per process summaries for efficiency. 10# 11# Copyright 2016 Netflix, Inc. 12# Licensed under the Apache License, Version 2.0 (the "License") 13# 14# 06-Feb-2016 Brendan Gregg Created this. 15 16from __future__ import print_function 17from bcc import BPF 18from time import sleep, strftime 19import argparse 20from subprocess import call 21 22# arguments 23examples = """examples: 24 ./filetop # file I/O top, 1 second refresh 25 ./filetop -C # don't clear the screen 26 ./filetop -p 181 # PID 181 only 27 ./filetop 5 # 5 second summaries 28 ./filetop 5 10 # 5 second summaries, 10 times only 29""" 30parser = argparse.ArgumentParser( 31 description="File reads and writes by process", 32 formatter_class=argparse.RawDescriptionHelpFormatter, 33 epilog=examples) 34parser.add_argument("-a", "--all-files", action="store_true", 35 help="include non-regular file types (sockets, FIFOs, etc)") 36parser.add_argument("-C", "--noclear", action="store_true", 37 help="don't clear the screen") 38parser.add_argument("-r", "--maxrows", default=20, 39 help="maximum rows to print, default 20") 40parser.add_argument("-s", "--sort", default="all", 41 choices=["all", "reads", "writes", "rbytes", "wbytes"], 42 help="sort column, default all") 43parser.add_argument("-p", "--pid", type=int, metavar="PID", dest="tgid", 44 help="trace this PID only") 45parser.add_argument("interval", nargs="?", default=1, 46 help="output interval, in seconds") 47parser.add_argument("count", nargs="?", default=99999999, 48 help="number of outputs") 49parser.add_argument("--ebpf", action="store_true", 50 help=argparse.SUPPRESS) 51args = parser.parse_args() 52interval = int(args.interval) 53countdown = int(args.count) 54maxrows = int(args.maxrows) 55clear = not int(args.noclear) 56debug = 0 57 58# linux stats 59loadavg = "/proc/loadavg" 60 61# define BPF program 62bpf_text = """ 63#include <uapi/linux/ptrace.h> 64#include <linux/blkdev.h> 65 66// the key for the output summary 67struct info_t { 68 unsigned long inode; 69 dev_t dev; 70 dev_t rdev; 71 u32 pid; 72 u32 name_len; 73 char comm[TASK_COMM_LEN]; 74 // de->d_name.name may point to de->d_iname so limit len accordingly 75 char name[DNAME_INLINE_LEN]; 76 char type; 77}; 78 79// the value of the output summary 80struct val_t { 81 u64 reads; 82 u64 writes; 83 u64 rbytes; 84 u64 wbytes; 85}; 86 87BPF_HASH(counts, struct info_t, struct val_t); 88 89static int do_entry(struct pt_regs *ctx, struct file *file, 90 char __user *buf, size_t count, int is_read) 91{ 92 u32 tgid = bpf_get_current_pid_tgid() >> 32; 93 if (TGID_FILTER) 94 return 0; 95 96 u32 pid = bpf_get_current_pid_tgid(); 97 98 // skip I/O lacking a filename 99 struct dentry *de = file->f_path.dentry; 100 int mode = file->f_inode->i_mode; 101 struct qstr d_name = de->d_name; 102 if (d_name.len == 0 || TYPE_FILTER) 103 return 0; 104 105 // store counts and sizes by pid & file 106 struct info_t info = { 107 .pid = pid, 108 .inode = file->f_inode->i_ino, 109 .dev = file->f_inode->i_sb->s_dev, 110 .rdev = file->f_inode->i_rdev, 111 }; 112 bpf_get_current_comm(&info.comm, sizeof(info.comm)); 113 info.name_len = d_name.len; 114 bpf_probe_read_kernel(&info.name, sizeof(info.name), d_name.name); 115 if (S_ISREG(mode)) { 116 info.type = 'R'; 117 } else if (S_ISSOCK(mode)) { 118 info.type = 'S'; 119 } else { 120 info.type = 'O'; 121 } 122 123 struct val_t *valp, zero = {}; 124 valp = counts.lookup_or_try_init(&info, &zero); 125 if (valp) { 126 if (is_read) { 127 valp->reads++; 128 valp->rbytes += count; 129 } else { 130 valp->writes++; 131 valp->wbytes += count; 132 } 133 } 134 135 return 0; 136} 137 138int trace_read_entry(struct pt_regs *ctx, struct file *file, 139 char __user *buf, size_t count) 140{ 141 return do_entry(ctx, file, buf, count, 1); 142} 143 144int trace_write_entry(struct pt_regs *ctx, struct file *file, 145 char __user *buf, size_t count) 146{ 147 return do_entry(ctx, file, buf, count, 0); 148} 149 150""" 151if args.tgid: 152 bpf_text = bpf_text.replace('TGID_FILTER', 'tgid != %d' % args.tgid) 153else: 154 bpf_text = bpf_text.replace('TGID_FILTER', '0') 155if args.all_files: 156 bpf_text = bpf_text.replace('TYPE_FILTER', '0') 157else: 158 bpf_text = bpf_text.replace('TYPE_FILTER', '!S_ISREG(mode)') 159 160if debug or args.ebpf: 161 print(bpf_text) 162 if args.ebpf: 163 exit() 164 165# initialize BPF 166b = BPF(text=bpf_text) 167b.attach_kprobe(event="vfs_read", fn_name="trace_read_entry") 168b.attach_kprobe(event="vfs_write", fn_name="trace_write_entry") 169 170DNAME_INLINE_LEN = 32 # linux/dcache.h 171 172print('Tracing... Output every %d secs. Hit Ctrl-C to end' % interval) 173 174def sort_fn(counts): 175 if args.sort == "all": 176 return (counts[1].rbytes + counts[1].wbytes + counts[1].reads + counts[1].writes) 177 else: 178 return getattr(counts[1], args.sort) 179 180# output 181exiting = 0 182while 1: 183 try: 184 sleep(interval) 185 except KeyboardInterrupt: 186 exiting = 1 187 188 # header 189 if clear: 190 call("clear") 191 else: 192 print() 193 with open(loadavg) as stats: 194 print("%-8s loadavg: %s" % (strftime("%H:%M:%S"), stats.read())) 195 print("%-7s %-16s %-6s %-6s %-7s %-7s %1s %s" % ("TID", "COMM", 196 "READS", "WRITES", "R_Kb", "W_Kb", "T", "FILE")) 197 198 # by-TID output 199 counts = b.get_table("counts") 200 line = 0 201 for k, v in reversed(sorted(counts.items(), 202 key=sort_fn)): 203 name = k.name.decode('utf-8', 'replace') 204 if k.name_len > DNAME_INLINE_LEN: 205 name = name[:-3] + "..." 206 207 # print line 208 print("%-7d %-16s %-6d %-6d %-7d %-7d %1s %s" % (k.pid, 209 k.comm.decode('utf-8', 'replace'), v.reads, v.writes, 210 v.rbytes / 1024, v.wbytes / 1024, 211 k.type.decode('utf-8', 'replace'), name)) 212 213 line += 1 214 if line >= maxrows: 215 break 216 counts.clear() 217 218 countdown -= 1 219 if exiting or countdown == 0: 220 print("Detaching...") 221 exit() 222