Skip to contentSkip to conversation

Don't apply for write lock on tasklist_lock if parent doesn't ptrace other processes

PATCH In review Zhang, Yanmin 6 participants 15 messages Jul 15, 2010 – Jul 29, 2010 ptrace support

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 Don't apply for write lock on tasklist_lock if parent doesn't ptrace other processes

We run some sub-cases (fork, exec, pipe, tcp, udp) of aim7 on 8-socket machine. Perf shows write_lock_irq(&tasklist_lock) consumes more than 50% cpu time.

One hot caller is exit_ptrace. If the exiting process doesn't ptrace other processes, kernel needn't apply for the write lock on tasklist_lock.

With below patch against kernel 2.6.35-rc5, we get more than 10% result improvement.

---

kernel/ptrace.c +3 -0
diff -Nraup linux-2.6.35-rc5/kernel/ptrace.c linux-2.6.35-rc5_ptrace/kernel/ptrace.c
@@ -331,6 +331,9 @@ void exit_ptrace(struct task_struct *tra
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
+	if (list_empty(&tracer->ptraced))
+		return;
+
 	write_lock_irq(&tasklist_lock);
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))
Acked-by on v1 1/1
#1 lore raw
Quoted 9 lines · Yanmin

We run some sub-cases (fork, exec, pipe, tcp, udp) of aim7 on 8-socket machine. Perf shows write_lock_irq(&tasklist_lock) consumes more than 50% cpu time.

One hot caller is exit_ptrace. If the exiting process doesn't ptrace other processes, kernel needn't apply for the write lock on tasklist_lock.

With below patch against kernel 2.6.35-rc5, we get more than 10% result improvement.

We're guarded against ptrace_attach() because tracer->exit_state is non-zero at this point in the exit path.

#2 lore raw
In reply to Zhang, Yanmin
Quoted 16 lines · Yanmin

We run some sub-cases (fork, exec, pipe, tcp, udp) of aim7 on 8-socket machine. Perf shows write_lock_irq(&tasklist_lock) consumes more than 50% cpu time.

One hot caller is exit_ptrace. If the exiting process doesn't ptrace other processes, kernel needn't apply for the write lock on tasklist_lock.

With below patch against kernel 2.6.35-rc5, we get more than 10% result improvement.

---

diff -Nraup linux-2.6.35-rc5/kernel/ptrace.c linux-2.6.35-rc5_ptrace/kernel/ptrace.c

kernel/ptrace.c void exit_ptrace(struct task_struct *tra · line 339

Quoted 9 lines
kernel/ptrace.c +3 -0
@@ -331,6 +331,9 @@ void exit_ptrace(struct task_struct *tra
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
+	if (list_empty(&tracer->ptraced))
+		return;
+
 	write_lock_irq(&tasklist_lock);
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))

hah, nice patch - an easy 10%. I snuck a cc:stable into the changelog in the hope that those guys mistake it for a bugfix ;)

#3 lore raw
Quoted 10 lines

kernel/ptrace.c void exit_ptrace(struct task_struct *tra · line 339

Quoted 10 lines
kernel/ptrace.c +3 -0
@@ -331,6 +331,9 @@ void exit_ptrace(struct task_struct *tra
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
+	if (list_empty(&tracer->ptraced))
+		return;
+
 	write_lock_irq(&tasklist_lock);
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))

I think we may have tried that before. Oleg can tell us if it's really safe vs a race with PTRACE_TRACEME or something like that.

Thanks,
Roland

maintainer
#4 lore raw

I am not surpized perf blaims tasklist, but I am really surpized this patch adds 10% improvement...

Quoted 5 lines · Roland McGrath
Quoted 5 lines
Quoted 5 lines
(quoted diff) +0 -0
@@ -331,6 +331,9 @@ void exit_ptrace(struct task_struct *tra
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
Quoted 9 lines
Quoted 6 lines

Review void exit_ptrace(struct task_struct *tra · line 339

Quoted 6 lines
(quoted diff) +3 -0
+	if (list_empty(&tracer->ptraced))
+		return;
+
 	write_lock_irq(&tasklist_lock);
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))

I think we may have tried that before. Oleg can tell us if it's really safe vs a race with PTRACE_TRACEME or something like that.

