Skip to contentSkip to conversation

perf,x86: Fix shared register mutual exclusion enforcement

PATCH In review Stephane Eranian 3 participants 6 messages Jun 20, 2013 – Jun 27, 2013

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

PATCH v2 1/1 perf,x86: Fix shared register mutual exclusion enforcement

This patch fixes a problem with the shared registers mutual exclusion code and incremental event scheduling by the generic perf_event code.

There was a bug whereby the mutual exclusion on the shared registers was not enforced because of incremental scheduling abort due to event constraints. As an example on Intel Nehalem, consider the following events:

group1= L1D_CACHE_LD:E_STATE,OFFCORE_RESPONSE_0:PF_RFO,L1D_CACHE_LD:I_STATE group2= L1D_CACHE_LD:I_STATE

The L1D_CACHE_LD event can only be measured by 2 counters. Yet, there are 3 instances here. The first group can be scheduled and is committed. Then, the generic code tries to schedule group2 and this fails (because there is no more counter to support the 3rd instance of L1D_CACHE_LD). But in x86_schedule_events() error path, put_event_contraints() is invoked on ALL the events and not just the ones that just failed. That causes the "lock" on the shared offcore_response MSR to be released. Yet the first group is actually scheduled and is exposed to reprogramming of that shared msr by the sibling HT thread. In other words, there is no guarantee on what is measured.

This patch fixes the problem by tagging committed events with the PERF_X86_EVENT_COMMITTED tag. In the error path of x86_schedule_events(), only the events NOT tagged have their constraint released. The tag is eventually removed when the event in descheduled.

---
 arch/x86/kernel/cpu/perf_event.c       |   26 +++++++++++++++++++++++++-
 arch/x86/kernel/cpu/perf_event.h       |    3 ++-
 arch/x86/kernel/cpu/perf_event_intel.c |    2 --
 3 files changed, 27 insertions(+), 4 deletions(-)
arch/x86/kernel/cpu/perf_event.c +25 -1
index ab33952..f585ae1 100644
@@ -726,6 +726,7 @@ int x86_schedule_events(struct cpu_hw_events *cpuc, int n, int *assign)
 {
 	struct event_constraint *c;
 	unsigned long used_mask[BITS_TO_LONGS(X86_PMC_IDX_MAX)];
+	struct perf_event *e;
 	int i, wmin, wmax, num = 0;
 	struct hw_perf_event *hwc;
 
@@ -770,13 +771,31 @@ int x86_schedule_events(struct cpu_hw_events *cpuc, int n, int *assign)
 					 wmax, assign);
 
 	/*
+	 * Mark the event as committed, so we do not put_constraint()
+	 * in case new events are added and fail scheduling.
+	 */
+	if (!num && assign) {
+		for (i = 0; i < n; i++) {
+			e = cpuc->event_list[i];
+			e->hw.flags |= PERF_X86_EVENT_COMMITTED;
+		}
+	}
+	/*
 	 * scheduling failed or is just a simulation,
 	 * free resources if necessary
 	 */
 	if (!assign || num) {
 		for (i = 0; i < n; i++) {
+			e = cpuc->event_list[i];
+			/*
+			 * do not put_constraint() on comitted events,
+			 * because they are good to go
+			 */
+			if ((e->hw.flags & PERF_X86_EVENT_COMMITTED))
+				continue;
+
 			if (x86_pmu.put_event_constraints)
-				x86_pmu.put_event_constraints(cpuc, cpuc->event_list[i]);
+				x86_pmu.put_event_constraints(cpuc, e);
 		}
 	}
 	return num ? -EINVAL : 0;
@@ -1156,6 +1175,11 @@ static void x86_pmu_del(struct perf_event *event, int flags)
 	int i;
 
 	/*
+	 * event is descheduled
+	 */
+	event->hw.flags &= ~PERF_X86_EVENT_COMMITTED;
+
+	/*
 	 * If we're called during a txn, we don't need to do anything.
 	 * The events never got scheduled and ->cancel_txn will truncate
 	 * the event_list.
arch/x86/kernel/cpu/perf_event.h +2 -1
index 108dc75..5521e04 100644
@@ -63,11 +63,12 @@ struct event_constraint {
 	int	flags;
 };
 /*
- * struct event_constraint flags
+ * struct hw_perf_event.flags flags
  */
 #define PERF_X86_EVENT_PEBS_LDLAT	0x1 /* ld+ldlat data address sampling */
 #define PERF_X86_EVENT_PEBS_ST		0x2 /* st data address sampling */
 #define PERF_X86_EVENT_PEBS_ST_HSW	0x4 /* haswell style st data sampling */
