Allow resuming per-CPU arena selection via thread.arena (#2957)

* Allow resuming per-CPU arena selection via thread.arena

With percpu_arena enabled, thread.arena control is one-directional. A
thread can be bound to a manually created arena (an index at or above the
per-CPU auto range) to route a bounded region of work to a dedicated,
long-lived arena, but there is no way back: thread_arena_ctl returns EPERM
for any index within the auto range, and arena_choose_impl only re-selects
a per-CPU arena for threads whose current arena is already in that range.
So once a thread is bound to a manual arena it stays pinned there forever,
and its later allocations land there instead of following the CPU.

Treat setting thread.arena to an index within the per-CPU range as a
request to resume automatic per-CPU selection: hand the thread back to
percpu management (rebinding it to the current CPU's arena via
percpu_arena_update) and return 0 instead of EPERM. Binding to a manual
arena is unchanged. The requested index is advisory; under percpu the
thread is governed by its current CPU, so it resumes on the current CPU's
arena regardless of the value passed.

Add test/unit/percpu_arena_resume covering the manual-arena to resume
round trip with allocs & dallocs happening meanwhile, and update 
test_thread_arena, which asserted the old EPERM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raúl Marín 2026-07-16 23:53:25 +02:00 committed by GitHub
parent 8361239b03
commit 68c35f6557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 104 additions and 6 deletions

View file

@ -276,6 +276,7 @@ TESTS_UNIT := \
$(srcroot)test/unit/pack.c \
$(srcroot)test/unit/pages.c \
$(srcroot)test/unit/peak.c \
$(srcroot)test/unit/percpu_arena_resume.c \
$(srcroot)test/unit/ph.c \
$(srcroot)test/unit/prng.c \
$(srcroot)test/unit/prof_accum.c \

View file

@ -2374,10 +2374,18 @@ thread_arena_ctl(tsd_t *tsd, const size_t *mib, size_t miblen, void *oldp,
if (have_percpu_arena && PERCPU_ARENA_ENABLED(opt_percpu_arena)) {
if (newind < percpu_arena_ind_limit(opt_percpu_arena)) {
/*
* If perCPU arena is enabled, thread_arena control is
* not allowed for the auto arena range.
* Setting thread.arena to an arena in the auto range
* means "resume automatic per-CPU selection" rather than
* pinning to a specific per-CPU arena. This lets a caller
* that temporarily switched to a manually managed arena
* (e.g. a scoped guard) hand the thread back to per-CPU
* management. It is otherwise impossible: a thread bound
* to a manual arena is never reclaimed by percpu (see
* arena_choose_impl), so without this it would stay
* pinned forever.
*/
return EPERM;
percpu_arena_update(tsd, percpu_arena_choose());
return 0;
}
}

View file

@ -764,12 +764,17 @@ TEST_BEGIN(test_thread_arena) {
0, "Unexpected mallctl() failure");
new_arena_ind = percpu_arena_ind_limit(opt_percpu_arena) - 1;
if (old_arena_ind != new_arena_ind) {
/*
* Setting thread.arena to an index within the per-CPU
* range resumes automatic per-CPU selection rather than
* failing (see test/unit/percpu_arena_resume.c).
*/
expect_d_eq(
mallctl("thread.arena", (void *)&old_arena_ind, &sz,
(void *)&new_arena_ind, sizeof(unsigned)),
EPERM,
"thread.arena ctl "
"should not be allowed with percpu arena");
0,
"thread.arena within the per-CPU range should "
"resume per-CPU selection");
}
}
}

View file

@ -0,0 +1,81 @@
#include "test/jemalloc_test.h"
/*
* Under percpu_arena, binding a thread to a manual arena (an index at or above
* the per-CPU auto range) is one-way: percpu never reclaims it (see
* arena_choose_impl). Setting thread.arena back to an index in the auto range
* resumes per-CPU selection instead of failing with EPERM.
*/
TEST_BEGIN(test_thread_arena_resume_percpu) {
test_skip_if(!have_percpu_arena
|| !PERCPU_ARENA_ENABLED(opt_percpu_arena));
unsigned limit = percpu_arena_ind_limit(opt_percpu_arena);
/* Bypass the tcache so every allocation and free hits the arena. */
const int flags = MALLOCX_TCACHE_NONE;
void *warm = mallocx(1, 0);
expect_ptr_not_null(warm, "Unexpected mallocx() failure");
dallocx(warm, 0);
unsigned cur;
size_t sz = sizeof(cur);
expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0,
"Unexpected mallctl() failure");
expect_u_lt(cur, limit, "Thread should start on a per-CPU arena");
unsigned manual;
sz = sizeof(manual);
expect_d_eq(mallctl("arenas.create", (void *)&manual, &sz, NULL, 0), 0,
"Unexpected arenas.create() failure");
expect_u_ge(manual, limit, "A manual arena is outside the per-CPU range");
unsigned old;
sz = sizeof(old);
expect_d_eq(mallctl("thread.arena", (void *)&old, &sz, (void *)&manual,
sizeof(manual)), 0, "Binding to a manual arena should be allowed");
sz = sizeof(cur);
expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0,
"Unexpected mallctl() failure");
expect_u_eq(cur, manual, "Thread should be bound to the manual arena");
void *p_manual = mallocx(1024, flags);
expect_ptr_not_null(p_manual, "Unexpected mallocx() failure");
unsigned found;
sz = sizeof(found);
expect_d_eq(mallctl("arenas.lookup", (void *)&found, &sz,
(void *)&p_manual, sizeof(p_manual)), 0,
"Unexpected arenas.lookup() failure");
expect_u_eq(found, manual, "Allocation should come from the manual arena");
void *scratch = mallocx(1024, flags);
expect_ptr_not_null(scratch, "Unexpected mallocx() failure");
dallocx(scratch, flags);
unsigned resume = 0;
expect_d_eq(mallctl("thread.arena", NULL, NULL, (void *)&resume,
sizeof(resume)), 0, "Should resume per-CPU selection, not fail");
sz = sizeof(cur);
expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0,
"Unexpected mallctl() failure");
expect_u_lt(cur, limit, "Thread should be back on a per-CPU arena");
void *p_percpu = mallocx(1024, flags);
expect_ptr_not_null(p_percpu, "Unexpected mallocx() failure");
sz = sizeof(found);
expect_d_eq(mallctl("arenas.lookup", (void *)&found, &sz,
(void *)&p_percpu, sizeof(p_percpu)), 0,
"Unexpected arenas.lookup() failure");
expect_u_lt(found, limit, "Allocation should come from a per-CPU arena");
dallocx(p_percpu, flags);
/* Free the manual-arena region while bound to a different arena. */
dallocx(p_manual, flags);
}
TEST_END
int
main(void) {
return test(
test_thread_arena_resume_percpu);
}

View file

@ -0,0 +1,3 @@
#!/bin/sh
export MALLOC_CONF="percpu_arena:percpu"