- C 40%
- Shell 38.7%
- Dockerfile 21.3%
|
|
||
|---|---|---|
| .woodpecker.yml | ||
| copy.sh | ||
| Dockerfile | ||
| numcpu_override.c | ||
| README.md | ||
| renovate.json | ||
numcpu_override
A lightweight shared library that intercepts sysconf() system calls to override the reported CPU count. This allows applications to see a different number of CPUs than physically available, useful for testing, resource limiting, and container environments.
Features
- ✅ Overrides
_SC_NPROCESSORS_CONFand_SC_NPROCESSORS_ONLN - ✅ Works with any application using
sysconf()for CPU detection - ✅ Builds both musl and glibc variants in one Docker build
- ✅ Optional debug logging
- ✅ Input validation and error handling
- ✅ Zero overhead when not overriding
How It Works
The library uses LD_PRELOAD to intercept sysconf() calls and returns the value specified in the NUMCPUS environment variable instead of the actual CPU count.
Why Two Libraries?
musl vs glibc are different C standard library implementations with incompatible ABIs (Application Binary Interfaces). A shared library compiled against one cannot be used with the other:
- musl: Used by Alpine Linux and other minimal distributions
- glibc: Used by Debian, Ubuntu, RHEL, Fedora, and most mainstream Linux distributions
The Docker build creates both variants so you can use the library on any Linux system. Use the matching variant for your target environment.
Building
Local Build
# Alpine/musl
gcc -O2 -shared -fPIC -Wall -Wextra -Werror numcpu_override.c -o numcpu_override-musl.so -ldl
# Debian/glibc
gcc -O2 -shared -fPIC -Wall -Wextra -Werror numcpu_override.c -o numcpu_override-glibc.so -ldl
Docker Build
The Dockerfile builds both musl and glibc variants in parallel stages:
# Build both libraries in one command
docker build -t numcpu-override .
# Option 1: Extract both libraries to organized subdirectories
mkdir -p _libs
docker run --rm -v $(pwd)/_libs:/_libs numcpu-override
# Result:
# _libs/musl/numcpu_override.so (for Alpine, musl-based distros)
# _libs/glibc/numcpu_override.so (for Debian, Ubuntu, most Linux distros)
# Option 2: Extract only the variant you need using TARGET_LIBC
# For Alpine/musl systems:
docker run --rm -e TARGET_LIBC=musl -v $(pwd)/_libs:/_libs numcpu-override
# Result: _libs/numcpu_override.so (musl variant)
# For Debian/Ubuntu/glibc systems:
docker run --rm -e TARGET_LIBC=glibc -v $(pwd)/_libs:/_libs numcpu-override
# Result: _libs/numcpu_override.so (glibc variant)
Choosing the Right Library
| Your System | Use This Library | Common Distros |
|---|---|---|
| musl | numcpu_override-musl.so |
Alpine Linux, Void Linux (musl) |
| glibc | numcpu_override-glibc.so |
Debian, Ubuntu, RHEL, Fedora, CentOS, Arch |
How to check your system:
ldd --version 2>&1 | head -n1
# musl: "musl libc"
# glibc: "ldd (GNU libc)" or "ldd (Ubuntu GLIBC..."
Usage
Basic Usage
# On glibc systems (Debian, Ubuntu, etc.)
NUMCPUS=4 LD_PRELOAD=./_libs/glibc/numcpu_override.so ./your_application
# On musl systems (Alpine)
NUMCPUS=4 LD_PRELOAD=./_libs/musl/numcpu_override.so ./your_application
# Or export for multiple commands
export NUMCPUS=4
export LD_PRELOAD=/path/to/numcpu_override.so
./your_application
Docker/Container Usage
# Alpine-based container (musl)
FROM alpine:3.23
COPY _libs/musl/numcpu_override.so /usr/local/lib/
ENV LD_PRELOAD=/usr/local/lib/numcpu_override.so
ENV NUMCPUS=2
# Debian-based container (glibc)
FROM debian:stable
COPY _libs/glibc/numcpu_override.so /usr/local/lib/
ENV LD_PRELOAD=/usr/local/lib/numcpu_override.so
ENV NUMCPUS=2
# Or at runtime
docker run -e NUMCPUS=4 -e LD_PRELOAD=/path/to/numcpu_override.so ...
Python Example
# Detect your libc first
LIBC=$(ldd --version 2>&1 | grep -q musl && echo "musl" || echo "glibc")
# Run with appropriate library
NUMCPUS=2 LD_PRELOAD=./_libs/${LIBC}/numcpu_override.so python3 -c "import os; print(os.cpu_count())"
# Output: 2
Debug Mode
Enable debug logging to see when the override is triggered:
export NUMCPUS_DEBUG=1
export NUMCPUS=4
export LD_PRELOAD=./numcpu_override.so
./your_application
Debug output goes to stderr:
numcpu_override: intercepting sysconf(84)
numcpu_override: NUMCPUS=4
numcpu_override: returning 4 CPUs
Environment Variables
| Variable | Description | Default |
|---|---|---|
NUMCPUS |
Number of CPUs to report (1-65536) | Real CPU count |
NUMCPUS_DEBUG |
Enable debug logging (set to "1") | Disabled |
TARGET_LIBC |
Extract specific library: musl or glibc (Docker only) |
Both |
Behavior
- If
NUMCPUSis not set or empty → returns actual CPU count - If
NUMCPUSis invalid (negative, non-numeric, >65536) → returns actual CPU count and logs error - If
NUMCPUSis valid → returns the specified value - Debug messages only appear when
NUMCPUS_DEBUG=1
Use Cases
Testing Multi-Core Applications
Test how your application behaves with different CPU counts without needing multiple machines.
Resource Limiting
Make applications believe they have fewer CPUs available, useful for:
- Fair resource sharing in containers
- Limiting parallel execution
- Testing resource-constrained scenarios
Build Systems
Control parallelism in build tools that auto-detect CPU count:
NUMCPUS=2 LD_PRELOAD=./numcpu_override.so make -j$(nproc)
Container Environments
Override CPU count in containers where cgroup limits don't properly restrict CPU detection.
Compatibility
Supported:
- ✅ Linux x86_64, ARM64 (with matching architecture build)
- ✅ musl-libc systems: Alpine Linux, Void Linux (musl)
- ✅ glibc systems: Debian, Ubuntu, RHEL, Fedora, CentOS, Arch Linux
- ✅ Applications using
sysconf(_SC_NPROCESSORS_CONF)orsysconf(_SC_NPROCESSORS_ONLN) - ✅ Python, Java, Node.js, Go (with cgo), C/C++ applications
Not Supported:
- ❌ Static binaries (no dynamic linking means no
LD_PRELOAD) - ❌ Applications reading
/proc/cpuinfodirectly - ❌ cgroup CPU quotas (kernel-level limits)
- ❌ Cross-libc usage (musl library on glibc system or vice versa)
Technical Details
Intercepted Calls
_SC_NPROCESSORS_CONF- Number of configured processors_SC_NPROCESSORS_ONLN- Number of processors currently online
Implementation
- Uses
dlsym(RTLD_NEXT, "sysconf")to chain to the real implementation - Thread-safe environment variable reading
- Validates input range (0-65536)
- Falls back gracefully on invalid input
- Minimal performance overhead
Troubleshooting
Override not working?
- Verify
LD_PRELOADpoints to the correct.sofile - Use the correct libc variant (musl vs glibc) - this is the most common issue!
- Check the application uses
sysconf()for CPU detection - Enable
NUMCPUS_DEBUG=1to verify interception
Application crashes?
- Wrong libc variant - make sure you use musl for Alpine, glibc for Debian/Ubuntu
- Check library architecture matches (x86_64, arm64, etc.)
- Verify dynamic linking:
ldd /path/to/numcpu_override.so
How to identify your libc:
ldd --version 2>&1 | head -n1
# musl libc (x86_64) -> use musl variant
# ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35 -> use glibc variant
Wrong CPU count?
- Some applications cache CPU count at startup
- Some use
/proc/cpuinfoinstead ofsysconf() - Some are statically linked and ignore
LD_PRELOAD
License
See LICENSE file for details.
Contributing
Issues and pull requests welcome!