Yes, this can race with ptrace_traceme(). Without tasklist_lock in exit_ptrace(), it is possible that ptrace_traceme() starts __ptrace_link() before it sees PF_EXITING, and completes before the result of list_add() is visible to the exiting parent. tasklist acts as a barrier.

So, this list_empty() check needs taskslit at least for reading. But, we are going to take it for writing right after exit_ptrace() returns, afaics we can add this fastpatch check for free.

Uncompiled/untested.

Oleg.

 kernel/ptrace.c |   10 +++++++---
 kernel/exit.c   |    3 ++-
 2 files changed, 9 insertions(+), 4 deletions(-)
kernel/ptrace.c +7 -3
@@ -324,26 +324,30 @@ int ptrace_detach(struct task_struct *ch
 }
 
 /*
- * Detach all tasks we were using ptrace on.
+ * Detach all tasks we were using ptrace on. Called with tasklist held.
  */
 void exit_ptrace(struct task_struct *tracer)
 {
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
-	write_lock_irq(&tasklist_lock);
+	if (likely(list_empty(&tracer->ptraced)))
+		return;
+
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))
 			list_add(&p->ptrace_entry, &ptrace_dead);
 	}
-	write_unlock_irq(&tasklist_lock);
 
+	write_unlock_irq(&tasklist_lock);
 	BUG_ON(!list_empty(&tracer->ptraced));
 
 	list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
 		list_del_init(&p->ptrace_entry);
 		release_task(p);
 	}
+
+	write_lock_irq(&tasklist_lock);
 }
 
 int ptrace_readdata(struct task_struct *tsk, unsigned long src, char __user *dst, int len)