+#define PERF_X86_EVENT_COMMITTED	0x8 /* event passed commit_txn */
 
 struct amd_nb {
 	int nb_id;  /* NorthBridge id */
arch/x86/kernel/cpu/perf_event_intel.c +0 -2
index a6eccf1..7318e77 100644
@@ -1450,7 +1450,6 @@ x86_get_event_constraints(struct cpu_hw_events *cpuc, struct perf_event *event)
 	if (x86_pmu.event_constraints) {
 		for_each_event_constraint(c, x86_pmu.event_constraints) {
 			if ((event->hw.config & c->cmask) == c->code) {
-				/* hw.flags zeroed at initialization */
 				event->hw.flags |= c->flags;
 				return c;
 			}
@@ -1498,7 +1497,6 @@ intel_put_shared_regs_event_constraints(struct cpu_hw_events *cpuc,
 static void intel_put_event_constraints(struct cpu_hw_events *cpuc,
 					struct perf_event *event)
 {
-	event->hw.flags = 0;
 	intel_put_shared_regs_event_constraints(cpuc, event);
 }
 
Signature · 1 line
1.7.10.4
maintainer
#1 lore raw
Quoted 24 lines · Stephane Eranian

This patch fixes a problem with the shared registers mutual exclusion code and incremental event scheduling by the generic perf_event code.

There was a bug whereby the mutual exclusion on the shared registers was not enforced because of incremental scheduling abort due to event constraints. As an example on Intel Nehalem, consider the following events:

group1= L1D_CACHE_LD:E_STATE,OFFCORE_RESPONSE_0:PF_RFO,L1D_CACHE_LD:I_STATE group2= L1D_CACHE_LD:I_STATE

The L1D_CACHE_LD event can only be measured by 2 counters. Yet, there are 3 instances here. The first group can be scheduled and is committed. Then, the generic code tries to schedule group2 and this fails (because there is no more counter to support the 3rd instance of L1D_CACHE_LD). But in x86_schedule_events() error path, put_event_contraints() is invoked on ALL the events and not just the ones that just failed. That causes the "lock" on the shared offcore_response MSR to be released. Yet the first group is actually scheduled and is exposed to reprogramming of that shared msr by the sibling HT thread. In other words, there is no guarantee on what is measured.

Quoted 6 lines

This patch fixes the problem by tagging committed events with the PERF_X86_EVENT_COMMITTED tag. In the error path of x86_schedule_events(), only the events NOT tagged have their constraint released. The tag is eventually removed when the event in descheduled.

OK, so I 'accidentally' read the patch again; and noticed something.

In your case above; the get/put constraints are still fully matched. That is; the first group, which was successful, will not have done a put yet. So a subsequent get+put should still leave us with a positive 'ref' count and not undo things.

Only once these events pass through x86_pmu_del() will they get a final put and the 'ref' count will drop to 0.

Now the problem seems to be the get/put things don't actually count properly.

However, if we look at __intel_shared_reg_{get,put}_constraints() there is a refcount in there; namely era->ref; however we don't appear to clear reg->alloc based on it.

Should we?

#2 lore raw
Quoted 30 lines · Peter Zijlstra
Quoted 29 lines · Stephane Eranian

This patch fixes a problem with the shared registers mutual exclusion code and incremental event scheduling by the generic perf_event code.

There was a bug whereby the mutual exclusion on the shared registers was not enforced because of incremental scheduling abort due to event constraints. As an example on Intel Nehalem, consider the following events:

group1= L1D_CACHE_LD:E_STATE,OFFCORE_RESPONSE_0:PF_RFO,L1D_CACHE_LD:I_STATE group2= L1D_CACHE_LD:I_STATE

The L1D_CACHE_LD event can only be measured by 2 counters. Yet, there are 3 instances here. The first group can be scheduled and is committed. Then, the generic code tries to schedule group2 and this fails (because there is no more counter to support the 3rd instance of L1D_CACHE_LD). But in x86_schedule_events() error path, put_event_contraints() is invoked on ALL the events and not just the ones that just failed. That causes the "lock" on the shared offcore_response MSR to be released. Yet the first group is actually scheduled and is exposed to reprogramming of that shared msr by the sibling HT thread. In other words, there is no guarantee on what is measured.

This patch fixes the problem by tagging committed events with the PERF_X86_EVENT_COMMITTED tag. In the error path of x86_schedule_events(), only the events NOT tagged have their constraint released. The tag is eventually removed when the event in descheduled.

Quoted 9 lines
Quoted 1 line

OK, so I 'accidentally' read the patch again; and noticed something.

In your case above; the get/put constraints are still fully matched. That is; the first group, which was successful, will not have done a put yet. So a subsequent get+put should still leave us with a positive 'ref' count and not undo things.

You are missing the error path in schedule_events():

if (!assign || num) {

                for (i = 0; i < n; i++) {
                        if (x86_pmu.put_event_constraints)
                                x86_pmu.put_event_constraints(cpuc,
cpuc->event_list[i]);
                }

        }

That one wipes out on get() even on events that were correctly schedule in the previous
invocation. So here group2 fails, but it should not release the constraints from group1.

Quoted 10 lines

Only once these events pass through x86_pmu_del() will they get a final put and the 'ref' count will drop to 0.

Now the problem seems to be the get/put things don't actually count properly.

However, if we look at __intel_shared_reg_{get,put}_constraints() there is a refcount in there; namely era->ref; however we don't appear to clear reg->alloc based on it.

The era->ref is not used to ref count the number of successful attempts at scheduling. It is used to count the number of CPU sharing the resource. So it goes from 0, 1, to 2. You can invoke schedule_events() many more times. The reg->alloc is a bypass, to avoid checking the shared reg again and again if it succeeded once.

For a while I thought I could leverage the era->ref to account the get/put. But it does not work. Because the of the put().

Quoted 1 line

Should we?

maintainer
#3 lore raw
Quoted 12 lines · Stephane Eranian

You are missing the error path in schedule_events():

if (!assign || num) {

                for (i = 0; i < n; i++) {
                        if (x86_pmu.put_event_constraints)
                                x86_pmu.put_event_constraints(cpuc,
cpuc->event_list[i]);
                }

        }
Quoted 4 lines

That one wipes out on get() even on events that were correctly schedule in the previous
invocation. So here group2 fails, but it should not release the constraints from group1.

What I was saying:

schedule(group1)
get_event_constraints() +1
no error path, no puts

schedule(group2)
get_event_constraints() +1
*fail*
put_event_constraints() -1

This leaves the constraints of group1 with a net +1 'ref' count and thus if we were to treat the get/put as such, the put wouldn't be the last and thus shouldn't release resources.

Quoted 10 lines
Quoted 10 lines

Only once these events pass through x86_pmu_del() will they get a final put and the 'ref' count will drop to 0.

Now the problem seems to be the get/put things don't actually count properly.

However, if we look at __intel_shared_reg_{get,put}_constraints() there is a refcount in there; namely era->ref; however we don't appear to clear reg->alloc based on it.

Quoted 5 lines

The era->ref is not used to ref count the number of successful attempts at scheduling. It is used to count the number of CPU sharing the resource. So it goes from 0, 1, to 2. You can invoke schedule_events() many more times. The reg->alloc is a bypass, to avoid checking the shared reg again and again if it succeeded once.

Oh right, I knew I was missing something.. :/

Quoted 2 lines

For a while I thought I could leverage the era->ref to account the get/put. But it does not work. Because the of the put().

Crud, right you are.

Also, I don't think we could even use them as I outlined; suppose it would have worked; then we'd have:

schedule(group1)
get_event_constraints() +1

schedule(group2)
get_event_constraints() +1

And we'd be stuck with a ref of 2, the put at x86_pmu_del() would never be sufficient to drop them back to 0 again.

A well, your patch does indeed make it work so I'll grab that.

#4 lore raw
Quoted 60 lines · Peter Zijlstra
Quoted 12 lines · Stephane Eranian

You are missing the error path in schedule_events():

if (!assign || num) {

                for (i = 0; i < n; i++) {
                        if (x86_pmu.put_event_constraints)
                                x86_pmu.put_event_constraints(cpuc,
cpuc->event_list[i]);
                }

        }
Quoted 4 lines

That one wipes out on get() even on events that were correctly schedule in the previous
invocation. So here group2 fails, but it should not release the constraints from group1.

What I was saying:

schedule(group1)
get_event_constraints() +1
no error path, no puts

schedule(group2)
get_event_constraints() +1
*fail*
put_event_constraints() -1

This leaves the constraints of group1 with a net +1 'ref' count and thus if we were to treat the get/put as such, the put wouldn't be the last and thus shouldn't release resources.

Quoted 10 lines
Quoted 10 lines

Only once these events pass through x86_pmu_del() will they get a final put and the 'ref' count will drop to 0.

Now the problem seems to be the get/put things don't actually count properly.

However, if we look at __intel_shared_reg_{get,put}_constraints() there is a refcount in there; namely era->ref; however we don't appear to clear reg->alloc based on it.

Quoted 5 lines

The era->ref is not used to ref count the number of successful attempts at scheduling. It is used to count the number of CPU sharing the resource. So it goes from 0, 1, to 2. You can invoke schedule_events() many more times. The reg->alloc is a bypass, to avoid checking the shared reg again and again if it succeeded once.

Oh right, I knew I was missing something.. :/

Quoted 2 lines

For a while I thought I could leverage the era->ref to account the get/put. But it does not work. Because the of the put().

Crud, right you are.

Also, I don't think we could even use them as I outlined; suppose it would have worked; then we'd have:

Quoted 9 lines

schedule(group1)
get_event_constraints() +1

schedule(group2)
get_event_constraints() +1

And we'd be stuck with a ref of 2, the put at x86_pmu_del() would never be sufficient to drop them back to 0 again.

Yes, imagine we add 3 groups:

    schedule(grp1)  grp1=1

    schedule(grp2) grp1=2, grp2 =1

schedule(grp3) -> *failed*, put, grp1=2, grp2=1, grp3=0

And then in the del side, we'd have no way of knowing that to get the current assignment we incrementally scheduled twice, and thus we have to do grp1 -=2 and not just -=1.

Quoted 1 line

A well, your patch does indeed make it work so I'll grab that.

Thanks,

#5 lore raw

[tip:perf/core] perf/x86: Fix shared register mutual exclusion enforcement

In reply to Stephane Eranian
perf/x86: Fix shared register mutual exclusion enforcement
Commit-ID:  2f7f73a52078b667d64df16eaebdb97d98c9a410
Gitweb:     http://git.kernel.org/tip/2f7f73a52078b667d64df16eaebdb97d98c9a410
Author:     Stephane Eranian <[email protected]>
AuthorDate: Thu, 20 Jun 2013 18:42:54 +0200
Committer:  Ingo Molnar <[email protected]>
CommitDate: Wed, 26 Jun 2013 21:58:49 +0200

perf/x86: Fix shared register mutual exclusion enforcement

This patch fixes a problem with the shared registers mutual exclusion code and incremental event scheduling by the generic perf_event code.

There was a bug whereby the mutual exclusion on the shared registers was not enforced because of incremental scheduling abort due to event constraints. As an example on Intel Nehalem, consider the following events:

group1= L1D_CACHE_LD:E_STATE,OFFCORE_RESPONSE_0:PF_RFO,L1D_CACHE_LD:I_STATE group2= L1D_CACHE_LD:I_STATE

The L1D_CACHE_LD event can only be measured by 2 counters. Yet, there are 3 instances here. The first group can be scheduled and is committed. Then, the generic code tries to schedule group2 and this fails (because there is no more counter to support the 3rd instance of L1D_CACHE_LD). But in x86_schedule_events() error path, put_event_contraints() is invoked on ALL the events and not just the ones that just failed. That causes the "lock" on the shared offcore_response MSR to be released. Yet the first group is actually scheduled and is exposed to reprogramming of that shared msr by the sibling HT thread. In other words, there is no guarantee on what is measured.

This patch fixes the problem by tagging committed events with the PERF_X86_EVENT_COMMITTED tag. In the error path of x86_schedule_events(), only the events NOT tagged have their constraint released. The tag is eventually removed when the event in descheduled.

---
 arch/x86/kernel/cpu/perf_event.c       | 26 +++++++++++++++++++++++++-
 arch/x86/kernel/cpu/perf_event.h       |  3 ++-
 arch/x86/kernel/cpu/perf_event_intel.c |  2 --
 3 files changed, 27 insertions(+), 4 deletions(-)
arch/x86/kernel/cpu/perf_event.c +25 -1
index afc2413..9e581c5 100644
@@ -726,6 +726,7 @@ int x86_schedule_events(struct cpu_hw_events *cpuc, int n, int *assign)
 {
 	struct event_constraint *c;
 	unsigned long used_mask[BITS_TO_LONGS(X86_PMC_IDX_MAX)];
+	struct perf_event *e;
 	int i, wmin, wmax, num = 0;
 	struct hw_perf_event *hwc;
 
@@ -770,13 +771,31 @@ int x86_schedule_events(struct cpu_hw_events *cpuc, int n, int *assign)
 					 wmax, assign);
 
 	/*
+	 * Mark the event as committed, so we do not put_constraint()
+	 * in case new events are added and fail scheduling.
+	 */
+	if (!num && assign) {
+		for (i = 0; i < n; i++) {
+			e = cpuc->event_list[i];
+			e->hw.flags |= PERF_X86_EVENT_COMMITTED;
+		}
+	}
+	/*
 	 * scheduling failed or is just a simulation,
 	 * free resources if necessary
 	 */
 	if (!assign || num) {
 		for (i = 0; i < n; i++) {
+			e = cpuc->event_list[i];
+			/*
+			 * do not put_constraint() on comitted events,
+			 * because they are good to go
+			 */
+			if ((e->hw.flags & PERF_X86_EVENT_COMMITTED))
+				continue;
+
 			if (x86_pmu.put_event_constraints)
-				x86_pmu.put_event_constraints(cpuc, cpuc->event_list[i]);
+				x86_pmu.put_event_constraints(cpuc, e);
 		}
 	}
 	return num ? -EINVAL : 0;
@@ -1156,6 +1175,11 @@ static void x86_pmu_del(struct perf_event *event, int flags)
 	int i;
 
 	/*
+	 * event is descheduled
+	 */
+	event->hw.flags &= ~PERF_X86_EVENT_COMMITTED;
+
+	/*
 	 * If we're called during a txn, we don't need to do anything.
 	 * The events never got scheduled and ->cancel_txn will truncate
 	 * the event_list.
arch/x86/kernel/cpu/perf_event.h +2 -1
index 4809f07..97e557b 100644
@@ -63,11 +63,12 @@ struct event_constraint {
 	int	flags;
 };
 /*
- * struct event_constraint flags
+ * struct hw_perf_event.flags flags
  */
 #define PERF_X86_EVENT_PEBS_LDLAT	0x1 /* ld+ldlat data address sampling */
 #define PERF_X86_EVENT_PEBS_ST		0x2 /* st data address sampling */
 #define PERF_X86_EVENT_PEBS_ST_HSW	0x4 /* haswell style st data sampling */
+#define PERF_X86_EVENT_COMMITTED	0x8 /* event passed commit_txn */
 
 struct amd_nb {
 	int nb_id;  /* NorthBridge id */
arch/x86/kernel/cpu/perf_event_intel.c +0 -2
index 5877f37..fbc9210 100644
@@ -1450,7 +1450,6 @@ x86_get_event_constraints(struct cpu_hw_events *cpuc, struct perf_event *event)
 	if (x86_pmu.event_constraints) {
 		for_each_event_constraint(c, x86_pmu.event_constraints) {
 			if ((event->hw.config & c->cmask) == c->code) {
-				/* hw.flags zeroed at initialization */
 				event->hw.flags |= c->flags;
 				return c;
 			}
@@ -1498,7 +1497,6 @@ intel_put_shared_regs_event_constraints(struct cpu_hw_events *cpuc,
 static void intel_put_event_constraints(struct cpu_hw_events *cpuc,
 					struct perf_event *event)
 {
-	event->hw.flags = 0;
 	intel_put_shared_regs_event_constraints(cpuc, event);
 }