kvmexit.py: introduce a tool to show kvm exit reasons and counts
[platform/upstream/bcc.git] / tools / kvmexit.py
1 #!/usr/bin/env python
2 #
3 # kvmexit.py
4 #
5 # Display the exit_reason and its statistics of each vm exit
6 # for all vcpus of all virtual machines. For example:
7 # $./kvmexit.py
8 #  PID      TID      KVM_EXIT_REASON                     COUNT
9 #  1273551  1273568  EXIT_REASON_MSR_WRITE               6
10 #  1274253  1274261  EXIT_REASON_EXTERNAL_INTERRUPT      1
11 #  1274253  1274261  EXIT_REASON_HLT                     12
12 #  ...
13 #
14 # Besides, we also allow users to specify one pid, tid(s), or one
15 # pid and its vcpu. See kvmexit_example.txt for more examples.
16 #
17 # @PID: each vitual machine's pid in the user space.
18 # @TID: the user space's thread of each vcpu of that virtual machine.
19 # @KVM_EXIT_REASON: the reason why the vm exits.
20 # @COUNT: the counts of the @KVM_EXIT_REASONS.
21 #
22 # REQUIRES: Linux 4.7+ (BPF_PROG_TYPE_TRACEPOINT support)
23 #
24 # Copyright (c) 2021 ByteDance Inc. All rights reserved.
25 #
26 # Author(s):
27 #   Fei Li <lifei.shirley@bytedance.com>
28
29
30 from __future__ import print_function
31 from time import sleep, strftime
32 from bcc import BPF
33 import argparse
34 import multiprocessing
35 import os
36 import signal
37 import subprocess
38
39 #
40 # Process Arguments
41 #
42 def valid_args_list(args):
43     args_list = args.split(",")
44     for arg in args_list:
45         try:
46             int(arg)
47         except:
48             raise argparse.ArgumentTypeError("must be valid integer")
49     return args_list
50
51 # arguments
52 examples = """examples:
53     ./kvmexit                              # Display kvm_exit_reason and its statistics in real-time until Ctrl-C
54     ./kvmexit 5                            # Display in real-time after sleeping 5s
55     ./kvmexit -p 3195281                   # Collpase all tids for pid 3195281 with exit reasons sorted in descending order
56     ./kvmexit -p 3195281 20                # Collpase all tids for pid 3195281 with exit reasons sorted in descending order, and display after sleeping 20s
57     ./kvmexit -p 3195281 -v 0              # Display only vcpu0 for pid 3195281, descending sort by default
58     ./kvmexit -p 3195281 -a                # Display all tids for pid 3195281
59     ./kvmexit -t 395490                    # Display only for tid 395490 with exit reasons sorted in descending order
60     ./kvmexit -t 395490 20                 # Display only for tid 395490 with exit reasons sorted in descending order after sleeping 20s
61     ./kvmexit -T '395490,395491'           # Display for a union like {395490, 395491}
62 """
63 parser = argparse.ArgumentParser(
64     description="Display kvm_exit_reason and its statistics at a timed interval",
65     formatter_class=argparse.RawDescriptionHelpFormatter,
66     epilog=examples)
67 parser.add_argument("duration", nargs="?", default=99999999, type=int, help="show delta for next several seconds")
68 parser.add_argument("-p", "--pid", type=int, help="trace this PID only")
69 exgroup = parser.add_mutually_exclusive_group()
70 exgroup.add_argument("-t", "--tid", type=int, help="trace this TID only")
71 exgroup.add_argument("-T", "--tids", type=valid_args_list, help="trace a comma separated series of tids with no space in between")
72 exgroup.add_argument("-v", "--vcpu", type=int, help="trace this vcpu only")
73 exgroup.add_argument("-a", "--alltids", action="store_true", help="trace all tids for this pid")
74 args = parser.parse_args()
75 duration = int(args.duration)
76
77 #
78 # Setup BPF
79 #
80
81 # load BPF program
82 bpf_text = """
83 #include <linux/delay.h>
84
85 #define REASON_NUM 69
86 #define TGID_NUM 1024
87
88 struct exit_count {
89     u64 exit_ct[REASON_NUM];
90 };
91 BPF_PERCPU_ARRAY(init_value, struct exit_count, 1);
92 BPF_TABLE("percpu_hash", u64, struct exit_count, pcpu_kvm_stat, TGID_NUM);
93
94 struct cache_info {
95     u64 cache_pid_tgid;
96     struct exit_count cache_exit_ct;
97 };
98 BPF_PERCPU_ARRAY(pcpu_cache, struct cache_info, 1);
99
100 FUNC_ENTRY {
101     int cache_miss = 0;
102     int zero = 0;
103     u32 er = GET_ER;
104     if (er >= REASON_NUM) {
105         return 0;
106     }
107
108     u64 cur_pid_tgid = bpf_get_current_pid_tgid();
109     u32 tgid = cur_pid_tgid >> 32;
110     u32 pid = cur_pid_tgid;
111
112     if (THREAD_FILTER)
113         return 0;
114
115     struct exit_count *tmp_info = NULL, *initial = NULL;
116     struct cache_info *cache_p;
117     cache_p = pcpu_cache.lookup(&zero);
118     if (cache_p == NULL) {
119         return 0;
120     }
121
122     if (cache_p->cache_pid_tgid == cur_pid_tgid) {
123         //a. If the cur_pid_tgid hit this physical cpu consecutively, save it to pcpu_cache
124         tmp_info = &cache_p->cache_exit_ct;
125     } else {
126         //b. If another pid_tgid matches this pcpu for the last hit, OR it is the first time to hit this physical cpu.
127         cache_miss = 1;
128
129         // b.a Try to load the last cache struct if exists.
130         tmp_info = pcpu_kvm_stat.lookup(&cur_pid_tgid);
131
132         // b.b If it is the first time for the cur_pid_tgid to hit this pcpu, employ a
133         // per_cpu array to initialize pcpu_kvm_stat's exit_count with each exit reason's count is zero
134         if (tmp_info == NULL) {
135             initial = init_value.lookup(&zero);
136             if (initial == NULL) {
137                 return 0;
138             }
139
140             pcpu_kvm_stat.update(&cur_pid_tgid, initial);
141             tmp_info = pcpu_kvm_stat.lookup(&cur_pid_tgid);
142             // To pass the verifier
143             if (tmp_info == NULL) {
144                 return 0;
145             }
146         }
147     }
148
149     if (er < REASON_NUM) {
150         tmp_info->exit_ct[er]++;
151         if (cache_miss == 1) {
152             if (cache_p->cache_pid_tgid != 0) {
153                 // b.*.a Let's save the last hit cache_info into kvm_stat.
154                 pcpu_kvm_stat.update(&cache_p->cache_pid_tgid, &cache_p->cache_exit_ct);
155             }
156             // b.* As the cur_pid_tgid meets current pcpu_cache_array for the first time, save it.
157             cache_p->cache_pid_tgid = cur_pid_tgid;
158             bpf_probe_read(&cache_p->cache_exit_ct, sizeof(*tmp_info), tmp_info);
159         }
160         return 0;
161     }
162
163     return 0;
164 }
165 """
166
167 # format output
168 exit_reasons = (
169     "EXCEPTION_NMI",
170     "EXTERNAL_INTERRUPT",
171     "TRIPLE_FAULT",
172     "INIT_SIGNAL",
173     "N/A",
174     "N/A",
175     "N/A",
176     "INTERRUPT_WINDOW",
177     "NMI_WINDOW",
178     "TASK_SWITCH",
179     "CPUID",
180     "N/A",
181     "HLT",
182     "INVD",
183     "INVLPG",
184     "RDPMC",
185     "RDTSC",
186     "N/A",
187     "VMCALL",
188     "VMCLEAR",
189     "VMLAUNCH",
190     "VMPTRLD",
191     "VMPTRST",
192     "VMREAD",
193     "VMRESUME",
194     "VMWRITE",
195     "VMOFF",
196     "VMON",
197     "CR_ACCESS",
198     "DR_ACCESS",
199     "IO_INSTRUCTION",
200     "MSR_READ",
201     "MSR_WRITE",
202     "INVALID_STATE",
203     "MSR_LOAD_FAIL",
204     "N/A",
205     "MWAIT_INSTRUCTION",
206     "MONITOR_TRAP_FLAG",
207     "N/A",
208     "MONITOR_INSTRUCTION",
209     "PAUSE_INSTRUCTION",
210     "MCE_DURING_VMENTRY",
211     "N/A",
212     "TPR_BELOW_THRESHOLD",
213     "APIC_ACCESS",
214     "EOI_INDUCED",
215     "GDTR_IDTR",
216     "LDTR_TR",
217     "EPT_VIOLATION",
218     "EPT_MISCONFIG",
219     "INVEPT",
220     "RDTSCP",
221     "PREEMPTION_TIMER",
222     "INVVPID",
223     "WBINVD",
224     "XSETBV",
225     "APIC_WRITE",
226     "RDRAND",
227     "INVPCID",
228     "VMFUNC",
229     "ENCLS",
230     "RDSEED",
231     "PML_FULL",
232     "XSAVES",
233     "XRSTORS",
234     "N/A",
235     "N/A",
236     "UMWAIT",
237     "TPAUSE"
238 )
239
240 #
241 # Do some checks
242 #
243 try:
244     # Currently, only adapte on intel architecture
245     cmd = "cat /proc/cpuinfo | grep vendor_id | head -n 1"
246     arch_info = subprocess.check_output(cmd, shell=True).strip()
247     if b"Intel" in arch_info:
248         pass
249     else:
250         raise Exception("Currently we only support Intel architecture, please do expansion if needs more.")
251
252     # Check if kvm module is loaded
253     if os.access("/dev/kvm", os.R_OK | os.W_OK):
254         pass
255     else:
256         raise Exception("Please insmod kvm module to use kvmexit tool.")
257 except Exception as e:
258     raise Exception("Failed to do precondition check, due to: %s." % e)
259
260 try:
261     if BPF.support_raw_tracepoint_in_module():
262         # Let's firstly try raw_tracepoint_in_module
263         func_entry = "RAW_TRACEPOINT_PROBE(kvm_exit)"
264         get_er = "ctx->args[0]"
265     else:
266         # If raw_tp_in_module is not supported, fall back to regular tp
267         func_entry = "TRACEPOINT_PROBE(kvm, kvm_exit)"
268         get_er = "args->exit_reason"
269 except Exception as e:
270     raise Exception("Failed to catch kvm exit reasons due to: %s" % e)
271
272
273 def find_tid(tgt_dir, tgt_vcpu):
274     for tid in os.listdir(tgt_dir):
275         path = tgt_dir + "/" + tid + "/comm"
276         fp = open(path, "r")
277         comm = fp.read()
278         if (comm.find(tgt_vcpu) != -1):
279             return tid
280     return -1
281
282 # set process/thread filter
283 thread_context = ""
284 header_format = ""
285 need_collapse = not args.alltids
286 if args.tid is not None:
287     thread_context = "TID %s" % args.tid
288     thread_filter = 'pid != %s' % args.tid
289 elif args.tids is not None:
290     thread_context = "TIDS %s" % args.tids
291     thread_filter = "pid != " + " && pid != ".join(args.tids)
292     header_format = "TIDS     "
293 elif args.pid is not None:
294     thread_context = "PID %s" % args.pid
295     thread_filter = 'tgid != %s' % args.pid
296     if args.vcpu is not None:
297         thread_context = "PID %s VCPU %s" % (args.pid, args.vcpu)
298         # transfer vcpu to tid
299         tgt_dir = '/proc/' + str(args.pid) + '/task'
300         tgt_vcpu = "CPU " + str(args.vcpu)
301         args.tid = find_tid(tgt_dir, tgt_vcpu)
302         if args.tid == -1:
303             raise Exception("There's no v%s for PID %d." % (tgt_vcpu, args.pid))
304         thread_filter = 'pid != %s' % args.tid
305     elif args.alltids:
306         thread_context = "PID %s and its all threads" % args.pid
307         header_format = "TID      "
308 else:
309     thread_context = "all threads"
310     thread_filter = '0'
311     header_format = "PID      TID      "
312 bpf_text = bpf_text.replace('THREAD_FILTER', thread_filter)
313
314 # For kernel >= 5.0, use RAW_TRACEPOINT_MODULE for performance consideration
315 bpf_text = bpf_text.replace('FUNC_ENTRY', func_entry)
316 bpf_text = bpf_text.replace('GET_ER', get_er)
317 b = BPF(text=bpf_text)
318
319
320 # header
321 print("Display kvm exit reasons and statistics for %s" % thread_context, end="")
322 if duration < 99999999:
323     print(" after sleeping %d secs." % duration)
324 else:
325     print("... Hit Ctrl-C to end.")
326 print("%s%-35s %s" % (header_format, "KVM_EXIT_REASON", "COUNT"))
327
328 # signal handler
329 def signal_ignore(signal, frame):
330         print()
331 try:
332     sleep(duration)
333 except KeyboardInterrupt:
334     signal.signal(signal.SIGINT, signal_ignore)
335
336
337 # Currently, sort multiple tids in descending order is not supported.
338 if (args.pid or args.tid):
339     ct_reason = []
340     if args.pid:
341         tgid_exit = [0 for i in range(len(exit_reasons))]
342
343 # output
344 pcpu_kvm_stat = b["pcpu_kvm_stat"]
345 pcpu_cache = b["pcpu_cache"]
346 for k, v in pcpu_kvm_stat.items():
347     tgid = k.value >> 32
348     pid = k.value & 0xffffffff
349     for i in range(0, len(exit_reasons)):
350         sum1 = 0
351         for inner_cpu in range(0, multiprocessing.cpu_count()):
352             cachePIDTGID = pcpu_cache[0][inner_cpu].cache_pid_tgid
353             # Take priority to check if it is in cache
354             if cachePIDTGID == k.value:
355                 sum1 += pcpu_cache[0][inner_cpu].cache_exit_ct.exit_ct[i]
356             # If not in cache, find from kvm_stat
357             else:
358                 sum1 += v[inner_cpu].exit_ct[i]
359         if sum1 == 0:
360             continue
361
362         if (args.pid and args.pid == tgid and need_collapse):
363             tgid_exit[i] += sum1
364         elif (args.tid and args.tid == pid):
365             ct_reason.append((sum1, i))
366         elif not need_collapse or args.tids:
367             print("%-8u %-35s %-8u" % (pid, exit_reasons[i], sum1))
368         else:
369             print("%-8u %-8u %-35s %-8u" % (tgid, pid, exit_reasons[i], sum1))
370
371     # Display only for the target tid in descending sort
372     if (args.tid and args.tid == pid):
373         ct_reason.sort(reverse=True)
374         for i in range(0, len(ct_reason)):
375             if ct_reason[i][0] == 0:
376                 continue
377             print("%-35s %-8u" % (exit_reasons[ct_reason[i][1]], ct_reason[i][0]))
378         break
379
380
381 # Aggregate all tids' counts for this args.pid in descending sort
382 if args.pid and need_collapse:
383     for i in range(0, len(exit_reasons)):
384         ct_reason.append((tgid_exit[i], i))
385     ct_reason.sort(reverse=True)
386     for i in range(0, len(ct_reason)):
387         if ct_reason[i][0] == 0:
388             continue
389         print("%-35s %-8u" % (exit_reasons[ct_reason[i][1]], ct_reason[i][0]))