kernel/exit.c +2 -1
@@ -771,9 +771,10 @@ static void forget_original_parent(struc
 	struct task_struct *p, *n, *reaper;
 	LIST_HEAD(dead_children);
 
+	write_lock_irq(&tasklist_lock);
+
 	exit_ptrace(father);
 
-	write_lock_irq(&tasklist_lock);
 	reaper = find_new_reaper(father);
 
 	list_for_each_entry_safe(p, n, &father->children, sibling) {
#5 lore raw
Quoted 3 lines

So, this list_empty() check needs taskslit at least for reading. But, we are going to take it for writing right after exit_ptrace() returns, afaics we can add this fastpatch check for free.

That looks good to me, but it could use some more scare comments.

Quoted 3 lines

/*
- * Detach all tasks we were using ptrace on.
+ * Detach all tasks we were using ptrace on. Called with tasklist held.

* Called with tasklist held for writing, and returns with it held too.
* But note it can release and reacquire the lock.

kernel/exit.c static void forget_original_parent(struc · line 775

Quoted 2 lines
kernel/exit.c +2 -0
+	write_lock_irq(&tasklist_lock);
+
	/*
	 * Note that exit_ptrace() might drop tasklist_lock and reacquire it.
	 */

kernel/exit.c static void forget_original_parent(struc · line 778

Quoted 4 lines
kernel/exit.c +0 -1
 	exit_ptrace(father);
 
-	write_lock_irq(&tasklist_lock);
 	reaper = find_new_reaper(father);

Thanks,
Roland

maintainer
#6 lore raw
Quoted 6 lines · Roland McGrath
Quoted 4 lines

So, this list_empty() check needs taskslit at least for reading. But, we are going to take it for writing right after exit_ptrace() returns, afaics we can add this fastpatch check for free.

That looks good to me, but it could use some more scare comments.

Good. Hopfully Zhang can test it to confirm it has the same effect. It should, but I am still wondering about 10% improvement.

Quoted 6 lines
Quoted 3 lines

/*
- * Detach all tasks we were using ptrace on.
+ * Detach all tasks we were using ptrace on. Called with tasklist held.

* Called with tasklist held for writing, and returns with it held too.
* But note it can release and reacquire the lock.

OK.

Quoted 6 lines

kernel/exit.c static void forget_original_parent(struc · line 775

Quoted 2 lines
kernel/exit.c +2 -0
+	write_lock_irq(&tasklist_lock);
+
	/*
	 * Note that exit_ptrace() might drop tasklist_lock and reacquire it.
	 */
Quoted 1 line

exit_ptrace(father);

Well, this comment a bit "unfair", please see below.

Quoted 2 lines

kernel/exit.c static void forget_original_parent(struc · line 778

Quoted 2 lines
kernel/exit.c +0 -1
-	write_lock_irq(&tasklist_lock);
 	reaper = find_new_reaper(father);

Note that find_new_reaper() can drop/reacquire tasklist too.

Perhaps,

	/* These two might drop and reacquire tasklist_lock */
	exit_ptrace(father);
	reaper = find_new_reaper(father);

	...

?

Oleg.

#7 lore raw
In reply to Oleg Nesterov
Quoted 2 lines · Oleg Nesterov

I am not surpized perf blaims tasklist, but I am really surpized this patch adds 10% improvement...

I changed aim7 workfile to focus on fork/exec and other a couple of sub-cases. And this behavior is clear on 8-socket machines.

Quoted 17 lines · Roland McGrath
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
kernel/ptrace.c +0 -0
@@ -331,6 +331,9 @@ void exit_ptrace(struct task_struct *tra
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
Quoted 9 lines
Quoted 6 lines

kernel/ptrace.c int ptrace_detach(struct task_struct *ch · line 338

Quoted 6 lines
kernel/ptrace.c +3 -0
+	if (list_empty(&tracer->ptraced))
+		return;
+
 	write_lock_irq(&tasklist_lock);
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))

I think we may have tried that before. Oleg can tell us if it's really safe vs a race with PTRACE_TRACEME or something like that.

Quoted 4 lines

Yes, this can race with ptrace_traceme(). Without tasklist_lock in exit_ptrace(), it is possible that ptrace_traceme() starts __ptrace_link() before it sees PF_EXITING, and completes before the result of list_add() is visible to the exiting parent. tasklist acts as a barrier.

Thanks for your kind explanation.

kernel/exit.c static void forget_original_parent(struc

Quoted 52 lines

So, this list_empty() check needs taskslit at least for reading. But, we are going to take it for writing right after exit_ptrace() returns, afaics we can add this fastpatch check for free.

Uncompiled/untested.

Oleg.

 kernel/ptrace.c |   10 +++++++---
 kernel/exit.c   |    3 ++-
 2 files changed, 9 insertions(+), 4 deletions(-)
kernel/ptrace.c +7 -3
@@ -324,26 +324,30 @@ int ptrace_detach(struct task_struct *ch
 }
 
 /*
- * Detach all tasks we were using ptrace on.
+ * Detach all tasks we were using ptrace on. Called with tasklist held.
  */
 void exit_ptrace(struct task_struct *tracer)
 {
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
-	write_lock_irq(&tasklist_lock);
+	if (likely(list_empty(&tracer->ptraced)))
+		return;
+
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))
 			list_add(&p->ptrace_entry, &ptrace_dead);
 	}
-	write_unlock_irq(&tasklist_lock);
 
+	write_unlock_irq(&tasklist_lock);
 	BUG_ON(!list_empty(&tracer->ptraced));
 
 	list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
 		list_del_init(&p->ptrace_entry);
 		release_task(p);
 	}
+
+	write_lock_irq(&tasklist_lock);
 }
 
 int ptrace_readdata(struct task_struct *tsk, unsigned long src, char __user *dst, int len)
kernel/exit.c +0 -0
@@ -771,9 +771,10 @@ static void forget_original_parent(struc

After applying my patch (although it's incorrect as there is a race with TRACEME), perf shows write_lock_irq in forget_original_parent consumes less than 40% cpu time on 8-socket machine.

Is it possible to optimize it to use finer locks instead of the global tasklist_lock?

Quoted 12 lines
kernel/exit.c +2 -1
 	struct task_struct *p, *n, *reaper;
 	LIST_HEAD(dead_children);
 
+	write_lock_irq(&tasklist_lock);
+
 	exit_ptrace(father);
 
-	write_lock_irq(&tasklist_lock);
 	reaper = find_new_reaper(father);
 
 	list_for_each_entry_safe(p, n, &father->children, sibling) {
maintainer
#8 lore raw
Quoted 6 lines · Yanmin
Quoted 2 lines · Oleg Nesterov

I am not surpized perf blaims tasklist, but I am really surpized this patch adds 10% improvement...

I changed aim7 workfile to focus on fork/exec and other a couple of sub-cases. And this behavior is clear on 8-socket machines.

Thanks...

Quoted 3 lines

After applying my patch (although it's incorrect as there is a race with TRACEME), perf shows write_lock_irq in forget_original_parent consumes less than 40% cpu time on 8-socket machine.

Any chance you can test the patch I sent? It should have the same effect, otherwise there is something interesting.

Quoted 1 line

Is it possible to optimize it to use finer locks instead of the global tasklist_lock?

Heh. We must optimize it. But it is not clear when ;)

Oleg.

#9 lore raw
Quoted 8 lines · Oleg Nesterov
Quoted 6 lines · Yanmin
Quoted 2 lines · Oleg Nesterov

I am not surpized perf blaims tasklist, but I am really surpized this patch adds 10% improvement...

I changed aim7 workfile to focus on fork/exec and other a couple of sub-cases. And this behavior is clear on 8-socket machines.

Quoted 8 lines

Thanks...

Quoted 3 lines

After applying my patch (although it's incorrect as there is a race with TRACEME), perf shows write_lock_irq in forget_original_parent consumes less than 40% cpu time on 8-socket machine.

Any chance you can test the patch I sent? It should have the same effect, otherwise there is something interesting.

1) with my patch, we got about 13% improvement;
2) With your patch, we got about 11% improvement;

Performance is very sensitive to spinlock contention on large machines.

Quoted 4 lines
Quoted 2 lines

Is it possible to optimize it to use finer locks instead of the global tasklist_lock?

Heh. We must optimize it. But it is not clear when ;)

Thanks. It's better to remove the big lock.

maintainer
#10 lore raw
Quoted 8 lines · Yanmin
Quoted 6 lines · Oleg Nesterov
Quoted 4 lines · Yanmin

After applying my patch (although it's incorrect as there is a race with TRACEME), perf shows write_lock_irq in forget_original_parent consumes less than 40% cpu time on 8-socket machine.

Quoted 6 lines
Quoted 2 lines

Any chance you can test the patch I sent? It should have the same effect, otherwise there is something interesting.

1) with my patch, we got about 13% improvement;
2) With your patch, we got about 11% improvement;

Performance is very sensitive to spinlock contention on large machines.

Zhang, thank you very much.

But. In this case I do not trust these results or I missed something. I mean, they do not look 100% accurate.

With your patch:

	forget_original_parent:

		exit_ptrace:
			if (list_empty(ptraced))
				return;


		write_lock_irq(tasklist);

		... do a lot more work ...

With my patch:

	forget_original_parent:

		write_lock_irq(tasklist);
	
		exit_ptrace:
			if (list_empty(ptraced))
				return;

		... do a lot more work ...

The only difference is that we are doing the function call + list_empty() under tasklist, just a few instructions compared to "do a lot more work" in forget_original_parent().

How this can make the 2% difference ? This looks like a noise to me, or do you think I missed something?

Quoted 2 lines
Quoted 1 line

Heh. We must optimize it. But it is not clear when ;)

