cpuhotplug: make get_online_cpus() scalability by using percpu counter
Subsystems (4)
PATCH 2/2 cpuhotplug: make get_online_cpus() scalability by using percpu counter
Current get_online_cpus() acquires a mutex lock and then release it. It is not scale and it hurts cache. This patch rewrite it.
1) get_online_cpus() must be allowed to be called recursively, so I added get_online_cpus_nest for every task for new code.
This patch just allows get_online_cpus() to be called recursively, but when it is not nested, get_online_cpus() will wait until cpuhotplug finished, so the potential starvation is avoided.
And, the livelock of cpu_hotplug_begin() is avoided, so the comment is removed.
2) This new code use PER_CPU counters, and this counters protected by RCU. These counters acts like the reference counters of a modules. (Actually, all these code is stolen from module.c: try_refcount_get() is stolen from try_module_get(), put_online_cpus() from module_put()...)
After this patch applied, get_online_cpus() is very light and scale when cpuhotplug is not running. It just disables preemption and increase the cpu counter and then enables preemption.
3) Since we have try_refcount_get(), I add a new API try_get_online_cpus().
--- include/linux/cpu.h | 2 include/linux/sched.h | 3 + kernel/cpu.c | 131 ++++++++++++++++++++++++++++++++------------------ kernel/fork.c | 3 + 4 files changed, 94 insertions(+), 45 deletions(-)
include/linux/cpu.h +2 -0
@@ -112,6 +112,7 @@ extern struct sysdev_class cpu_sysdev_class;extern void get_online_cpus (void );extern void put_online_cpus (void ); +extern int try_get_online_cpus (void );#define hotcpu_notifier (fn, pri) cpu_notifier(fn, pri)#define register_hotcpu_notifier (nb) register_cpu_notifier(nb)#define unregister_hotcpu_notifier (nb) unregister_cpu_notifier(nb)
@@ -134,6 +135,7 @@ static inline void cpu_hotplug_driver_unlock(void)#define get_online_cpus () do { } while (0)#define put_online_cpus () do { } while (0) +#define try_get_online_cpus () (1)#define hotcpu_notifier (fn, pri) do { (void)(fn); } while (0)/* These aren't inline functions due to a GCC bug. */ #define register_hotcpu_notifier (nb) ({ (void)(nb); 0; })
include/linux/sched.h +3 -0
@@ -1501,6 +1501,9 @@ struct task_struct { unsigned long memsw_bytes; /* uncharged mem+swap usage */ } memcg_batch;#endif +#ifdef CONFIG_HOTPLUG_CPU +int get_online_cpus_nest; +#endif };/* Future-safe accessor for struct task_struct's cpus_allowed. */
kernel/cpu.c +86 -45
174 lines not on this page · raw message
kernel/fork.c +3 -0
@@ -1109,6 +1109,9 @@ static struct task_struct *copy_process(unsigned long clone_flags, p->memcg_batch.memcg =NULL ;#endif p->stack_start = stack_start; +#ifdef CONFIG_HOTPLUG_CPU + p->get_online_cpus_nest =0 ; +#endif /* Perform scheduler related setup. Assign this task to a CPU. */ sched_fork (p, clone_flags);
Quoted 3 lines · Lai Jiangshan
1)
get_online_cpus()must be allowed to be called recursively, so I addedget_online_cpus_nestfor every task for new code.
Well, iirc one of the goals of
cpu-hotplug: replace lock_cpu_hotplug() with get_online_cpus() 86ef5c9a8edd78e6bf92879f32329d89b2d55b5a
was avoiding the new members in task_struct. I leave this up to you and Gautham.
Lai, I didn't read this patch carefully yet (and I can't apply it to Linus's tree). But at first glance,
kernel/cpu.c static int cpu_hotplug_disabled; · line 119
Quoted 8 lines
void
put_online_cpus(void)
{
...kernel/cpu.c +5
-0+if (!--current->get_online_cpus_nest) { +preempt_disable (); +__get_cpu_var (refcount)--; +if (cpu_hotplug_task) +wake_up_process (cpu_hotplug_task);
This looks unsafe. In theory nothing protects cpu_hotplug_task from exiting if refcount_sum() becomes zero, this means wake_up_process() can hit the freed/reused/unmapped task_struct. Probably cpu_hotplug_done() needs another synhronize_sched() before return.
OTOH, I do not understand why the result of __get_cpu_var(refcount) must be visible to refcount_sum() if we race with cpu_hotplug_begin(), so it seems to me cpu_hotplug_begin() also needs synchronize_sched() before refcount_sum().
Oleg.
Quoted 11 lines · Oleg Nesterov
Quoted 3 lines · Lai Jiangshan
1)
get_online_cpus()must be allowed to be called recursively, so I addedget_online_cpus_nestfor every task for new code.Well, iirc one of the goals of
cpu-hotplug: replace lock_cpu_hotplug() with get_online_cpus() 86ef5c9a8edd78e6bf92879f32329d89b2d55b5a
Quoted 6 lines
was avoiding the new members in
task_struct. I leave this up to you and Gautham.Lai, I didn't read this patch carefully yet (and I can't apply it to Linus's tree). But at first glance,
because I tried to apply it without 1/2 ;)
Quoted 5 lines
Quoted 5 lines
void
put_online_cpus(void)
{
...kernel/cpu.c +2
-0+if (!--current->get_online_cpus_nest) { +preempt_disable ();
Quoted 8 lines
kernel/cpu.c static int cpu_hotplug_disabled; · line 119
Quoted 3 lines
kernel/cpu.c +3
-0+__get_cpu_var (refcount)--; +if (cpu_hotplug_task) +wake_up_process (cpu_hotplug_task);This looks unsafe. In theory nothing protects
cpu_hotplug_taskfrom exiting ifrefcount_sum()becomes zero, this meanswake_up_process()can hit the freed/reused/unmappedtask_struct. Probablycpu_hotplug_done()needs anothersynhronize_sched()before return.
Yes, I think this is true, at least in theory.
Quoted 4 lines
OTOH, I do not understand why the result of
__get_cpu_var(refcount)must be visible torefcount_sum()if we race withcpu_hotplug_begin(), so it seems to mecpu_hotplug_begin()also needssynchronize_sched()beforerefcount_sum().
No, I misread the unapplied patch, sorry for noise.
Oleg.
Quoted 11 lines · Oleg Nesterov
Quoted 10 lines · Oleg Nesterov
Quoted 2 lines · Lai Jiangshan
1)
get_online_cpus()must be allowed to be called recursively, so I addedget_online_cpus_nestfor every task for new code.Well, iirc one of the goals of
cpu-hotplug: replace lock_cpu_hotplug() with get_online_cpus() 86ef5c9a8edd78e6bf92879f32329d89b2d55b5awas avoiding the new members in
task_struct. I leave this up to you and Gautham.
Old get_online_cpus() is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.
But read-preference RWL may cause write side starvation, so I abandon this ability, and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
Quoted 20 lines
Quoted 4 lines
Lai, I didn't read this patch carefully yet (and I can't apply it to Linus's tree). But at first glance,
because I tried to apply it without 1/2 ;)
Quoted 4 lines
Quoted 4 lines
void
put_online_cpus(void)
{
...kernel/cpu.c +1
-0+if (!--current->get_online_cpus_nest) {Quoted 8 lines
kernel/cpu.c static int cpu_hotplug_disabled; · line 119
Quoted 4 lines
kernel/cpu.c +4
-0+preempt_disable (); +__get_cpu_var (refcount)--; +if (cpu_hotplug_task) +wake_up_process (cpu_hotplug_task);This looks unsafe. In theory nothing protects
cpu_hotplug_taskfrom exiting ifrefcount_sum()becomes zero, this meanswake_up_process()can hit the freed/reused/unmappedtask_struct. Probablycpu_hotplug_done()needs anothersynhronize_sched()before return.
Quoted 1 line
Yes, I think this is true, at least in theory.
preempt_disable() prevent cpu_hotplug_task from exiting.
Thanks, Lai
Quoted 8 lines · Lai Jiangshan
Quoted 6 lines · Oleg Nesterov
Quoted 5 lines · Oleg Nesterov
Quoted 2 lines · Lai Jiangshan
1)
get_online_cpus()must be allowed to be called recursively, so I addedget_online_cpus_nestfor every task for new code.Well, iirc one of the goals of
Quoted 8 lines
Quoted 5 lines
Quoted 5 lines
cpu-hotplug: replace lock_cpu_hotplug() with get_online_cpus() 86ef5c9a8edd78e6bf92879f32329d89b2d55b5awas avoiding the new members in
task_struct. I leave this up to you and Gautham.Old
get_online_cpus()is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.
Sure, I understand why you added task_struct->get_online_cpus_nest.
Quoted 2 lines
and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
As I said, I am not going to argue. I can't justify this tradeoff.
Quoted 13 lines
Quoted 13 lines
Quoted 12 lines
kernel/cpu.c static int cpu_hotplug_disabled; · line 119
Quoted 8 lines
void
put_online_cpus(void)
{
...kernel/cpu.c +5
-0+if (!--current->get_online_cpus_nest) { +preempt_disable (); +__get_cpu_var (refcount)--; +if (cpu_hotplug_task) +wake_up_process (cpu_hotplug_task);This looks unsafe. In theory nothing protects
cpu_hotplug_taskfrom exiting ifrefcount_sum()becomes zero, this meanswake_up_process()can hit the freed/reused/unmappedtask_struct. Probablycpu_hotplug_done()needs anothersynhronize_sched()before return.
Quoted 3 lines
Quoted 1 line
Yes, I think this is true, at least in theory.
preempt_disable()preventcpu_hotplug_taskfrom exiting.
If the cpu_down() is the caller of cpu_hotplug_begin/done, then yes.
But unless I missed something, nothing protects from cpu_up() which takes this lock too.
Just in case... I am not saying this is really possible in practice.
Oleg.
Quoted 6 lines · Oleg Nesterov
Quoted 3 lines · Lai Jiangshan
Old
get_online_cpus()is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.
Quoted 6 lines
Sure, I understand why you added
task_struct->get_online_cpus_nest.Quoted 2 lines
and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
As I said, I am not going to argue. I can't justify this tradeoff.
But, I must admit, I'd like to avoid adding the new member to task_struct.
What do you think about the code below?
I didn't even try to compile it, just to explain what I mean.
In short: we have the per-cpu fast counters, plus the slow counter which is only used when cpu_hotplug_begin() is in progress.
Oleg.
static DEFINE_PER_CPU(long, cpuhp_fast_ctr);
static struct task_struct *cpuhp_writer;
static DEFINE_MUTEX(cpuhp_slow_lock)
static long cpuhp_slow_ctr;
static bool update_fast_ctr(int inc)
{
bool success = true;
preempt_disable();
if (likely(!cpuhp_writer))
__get_cpu_var(cpuhp_fast_ctr) += inc;
else if (cpuhp_writer != current)
success = false;
preempt_enable();
return success;
}void get_online_cpus(void)
{
if (likely(update_fast_ctr(+1));
return;
mutex_lock(&cpuhp_slow_lock);
cpuhp_slow_ctr++;
mutex_unlock(&cpuhp_slow_lock);
}void put_online_cpus(void)
{
if (likely(update_fast_ctr(-1));
return;
mutex_lock(&cpuhp_slow_lock);
if (!--cpuhp_slow_ctr && cpuhp_writer)
wake_up_process(cpuhp_writer);
mutex_unlock(&cpuhp_slow_lock);
}static void clear_fast_ctr(void)
{
long total = 0;
int cpu;
for_each_possible_cpu(cpu) {
total += per_cpu(cpuhp_fast_ctr, cpu);
per_cpu(cpuhp_fast_ctr, cpu) = 0;
}
return total;
}static void cpu_hotplug_begin(void)
{
cpuhp_writer = current;
synchronize_sched();
/* Nobody except us can use can use cpuhp_fast_ctr */
mutex_lock(&cpuhp_slow_lock);
cpuhp_slow_ctr += clear_fast_ctr();
while (cpuhp_slow_ctr) {
__set_current_state(TASK_UNINTERRUPTIBLE);
mutex_unlock(&&cpuhp_slow_lock);
schedule();
mutex_lock(&cpuhp_slow_lock);
}
}static void cpu_hotplug_done(void)
{
cpuhp_writer = NULL;
mutex_unlock(&cpuhp_slow_lock);
}Quoted 12 lines · Oleg Nesterov
Quoted 8 lines · Oleg Nesterov
Quoted 2 lines · Lai Jiangshan
Old
get_online_cpus()is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.Sure, I understand why you added
task_struct->get_online_cpus_nest.Quoted 2 lines
and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
As I said, I am not going to argue. I can't justify this tradeoff.
But, I must admit, I'd like to avoid adding the new member to
task_struct.
Quoted 9 lines
What do you think about the code below?
I didn't even try to compile it, just to explain what I mean.
In short: we have the per-cpu fast counters, plus the slow counter which is only used when
cpu_hotplug_begin()is in progress.Oleg.
get_online_cpus() in your code is still read-preference. I wish we quit this ability of get_online_cpus().
Lai.
Peter Zijlstra
maintainerQuoted 17 lines · Lai Jiangshan
Quoted 16 lines · Oleg Nesterov
Quoted 8 lines · Oleg Nesterov
Quoted 2 lines · Lai Jiangshan
Old
get_online_cpus()is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.Sure, I understand why you added
task_struct->get_online_cpus_nest.Quoted 2 lines
and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
As I said, I am not going to argue. I can't justify this tradeoff.
But, I must admit, I'd like to avoid adding the new member to
task_struct.What do you think about the code below?
I didn't even try to compile it, just to explain what I mean.
Quoted 8 lines
Quoted 5 lines
In short: we have the per-cpu fast counters, plus the slow counter which is only used when
cpu_hotplug_begin()is in progress.Oleg.
get_online_cpus()in your code is still read-preference. I wish we quit this ability ofget_online_cpus().
Why?
Quoted 20 lines · Peter Zijlstra
Quoted 19 lines · Lai Jiangshan
Quoted 18 lines · Oleg Nesterov
Quoted 8 lines · Oleg Nesterov
Quoted 2 lines · Lai Jiangshan
Old
get_online_cpus()is read-preference, I think the goal of this ability is allow get_online_cpus()/put_online_cpus() to be called nested.Sure, I understand why you added
task_struct->get_online_cpus_nest.Quoted 2 lines
and use per-task counter for allowing get_online_cpus()/put_online_cpus() to be called nested, I think this deal is absolutely worth.
As I said, I am not going to argue. I can't justify this tradeoff.
But, I must admit, I'd like to avoid adding the new member to
task_struct.What do you think about the code below?
I didn't even try to compile it, just to explain what I mean.
In short: we have the per-cpu fast counters, plus the slow counter which is only used when
cpu_hotplug_begin()is in progress.
Quoted 6 lines
Quoted 4 lines
Quoted 2 lines
Oleg.
get_online_cpus()in your code is still read-preference. I wish we quit this ability ofget_online_cpus().Why?
Because read-preference RWL will cause write site starvation.
A user run the following code will cause cpuhotplug starvation. (100 processes run sched_setaffinity().)
Lai
#define _GNU_SOURCE
#include <sched.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define NCPU 4
#define NPROCESS 100
cpu_set_t set;pid_t target;
void stress_test(void)
{
int cpu;
srand((int)target);
for (;;) {
cpu = rand() % NCPU;
CPU_SET(cpu, &set);
sched_setaffinity(target, sizeof(set), &set);
CPU_CLR(cpu, &set);
}
}int main(int argc, char *argv[])
{
pid_t ret;
int i;
target = getpid();
for (i = 1; i < NPROCESS; i++) {
ret = fork();
if (ret < 0)
break;
else if (ret)
target = ret;
else
stress_test();
}
stress_test();
}Peter Zijlstra
maintainerQuoted 1 line · Lai Jiangshan
Because read-preference RWL will cause write site starvation.
Sure, but why is that a problem? Hotplug should be a rare event, who cares if it takes a while to come through.
Quoted 7 lines · Peter Zijlstra
Quoted 1 line · Lai Jiangshan
Because read-preference RWL will cause write site starvation.
Sure, but why is that a problem? Hotplug should be a rare event,
who cares if it takes a while to come through.
It is totally starvation, hotplug can not complete. I think it is a kind of DOS attack.
Quoted 5 lines · Lai Jiangshan
Quoted 3 lines · Oleg Nesterov
But, I must admit, I'd like to avoid adding the new member to
task_struct.
Quoted 8 lines
Quoted 6 lines
What do you think about the code below?
I didn't even try to compile it, just to explain what I mean.
In short: we have the per-cpu fast counters, plus the slow counter which is only used when
cpu_hotplug_begin()is in progress.
get_online_cpus()in your code is still read-preference.
Yes,
Quoted 1 line
I wish we quit this ability of
get_online_cpus().
Heh. Since I never read the changelogs, I didn't even notice this was one of the goals of your patch. I thought this is just the side-effect.
Yes, if we want to block the new readers, I don't see how it is possible to avoid the counter in task_struct.
I can't judge whether this new member worth the trouble. Once again, I am not arguing, just I don't know. And I think your patch is correct (apart from pure theoretical race with cpu_hotplug_done afaics).
Oleg.