perf: Use NMIs for sampling HW and VM TFs
authorBarret Rhoden <brho@cs.berkeley.edu>
Wed, 27 Jul 2016 22:25:08 +0000 (18:25 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Fri, 29 Jul 2016 21:43:07 +0000 (17:43 -0400)
Using NMIs allows us to sampling when interrupts are disabled.  It's
extremely useful.

To limit the amount of code we run from NMI context, we only record the
sample into a pre-allocated, per-cpu buffer.  Keep in mind the NMI rules
about writing and reading at the top of handle_nmi().

So NMIs record the backtrace, but don't emit the sample.  We self_ipi()
to trigger emitting the sample as soon as IRQs are reenabled.  If IRQs
were not disabled, this IRQ will hit as soon as the NMI returns.

vmexits for an NMI will also record the PC of the guest, but not attempt
a backtrace.  Previously, we were probably trying to backtrace.  Due to
the x86 NMI blocking issues, we don't attempt to do anything that might
fault from the vmexit NMI handler.

NMIs are now used for both perf monitoring and the monitor "trace
coretf".  The monitor's trace command is still a little dangerous.  This
commit also just moves all of that mess into monitor.c.

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
kern/arch/x86/perfmon.c
kern/arch/x86/perfmon.h
kern/arch/x86/trap.c
kern/include/monitor.h
kern/src/monitor.c

index 0038146..cab0f3d 100644 (file)
@@ -67,6 +67,15 @@ static struct perfmon_cpu_caps cpu_caps;
 static DEFINE_PERCPU(struct perfmon_cpu_context, counters_env);
 DEFINE_PERCPU_INIT(perfmon_counters_env_init);
 
+#define PROFILER_BT_DEPTH 16
+
+struct sample_snapshot {
+       struct user_context                     ctx;
+       uintptr_t                                       pc_list[PROFILER_BT_DEPTH];
+       size_t                                          nr_pcs;
+};
+static DEFINE_PERCPU(struct sample_snapshot, sample_snapshots);
+
 static void perfmon_counters_env_init(void)
 {
        for (int i = 0; i < num_cores; i++) {
@@ -401,7 +410,9 @@ static struct perfmon_status *perfmon_status_alloc(void)
 
 static void perfmon_arm_irq(void)
 {
-       apicrput(MSR_LAPIC_LVT_PERFMON, IdtLAPIC_PCINT);
+       /* Actually, the vector is ignored, I'm just adding T_NMI to avoid
+        * confusion.  The important part is the NMI-bits (0x4) */
+       apicrput(MSR_LAPIC_LVT_PERFMON, (0x4 << 8) | T_NMI);
 }
 
 bool perfmon_supported(void)
@@ -442,20 +453,63 @@ static uint64_t perfmon_make_sample_event(const struct perfmon_event *pev)
        return pev->user_data;
 }
 
-static void profiler_add_hw_sample(struct hw_trapframe *hw_tf, uint64_t info)
+/* Called from NMI context! */
+void perfmon_snapshot_hwtf(struct hw_trapframe *hw_tf)
 {
-       #define PROFILER_BT_DEPTH 16
-       uintptr_t pc_list[PROFILER_BT_DEPTH];
-       size_t n;
+       struct sample_snapshot *sample = PERCPU_VARPTR(sample_snapshots);
        uintptr_t pc = get_hwtf_pc(hw_tf);
        uintptr_t fp = get_hwtf_fp(hw_tf);
 
+       sample->ctx.type = ROS_HW_CTX;
+       sample->ctx.tf.hw_tf = *hw_tf;
        if (in_kernel(hw_tf)) {
-               n = backtrace_list(pc, fp, pc_list, PROFILER_BT_DEPTH);
-               profiler_push_kernel_backtrace(pc_list, n, info);
+               sample->nr_pcs = backtrace_list(pc, fp, sample->pc_list,
+                                               PROFILER_BT_DEPTH);
        } else {
-               n = backtrace_user_list(pc, fp, pc_list, PROFILER_BT_DEPTH);
-               profiler_push_user_backtrace(pc_list, n, info);
+               sample->nr_pcs = backtrace_user_list(pc, fp, sample->pc_list,
+                                                    PROFILER_BT_DEPTH);
+       }
+}
+
+/* Called from NMI context, *and* this cannot fault (e.g. breakpoint tracing)!
+ * The latter restriction is due to the vmexit NMI handler not being
+ * interruptible.  Because of this, we just copy out the VM TF. */
+void perfmon_snapshot_vmtf(struct vm_trapframe *vm_tf)
+{
+       struct sample_snapshot *sample = PERCPU_VARPTR(sample_snapshots);
+
+       sample->ctx.type = ROS_VM_CTX;
+       sample->ctx.tf.vm_tf = *vm_tf;
+       sample->nr_pcs = 1;
+       sample->pc_list[0] = get_vmtf_pc(vm_tf);
+}
+
+static void profiler_add_sample(uint64_t info)
+{
+       struct sample_snapshot *sample = PERCPU_VARPTR(sample_snapshots);
+
+       /* We shouldn't need to worry about another NMI that concurrently mucks with
+        * the sample.  The PMU won't rearm the interrupt until we're done here.  In
+        * the event that we do get another NMI from another source, we may get a
+        * weird backtrace in the perf output. */
+       switch (sample->ctx.type) {
+       case ROS_HW_CTX:
+               if (in_kernel(&sample->ctx.tf.hw_tf)) {
+                       profiler_push_kernel_backtrace(sample->pc_list, sample->nr_pcs,
+                                                      info);
+               } else {
+                       profiler_push_user_backtrace(sample->pc_list, sample->nr_pcs, info);
+               }
+               break;
+       case ROS_VM_CTX:
+               /* TODO: add VM support to perf.  For now, just treat it like a user
+                * addr.  Note that the address is a guest-virtual address, not
+                * guest-physical (which would be host virtual), and our VM_CTXs don't
+                * make a distinction between user and kernel TFs (yet). */
+               profiler_push_user_backtrace(sample->pc_list, sample->nr_pcs, info);
+               break;
+       default:
+               warn("Bad perf sample type %d!", sample->ctx.type);
        }
 }
 
@@ -476,8 +530,8 @@ void perfmon_interrupt(struct hw_trapframe *hw_tf, void *data)
        for (i = 0; i < (int) cpu_caps.counters_x_proc; i++) {
                if (status & ((uint64_t) 1 << i)) {
                        if (cctx->counters[i].event) {
-                               profiler_add_hw_sample(
-                                   hw_tf, perfmon_make_sample_event(cctx->counters + i));
+                               profiler_add_sample(
+                                   perfmon_make_sample_event(cctx->counters + i));
                                perfmon_set_unfixed_trigger(i, cctx->counters[i].trigger_count);
                        }
                }
@@ -485,8 +539,8 @@ void perfmon_interrupt(struct hw_trapframe *hw_tf, void *data)
        for (i = 0; i < (int) cpu_caps.fix_counters_x_proc; i++) {
                if (status & ((uint64_t) 1 << (32 + i))) {
                        if (cctx->fixed_counters[i].event) {
-                               profiler_add_hw_sample(
-                                   hw_tf, perfmon_make_sample_event(cctx->fixed_counters + i));
+                               profiler_add_sample(
+                                   perfmon_make_sample_event(cctx->fixed_counters + i));
                                perfmon_set_fixed_trigger(i,
                                        cctx->fixed_counters[i].trigger_count);
                        }
index 3e7d6f3..060eecc 100644 (file)
@@ -50,6 +50,8 @@ struct perfmon_status {
 bool perfmon_supported(void);
 void perfmon_global_init(void);
 void perfmon_pcpu_init(void);
+void perfmon_snapshot_hwtf(struct hw_trapframe *hw_tf);
+void perfmon_snapshot_vmtf(struct vm_trapframe *vm_tf);
 void perfmon_interrupt(struct hw_trapframe *hw_tf, void *data);
 void perfmon_get_cpu_caps(struct perfmon_cpu_caps *pcc);
 int perfmon_open_event(const struct core_set *cset, struct perfmon_session *ps,
index 3b44f95..f818cae 100644 (file)
@@ -333,25 +333,19 @@ static bool __handle_page_fault(struct hw_trapframe *hw_tf, unsigned long *aux)
 /* Actual body of work done when an NMI arrives */
 static void do_nmi_work(struct hw_trapframe *hw_tf)
 {
-       /* TODO: this is all racy and needs to go */
-       struct per_cpu_info *pcpui = &per_cpu_info[core_id()];
-       char *fn_name;
-       /* This is a bit hacky, but we don't have a decent API yet */
-       extern bool mon_verbose_trace;
-
-       /* Temporarily disable deadlock detection when we print.  We could
-        * deadlock if we were printing when we NMIed. */
-       pcpui->__lock_checking_enabled--;
-       if (mon_verbose_trace) {
-               print_trapframe(hw_tf);
-               backtrace_hwtf(hw_tf);
-       }
-       fn_name = get_fn_name(get_hwtf_pc(hw_tf));
-       printk("Core %d is at %p (%s)\n", core_id(), get_hwtf_pc(hw_tf),
-              fn_name);
-       kfree(fn_name);
-       print_kmsgs(core_id());
-       pcpui->__lock_checking_enabled++;
+       assert(!irq_is_enabled());
+       /* It's mostly harmless to snapshot the TF, and we can send a spurious PCINT
+        * interrupt.  perfmon.c just uses the interrupt to tell it to check its
+        * counters for overflow.  Note that the PCINT interrupt is just a regular
+        * IRQ.  The backtrace was recorded during the NMI and emitted during IRQ.
+        *
+        * That being said, it's OK if the monitor triggers debugging NMIs while
+        * perf is running.  If perf triggers an NMI when the monitor wants to
+        * print, the monitor will debug *that* NMI, and not the one that gets sent
+        * moments later.  That's fine. */
+       emit_monitor_backtrace(ROS_HW_CTX, hw_tf);
+       perfmon_snapshot_hwtf(hw_tf);
+       send_self_ipi(IdtLAPIC_PCINT);
 }
 
 /* NMI HW_TF hacking involves four symbols:
@@ -928,20 +922,22 @@ static bool handle_vmexit_ept_fault(struct vm_trapframe *tf)
        return TRUE;
 }
 
+/* Regarding NMI blocking,
+ *             "An NMI causes subsequent NMIs to be blocked, but only after the VM exit
+ *             completes." (SDM)
+ *
+ * Like handle_nmi(), this function and anything it calls directly cannot fault,
+ * or else we lose our NMI protections. */
 static bool handle_vmexit_nmi(struct vm_trapframe *tf)
 {
        /* Sanity checks, make sure we really got an NMI.  Feel free to remove. */
        assert((tf->tf_intrinfo2 & INTR_INFO_INTR_TYPE_MASK) == INTR_TYPE_NMI_INTR);
        assert((tf->tf_intrinfo2 & INTR_INFO_VECTOR_MASK) == T_NMI);
-       /* our NMI handler from trap.c won't run.  but we don't need the lock
-        * disabling stuff. */
-       extern bool mon_verbose_trace;
+       assert(!irq_is_enabled());
 
-       if (mon_verbose_trace) {
-               print_vmtrapframe(tf);
-               /* TODO: a backtrace of the guest would be nice here. */
-       }
-       printk("Core %d is at %p\n", core_id(), get_vmtf_pc(tf));
+       emit_monitor_backtrace(ROS_VM_CTX, tf);
+       perfmon_snapshot_vmtf(tf);
+       send_self_ipi(IdtLAPIC_PCINT);
        return TRUE;
 }
 
index 887058b..94c8f0d 100644 (file)
@@ -7,6 +7,7 @@
 // optionally providing a trap frame indicating the current state
 // (NULL if none).
 void monitor(struct hw_trapframe *hw_tf);
+void emit_monitor_backtrace(int type, void *tf);
 
 // Functions implementing monitor commands.
 int mon_help(int argc, char **argv, struct hw_trapframe *hw_tf);
index f24c87b..ba0e46b 100644 (file)
@@ -24,6 +24,7 @@
 #include <event.h>
 #include <trap.h>
 #include <time.h>
+#include <percpu.h>
 
 #include <ros/memlayout.h>
 #include <ros/event.h>
@@ -682,8 +683,53 @@ int mon_measure(int argc, char **argv, struct hw_trapframe *hw_tf)
        return 0;
 }
 
-/* Used in various debug locations.  Not a kernel API or anything. */
-bool mon_verbose_trace = FALSE;
+static bool mon_verbose_trace = FALSE;
+static DEFINE_PERCPU(bool, mon_nmi_trace);
+
+static void emit_hwtf_backtrace(struct hw_trapframe *hw_tf)
+{
+       char *fn_name;
+
+       if (mon_verbose_trace) {
+               print_trapframe(hw_tf);
+               backtrace_hwtf(hw_tf);
+       }
+       fn_name = get_fn_name(get_hwtf_pc(hw_tf));
+       printk("Core %d is at %p (%s)\n", core_id(), get_hwtf_pc(hw_tf),
+              fn_name);
+       kfree(fn_name);
+}
+
+static void emit_vmtf_backtrace(struct vm_trapframe *vm_tf)
+{
+       if (mon_verbose_trace)
+               print_vmtrapframe(vm_tf);
+       printk("Core %d is at %p\n", core_id(), get_vmtf_pc(vm_tf));
+}
+
+/* This is dangerous and could cause a deadlock, since it runs in NMI context.
+ * It's only for monitor debugging, so YMMV.  We pass the type since the kernel
+ * doesn't deal in contexts (yet) */
+void emit_monitor_backtrace(int type, void *tf)
+{
+       struct per_cpu_info *pcpui = &per_cpu_info[core_id()];
+
+       if (!PERCPU_VAR(mon_nmi_trace))
+               return;
+       /* To prevent a spew of output during a lot of perf NMIs, we'll turn off the
+        * monitor output as soon as any NMI hits our core. */
+       PERCPU_VAR(mon_nmi_trace) = FALSE;
+       /* Temporarily disable deadlock detection when we print.  We could
+        * deadlock if we were printing when we NMIed. */
+       pcpui->__lock_checking_enabled--;
+       if (type == ROS_HW_CTX)
+               emit_hwtf_backtrace((struct hw_trapframe*)tf);
+       else
+               emit_vmtf_backtrace((struct vm_trapframe*)tf);
+       print_kmsgs(core_id());
+       pcpui->__lock_checking_enabled++;
+}
+
 
 int mon_trace(int argc, char **argv, struct hw_trapframe *hw_tf)
 {
@@ -719,14 +765,17 @@ int mon_trace(int argc, char **argv, struct hw_trapframe *hw_tf)
                core = strtol(argv[2], 0, 0);
                if (core < 0) {
                        printk("Sending NMIs to all cores:\n");
-                       for (int i = 0; i < num_cores; i++)
+                       for (int i = 0; i < num_cores; i++) {
+                               _PERCPU_VAR(mon_nmi_trace, i) = TRUE;
                                send_nmi(i);
+                       }
                } else {
                        printk("Sending NMI core %d:\n", core);
                        if (core >= num_cores) {
                                printk("No such core!  Maybe it's in another cell...\n");
                                return 1;
                        }
+                       _PERCPU_VAR(mon_nmi_trace, core) = TRUE;
                        send_nmi(core);
                }
                udelay(1000000);