Thanks. It's better to remove the big lock.

Yes. The only problem this is very much nontrival with the current code.

Oleg.

#11 lore raw
Quoted 8 lines

The only difference is that we are doing the function call + list_empty()
under tasklist, just a few instructions compared to "do a lot more work"
in forget_original_parent().

How this can make the 2% difference ? This looks like a noise to me, or do you think I missed something?

It could be a cache miss or something like that. Instructions are not all the same cost. Only detailed profiling with different performance counters could give you more information.

-Andi

#12 lore raw
In reply to Oleg Nesterov
Quoted 46 lines · Oleg Nesterov
Quoted 8 lines · Yanmin
Quoted 6 lines · Oleg Nesterov
Quoted 4 lines · Yanmin

After applying my patch (although it's incorrect as there is a race with TRACEME), perf shows write_lock_irq in forget_original_parent consumes less than 40% cpu time on 8-socket machine.

Quoted 6 lines
Quoted 2 lines

Any chance you can test the patch I sent? It should have the same effect, otherwise there is something interesting.

1) with my patch, we got about 13% improvement;
2) With your patch, we got about 11% improvement;

Performance is very sensitive to spinlock contention on large machines.

Zhang, thank you very much.

But. In this case I do not trust these results or I missed something. I mean, they do not look 100% accurate.

