From dab74b3a6ab8ddf9a468b292432277e2a49daaba Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Wed, 15 Apr 2026 00:30:09 +0200 Subject: [PATCH 1/5] [release/10.0] Support hardware with more than 1024 CPUs Backport of #126763 to release/10.0 Customer Impact - [x] Customer reported - [ ] Found internally A customer has reported that .NET runtime fails to initialize on machines that have more than 1024 CPUs due to sched_getaffinity being passed the default instance of cpu_set_t that supports max 1024 CPUs and fails if there are more CPUs on the current machine. This happens even when .NET is running in a container limited to a small number of CPUs on such machine. This change fixes sched_getaffinity calls to use a dynamically allocated CPU set data structure so that it can support any number of CPUs. Regression - [ ] Yes - [x] No Testing CI tests, local manual debugging, Risk --- src/coreclr/gc/env/gcenv.os.h | 58 ++++++++-- src/coreclr/gc/gc.cpp | 109 +++++++++++------- src/coreclr/gc/gcconfig.cpp | 3 +- src/coreclr/gc/gcpriv.h | 2 +- src/coreclr/gc/unix/gcenv.unix.cpp | 87 ++++++++++---- src/coreclr/gc/windows/gcenv.windows.cpp | 16 ++- .../nativeaot/Runtime/unix/PalUnix.cpp | 39 ++++++- src/coreclr/pal/src/misc/sysinfo.cpp | 43 ++++++- src/coreclr/pal/src/thread/thread.cpp | 52 ++++++--- 9 files changed, 302 insertions(+), 107 deletions(-) diff --git a/src/coreclr/gc/env/gcenv.os.h b/src/coreclr/gc/env/gcenv.os.h index 08e9b39e36eb1a..e9b3cce3f03bd9 100644 --- a/src/coreclr/gc/env/gcenv.os.h +++ b/src/coreclr/gc/env/gcenv.os.h @@ -6,6 +6,9 @@ #ifndef __GCENV_OS_H__ #define __GCENV_OS_H__ +#include +using std::nothrow; + #include #define NUMA_NODE_UNDEFINED UINT16_MAX @@ -138,12 +141,12 @@ class GCEvent { typedef void (*GCThreadFunction)(void* param); #ifdef HOST_64BIT -// Right now we support maximum 1024 procs - meaning that we will create at most +// Right now we support maximum 1024 heaps - meaning that we will create at most // that many GC threads and GC heaps. -#define MAX_SUPPORTED_CPUS 1024 +#define MAX_SUPPORTED_HEAPS 1024 #define MAX_SUPPORTED_NODES 64 #else -#define MAX_SUPPORTED_CPUS 64 +#define MAX_SUPPORTED_HEAPS 64 #define MAX_SUPPORTED_NODES 16 #endif // HOST_64BIT @@ -152,7 +155,8 @@ class AffinitySet { static const size_t BitsPerBitsetEntry = 8 * sizeof(uintptr_t); - uintptr_t m_bitset[MAX_SUPPORTED_CPUS / BitsPerBitsetEntry]; + uintptr_t *m_bitset = nullptr; + size_t m_bitsetDataSize = 0; static uintptr_t GetBitsetEntryMask(size_t cpuIndex) { @@ -166,11 +170,31 @@ class AffinitySet public: - static const size_t BitsetDataSize = MAX_SUPPORTED_CPUS / BitsPerBitsetEntry; + // Delete copy and move constructors and assignment operators since this class manages a raw pointer. + AffinitySet() = default; + AffinitySet(const AffinitySet&) = delete; + AffinitySet& operator=(const AffinitySet&) = delete; + AffinitySet(AffinitySet&&) = delete; + AffinitySet& operator=(AffinitySet&&) = delete; + + bool Initialize(int cpuCount) + { + assert(m_bitset == nullptr); + + m_bitsetDataSize = (cpuCount + BitsPerBitsetEntry - 1) / BitsPerBitsetEntry; + m_bitset = new (nothrow) uintptr_t[m_bitsetDataSize]; + if (m_bitset == nullptr) + { + return false; + } + + memset(m_bitset, 0, sizeof(uintptr_t) * m_bitsetDataSize); + return true; + } - AffinitySet() + ~AffinitySet() { - memset(m_bitset, 0, sizeof(m_bitset)); + delete[] m_bitset; } uintptr_t* GetBitsetData() @@ -181,25 +205,28 @@ class AffinitySet // Check if the set contains a processor bool Contains(size_t cpuIndex) const { + assert(GetBitsetEntryIndex(cpuIndex) < m_bitsetDataSize); return (m_bitset[GetBitsetEntryIndex(cpuIndex)] & GetBitsetEntryMask(cpuIndex)) != 0; } // Add a processor to the set void Add(size_t cpuIndex) { + assert(GetBitsetEntryIndex(cpuIndex) < m_bitsetDataSize); m_bitset[GetBitsetEntryIndex(cpuIndex)] |= GetBitsetEntryMask(cpuIndex); } // Remove a processor from the set void Remove(size_t cpuIndex) { + assert(GetBitsetEntryIndex(cpuIndex) < m_bitsetDataSize); m_bitset[GetBitsetEntryIndex(cpuIndex)] &= ~GetBitsetEntryMask(cpuIndex); } // Check if the set is empty bool IsEmpty() const { - for (size_t i = 0; i < MAX_SUPPORTED_CPUS / BitsPerBitsetEntry; i++) + for (size_t i = 0; i < m_bitsetDataSize; i++) { if (m_bitset[i] != 0) { @@ -210,11 +237,17 @@ class AffinitySet return true; } + // Return the capacity of the affinity set (maximum number of processor indices it can hold) + size_t MaxCpuCount() const + { + return m_bitsetDataSize * BitsPerBitsetEntry; + } + // Return number of processors in the affinity set size_t Count() const { size_t count = 0; - for (size_t i = 0; i < MAX_SUPPORTED_CPUS; i++) + for (size_t i = 0; i < m_bitsetDataSize * BitsPerBitsetEntry; i++) { if (Contains(i)) { @@ -482,6 +515,13 @@ class GCToOSInterface // Number of processors on the machine static uint32_t GetTotalProcessorCount(); + // Gets the maximum number of processors that could potentially exist on + // the machine (including offlined ones). Processor indices returned by + // GetCurrentProcessorNumber are guaranteed to be less than this value. + // Return: + // Maximum number of processors + static uint32_t GetMaxProcessorCount(); + // Is NUMA support available static bool CanEnableGCNumaAware(); diff --git a/src/coreclr/gc/gc.cpp b/src/coreclr/gc/gc.cpp index 4c3fcd1b6c03fd..d32d06841596dd 100644 --- a/src/coreclr/gc/gc.cpp +++ b/src/coreclr/gc/gc.cpp @@ -744,7 +744,7 @@ class t_join gc_join_flavor flavor; #ifdef JOIN_STATS - uint64_t start[MAX_SUPPORTED_CPUS], end[MAX_SUPPORTED_CPUS], start_seq; + uint64_t start[MAX_SUPPORTED_HEAPS], end[MAX_SUPPORTED_HEAPS], start_seq; // remember join id and last thread to arrive so restart can use these int thd; // we want to print statistics every 10 seconds - this is to remember the start of the 10 sec interval @@ -5397,7 +5397,7 @@ BOOL gc_heap::reserve_initial_memory (size_t normal_size, size_t large_size, siz highest_numa_node = max (highest_numa_node, heap_numa_node); } - assert (highest_numa_node < MAX_SUPPORTED_CPUS); + assert (highest_numa_node < MAX_SUPPORTED_HEAPS); numa_node_count = highest_numa_node + 1; memory_details.numa_reserved_block_count = numa_node_count * (1 + separated_poh_p); @@ -6383,10 +6383,10 @@ class heap_select static unsigned n_sniff_buffers; static unsigned cur_sniff_index; - static uint16_t proc_no_to_heap_no[MAX_SUPPORTED_CPUS]; - static uint16_t heap_no_to_proc_no[MAX_SUPPORTED_CPUS]; - static uint16_t heap_no_to_numa_node[MAX_SUPPORTED_CPUS]; - static uint16_t numa_node_to_heap_map[MAX_SUPPORTED_CPUS+4]; + static uint16_t *proc_no_to_heap_no; + static uint16_t heap_no_to_proc_no[MAX_SUPPORTED_HEAPS]; + static uint16_t heap_no_to_numa_node[MAX_SUPPORTED_HEAPS]; + static uint16_t *numa_node_to_heap_map; #ifdef HEAP_BALANCE_INSTRUMENTATION // Note this is the total numa nodes GC heaps are on. There might be @@ -6410,6 +6410,16 @@ class heap_select static BOOL init(int n_heaps) { assert (sniff_buffer == NULL && n_sniff_buffers == 0); + + uint32_t maxCpuCount = GCToOSInterface::GetMaxProcessorCount(); + proc_no_to_heap_no = new (nothrow) uint16_t[maxCpuCount]; + if (proc_no_to_heap_no == NULL) + { + return FALSE; + } + + memset(proc_no_to_heap_no, 0, maxCpuCount*sizeof(uint16_t)); + if (!GCToOSInterface::CanGetCurrentProcessorNumber()) { n_sniff_buffers = n_heaps*2+1; @@ -6436,15 +6446,15 @@ class heap_select // 2. assign heap numbers for each numa node // Pass 1: gather processor numbers and numa node numbers - uint16_t proc_no[MAX_SUPPORTED_CPUS]; - uint16_t node_no[MAX_SUPPORTED_CPUS]; + uint16_t proc_no[MAX_SUPPORTED_HEAPS]; + uint16_t node_no[MAX_SUPPORTED_HEAPS]; uint16_t max_node_no = 0; uint16_t heap_num; for (heap_num = 0; heap_num < n_heaps; heap_num++) { if (!GCToOSInterface::GetProcessorForHeap (heap_num, &proc_no[heap_num], &node_no[heap_num])) break; - assert(proc_no[heap_num] < MAX_SUPPORTED_CPUS); + assert(proc_no[heap_num] < GCToOSInterface::GetMaxProcessorCount()); if (!do_numa || node_no[heap_num] == NUMA_NODE_UNDEFINED) node_no[heap_num] = 0; max_node_no = max(max_node_no, node_no[heap_num]); @@ -6475,11 +6485,7 @@ class heap_select if (GCToOSInterface::CanGetCurrentProcessorNumber()) { uint32_t proc_no = GCToOSInterface::GetCurrentProcessorNumber(); - // For a 32-bit process running on a machine with > 64 procs, - // even though the process can only use up to 32 procs, the processor - // index can be >= 64; or in the cpu group case, if the process is not running in cpu group #0, - // the GetCurrentProcessorNumber will return a number that's >= 64. - proc_no_to_heap_no[proc_no % MAX_SUPPORTED_CPUS] = (uint16_t)heap_number; + proc_no_to_heap_no[proc_no] = (uint16_t)heap_number; } } @@ -6501,11 +6507,7 @@ class heap_select if (GCToOSInterface::CanGetCurrentProcessorNumber()) { uint32_t proc_no = GCToOSInterface::GetCurrentProcessorNumber(); - // For a 32-bit process running on a machine with > 64 procs, - // even though the process can only use up to 32 procs, the processor - // index can be >= 64; or in the cpu group case, if the process is not running in cpu group #0, - // the GetCurrentProcessorNumber will return a number that's >= 64. - int adjusted_heap = proc_no_to_heap_no[proc_no % MAX_SUPPORTED_CPUS]; + int adjusted_heap = proc_no_to_heap_no[proc_no]; // with dynamic heap count, need to make sure the value is in range. if (adjusted_heap >= gc_heap::n_heaps) { @@ -6567,9 +6569,21 @@ class heap_select return heap_no_to_numa_node[heap_number]; } - static void init_numa_node_to_heap_map(int nheaps) + static bool init_numa_node_to_heap_map(int nheaps) { // Called right after GCHeap::Init() for each heap + + uint32_t maxCpuCount = GCToOSInterface::GetMaxProcessorCount(); + // The upper limit of the numa node numbers is maxCpuCount - 1 since in the worst case each processor could be on a different NUMA node. + // We add +1 here to make it easier to calculate the heap number range for the last NUMA node. + numa_node_to_heap_map = new (nothrow) uint16_t[maxCpuCount + 1]; + if (numa_node_to_heap_map == nullptr) + { + return false; + } + + memset(numa_node_to_heap_map, 0, (maxCpuCount + 1)*sizeof(uint16_t)); + // For each NUMA node used by the heaps, the // numa_node_to_heap_map[numa_node] is set to the first heap number on that node and // numa_node_to_heap_map[numa_node + 1] is set to the first heap number not on that node @@ -6590,7 +6604,8 @@ class heap_select total_numa_nodes++; heaps_on_node[total_numa_nodes].node_no = heap_no_to_numa_node[i]; #endif - + assert(heap_no_to_numa_node[i-1] < maxCpuCount); + assert(heap_no_to_numa_node[i] < maxCpuCount); // Set the end of the heap number range for the previous NUMA node numa_node_to_heap_map[heap_no_to_numa_node[i-1] + 1] = // Set the start of the heap number range for the current NUMA node @@ -6601,12 +6616,14 @@ class heap_select #endif } + assert(heap_no_to_numa_node[nheaps-1] < maxCpuCount); // Set the end of the heap range for the last NUMA node numa_node_to_heap_map[heap_no_to_numa_node[nheaps-1] + 1] = (uint16_t)nheaps; //mark the end with nheaps #ifdef HEAP_BALANCE_INSTRUMENTATION total_numa_nodes++; #endif + return true; } static bool get_info_proc (int index, uint16_t* proc_no, uint16_t* node_no, int* start_heap, int* end_heap) @@ -6630,7 +6647,7 @@ class heap_select if (distribute_all_p) { - uint16_t current_heap_no_on_node[MAX_SUPPORTED_CPUS]; + uint16_t current_heap_no_on_node[MAX_SUPPORTED_HEAPS]; memset (current_heap_no_on_node, 0, sizeof (current_heap_no_on_node)); uint16_t current_heap_no = 0; @@ -6712,10 +6729,10 @@ class heap_select uint8_t* heap_select::sniff_buffer; unsigned heap_select::n_sniff_buffers; unsigned heap_select::cur_sniff_index; -uint16_t heap_select::proc_no_to_heap_no[MAX_SUPPORTED_CPUS]; -uint16_t heap_select::heap_no_to_proc_no[MAX_SUPPORTED_CPUS]; -uint16_t heap_select::heap_no_to_numa_node[MAX_SUPPORTED_CPUS]; -uint16_t heap_select::numa_node_to_heap_map[MAX_SUPPORTED_CPUS+4]; +uint16_t* heap_select::proc_no_to_heap_no; +uint16_t heap_select::heap_no_to_proc_no[MAX_SUPPORTED_HEAPS]; +uint16_t heap_select::heap_no_to_numa_node[MAX_SUPPORTED_HEAPS]; +uint16_t* heap_select::numa_node_to_heap_map; #ifdef HEAP_BALANCE_INSTRUMENTATION uint16_t heap_select::total_numa_nodes; node_heap_count heap_select::heaps_on_node[MAX_SUPPORTED_NODES]; @@ -10556,7 +10573,7 @@ static size_t target_mark_count_for_heap (size_t total_mark_count, int heap_coun NOINLINE uint8_t** gc_heap::equalize_mark_lists (size_t total_mark_list_size) { - size_t local_mark_count[MAX_SUPPORTED_CPUS]; + size_t local_mark_count[MAX_SUPPORTED_HEAPS]; size_t total_mark_count = 0; // compute mark count per heap into a local array @@ -10967,9 +10984,9 @@ void gc_heap::merge_mark_lists (size_t total_mark_list_size) int source_number = (size_t)heap_number; #endif //USE_REGIONS - uint8_t** source[MAX_SUPPORTED_CPUS]; - uint8_t** source_end[MAX_SUPPORTED_CPUS]; - int source_heap[MAX_SUPPORTED_CPUS]; + uint8_t** source[MAX_SUPPORTED_HEAPS]; + uint8_t** source_end[MAX_SUPPORTED_HEAPS]; + int source_heap[MAX_SUPPORTED_HEAPS]; int source_count = 0; for (int i = 0; i < n_heaps; i++) @@ -10980,7 +10997,7 @@ void gc_heap::merge_mark_lists (size_t total_mark_list_size) source[source_count] = heap->mark_list_piece_start[source_number]; source_end[source_count] = heap->mark_list_piece_end[source_number]; source_heap[source_count] = i; - if (source_count < MAX_SUPPORTED_CPUS) + if (source_count < MAX_SUPPORTED_HEAPS) source_count++; } } @@ -13503,8 +13520,8 @@ void gc_heap::distribute_free_regions() size_t total_num_free_regions[count_distributed_free_region_kinds] = { 0, 0 }; size_t total_budget_in_region_units[count_distributed_free_region_kinds] = { 0, 0 }; - size_t heap_budget_in_region_units[count_distributed_free_region_kinds][MAX_SUPPORTED_CPUS] = {}; - size_t min_heap_budget_in_region_units[count_distributed_free_region_kinds][MAX_SUPPORTED_CPUS] = {}; + size_t heap_budget_in_region_units[count_distributed_free_region_kinds][MAX_SUPPORTED_HEAPS] = {}; + size_t min_heap_budget_in_region_units[count_distributed_free_region_kinds][MAX_SUPPORTED_HEAPS] = {}; region_free_list aged_regions[count_free_region_kinds]; region_free_list surplus_regions[count_distributed_free_region_kinds]; @@ -13800,8 +13817,8 @@ void gc_heap::move_regions_to_decommit(region_free_list regions[count_free_regio } size_t gc_heap::compute_basic_region_budgets( - size_t heap_basic_budget_in_region_units[MAX_SUPPORTED_CPUS], - size_t min_heap_basic_budget_in_region_units[MAX_SUPPORTED_CPUS], + size_t heap_basic_budget_in_region_units[MAX_SUPPORTED_HEAPS], + size_t min_heap_basic_budget_in_region_units[MAX_SUPPORTED_HEAPS], size_t total_basic_free_regions) { const size_t region_size = global_region_allocator.get_region_alignment(); @@ -19429,8 +19446,8 @@ void gc_heap::balance_heaps (alloc_context* acontext) if (set_home_heap) { /* - // Since we are balancing up to MAX_SUPPORTED_CPUS, no need for this. - if (n_heaps > MAX_SUPPORTED_CPUS) + // Since we are balancing up to MAX_SUPPORTED_HEAPS, no need for this. + if (n_heaps > MAX_SUPPORTED_HEAPS) { // on machines with many processors cache affinity is really king, so don't even try // to balance on these. @@ -25113,7 +25130,7 @@ void gc_heap::equalize_promoted_bytes(int condemned_gen_number) // compute total promoted bytes per gen size_t total_surv = 0; size_t max_surv_per_heap = 0; - size_t surv_per_heap[MAX_SUPPORTED_CPUS]; + size_t surv_per_heap[MAX_SUPPORTED_HEAPS]; for (int i = 0; i < n_heaps; i++) { surv_per_heap[i] = 0; @@ -25254,7 +25271,7 @@ void gc_heap::equalize_promoted_bytes(int condemned_gen_number) surplus_regions_by_size_class[size_class] = region; } - int next_heap_in_size_class[MAX_SUPPORTED_CPUS]; + int next_heap_in_size_class[MAX_SUPPORTED_HEAPS]; int heaps_by_deficit_size_class[NUM_SIZE_CLASSES]; for (int i = 0; i < NUM_SIZE_CLASSES; i++) { @@ -49234,6 +49251,12 @@ HRESULT GCHeap::Initialize() #else //!MULTIPLE_HEAPS GCConfig::SetServerGC(true); AffinitySet config_affinity_set; + if (!config_affinity_set.Initialize(GCToOSInterface::GetMaxProcessorCount())) + { + log_init_error_to_host ("Failed to initialize affinity set for GC heap affinity configuration"); + return E_OUTOFMEMORY; + } + GCConfigStringHolder cpu_index_ranges_holder(GCConfig::GetGCHeapAffinitizeRanges()); uintptr_t config_affinity_mask = static_cast(GCConfig::GetGCHeapAffinitizeMask()); @@ -49276,7 +49299,7 @@ HRESULT GCHeap::Initialize() nhp = ((nhp_from_config == 0) ? g_num_active_processors : nhp_from_config); - nhp = min (nhp, (uint32_t)MAX_SUPPORTED_CPUS); + nhp = min (nhp, (uint32_t)MAX_SUPPORTED_HEAPS); gc_heap::gc_thread_no_affinitize_p = (gc_heap::heap_hard_limit ? !affinity_config_specified_p : (GCConfig::GetNoAffinitize() != 0)); @@ -49543,7 +49566,11 @@ HRESULT GCHeap::Initialize() } } - heap_select::init_numa_node_to_heap_map (nhp); + if (!heap_select::init_numa_node_to_heap_map (nhp)) + { + log_init_error_to_host ("Initialization of NUMA node to heap map failed"); + return E_OUTOFMEMORY; + } // If we have more active processors than heaps we still want to initialize some of the // mapping for the rest of the active processors because user threads can still run on diff --git a/src/coreclr/gc/gcconfig.cpp b/src/coreclr/gc/gcconfig.cpp index 02d95dff997353..0e3c4fc474a0b4 100644 --- a/src/coreclr/gc/gcconfig.cpp +++ b/src/coreclr/gc/gcconfig.cpp @@ -175,7 +175,8 @@ bool ParseGCHeapAffinitizeRanges(const char* cpu_index_ranges, AffinitySet* conf break; } - if ((start_index >= MAX_SUPPORTED_CPUS) || (end_index >= MAX_SUPPORTED_CPUS) || (end_index < start_index)) + size_t maxCpuCount = GCToOSInterface::GetMaxProcessorCount(); + if ((start_index >= maxCpuCount) || (end_index >= maxCpuCount) || (end_index < start_index)) { // Invalid CPU index values or range break; diff --git a/src/coreclr/gc/gcpriv.h b/src/coreclr/gc/gcpriv.h index 9674423d0a46cb..c34220c08df473 100644 --- a/src/coreclr/gc/gcpriv.h +++ b/src/coreclr/gc/gcpriv.h @@ -1748,7 +1748,7 @@ class gc_heap PER_HEAP_ISOLATED_METHOD void move_aged_regions(region_free_list dest[count_free_region_kinds], region_free_list& src, free_region_kind kind, bool joined_last_gc_before_oom); PER_HEAP_ISOLATED_METHOD bool aged_region_p(heap_segment* region, free_region_kind kind); PER_HEAP_ISOLATED_METHOD void move_regions_to_decommit(region_free_list oregions[count_free_region_kinds]); - PER_HEAP_ISOLATED_METHOD size_t compute_basic_region_budgets(size_t heap_basic_budget_in_region_units[MAX_SUPPORTED_CPUS], size_t min_heap_basic_budget_in_region_units[MAX_SUPPORTED_CPUS], size_t total_basic_free_regions); + PER_HEAP_ISOLATED_METHOD size_t compute_basic_region_budgets(size_t heap_basic_budget_in_region_units[MAX_SUPPORTED_HEAPS], size_t min_heap_basic_budget_in_region_units[MAX_SUPPORTED_HEAPS], size_t total_basic_free_regions); PER_HEAP_ISOLATED_METHOD bool near_heap_hard_limit_p(); PER_HEAP_ISOLATED_METHOD bool distribute_surplus_p(ptrdiff_t balance, int kind, bool aggressive_decommit_large_p); PER_HEAP_ISOLATED_METHOD void decide_on_decommit_strategy(bool joined_last_gc_before_oom); diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 33855147497c2b..86a07028d20da6 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -182,6 +182,9 @@ static uint8_t* g_helperPage = 0; // Mutex to make the FlushProcessWriteBuffersMutex thread safe static pthread_mutex_t g_flushProcessWriteBuffersMutex; +// The number of CPUs that are configured in the OS. +uint32_t g_configuredCpuCount = 0; + size_t GetRestrictedPhysicalMemoryLimit(); bool GetPhysicalMemoryUsed(size_t* val); @@ -217,7 +220,19 @@ bool GCToOSInterface::Initialize() return false; } + int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + if (configuredCpuCount == -1) + { + return false; + } + g_totalCpuCount = cpuCount; + g_configuredCpuCount = configuredCpuCount; + + if (!g_processAffinitySet.Initialize(configuredCpuCount)) + { + return false; + } // // support for FlusProcessWriteBuffers @@ -268,29 +283,42 @@ bool GCToOSInterface::Initialize() #if HAVE_SCHED_GETAFFINITY - cpu_set_t cpuSet; - int st = sched_getaffinity(getpid(), sizeof(cpu_set_t), &cpuSet); - - if (st == 0) { - for (size_t i = 0; i < CPU_SETSIZE; i++) + // Use a dynamically allocated cpu_set_t to support systems with more than CPU_SETSIZE (typically 1024) CPUs. + cpu_set_t* pCpuSet = CPU_ALLOC(configuredCpuCount); + if (pCpuSet == nullptr) + { + return false; + } + + size_t cpuSetSize = CPU_ALLOC_SIZE(configuredCpuCount); + CPU_ZERO_S(cpuSetSize, pCpuSet); + + int st = sched_getaffinity(getpid(), cpuSetSize, pCpuSet); + + if (st == 0) { - if (CPU_ISSET(i, &cpuSet)) + for (size_t i = 0; i < (size_t)configuredCpuCount; i++) { - g_processAffinitySet.Add(i); + if (CPU_ISSET_S(i, cpuSetSize, pCpuSet)) + { + g_processAffinitySet.Add(i); + } } } - } - else - { - // We should not get any of the errors that the sched_getaffinity can return since none - // of them applies for the current thread, so this is an unexpected kind of failure. - assert(false); + else + { + // We should not get any of the errors that the sched_getaffinity can return since none + // of them applies for the current thread, so this is an unexpected kind of failure. + assert(false); + } + + CPU_FREE(pCpuSet); } #else // HAVE_SCHED_GETAFFINITY - for (size_t i = 0; i < g_totalCpuCount; i++) + for (size_t i = 0; i < configuredCpuCount; i++) { g_processAffinitySet.Add(i); } @@ -1085,9 +1113,11 @@ size_t GCToOSInterface::GetCacheSizePerLogicalCpu(bool trueSize) bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) { #if HAVE_SCHED_SETAFFINITY || HAVE_PTHREAD_SETAFFINITY_NP - cpu_set_t cpuSet; - CPU_ZERO(&cpuSet); - CPU_SET((int)procNo, &cpuSet); + + cpu_set_t* pCpuSet = CPU_ALLOC(g_configuredCpuCount); + size_t cpuSetSize = CPU_ALLOC_SIZE(g_configuredCpuCount); + CPU_ZERO_S(cpuSetSize, pCpuSet); + CPU_SET_S((int)procNo, cpuSetSize, pCpuSet); // Snap's default strict confinement does not allow sched_setaffinity(, ...) without manually connecting the // process-control plug. sched_setaffinity(, ...) is also currently not allowed, only @@ -1098,11 +1128,13 @@ bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) // - https://github.com/dotnet/runtime/pull/38795 // - https://github.com/dotnet/runtime/issues/1634 // - https://forum.snapcraft.io/t/requesting-autoconnect-for-interfaces-in-pigmeat-process-control-home/17987/13 -#if HAVE_SCHED_SETAFFINITY - int st = sched_setaffinity(0, sizeof(cpu_set_t), &cpuSet); -#else - int st = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuSet); -#endif + #if HAVE_SCHED_SETAFFINITY + int st = sched_setaffinity(0, cpuSetSize, pCpuSet); + #else + int st = pthread_setaffinity_np(pthread_self(), cpuSetSize, pCpuSet); + #endif + + CPU_FREE(pCpuSet); return (st == 0); @@ -1135,7 +1167,7 @@ const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffi if (!configAffinitySet->IsEmpty()) { // Update the process affinity set using the configured set - for (size_t i = 0; i < MAX_SUPPORTED_CPUS; i++) + for (size_t i = 0; i < g_totalCpuCount; i++) { if (g_processAffinitySet.Contains(i) && !configAffinitySet->Contains(i)) { @@ -1488,6 +1520,11 @@ uint32_t GCToOSInterface::GetTotalProcessorCount() return g_totalCpuCount; } +uint32_t GCToOSInterface::GetMaxProcessorCount() +{ + return (uint32_t)g_processAffinitySet.MaxCpuCount(); +} + bool GCToOSInterface::CanEnableGCNumaAware() { return g_numaAvailable; @@ -1510,7 +1547,9 @@ bool GCToOSInterface::GetProcessorForHeap(uint16_t heap_number, uint16_t* proc_n bool success = false; uint16_t availableProcNumber = 0; - for (size_t procNumber = 0; procNumber < MAX_SUPPORTED_CPUS; procNumber++) + + size_t maxCpuCount = g_processAffinitySet.MaxCpuCount(); + for (size_t procNumber = 0; procNumber < maxCpuCount; procNumber++) { if (g_processAffinitySet.Contains(procNumber)) { diff --git a/src/coreclr/gc/windows/gcenv.windows.cpp b/src/coreclr/gc/windows/gcenv.windows.cpp index 3e8040be0bcbb1..28c443c4315653 100644 --- a/src/coreclr/gc/windows/gcenv.windows.cpp +++ b/src/coreclr/gc/windows/gcenv.windows.cpp @@ -512,6 +512,11 @@ bool GCToOSInterface::Initialize() InitNumaNodeInfo(); InitCPUGroupInfo(); + if (!g_processAffinitySet.Initialize(GCToOSInterface::GetTotalProcessorCount())) + { + return false; + } + if (CanEnableGCCPUGroups()) { // When CPU groups are enabled, then the process is not bound by the process affinity set at process launch. @@ -916,7 +921,7 @@ const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffi if (!configAffinitySet->IsEmpty()) { // Update the process affinity set using the configured set - for (size_t i = 0; i < MAX_SUPPORTED_CPUS; i++) + for (size_t i = 0; i < GCToOSInterface::GetTotalProcessorCount(); i++) { if (g_processAffinitySet.Contains(i) && !configAffinitySet->Contains(i)) { @@ -1116,6 +1121,11 @@ uint32_t GCToOSInterface::GetTotalProcessorCount() } } +uint32_t GCToOSInterface::GetMaxProcessorCount() +{ + return (uint32_t)g_processAffinitySet.MaxCpuCount(); +} + bool GCToOSInterface::CanEnableGCNumaAware() { return g_fEnableGCNumaAware; @@ -1186,13 +1196,13 @@ bool GCToOSInterface::GetProcessorForHeap(uint16_t heap_number, uint16_t* proc_n // Locate heap_number-th available processor uint16_t procIndex = 0; size_t cnt = heap_number; - for (uint16_t i = 0; i < MAX_SUPPORTED_CPUS; i++) + for (uint32_t i = 0; i < GCToOSInterface::GetTotalProcessorCount(); i++) { if (g_processAffinitySet.Contains(i)) { if (cnt == 0) { - procIndex = i; + procIndex = (uint16_t)i; success = true; break; } diff --git a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp index 14efb5a984e641..c326bc4f0ff444 100644 --- a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp @@ -438,7 +438,7 @@ void ConfigureSignals() void InitializeCurrentProcessCpuCount() { - uint32_t count; + uint32_t count = 0; // If the configuration value has been set, it takes precedence. Otherwise, take into account // process affinity and CPU quota limit. @@ -455,14 +455,41 @@ void InitializeCurrentProcessCpuCount() { #if HAVE_SCHED_GETAFFINITY - cpu_set_t cpuSet; - int st = sched_getaffinity(getpid(), sizeof(cpu_set_t), &cpuSet); - if (st != 0) + int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + if (configuredCpuCount == -1) + { + // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing. + configuredCpuCount = CPU_SETSIZE; + } + + cpu_set_t* pCpuSet = CPU_ALLOC(configuredCpuCount); + if (pCpuSet != nullptr) + { + size_t cpuSetSize = CPU_ALLOC_SIZE(configuredCpuCount); + CPU_ZERO_S(cpuSetSize, pCpuSet); + + int st = sched_getaffinity(getpid(), cpuSetSize, pCpuSet); + if (st == 0) + { + count = (uint32_t)CPU_COUNT_S(CPU_ALLOC_SIZE(configuredCpuCount), pCpuSet); + } + else + { + _ASSERTE(!"sched_getaffinity failed"); + } + + CPU_FREE(pCpuSet); + } + else { - _ASSERTE(!"sched_getaffinity failed"); + ASSERT("CPU_ALLOC failed!\n"); } - count = CPU_COUNT(&cpuSet); + if (count == 0) + { + // If we failed to get the number of CPUs from sched_getaffinity, fall back to getting the total number of CPUs in the system. + count = GCToOSInterface::GetTotalProcessorCount(); + } #else // HAVE_SCHED_GETAFFINITY count = GCToOSInterface::GetTotalProcessorCount(); #endif // HAVE_SCHED_GETAFFINITY diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index 2d6f9edb620a8f..9ab635c2259cd6 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -136,6 +136,12 @@ PAL_GetTotalCpuCount() #error "Don't know how to get total CPU count on this platform" #endif // HAVE_SYSCONF + if (nrcpus < 1) + { + // Default to 1 if we failed to get the number of CPUs or if the value is invalid. + nrcpus = 1; + } + return nrcpus; } @@ -149,14 +155,41 @@ PAL_GetLogicalCpuCountFromOS() { #if HAVE_SCHED_GETAFFINITY - cpu_set_t cpuSet; - int st = sched_getaffinity(gPID, sizeof(cpu_set_t), &cpuSet); - if (st != 0) + int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + if (configuredCpuCount == -1) { - ASSERT("sched_getaffinity failed (%d)\n", errno); + // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing. + configuredCpuCount = CPU_SETSIZE; } - nrcpus = CPU_COUNT(&cpuSet); + cpu_set_t* pCpuSet = CPU_ALLOC(configuredCpuCount); + if (pCpuSet != nullptr) + { + size_t cpuSetSize = CPU_ALLOC_SIZE(configuredCpuCount); + CPU_ZERO_S(cpuSetSize, pCpuSet); + + int st = sched_getaffinity(gPID, cpuSetSize, pCpuSet); + if (st == 0) + { + nrcpus = CPU_COUNT_S(CPU_ALLOC_SIZE(configuredCpuCount), pCpuSet); + } + else + { + ASSERT("sched_getaffinity failed (%d)\n", errno); + } + + CPU_FREE(pCpuSet); + } + else + { + ASSERT("CPU_ALLOC failed!\n"); + } + + if (nrcpus < 1) + { + // If we failed to get the number of CPUs from sched_getaffinity, fall back to getting the total number of CPUs in the system. + nrcpus = PAL_GetTotalCpuCount(); + } #else // HAVE_SCHED_GETAFFINITY nrcpus = PAL_GetTotalCpuCount(); #endif // HAVE_SCHED_GETAFFINITY diff --git a/src/coreclr/pal/src/thread/thread.cpp b/src/coreclr/pal/src/thread/thread.cpp index ec6920f922cade..d48db929fe808f 100644 --- a/src/coreclr/pal/src/thread/thread.cpp +++ b/src/coreclr/pal/src/thread/thread.cpp @@ -1502,7 +1502,6 @@ CPalThread::ThreadEntry( LPVOID pvPar; DWORD retValue; #if HAVE_SCHED_GETAFFINITY && HAVE_SCHED_SETAFFINITY - cpu_set_t cpuSet; int st; #endif @@ -1529,24 +1528,43 @@ CPalThread::ThreadEntry( // - https://github.com/dotnet/runtime/issues/1634 // - https://forum.snapcraft.io/t/requesting-autoconnect-for-interfaces-in-pigmeat-process-control-home/17987/13 - CPU_ZERO(&cpuSet); - - st = sched_getaffinity(gPID, sizeof(cpu_set_t), &cpuSet); - if (st != 0) { - ASSERT("sched_getaffinity failed!\n"); - // The sched_getaffinity should never fail for getting affinity of the current process - palError = ERROR_INTERNAL_ERROR; - goto fail; - } + int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + if (configuredCpuCount == -1) + { + // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing thread creation. + configuredCpuCount = CPU_SETSIZE; + } - st = sched_setaffinity(0, sizeof(cpu_set_t), &cpuSet); - if (st != 0) - { - ASSERT("sched_setaffinity failed!\n"); - // The sched_setaffinity should never fail when passed the mask extracted using sched_getaffinity - palError = ERROR_INTERNAL_ERROR; - goto fail; + cpu_set_t* pCpuSet = CPU_ALLOC(configuredCpuCount); + if (pCpuSet == nullptr) + { + ASSERT("CPU_ALLOC failed!\n"); + palError = ERROR_OUTOFMEMORY; + goto fail; + } + + size_t cpuSetSize = CPU_ALLOC_SIZE(configuredCpuCount); + CPU_ZERO_S(cpuSetSize, pCpuSet); + + st = sched_getaffinity(gPID, cpuSetSize, pCpuSet); + if (st == 0) + { + st = sched_setaffinity(0, CPU_ALLOC_SIZE(configuredCpuCount), pCpuSet); + if (st != 0) + { + ASSERT("sched_setaffinity failed!\n"); + CPU_FREE(pCpuSet); + palError = ERROR_INTERNAL_ERROR; + goto fail; + } + } + else + { + // Treat failure to get the affinity mask in release build as non-fatal. + ASSERT("sched_getaffinity failed!\n"); + } + CPU_FREE(pCpuSet); } #endif // HAVE_SCHED_GETAFFINITY && HAVE_SCHED_SETAFFINITY From 2609db6152b783dfb7eceac65c690ebb2676a0c2 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Wed, 20 May 2026 22:51:50 +0200 Subject: [PATCH 2/5] Add asserts --- src/coreclr/gc/gc.cpp | 12 +++++++++--- src/coreclr/gc/unix/gcenv.unix.cpp | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/coreclr/gc/gc.cpp b/src/coreclr/gc/gc.cpp index d32d06841596dd..c69c7a0c17a7a3 100644 --- a/src/coreclr/gc/gc.cpp +++ b/src/coreclr/gc/gc.cpp @@ -6485,6 +6485,7 @@ class heap_select if (GCToOSInterface::CanGetCurrentProcessorNumber()) { uint32_t proc_no = GCToOSInterface::GetCurrentProcessorNumber(); + assert(proc_no < GCToOSInterface::GetMaxProcessorCount()); proc_no_to_heap_no[proc_no] = (uint16_t)heap_number; } } @@ -6507,6 +6508,7 @@ class heap_select if (GCToOSInterface::CanGetCurrentProcessorNumber()) { uint32_t proc_no = GCToOSInterface::GetCurrentProcessorNumber(); + assert(proc_no < GCToOSInterface::GetMaxProcessorCount()); int adjusted_heap = proc_no_to_heap_no[proc_no]; // with dynamic heap count, need to make sure the value is in range. if (adjusted_heap >= gc_heap::n_heaps) @@ -6588,6 +6590,7 @@ class heap_select // numa_node_to_heap_map[numa_node] is set to the first heap number on that node and // numa_node_to_heap_map[numa_node + 1] is set to the first heap number not on that node // Set the start of the heap number range for the first NUMA node + assert(heap_no_to_numa_node[0] < maxCpuCount + 1); numa_node_to_heap_map[heap_no_to_numa_node[0]] = 0; #ifdef HEAP_BALANCE_INSTRUMENTATION total_numa_nodes = 0; @@ -6604,8 +6607,8 @@ class heap_select total_numa_nodes++; heaps_on_node[total_numa_nodes].node_no = heap_no_to_numa_node[i]; #endif - assert(heap_no_to_numa_node[i-1] < maxCpuCount); - assert(heap_no_to_numa_node[i] < maxCpuCount); + assert(heap_no_to_numa_node[i-1] + 1 < maxCpuCount + 1); + assert(heap_no_to_numa_node[i] < maxCpuCount + 1); // Set the end of the heap number range for the previous NUMA node numa_node_to_heap_map[heap_no_to_numa_node[i-1] + 1] = // Set the start of the heap number range for the current NUMA node @@ -6616,7 +6619,7 @@ class heap_select #endif } - assert(heap_no_to_numa_node[nheaps-1] < maxCpuCount); + assert(heap_no_to_numa_node[nheaps-1] + 1 < maxCpuCount + 1); // Set the end of the heap range for the last NUMA node numa_node_to_heap_map[heap_no_to_numa_node[nheaps-1] + 1] = (uint16_t)nheaps; //mark the end with nheaps @@ -6634,6 +6637,7 @@ class heap_select if (*node_no == NUMA_NODE_UNDEFINED) *node_no = 0; + assert(*node_no + 1 < GCToOSInterface::GetMaxProcessorCount() + 1); *start_heap = (int)numa_node_to_heap_map[*node_no]; *end_heap = (int)(numa_node_to_heap_map[*node_no + 1]); @@ -6719,6 +6723,7 @@ class heap_select static void get_heap_range_for_heap(int hn, int* start, int* end) { uint16_t numa_node = heap_no_to_numa_node[hn]; + assert(numa_node + 1 < GCToOSInterface::GetMaxProcessorCount() + 1); *start = (int)numa_node_to_heap_map[numa_node]; *end = (int)(numa_node_to_heap_map[numa_node+1]); #ifdef HEAP_BALANCE_INSTRUMENTATION @@ -19496,6 +19501,7 @@ void gc_heap::balance_heaps (alloc_context* acontext) last_proc_no = proc_no; } + assert(proc_no < GCToOSInterface::GetActiveProcessorCount ()); int new_home_hp_num = heap_select::proc_no_to_heap_no[proc_no]; #else int new_home_hp_num = heap_select::select_heap(acontext); diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 86a07028d20da6..5ac4e9a9bbec30 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -376,6 +376,9 @@ bool GCToOSInterface::Initialize() assert(g_totalPhysicalMemSize != 0); + printf("GCToOsInterface::Initialize: cpuCount=%u, configuredCpuCount=%u, processAffinitySet.Count()=%zu\n", + g_totalCpuCount, g_configuredCpuCount, g_processAffinitySet.Count()); + return true; } @@ -1136,6 +1139,7 @@ bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) CPU_FREE(pCpuSet); + assert(st == 0); return (st == 0); #else // !(HAVE_SCHED_SETAFFINITY || HAVE_PTHREAD_SETAFFINITY_NP) From 734f87bf26234382452babbf212cff095b45f51a Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Wed, 20 May 2026 23:19:39 +0200 Subject: [PATCH 3/5] Fix runtime initialization on linux with cpu hotplug enabled (#128069) On unix, during initialization, the runtime obtains the total number of CPUs via `sysconf(_SC_NPROCESSORS_CONF)`. This should return the current number of cpus that are currently present on the system. It turns out linux has cpu hotplug support, so this number can increase. When hotplug is enabled, the kernel reserves storage for the max possible number of CPUs. This max number is exported in `/sys/devices/system/cpu/possible`. The problem is that, when allocating the `cpu_set_t*` for use with `sched_getaffinity`, this api failed because the OS expected for the cpu set to have reserved space for the maximum amount of cpu's, not just for the ones that are currently present. --- src/coreclr/gc/unix/gcenv.unix.cpp | 3 +- .../nativeaot/Runtime/unix/PalUnix.cpp | 5 +- src/coreclr/pal/src/misc/sysinfo.cpp | 6 ++- src/coreclr/pal/src/thread/thread.cpp | 5 +- src/native/minipal/CMakeLists.txt | 4 ++ src/native/minipal/cpucount.c | 49 +++++++++++++++++++ src/native/minipal/cpucount.h | 24 +++++++++ 7 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/native/minipal/cpucount.c create mode 100644 src/native/minipal/cpucount.h diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 5ac4e9a9bbec30..b66827d3d754b0 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -25,6 +25,7 @@ #include "numasupport.h" #include #include +#include #if HAVE_SWAPCTL #include @@ -220,7 +221,7 @@ bool GCToOSInterface::Initialize() return false; } - int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + int configuredCpuCount = minipal_get_cpu_max_possible_count(); if (configuredCpuCount == -1) { return false; diff --git a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp index c326bc4f0ff444..06d6552ba3e596 100644 --- a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp @@ -28,6 +28,7 @@ #include "RhConfig.h" #include +#include #include #include #include @@ -455,10 +456,10 @@ void InitializeCurrentProcessCpuCount() { #if HAVE_SCHED_GETAFFINITY - int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + int configuredCpuCount = minipal_get_cpu_max_possible_count(); if (configuredCpuCount == -1) { - // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing. + // In the unlikely event that minipal_get_cpu_max_possible_count() fails, just assume a reasonable default maximum number of CPUs to avoid failing. configuredCpuCount = CPU_SETSIZE; } diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index 9ab635c2259cd6..719051aca07977 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -24,6 +24,8 @@ Revision History: #include #include #include +#include +#include #define __STDC_FORMAT_MACROS #include #include @@ -155,10 +157,10 @@ PAL_GetLogicalCpuCountFromOS() { #if HAVE_SCHED_GETAFFINITY - int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + int configuredCpuCount = minipal_get_cpu_max_possible_count(); if (configuredCpuCount == -1) { - // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing. + // In the unlikely event that minipal_get_cpu_max_possible_count() fails, just assume a reasonable default maximum number of CPUs to avoid failing. configuredCpuCount = CPU_SETSIZE; } diff --git a/src/coreclr/pal/src/thread/thread.cpp b/src/coreclr/pal/src/thread/thread.cpp index d48db929fe808f..0eecaa528dd403 100644 --- a/src/coreclr/pal/src/thread/thread.cpp +++ b/src/coreclr/pal/src/thread/thread.cpp @@ -29,6 +29,7 @@ SET_DEFAULT_DEBUG_CHANNEL(THREAD); // some headers have code with asserts, so do #include "pal/virtual.h" #include +#include #if defined(__NetBSD__) && !HAVE_PTHREAD_GETCPUCLOCKID #include @@ -1529,10 +1530,10 @@ CPalThread::ThreadEntry( // - https://forum.snapcraft.io/t/requesting-autoconnect-for-interfaces-in-pigmeat-process-control-home/17987/13 { - int configuredCpuCount = sysconf(_SC_NPROCESSORS_CONF); + int configuredCpuCount = minipal_get_cpu_max_possible_count(); if (configuredCpuCount == -1) { - // In the unlikely event that sysconf(_SC_NPROCESSORS_CONF) fails, just assume a reasonable default maximum number of CPUs to avoid failing thread creation. + // In the unlikely event that minipal_get_cpu_max_possible_count() fails, just assume a reasonable default maximum number of CPUs to avoid failing thread creation. configuredCpuCount = CPU_SETSIZE; } diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index 78011d4779694a..04c49331036923 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -14,6 +14,10 @@ set(SOURCES log.c ) +if(CLR_CMAKE_HOST_UNIX) + list(APPEND SOURCES cpucount.c) +endif() + # Provide an object library for scenarios where we ship static libraries include_directories(${CLR_SRC_NATIVE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/native/minipal/cpucount.c b/src/native/minipal/cpucount.c new file mode 100644 index 00000000000000..0298724a8b7d65 --- /dev/null +++ b/src/native/minipal/cpucount.c @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "cpucount.h" + +#include +#include + +int minipal_get_cpu_max_possible_count(void) +{ +#if defined(__linux__) + FILE* f = fopen("/sys/devices/system/cpu/possible", "r"); + if (f != NULL) + { + int maxCpu = -1; + for (;;) + { + int lo, hi; + int matched = fscanf(f, "%d-%d", &lo, &hi); + if (matched == 1) + { + hi = lo; + } + else if (matched != 2) + { + break; + } + + if (maxCpu < hi) + { + maxCpu = hi; + } + + int ch = fgetc(f); + if (ch == EOF || ch != ',') + { + break; + } + } + fclose(f); + if (maxCpu != -1) + { + return maxCpu + 1; + } + } +#endif + + return (int)sysconf(_SC_NPROCESSORS_CONF); +} diff --git a/src/native/minipal/cpucount.h b/src/native/minipal/cpucount.h new file mode 100644 index 00000000000000..5b2d6afa59bf33 --- /dev/null +++ b/src/native/minipal/cpucount.h @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_MINIPAL_CPUCOUNT_H +#define HAVE_MINIPAL_CPUCOUNT_H + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + +// Returns the maximum number of CPUs that could ever be available on this system, +// suitable for sizing cpu_set_t allocations via CPU_ALLOC. +// +// On Linux, this reads /sys/devices/system/cpu/possible to account for CPU hotplug. +// This may be larger than the number of online or present CPUs. +// Falls back to sysconf(_SC_NPROCESSORS_CONF) if the sysfs file is unavailable. +int minipal_get_cpu_max_possible_count(void); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // HAVE_MINIPAL_CPUCOUNT_H From 0126557c3a1d6f3578850752daa50ed4192bec8c Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Mon, 3 Aug 2026 16:25:14 +0200 Subject: [PATCH 4/5] Remove forgotten debug printf --- src/coreclr/gc/unix/gcenv.unix.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index b66827d3d754b0..e758fecd8bb872 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -377,9 +377,6 @@ bool GCToOSInterface::Initialize() assert(g_totalPhysicalMemSize != 0); - printf("GCToOsInterface::Initialize: cpuCount=%u, configuredCpuCount=%u, processAffinitySet.Count()=%zu\n", - g_totalCpuCount, g_configuredCpuCount, g_processAffinitySet.Count()); - return true; } From 8bb9f3c49f7350fe8806d489c0476ac90a35e42f Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Thu, 6 Aug 2026 16:56:00 +0200 Subject: [PATCH 5/5] Fix signed / unsigned comparison in few asserts --- src/coreclr/gc/gc.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/coreclr/gc/gc.cpp b/src/coreclr/gc/gc.cpp index c69c7a0c17a7a3..35aadaef7aa149 100644 --- a/src/coreclr/gc/gc.cpp +++ b/src/coreclr/gc/gc.cpp @@ -6607,8 +6607,8 @@ class heap_select total_numa_nodes++; heaps_on_node[total_numa_nodes].node_no = heap_no_to_numa_node[i]; #endif - assert(heap_no_to_numa_node[i-1] + 1 < maxCpuCount + 1); - assert(heap_no_to_numa_node[i] < maxCpuCount + 1); + assert(heap_no_to_numa_node[i-1] + 1u < maxCpuCount + 1); + assert(heap_no_to_numa_node[i] < maxCpuCount + 1u); // Set the end of the heap number range for the previous NUMA node numa_node_to_heap_map[heap_no_to_numa_node[i-1] + 1] = // Set the start of the heap number range for the current NUMA node @@ -6619,7 +6619,7 @@ class heap_select #endif } - assert(heap_no_to_numa_node[nheaps-1] + 1 < maxCpuCount + 1); + assert(heap_no_to_numa_node[nheaps-1] + 1u < maxCpuCount + 1); // Set the end of the heap range for the last NUMA node numa_node_to_heap_map[heap_no_to_numa_node[nheaps-1] + 1] = (uint16_t)nheaps; //mark the end with nheaps @@ -6637,7 +6637,7 @@ class heap_select if (*node_no == NUMA_NODE_UNDEFINED) *node_no = 0; - assert(*node_no + 1 < GCToOSInterface::GetMaxProcessorCount() + 1); + assert(*node_no + 1u < GCToOSInterface::GetMaxProcessorCount() + 1); *start_heap = (int)numa_node_to_heap_map[*node_no]; *end_heap = (int)(numa_node_to_heap_map[*node_no + 1]); @@ -6723,7 +6723,7 @@ class heap_select static void get_heap_range_for_heap(int hn, int* start, int* end) { uint16_t numa_node = heap_no_to_numa_node[hn]; - assert(numa_node + 1 < GCToOSInterface::GetMaxProcessorCount() + 1); + assert(numa_node + 1u < GCToOSInterface::GetMaxProcessorCount() + 1); *start = (int)numa_node_to_heap_map[numa_node]; *end = (int)(numa_node_to_heap_map[numa_node+1]); #ifdef HEAP_BALANCE_INSTRUMENTATION