With your patch:

	forget_original_parent:

		exit_ptrace:
			if (list_empty(ptraced))
				return;


		write_lock_irq(tasklist);

		... do a lot more work ...

With my patch:

	forget_original_parent:

		write_lock_irq(tasklist);
	
		exit_ptrace:
			if (list_empty(ptraced))
				return;

		... do a lot more work ...
Quoted 3 lines

The only difference is that we are doing the function call + list_empty() under tasklist, just a few instructions compared to "do a lot more work" in forget_original_parent().

If considering lock acquire/release on a big machine, plus cache-misses like what Andi said, the result is reasonable. We did lots of testing on 8-socket machine. Performance result is very sensitive to lock contentions and cache-misses.

Quoted 2 lines

How this can make the 2% difference ?

I reran the testing for a couple of times to make sure the result is stable.

Quoted 2 lines

This looks like a noise to me,
or do you think I missed something?

No, you didn't miss anything. Any patch shouldn't introduce bugs, so your patch is right and good.

Quoted 5 lines
Quoted 3 lines
Quoted 2 lines

Heh. We must optimize it. But it is not clear when ;)

Thanks. It's better to remove the big lock.

Yes. The only problem this is very much nontrival with the current code.

I agree that would be a big project.

maintainer
#13 lore raw

PATCH 1/1 ptrace: optimize exit_ptrace() for the likely case

(replaces ptrace-dont-run-write_locktasklist_lock-if-the-parent-doesnt-ptrace-other-processes.patch)

exit_ptrace() takes tasklist_lock unconditionally. We need this lock to avoid the race with ptrace_traceme(), it acts as a barrier.

Change its caller, forget_original_parent(), to call exit_ptrace() under tasklist_lock. Change exit_ptrace() to drop and reacquire this lock if needed.

This allows us to add the fastpath list_empty(ptraced) check. In the likely no-tracees case exit_ptrace() just returns and we avoid the lock() + unlock() sequence.

"Zhang, Yanmin" <[email protected]> suggested to add this check, and he reports that this change adds about 11% improvement in some tests.

---

 kernel/ptrace.c |   12 +++++++++---
 kernel/exit.c   |    7 +++++--
 2 files changed, 14 insertions(+), 5 deletions(-)
kernel/ptrace.c renamed from kernel/ptrace.c~exit_ptrace_fastpath_check +9 -3
@@ -324,26 +324,32 @@ int ptrace_detach(struct task_struct *ch
 }
 
 /*
- * Detach all tasks we were using ptrace on.
+ * Detach all tasks we were using ptrace on. Called with tasklist held
+ * for writing, and returns with it held too. But note it can release
+ * and reacquire the lock.
  */
 void exit_ptrace(struct task_struct *tracer)
 {
 	struct task_struct *p, *n;
 	LIST_HEAD(ptrace_dead);
 
-	write_lock_irq(&tasklist_lock);
+	if (likely(list_empty(&tracer->ptraced)))
+		return;
+
 	list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
 		if (__ptrace_detach(tracer, p))
 			list_add(&p->ptrace_entry, &ptrace_dead);
 	}
-	write_unlock_irq(&tasklist_lock);
 
+	write_unlock_irq(&tasklist_lock);
 	BUG_ON(!list_empty(&tracer->ptraced));
 
 	list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
 		list_del_init(&p->ptrace_entry);
 		release_task(p);
 	}
+
+	write_lock_irq(&tasklist_lock);
 }
 
 int ptrace_readdata(struct task_struct *tsk, unsigned long src, char __user *dst, int len)
kernel/exit.c renamed from kernel/exit.c~exit_ptrace_fastpath_check +5 -2
@@ -771,9 +771,12 @@ static void forget_original_parent(struc
 	struct task_struct *p, *n, *reaper;
 	LIST_HEAD(dead_children);
 
-	exit_ptrace(father);
-
 	write_lock_irq(&tasklist_lock);
+	/*
+	 * Note that exit_ptrace() and find_new_reaper() might
+	 * drop tasklist_lock and reacquire it.
+	 */
+	exit_ptrace(father);
 	reaper = find_new_reaper(father);
 
 	list_for_each_entry_safe(p, n, &father->children, sibling) {